PluginProbe
ElasticPress / 4.2.2
ElasticPress v4.2.2
5.3.5 5.3.4 3.6.5 3.6.6 4.0.0 4.0.1 4.1.0 4.2.0 4.2.1 4.2.2 4.3.0 4.3.1 4.4.0 4.4.1 4.5.0 4.5.1 4.5.2 4.6.0 4.6.1 4.7.0 4.7.1 4.7.2 5.0.0 5.0.1 5.0.2 All 108 releases
elasticpress / includes / classes / Indexable / Post / Post.php

Post.php in ElasticPress 4.2.2, at includes/classes/Indexable/Post/Post.php

2,264 lines 63.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * User indexable
4 *
5 * @since 3.0
6 * @package elasticpress
7 */
8
9 namespace ElasticPress\Indexable\Post;
10
11 use ElasticPress\Indexable as Indexable;
12 use ElasticPress\Elasticsearch as Elasticsearch;
13 use \WP_Query as WP_Query;
14 use \WP_User as WP_User;
15
16 if ( ! defined( 'ABSPATH' ) ) {
17 // @codeCoverageIgnoreStart
18 exit; // Exit if accessed directly.
19 // @codeCoverageIgnoreEnd
20 }
21
22 /**
23 * Post indexable class
24 */
25 class Post extends Indexable {
26
27 /**
28 * Indexable slug used for identification
29 *
30 * @var string
31 * @since 3.0
32 */
33 public $slug = 'post';
34
35 /**
36 * Flag to indicate if the indexable has support for
37 * `id_range` pagination method during a sync.
38 *
39 * @var boolean
40 * @since 4.1.0
41 */
42 public $support_indexing_advanced_pagination = true;
43
44 /**
45 * Create indexable and initialize dependencies
46 *
47 * @since 3.0
48 */
49 public function __construct() {
50 $this->labels = [
51 'plural' => esc_html__( 'Posts', 'elasticpress' ),
52 'singular' => esc_html__( 'Post', 'elasticpress' ),
53 ];
54
55 $this->sync_manager = new SyncManager( $this->slug );
56 $this->query_integration = new QueryIntegration( $this->slug );
57 }
58
59 /**
60 * Query database for posts
61 *
62 * @param array $args Query DB args
63 * @since 3.0
64 * @return array
65 */
66 public function query_db( $args ) {
67 $defaults = [
68 'posts_per_page' => $this->get_bulk_items_per_page(),
69 'post_type' => $this->get_indexable_post_types(),
70 'post_status' => $this->get_indexable_post_status(),
71 'offset' => 0,
72 'ignore_sticky_posts' => true,
73 'orderby' => 'ID',
74 'order' => 'desc',
75 'no_found_rows' => false,
76 'ep_indexing_advanced_pagination' => true,
77 'has_password' => false,
78 ];
79
80 if ( isset( $args['per_page'] ) ) {
81 $args['posts_per_page'] = $args['per_page'];
82 }
83
84 if ( isset( $args['include'] ) ) {
85 $args['post__in'] = $args['include'];
86 }
87
88 if ( isset( $args['exclude'] ) ) {
89 $args['post__not_in'] = $args['exclude'];
90 }
91
92 /**
93 * Filter arguments used to query posts from database
94 *
95 * @hook ep_post_query_db_args
96 * @param {array} $args Database arguments
97 * @return {array} New arguments
98 */
99 $args = apply_filters( 'ep_index_posts_args', apply_filters( 'ep_post_query_db_args', wp_parse_args( $args, $defaults ) ) );
100
101 if ( isset( $args['post__in'] ) || 0 < $args['offset'] ) {
102 // Disable advanced pagination. Not useful if only indexing specific IDs.
103 $args['ep_indexing_advanced_pagination'] = false;
104 }
105
106 // Enforce the following query args during advanced pagination to ensure things work correctly.
107 if ( $args['ep_indexing_advanced_pagination'] ) {
108 $args = array_merge(
109 $args,
110 [
111 'suppress_filters' => false,
112 'orderby' => 'ID',
113 'order' => 'DESC',
114 'paged' => 1,
115 'offset' => 0,
116 'no_found_rows' => true,
117 ]
118 );
119 add_filter( 'posts_where', array( $this, 'bulk_indexing_filter_posts_where' ), 9999, 2 );
120
121 $query = new WP_Query( $args );
122 $total_objects = $this->get_total_objects_for_query( $args );
123
124 remove_filter( 'posts_where', array( $this, 'bulk_indexing_filter_posts_where' ), 9999, 2 );
125 } else {
126 $query = new WP_Query( $args );
127 $total_objects = $query->found_posts;
128 }
129
130 return [
131 'objects' => $query->posts,
132 'total_objects' => $total_objects,
133 ];
134 }
135
136 /**
137 * Manipulate the WHERE clause of the bulk indexing query to paginate by ID in order to avoid performance issues with SQL offset.
138 *
139 * @param string $where The current $where clause.
140 * @param WP_Query $query WP_Query object.
141 * @return string WHERE clause with our pagination added if needed.
142 */
143 public function bulk_indexing_filter_posts_where( $where, $query ) {
144 $using_advanced_pagination = $query->get( 'ep_indexing_advanced_pagination', false );
145
146 if ( $using_advanced_pagination ) {
147 $requested_upper_limit_id = $query->get( 'ep_indexing_upper_limit_object_id', PHP_INT_MAX );
148 $requested_lower_limit_post_id = $query->get( 'ep_indexing_lower_limit_object_id', 0 );
149 $last_processed_id = $query->get( 'ep_indexing_last_processed_object_id', null );
150
151 // On the first loopthrough we begin with the requested upper limit ID. Afterwards, use the last processed ID to paginate.
152 $upper_limit_range_post_id = $requested_upper_limit_id;
153 if ( is_numeric( $last_processed_id ) ) {
154 $upper_limit_range_post_id = $last_processed_id - 1;
155 }
156
157 // Sanitize. Abort if unexpected data at this point.
158 if ( ! is_numeric( $upper_limit_range_post_id ) || ! is_numeric( $requested_lower_limit_post_id ) ) {
159 return $where;
160 }
161
162 $range = [
163 'upper_limit' => "{$GLOBALS['wpdb']->posts}.ID <= {$upper_limit_range_post_id}",
164 'lower_limit' => "{$GLOBALS['wpdb']->posts}.ID >= {$requested_lower_limit_post_id}",
165 ];
166
167 // Skip the end range if it's unnecessary.
168 $skip_ending_range = 0 === $requested_lower_limit_post_id;
169 $where = $skip_ending_range ? "AND {$range['upper_limit']} {$where}" : "AND {$range['upper_limit']} AND {$range['lower_limit']} {$where}";
170 }
171
172 return $where;
173 }
174
175 /**
176 * Get SQL_CALC_FOUND_ROWS for a specific query based on it's args.
177 *
178 * @param array $query_args The query args.
179 * @return int The query result's found_posts.
180 */
181 protected function get_total_objects_for_query( $query_args ) {
182 static $object_counts = [];
183
184 // Reset the pagination-related args for optimal caching.
185 $normalized_query_args = array_merge(
186 $query_args,
187 [
188 'offset' => 0,
189 'paged' => 1,
190 'posts_per_page' => 1,
191 'no_found_rows' => false,
192 'ep_indexing_last_processed_object_id' => null,
193 ]
194 );
195
196 $cache_key = md5( get_current_blog_id() . wp_json_encode( $normalized_query_args ) );
197
198 if ( ! isset( $object_counts[ $cache_key ] ) ) {
199 $object_counts[ $cache_key ] = ( new WP_Query( $normalized_query_args ) )->found_posts;
200 }
201
202 if ( 0 === $object_counts[ $cache_key ] ) {
203 // Do a DB count to make sure the query didn't just die and return 0.
204 $db_post_count = $this->get_total_objects_for_query_from_db( $normalized_query_args );
205
206 if ( $db_post_count !== $object_counts[ $cache_key ] ) {
207 $object_counts[ $cache_key ] = $db_post_count;
208 }
209 }
210
211 return $object_counts[ $cache_key ];
212 }
213
214 /**
215 * Get total posts from DB for a specific query based on it's args.
216 *
217 * @param array $query_args The query args.
218 * @since 4.0.0
219 * @return int The total posts.
220 */
221 protected function get_total_objects_for_query_from_db( $query_args ) {
222 $post_count = 0;
223
224 if ( ! isset( $query_args['post_type'] ) || isset( $query_args['ep_indexing_upper_limit_object_id'] )
225 || isset( $query_args['ep_indexing_lower_limit_object_id'] ) ) {
226 return $post_count;
227 }
228
229 foreach ( $query_args['post_type'] as $post_type ) {
230 $post_counts_by_post_status = wp_count_posts( $post_type );
231 foreach ( $post_counts_by_post_status as $post_status => $post_status_count ) {
232 if ( ! in_array( $post_status, $query_args['post_status'], true ) ) {
233 continue;
234 }
235 $post_count += $post_status_count;
236 }
237 }
238
239 return $post_count;
240 }
241
242 /**
243 * Returns indexable post types for the current site
244 *
245 * @since 0.9
246 * @return mixed|void
247 */
248 public function get_indexable_post_types() {
249 $post_types = get_post_types( array( 'public' => true ) );
250
251 /**
252 * Remove attachments by default
253 *
254 * @since 3.0
255 */
256 unset( $post_types['attachment'] );
257
258 /**
259 * Filter indexable post types
260 *
261 * @hook ep_indexable_post_types
262 * @param {array} $post_types Indexable post types
263 * @return {array} New post types
264 */
265 return apply_filters( 'ep_indexable_post_types', $post_types );
266 }
267
268 /**
269 * Return indexable post_status for the current site
270 *
271 * @since 1.3
272 * @return array
273 */
274 public function get_indexable_post_status() {
275 /**
276 * Filter indexable post statuses
277 *
278 * @hook ep_indexable_post_status
279 * @param {array} $post_statuses Indexable post statuses
280 * @return {array} New post statuses
281 */
282 return apply_filters( 'ep_indexable_post_status', array( 'publish' ) );
283 }
284
285 /**
286 * Determine required mapping file
287 *
288 * @since 3.6.2
289 * @return string
290 */
291 public function get_mapping_name() {
292 $es_version = Elasticsearch::factory()->get_elasticsearch_version();
293
294 if ( empty( $es_version ) ) {
295 /**
296 * Filter fallback Elasticsearch version
297 *
298 * @hook ep_fallback_elasticsearch_version
299 * @param {string} $version Fall back Elasticsearch version
300 * @return {string} New version
301 */
302 $es_version = apply_filters( 'ep_fallback_elasticsearch_version', '2.0' );
303 }
304
305 $mapping_file = '5-2.php';
306
307 if ( ! $es_version || version_compare( $es_version, '5.0' ) < 0 ) {
308 $mapping_file = 'pre-5-0.php';
309 } elseif ( version_compare( $es_version, '5.0', '>=' ) && version_compare( $es_version, '5.2', '<' ) ) {
310 $mapping_file = '5-0.php';
311 } elseif ( version_compare( $es_version, '5.2', '>=' ) && version_compare( $es_version, '7.0', '<' ) ) {
312 $mapping_file = '5-2.php';
313 } elseif ( version_compare( $es_version, '7.0', '>=' ) ) {
314 $mapping_file = '7-0.php';
315 }
316
317 return apply_filters( 'ep_post_mapping_version', $mapping_file );
318 }
319
320 /**
321 * Generate the mapping array
322 *
323 * @since 4.1.0
324 * @return array
325 */
326 public function generate_mapping() {
327 $mapping_file = $this->get_mapping_name();
328
329 /**
330 * Filter post indexable mapping file
331 *
332 * @hook ep_post_mapping_file
333 * @param {string} $file Path to file
334 * @return {string} New file path
335 */
336 $mapping = require apply_filters( 'ep_post_mapping_file', __DIR__ . '/../../../mappings/post/' . $mapping_file );
337
338 /**
339 * Filter post indexable mapping
340 *
341 * @hook ep_post_mapping
342 * @param {array} $mapping Mapping
343 * @return {array} New mapping
344 */
345 $mapping = apply_filters( 'ep_post_mapping', $mapping );
346
347 delete_transient( 'ep_post_mapping_version' );
348
349 return $mapping;
350 }
351
352 /**
353 * Determine version of mapping currently on the post index.
354 *
355 * @since 3.6.2
356 * @return string|WP_Error|false $version
357 */
358 public function determine_mapping_version() {
359 $version = get_transient( 'ep_post_mapping_version' );
360
361 if ( empty( $version ) ) {
362 $index = $this->get_index_name();
363 $mapping = Elasticsearch::factory()->get_mapping( $index );
364
365 if ( empty( $mapping ) ) {
366 return new \WP_Error( 'ep_failed_mapping_version', esc_html__( 'Error while fetching the mapping version.', 'elasticpress' ) );
367 }
368
369 if ( ! isset( $mapping[ $index ] ) ) {
370 return false;
371 }
372
373 $version = $this->determine_mapping_version_based_on_existing( $mapping, $index );
374
375 set_transient(
376 'ep_post_mapping_version',
377 $version,
378 /**
379 * Filter the post mapping version cache expiration.
380 *
381 * @hook ep_post_mapping_version_cache_expiration
382 * @since 3.6.5
383 * @param {int} $version Time in seconds for the transient expiration
384 * @return {int} New time
385 */
386 apply_filters( 'ep_post_mapping_version_cache_expiration', DAY_IN_SECONDS )
387 );
388 }
389
390 /**
391 * Filter the mapping version for posts.
392 *
393 * @hook ep_post_mapping_version_determined
394 * @since 3.6.2
395 * @param {string} $version Determined version string
396 * @return {string} New version string
397 */
398 return apply_filters( 'ep_post_mapping_version_determined', $version );
399 }
400
401 /**
402 * Prepare a post for syncing
403 *
404 * @param int $post_id Post ID.
405 * @since 0.9.1
406 * @return bool|array
407 */
408 public function prepare_document( $post_id ) {
409 $post = get_post( $post_id );
410
411 if ( empty( $post ) ) {
412 return false;
413 }
414
415 $user = get_userdata( $post->post_author );
416
417 if ( $user instanceof WP_User ) {
418 $user_data = array(
419 'raw' => $user->user_login,
420 'login' => $user->user_login,
421 'display_name' => $user->display_name,
422 'id' => $user->ID,
423 );
424 } else {
425 $user_data = array(
426 'raw' => '',
427 'login' => '',
428 'display_name' => '',
429 'id' => '',
430 );
431 }
432
433 $post_date = $post->post_date;
434 $post_date_gmt = $post->post_date_gmt;
435 $post_modified = $post->post_modified;
436 $post_modified_gmt = $post->post_modified_gmt;
437 $comment_count = absint( $post->comment_count );
438 $comment_status = $post->comment_status;
439 $ping_status = $post->ping_status;
440 $menu_order = absint( $post->menu_order );
441
442 /**
443 * Filter to ignore invalid dates
444 *
445 * @hook ep_ignore_invalid_dates
446 * @param {bool} $ignore True to ignore
447 * @param {int} $post_id Post ID
448 * @param {WP_Post} $post Post object
449 * @return {bool} New ignore value
450 */
451 if ( apply_filters( 'ep_ignore_invalid_dates', true, $post_id, $post ) ) {
452 if ( ! strtotime( $post_date ) || '0000-00-00 00:00:00' === $post_date ) {
453 $post_date = null;
454 }
455
456 if ( ! strtotime( $post_date_gmt ) || '0000-00-00 00:00:00' === $post_date_gmt ) {
457 $post_date_gmt = null;
458 }
459
460 if ( ! strtotime( $post_modified ) || '0000-00-00 00:00:00' === $post_modified ) {
461 $post_modified = null;
462 }
463
464 if ( ! strtotime( $post_modified_gmt ) || '0000-00-00 00:00:00' === $post_modified_gmt ) {
465 $post_modified_gmt = null;
466 }
467 }
468
469 // To prevent infinite loop, we don't queue when updated_postmeta.
470 remove_action( 'updated_postmeta', [ $this->sync_manager, 'action_queue_meta_sync' ], 10 );
471
472 /**
473 * Filter to allow indexing of filtered post content
474 *
475 * @hook ep_allow_post_content_filtered_index
476 * @param {bool} $ignore True to allow
477 * @return {bool} New value
478 */
479 $post_content_filtered_allowed = apply_filters( 'ep_allow_post_content_filtered_index', true );
480
481 $post_args = array(
482 'post_id' => $post_id,
483 'ID' => $post_id,
484 'post_author' => $user_data,
485 'post_date' => $post_date,
486 'post_date_gmt' => $post_date_gmt,
487 'post_title' => $post->post_title,
488 'post_excerpt' => $post->post_excerpt,
489 'post_content_filtered' => $post_content_filtered_allowed ? apply_filters( 'the_content', $post->post_content ) : '',
490 'post_content' => $post->post_content,
491 'post_status' => $post->post_status,
492 'post_name' => $post->post_name,
493 'post_modified' => $post_modified,
494 'post_modified_gmt' => $post_modified_gmt,
495 'post_parent' => $post->post_parent,
496 'post_type' => $post->post_type,
497 'post_mime_type' => $post->post_mime_type,
498 'permalink' => get_permalink( $post_id ),
499 'terms' => $this->prepare_terms( $post ),
500 'meta' => $this->prepare_meta_types( $this->prepare_meta( $post ) ), // post_meta removed in 2.4.
501 'date_terms' => $this->prepare_date_terms( $post_date ),
502 'comment_count' => $comment_count,
503 'comment_status' => $comment_status,
504 'ping_status' => $ping_status,
505 'menu_order' => $menu_order,
506 'guid' => $post->guid,
507 'thumbnail' => $this->prepare_thumbnail( $post ),
508 );
509
510 /**
511 * Filter sync arguments for a post. For backwards compatibility.
512 *
513 * @hook ep_post_sync_args
514 * @param {array} $post_args Post arguments
515 * @param {int} $post_id Post ID
516 * @return {array} New arguments
517 */
518 $post_args = apply_filters( 'ep_post_sync_args', $post_args, $post_id );
519
520 /**
521 * Filter sync arguments for a post after meta preparation.
522 *
523 * @hook ep_post_sync_args_post_prepare_meta
524 * @param {array} $post_args Post arguments
525 * @param {int} $post_id Post ID
526 * @return {array} New arguments
527 */
528 $post_args = apply_filters( 'ep_post_sync_args_post_prepare_meta', $post_args, $post_id );
529
530 // Turn back on updated_postmeta hook
531 add_action( 'updated_postmeta', [ $this->sync_manager, 'action_queue_meta_sync' ], 10, 4 );
532
533 return $post_args;
534 }
535
536 /**
537 * Prepare thumbnail to send to ES.
538 *
539 * @param WP_Post $post Post object.
540 * @return array|null Thumbnail data.
541 */
542 public function prepare_thumbnail( $post ) {
543 $attachment_id = get_post_thumbnail_id( $post );
544
545 if ( ! $attachment_id ) {
546 return null;
547 }
548
549 /**
550 * Filters the image size to use when indexing the post thumbnail.
551 *
552 * Defaults to the `woocommerce_thumbnail` size if WooCommerce is in
553 * use. Otherwise the `thumbnail` size is used.
554 *
555 * @hook ep_thumbnail_image_size
556 * @since 4.0.0
557 * @param {string|int[]} $image_size Image size. Can be any registered
558 * image size name, or an array of
559 * width and height values in pixels
560 * (in that order).
561 * @param {WP_Post} $post Post being indexed.
562 * @return {array} Image size to pass to wp_get_attachment_image_src().
563 */
564 $image_size = apply_filters(
565 'ep_post_thumbnail_image_size',
566 function_exists( 'WC' ) ? 'woocommerce_thumbnail' : 'thumbnail',
567 $post
568 );
569
570 $image_src = wp_get_attachment_image_src( $attachment_id, $image_size );
571 $image_alt = trim( wp_strip_all_tags( get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ) ) );
572
573 if ( ! $image_src ) {
574 return null;
575 }
576
577 return [
578 'ID' => $attachment_id,
579 'src' => $image_src[0],
580 'width' => $image_src[1],
581 'height' => $image_src[2],
582 'alt' => $image_alt,
583 ];
584 }
585
586 /**
587 * Prepare date terms to send to ES.
588 *
589 * @param string $date_to_prepare Post date
590 * @since 0.1.4
591 * @return array
592 */
593 public function prepare_date_terms( $date_to_prepare ) {
594 $terms_to_prepare = [
595 'year' => 'Y',
596 'month' => 'm',
597 'week' => 'W',
598 'dayofyear' => 'z',
599 'day' => 'd',
600 'dayofweek' => 'w',
601 'dayofweek_iso' => 'N',
602 'hour' => 'H',
603 'minute' => 'i',
604 'second' => 's',
605 'm' => 'Ym', // yearmonth
606 ];
607
608 // Combine all the date term formats and perform one single call to date_i18n() for performance.
609 $date_format = implode( '||', array_values( $terms_to_prepare ) );
610 $combined_dates = explode( '||', date_i18n( $date_format, strtotime( $date_to_prepare ) ) );
611
612 // Then split up the results for individual indexing.
613 $date_terms = [];
614 foreach ( $terms_to_prepare as $term_name => $date_format ) {
615 $index_in_combined_format = array_search( $term_name, array_keys( $terms_to_prepare ), true );
616 $date_terms[ $term_name ] = (int) $combined_dates[ $index_in_combined_format ];
617 }
618
619 return $date_terms;
620 }
621
622 /**
623 * Get an array of taxonomies that are indexable for the given post
624 *
625 * @since 4.0.0
626 * @param WP_Post $post Post object
627 * @return array Array of WP_Taxonomy objects that should be indexed
628 */
629 public function get_indexable_post_taxonomies( $post ) {
630 $taxonomies = get_object_taxonomies( $post->post_type, 'objects' );
631 $selected_taxonomies = [];
632
633 foreach ( $taxonomies as $taxonomy ) {
634 if ( $taxonomy->public || $taxonomy->publicly_queryable ) {
635 $selected_taxonomies[] = $taxonomy;
636 }
637 }
638
639 /**
640 * Filter taxonomies to be synced with post
641 *
642 * @hook ep_sync_taxonomies
643 * @param {array} $selected_taxonomies Selected taxonomies
644 * @param {WP_Post} Post object
645 * @return {array} New taxonomies
646 */
647 $selected_taxonomies = (array) apply_filters( 'ep_sync_taxonomies', $selected_taxonomies, $post );
648
649 // Important we validate here to ensure there are no invalid taxonomy values returned from the filter, as just one would cause wp_get_object_terms() to fail.
650 $validated_taxonomies = [];
651 foreach ( $selected_taxonomies as $selected_taxonomy ) {
652 // If we get a taxonomy name, we need to convert it to taxonomy object
653 if ( ! is_object( $selected_taxonomy ) && taxonomy_exists( (string) $selected_taxonomy ) ) {
654 $selected_taxonomy = get_taxonomy( $selected_taxonomy );
655 }
656
657 // We check if the $taxonomy object has a valid name property. Backward compatibility since WP_Taxonomy introduced in WP 4.7
658 if ( ! is_a( $selected_taxonomy, '\WP_Taxonomy' ) || ! property_exists( $selected_taxonomy, 'name' ) || ! taxonomy_exists( $selected_taxonomy->name ) ) {
659 continue;
660 }
661
662 $validated_taxonomies[] = $selected_taxonomy;
663 }
664
665 return $validated_taxonomies;
666 }
667
668 /**
669 * Prepare terms to send to ES.
670 *
671 * @param WP_Post $post Post object
672 * @since 0.1.0
673 * @return array
674 */
675 private function prepare_terms( $post ) {
676 $selected_taxonomies = $this->get_indexable_post_taxonomies( $post );
677
678 if ( empty( $selected_taxonomies ) ) {
679 return [];
680 }
681
682 $terms = [];
683
684 /**
685 * Filter to allow child terms to be indexed
686 *
687 * @hook ep_sync_terms_allow_hierarchy
688 * @param {bool} $allow True means allow
689 * @return {bool} New value
690 */
691 $allow_hierarchy = apply_filters( 'ep_sync_terms_allow_hierarchy', true );
692
693 foreach ( $selected_taxonomies as $taxonomy ) {
694 $object_terms = get_the_terms( $post->ID, $taxonomy->name );
695
696 if ( ! $object_terms || is_wp_error( $object_terms ) ) {
697 continue;
698 }
699
700 $terms_dic = [];
701
702 foreach ( $object_terms as $term ) {
703 if ( ! isset( $terms_dic[ $term->term_id ] ) ) {
704 $terms_dic[ $term->term_id ] = array(
705 'term_id' => $term->term_id,
706 'slug' => $term->slug,
707 'name' => $term->name,
708 'parent' => $term->parent,
709 'term_taxonomy_id' => $term->term_taxonomy_id,
710 'term_order' => (int) $this->get_term_order( $term->term_taxonomy_id, $post->ID ),
711 );
712
713 $terms_dic[ $term->term_id ]['facet'] = wp_json_encode( $terms_dic[ $term->term_id ] );
714
715 if ( $allow_hierarchy ) {
716 $terms_dic = $this->get_parent_terms( $terms_dic, $term, $taxonomy->name, $post->ID );
717 }
718 }
719 }
720 $terms[ $taxonomy->name ] = array_values( $terms_dic );
721 }
722
723 return $terms;
724 }
725
726 /**
727 * Recursively get all the ancestor terms of the given term
728 *
729 * @param array $terms Terms array
730 * @param WP_Term $term Current term
731 * @param string $tax_name Taxonomy
732 * @param int $object_id Post ID
733 *
734 * @return array
735 */
736 private function get_parent_terms( $terms, $term, $tax_name, $object_id ) {
737 $parent_term = get_term( $term->parent, $tax_name );
738 if ( ! $parent_term || is_wp_error( $parent_term ) ) {
739 return $terms;
740 }
741 if ( ! isset( $terms[ $parent_term->term_id ] ) ) {
742 $terms[ $parent_term->term_id ] = array(
743 'term_id' => $parent_term->term_id,
744 'slug' => $parent_term->slug,
745 'name' => $parent_term->name,
746 'parent' => $parent_term->parent,
747 'term_taxonomy_id' => $parent_term->term_taxonomy_id,
748 'term_order' => $this->get_term_order( $parent_term->term_taxonomy_id, $object_id ),
749 );
750
751 $terms[ $parent_term->term_id ]['facet'] = wp_json_encode( $terms[ $parent_term->term_id ] );
752
753 }
754 return $this->get_parent_terms( $terms, $parent_term, $tax_name, $object_id );
755 }
756
757 /**
758 * Retreives term order for the object/term_taxonomy_id combination
759 *
760 * @param int $term_taxonomy_id Term Taxonomy ID
761 * @param int $object_id Post ID
762 *
763 * @return int Term Order
764 */
765 protected function get_term_order( $term_taxonomy_id, $object_id ) {
766 global $wpdb;
767
768 $cache_key = "{$object_id}_term_order";
769 $term_orders = wp_cache_get( $cache_key );
770
771 if ( false === $term_orders ) {
772 $results = $wpdb->get_results(
773 $wpdb->prepare(
774 "SELECT term_taxonomy_id, term_order from $wpdb->term_relationships where object_id=%d;",
775 $object_id
776 ),
777 ARRAY_A
778 );
779
780 $term_orders = [];
781
782 foreach ( $results as $result ) {
783 $term_orders[ $result['term_taxonomy_id'] ] = $result['term_order'];
784 }
785
786 wp_cache_set( $cache_key, $term_orders );
787 }
788
789 return isset( $term_orders[ $term_taxonomy_id ] ) ? (int) $term_orders[ $term_taxonomy_id ] : 0;
790
791 }
792
793 /**
794 * Prepare post meta to send to ES
795 *
796 * @param WP_Post $post Post object
797 * @since 0.1.0
798 * @return array
799 */
800 public function prepare_meta( $post ) {
801 /**
802 * Filter pre-prepare meta for a post
803 *
804 * @hook ep_prepare_meta_data
805 * @param {array} $meta Meta data
806 * @param {WP_Post} $post Post object
807 * @return {array} New meta
808 */
809 $meta = apply_filters( 'ep_prepare_meta_data', (array) get_post_meta( $post->ID ), $post );
810
811 if ( empty( $meta ) ) {
812 /**
813 * Filter final list of prepared meta.
814 *
815 * @hook ep_prepared_post_meta
816 * @param {array} $prepared_meta Prepared meta
817 * @param {WP_Post} $post Post object
818 * @since 3.4
819 * @return {array} Prepared meta
820 */
821 return apply_filters( 'ep_prepared_post_meta', [], $post );
822 }
823
824 $prepared_meta = [];
825
826 /**
827 * Filter indexable protected meta keys for posts
828 *
829 * @hook ep_prepare_meta_allowed_protected_keys
830 * @param {array} $keys Allowed protected keys
831 * @param {WP_Post} $post Post object
832 * @since 1.7
833 * @return {array} New keys
834 */
835 $allowed_protected_keys = apply_filters( 'ep_prepare_meta_allowed_protected_keys', [], $post );
836
837 /**
838 * Filter public keys to exclude from indexed post
839 *
840 * @hook ep_prepare_meta_excluded_public_keys
841 * @param {array} $keys Excluded protected keys
842 * @param {WP_Post} $post Post object
843 * @since 1.7
844 * @return {array} New keys
845 */
846 $excluded_public_keys = apply_filters( 'ep_prepare_meta_excluded_public_keys', [], $post );
847
848 foreach ( $meta as $key => $value ) {
849
850 $allow_index = false;
851
852 if ( is_protected_meta( $key ) ) {
853
854 if ( true === $allowed_protected_keys || in_array( $key, $allowed_protected_keys, true ) ) {
855 $allow_index = true;
856 }
857 } else {
858
859 if ( true !== $excluded_public_keys && ! in_array( $key, $excluded_public_keys, true ) ) {
860 $allow_index = true;
861 }
862 }
863
864 /**
865 * Filter force whitelisting a meta key
866 *
867 * @hook ep_prepare_meta_whitelist_key
868 * @param {bool} $whitelist True to whitelist key
869 * @param {string} $key Meta key
870 * @param {WP_Post} $post Post object
871 * @return {bool} New whitelist value
872 */
873 if ( true === $allow_index || apply_filters( 'ep_prepare_meta_whitelist_key', false, $key, $post ) ) {
874 $prepared_meta[ $key ] = maybe_unserialize( $value );
875 }
876 }
877
878 /**
879 * Filter final list of prepared meta.
880 *
881 * @hook ep_prepared_post_meta
882 * @param {array} $prepared_meta Prepared meta
883 * @param {WP_Post} $post Post object
884 * @since 3.4
885 * @return {array} Prepared meta
886 */
887 return apply_filters( 'ep_prepared_post_meta', $prepared_meta, $post );
888
889 }
890
891 /**
892 * Format WP query args for ES
893 *
894 * @param array $args WP_Query arguments.
895 * @param WP_Query $wp_query WP_Query object
896 * @since 0.9.0
897 * @return array
898 */
899 public function format_args( $args, $wp_query ) {
900 if ( ! empty( $args['posts_per_page'] ) ) {
901 $posts_per_page = (int) $args['posts_per_page'];
902
903 // ES have a maximum size allowed so we have to convert "-1" to a maximum size.
904 if ( -1 === $posts_per_page ) {
905 /**
906 * Set the maximum results window size.
907 *
908 * The request will return a HTTP 500 Internal Error if the size of the
909 * request is larger than the [index.max_result_window] parameter in ES.
910 * See the scroll api for a more efficient way to request large data sets.
911 *
912 * @return int The max results window size.
913 *
914 * @since 2.3.0
915 */
916
917 /**
918 * Filter max result size if set to -1
919 *
920 * @hook ep_max_results_window
921 * @param {int} Max result window
922 * @return {int} New window
923 */
924 $posts_per_page = apply_filters( 'ep_max_results_window', 10000 );
925 }
926 } else {
927 $posts_per_page = (int) get_option( 'posts_per_page' );
928 }
929
930 $formatted_args = array(
931 'from' => 0,
932 'size' => $posts_per_page,
933 );
934
935 /**
936 * Order and Orderby arguments
937 *
938 * Used for how Elasticsearch will sort results
939 *
940 * @since 1.1
941 */
942
943 // Set sort order, default is 'desc'.
944 if ( ! empty( $args['order'] ) ) {
945 $order = $this->parse_order( $args['order'] );
946 } else {
947 $order = 'desc';
948 }
949
950 // Default sort for non-searches to date.
951 if ( empty( $args['orderby'] ) && ( ! isset( $args['s'] ) || '' === $args['s'] ) ) {
952 /**
953 * Filter default post query order by
954 *
955 * @hook ep_set_default_sort
956 * @param {string} $sort Default sort
957 * @param {string $order Order direction
958 * @return {string} New default
959 */
960 $args['orderby'] = apply_filters( 'ep_set_default_sort', 'date', $order );
961 }
962
963 // Set sort type.
964 if ( ! empty( $args['orderby'] ) ) {
965 $formatted_args['sort'] = $this->parse_orderby( $args['orderby'], $order, $args );
966 } else {
967 // Default sort is to use the score (based on relevance).
968 $default_sort = array(
969 array(
970 '_score' => array(
971 'order' => $order,
972 ),
973 ),
974 );
975
976 /**
977 * Filter the ES query order (`sort` clause)
978 *
979 * This filter is used in searches if `orderby` is not set in the WP_Query args.
980 * The default value is:
981 *
982 * $default_sort = array(
983 * array(
984 * '_score' => array(
985 * 'order' => $order,
986 * ),
987 * ),
988 * );
989 *
990 * @hook ep_set_sort
991 * @since 3.6.3
992 * @param {array} $sort Default sort.
993 * @param {string} $order Order direction
994 * @return {array} New default
995 */
996 $default_sort = apply_filters( 'ep_set_sort', $default_sort, $order );
997
998 $formatted_args['sort'] = $default_sort;
999 }
1000
1001 $filter = array(
1002 'bool' => array(
1003 'must' => [],
1004 ),
1005 );
1006 $use_filters = false;
1007
1008 // Sanitize array query args. Elasticsearch will error if a terms query contains empty items like an
1009 // empty string.
1010 $keys_to_sanitize = [
1011 'author__in',
1012 'author__not_in',
1013 'category__and',
1014 'category__in',
1015 'category__not_in',
1016 'tag__and',
1017 'tag__in',
1018 'tag__not_in',
1019 'tag_slug__and',
1020 'tag_slug__in',
1021 'post_parent__in',
1022 'post_parent__not_in',
1023 'post__in',
1024 'post__not_in',
1025 'post_name__in',
1026 ];
1027 foreach ( $keys_to_sanitize as $key ) {
1028 if ( ! isset( $args[ $key ] ) ) {
1029 continue;
1030 }
1031 $args[ $key ] = array_filter( (array) $args[ $key ] );
1032 }
1033
1034 /**
1035 * Tax Query support
1036 *
1037 * Support for the tax_query argument of WP_Query. Currently only provides support for the 'AND' relation
1038 * between taxonomies. Field only supports slug, term_id, and name defaulting to term_id.
1039 *
1040 * @use field = slug
1041 * terms array
1042 * @since 0.9.1
1043 */
1044 if ( ! empty( $wp_query->tax_query ) && ! empty( $wp_query->tax_query->queries ) ) {
1045 $args['tax_query'] = $wp_query->tax_query->queries;
1046 }
1047
1048 if ( ! empty( $args['tax_query'] ) ) {
1049 // Main tax_query array for ES.
1050 $es_tax_query = [];
1051
1052 $tax_queries = $this->parse_tax_query( $args['tax_query'] );
1053
1054 if ( ! empty( $tax_queries['tax_filter'] ) ) {
1055 $relation = 'must';
1056
1057 if ( ! empty( $args['tax_query']['relation'] ) && 'or' === strtolower( $args['tax_query']['relation'] ) ) {
1058 $relation = 'should';
1059 }
1060
1061 $es_tax_query[ $relation ] = $tax_queries['tax_filter'];
1062 }
1063
1064 if ( ! empty( $tax_queries['tax_must_not_filter'] ) ) {
1065 $es_tax_query['must_not'] = $tax_queries['tax_must_not_filter'];
1066 }
1067
1068 if ( ! empty( $es_tax_query ) ) {
1069 $filter['bool']['must'][]['bool'] = $es_tax_query;
1070 }
1071
1072 $use_filters = true;
1073 }
1074
1075 /**
1076 * 'post_parent' arg support.
1077 *
1078 * @since 2.0
1079 */
1080 if ( isset( $args['post_parent'] ) && '' !== $args['post_parent'] && 'any' !== strtolower( $args['post_parent'] ) ) {
1081 $filter['bool']['must'][]['bool']['must'] = array(
1082 'term' => array(
1083 'post_parent' => $args['post_parent'],
1084 ),
1085 );
1086
1087 $use_filters = true;
1088 }
1089
1090 /**
1091 * 'post__in' arg support.
1092 *
1093 * @since x.x
1094 */
1095 if ( ! empty( $args['post__in'] ) ) {
1096 $filter['bool']['must'][]['bool']['must'] = array(
1097 'terms' => array(
1098 'post_id' => array_values( (array) $args['post__in'] ),
1099 ),
1100 );
1101
1102 $use_filters = true;
1103 }
1104
1105 /**
1106 * 'post_name__in' arg support.
1107 *
1108 * @since 3.6.0
1109 */
1110 if ( ! empty( $args['post_name__in'] ) ) {
1111 $filter['bool']['must'][]['bool']['must'] = array(
1112 'terms' => array(
1113 'post_name.raw' => array_values( (array) $args['post_name__in'] ),
1114 ),
1115 );
1116
1117 $use_filters = true;
1118 }
1119
1120 /**
1121 * 'post__not_in' arg support.
1122 *
1123 * @since x.x
1124 */
1125 if ( ! empty( $args['post__not_in'] ) ) {
1126 $filter['bool']['must'][]['bool']['must_not'] = array(
1127 'terms' => array(
1128 'post_id' => (array) $args['post__not_in'],
1129 ),
1130 );
1131
1132 $use_filters = true;
1133 }
1134
1135 /**
1136 * 'category__not_in' arg support.
1137 *
1138 * @since 3.6.0
1139 */
1140 if ( ! empty( $args['category__not_in'] ) ) {
1141 $filter['bool']['must'][]['bool']['must_not'] = array(
1142 'terms' => array(
1143 'terms.category.term_id' => array_values( (array) $args['category__not_in'] ),
1144 ),
1145 );
1146
1147 $use_filters = true;
1148 }
1149
1150 /**
1151 * 'tag__not_in' arg support.
1152 *
1153 * @since 3.6.0
1154 */
1155 if ( ! empty( $args['tag__not_in'] ) ) {
1156 $filter['bool']['must'][]['bool']['must_not'] = array(
1157 'terms' => array(
1158 'terms.post_tag.term_id' => array_values( (array) $args['tag__not_in'] ),
1159 ),
1160 );
1161
1162 $use_filters = true;
1163 }
1164
1165 /**
1166 * Author query support
1167 *
1168 * @since 1.0
1169 */
1170 if ( ! empty( $args['author'] ) ) {
1171 $filter['bool']['must'][] = array(
1172 'term' => array(
1173 'post_author.id' => $args['author'],
1174 ),
1175 );
1176
1177 $use_filters = true;
1178 } elseif ( ! empty( $args['author_name'] ) ) {
1179 // Since this was set to use the display name initially, there might be some code that used this feature.
1180 // Let's ensure that any query vars coming in using author_name are in fact slugs.
1181 // This was changed back in ticket #1622 to use the display name, so we removed the sanitize_user() call.
1182 $filter['bool']['must'][] = array(
1183 'term' => array(
1184 'post_author.display_name' => $args['author_name'],
1185 ),
1186 );
1187
1188 $use_filters = true;
1189 } elseif ( ! empty( $args['author__in'] ) ) {
1190 $filter['bool']['must'][]['bool']['must'] = array(
1191 'terms' => array(
1192 'post_author.id' => array_values( (array) $args['author__in'] ),
1193 ),
1194 );
1195
1196 $use_filters = true;
1197 } elseif ( ! empty( $args['author__not_in'] ) ) {
1198 $filter['bool']['must'][]['bool']['must_not'] = array(
1199 'terms' => array(
1200 'post_author.id' => array_values( (array) $args['author__not_in'] ),
1201 ),
1202 );
1203
1204 $use_filters = true;
1205 }
1206
1207 /**
1208 * Add support for post_mime_type
1209 *
1210 * If we have array, it will be fool text search filter.
1211 * If we have string(like filter images in media screen), we will have mime type "image" so need to check it as
1212 * regexp filter.
1213 *
1214 * @since 2.3
1215 */
1216 if ( ! empty( $args['post_mime_type'] ) ) {
1217 if ( is_array( $args['post_mime_type'] ) ) {
1218
1219 $args_post_mime_type = [];
1220
1221 foreach ( $args['post_mime_type'] as $mime_type ) {
1222 /**
1223 * check if matches the MIME type pattern: type/subtype and
1224 * leave an empty string as posts, pages and CPTs don't have a MIME type
1225 */
1226 if ( preg_match( '/^[-._a-z0-9]+\/[-._a-z0-9]+$/i', $mime_type ) || empty( $mime_type ) ) {
1227 $args_post_mime_type[] = $mime_type;
1228 } else {
1229 $filtered_mime_type_by_type = wp_match_mime_types( $mime_type, wp_get_mime_types() );
1230
1231 $args_post_mime_type = array_merge( $args_post_mime_type, $filtered_mime_type_by_type[ $mime_type ] );
1232 }
1233 }
1234
1235 $filter['bool']['must'][] = array(
1236 'terms' => array(
1237 'post_mime_type' => $args_post_mime_type,
1238 ),
1239 );
1240
1241 $use_filters = true;
1242 } elseif ( is_string( $args['post_mime_type'] ) ) {
1243 $filter['bool']['must'][] = array(
1244 'regexp' => array(
1245 'post_mime_type' => $args['post_mime_type'] . '.*',
1246 ),
1247 );
1248
1249 $use_filters = true;
1250 }
1251 }
1252
1253 /**
1254 * Simple date params support
1255 *
1256 * @since 1.3
1257 */
1258 $date_filter = DateQuery::simple_es_date_filter( $args );
1259
1260 if ( ! empty( $date_filter ) ) {
1261 $filter['bool']['must'][] = $date_filter;
1262 $use_filters = true;
1263 }
1264
1265 /**
1266 * 'date_query' arg support.
1267 */
1268 if ( ! empty( $args['date_query'] ) ) {
1269
1270 $date_query = new DateQuery( $args['date_query'] );
1271
1272 $date_filter = $date_query->get_es_filter();
1273
1274 if ( array_key_exists( 'and', $date_filter ) ) {
1275 $filter['bool']['must'][] = $date_filter['and'];
1276 $use_filters = true;
1277 }
1278 }
1279
1280 $meta_queries = [];
1281
1282 /**
1283 * Support `meta_key`, `meta_value`, `meta_value_num`, and `meta_compare` query args
1284 */
1285 if ( ! empty( $args['meta_key'] ) ) {
1286 $meta_query_array = [
1287 'key' => $args['meta_key'],
1288 ];
1289
1290 if ( isset( $args['meta_value'] ) && '' !== $args['meta_value'] ) {
1291 $meta_query_array['value'] = $args['meta_value'];
1292 } elseif ( isset( $args['meta_value_num'] ) && '' !== $args['meta_value_num'] ) {
1293 $meta_query_array['value'] = $args['meta_value_num'];
1294 }
1295
1296 if ( isset( $args['meta_compare'] ) ) {
1297 $meta_query_array['compare'] = $args['meta_compare'];
1298 }
1299
1300 $meta_queries[] = $meta_query_array;
1301 }
1302
1303 /**
1304 * Todo: Support meta_type
1305 */
1306
1307 /**
1308 * 'meta_query' arg support.
1309 *
1310 * Relation supports 'AND' and 'OR'. 'AND' is the default. For each individual query, the
1311 * following 'compare' values are supported: =, !=, EXISTS, NOT EXISTS. '=' is the default.
1312 *
1313 * @since 1.3
1314 */
1315 if ( ! empty( $args['meta_query'] ) ) {
1316 $meta_queries = array_merge( $meta_queries, $args['meta_query'] );
1317 }
1318
1319 if ( ! empty( $meta_queries ) ) {
1320
1321 $relation = 'must';
1322 if ( ! empty( $args['meta_query'] ) && ! empty( $args['meta_query']['relation'] ) && 'or' === strtolower( $args['meta_query']['relation'] ) ) {
1323 $relation = 'should';
1324 }
1325
1326 // get meta query filter
1327 $meta_filter = $this->build_meta_query( $meta_queries );
1328
1329 if ( ! empty( $meta_filter ) ) {
1330 $filter['bool']['must'][] = $meta_filter;
1331
1332 $use_filters = true;
1333 }
1334 }
1335
1336 /**
1337 * Allow for search field specification
1338 *
1339 * @since 1.0
1340 */
1341 if ( ! empty( $args['search_fields'] ) ) {
1342 $search_field_args = $args['search_fields'];
1343 $search_fields = [];
1344
1345 if ( ! empty( $search_field_args['taxonomies'] ) ) {
1346 $taxes = (array) $search_field_args['taxonomies'];
1347
1348 foreach ( $taxes as $tax ) {
1349 $search_fields[] = 'terms.' . $tax . '.name';
1350 }
1351
1352 unset( $search_field_args['taxonomies'] );
1353 }
1354
1355 if ( ! empty( $search_field_args['meta'] ) ) {
1356 $metas = (array) $search_field_args['meta'];
1357
1358 foreach ( $metas as $meta ) {
1359 $search_fields[] = 'meta.' . $meta . '.value';
1360 }
1361
1362 unset( $search_field_args['meta'] );
1363 }
1364
1365 if ( in_array( 'author_name', $search_field_args, true ) ) {
1366 $search_fields[] = 'post_author.login';
1367
1368 $author_name_index = array_search( 'author_name', $search_field_args, true );
1369 unset( $search_field_args[ $author_name_index ] );
1370 }
1371
1372 $search_fields = array_merge( $search_field_args, $search_fields );
1373 } else {
1374 $search_fields = array(
1375 'post_title',
1376 'post_excerpt',
1377 'post_content',
1378 );
1379 }
1380
1381 /**
1382 * Filter default post search fields
1383 *
1384 * If you are using the weighting engine, this filter should not be used.
1385 * Instead, you should use the ep_weighting_configuration_for_search filter.
1386 *
1387 * @hook ep_search_fields
1388 * @param {array} $search_fields Default search fields
1389 * @param {array} $args WP Query arguments
1390 * @return {array} New defaults
1391 */
1392 $search_fields = apply_filters( 'ep_search_fields', $search_fields, $args );
1393
1394 $default_algorithm_version = '4.0';
1395 if ( defined( 'EP_IS_NETWORK' ) && EP_IS_NETWORK ) {
1396 $search_algorithm_version_option = get_site_option( 'ep_search_algorithm_version', $default_algorithm_version );
1397 } else {
1398 $search_algorithm_version_option = get_option( 'ep_search_algorithm_version', $default_algorithm_version );
1399 }
1400
1401 /**
1402 * Filter the algorithm version to be used.
1403 *
1404 * @since 3.5
1405 * @hook ep_search_algorithm_version
1406 * @param {string} $search_algorithm_version Algorithm version.
1407 * @return {string} New algorithm version
1408 */
1409 $search_algorithm_version = apply_filters( 'ep_search_algorithm_version', $search_algorithm_version_option );
1410
1411 $search_text = ( ! empty( $args['s'] ) ) ? $args['s'] : '';
1412
1413 if ( '4.0' === $search_algorithm_version ) {
1414 $query = array(
1415 'bool' => array(
1416 'should' => array(
1417 array(
1418 'multi_match' => array(
1419 'query' => $search_text,
1420 'type' => 'phrase',
1421 'fields' => $search_fields,
1422 /**
1423 * Filter boost for post match phrase query
1424 *
1425 * @hook ep_match_phrase_boost
1426 * @param {int} $boost Phrase boost
1427 * @param {array} $prepared_search_fields Search fields
1428 * @param {array} $query_vars Query variables
1429 * @return {int} New phrase boost
1430 */
1431 'boost' => apply_filters( 'ep_match_phrase_boost', 3, $search_fields, $args ),
1432 ),
1433 ),
1434 array(
1435 'multi_match' => array(
1436 'query' => $search_text,
1437 'fields' => $search_fields,
1438 /**
1439 * Filter boost for post match query
1440 *
1441 * @hook ep_match_boost
1442 * @param {int} $boost Boost
1443 * @param {array} $prepared_search_fields Search fields
1444 * @param {array} $query_vars Query variables
1445 * @return {int} New boost
1446 */
1447 'boost' => apply_filters( 'ep_match_boost', 1, $search_fields, $args ),
1448 /**
1449 * Filter fuzziness for post match query
1450 *
1451 * @hook ep_match_fuzziness
1452 * @since 4.0.0
1453 * @param {string|int} $fuzziness Fuzziness
1454 * @param {array} $prepared_search_fields Search fields
1455 * @param {array} $query_vars Query variables
1456 * @return {string} New boost
1457 */
1458 'fuzziness' => apply_filters( 'ep_match_fuzziness', 'auto', $search_fields, $args ),
1459 'operator' => 'and',
1460 ),
1461 ),
1462 array(
1463 'multi_match' => [
1464 'query' => $search_text,
1465 'type' => 'cross_fields',
1466 'fields' => $search_fields,
1467 /**
1468 * Filter boost for post match query
1469 *
1470 * @hook ep_match_cross_fields_boost
1471 * @since 4.0.0
1472 * @param {int} $boost Boost
1473 * @param {array} $prepared_search_fields Search fields
1474 * @param {array} $query_vars Query variables
1475 * @return {int} New boost
1476 */
1477 'boost' => apply_filters( 'ep_match_cross_fields_boost', 1, $search_fields, $args ),
1478 'analyzer' => 'standard',
1479 'tie_breaker' => 0.5,
1480 'operator' => 'and',
1481 ],
1482 ),
1483 ),
1484 ),
1485 );
1486 } elseif ( '3.5' === $search_algorithm_version ) {
1487 $query = array(
1488 'bool' => array(
1489 'should' => array(
1490 array(
1491 'multi_match' => array(
1492 'query' => $search_text,
1493 'type' => 'phrase',
1494 'fields' => $search_fields,
1495 /**
1496 * Filter boost for post match phrase query
1497 *
1498 * @hook ep_match_phrase_boost
1499 * @param {int} $boost Phrase boost
1500 * @param {array} $prepared_search_fields Search fields
1501 * @param {array} $query_vars Query variables
1502 * @return {int} New phrase boost
1503 */
1504 'boost' => apply_filters( 'ep_match_phrase_boost', 3, $search_fields, $args ),
1505 ),
1506 ),
1507 array(
1508 'multi_match' => array(
1509 'query' => $search_text,
1510 'fields' => $search_fields,
1511 'type' => 'phrase',
1512 'slop' => 5,
1513 ),
1514 ),
1515 ),
1516 ),
1517 );
1518 } else {
1519 $query = array(
1520 'bool' => array(
1521 'should' => array(
1522 array(
1523 'multi_match' => array(
1524 'query' => $search_text,
1525 'type' => 'phrase',
1526 'fields' => $search_fields,
1527 /**
1528 * Filter boost for post match phrase query
1529 *
1530 * @hook ep_match_phrase_boost
1531 * @param {int} $boost Phrase boost
1532 * @param {array} $prepared_search_fields Search fields
1533 * @param {array} $query_vars Query variables
1534 * @return {int} New phrase boost
1535 */
1536 'boost' => apply_filters( 'ep_match_phrase_boost', 4, $search_fields, $args ),
1537 ),
1538 ),
1539 array(
1540 'multi_match' => array(
1541 'query' => $search_text,
1542 'fields' => $search_fields,
1543 /**
1544 * Filter boost for post match query
1545 *
1546 * @hook ep_match_boost
1547 * @param {int} $boost Boost
1548 * @param {array} $prepared_search_fields Search fields
1549 * @param {array} $query_vars Query variables
1550 * @return {int} New boost
1551 */
1552 'boost' => apply_filters( 'ep_match_boost', 2, $search_fields, $args ),
1553 'fuzziness' => 0,
1554 'operator' => 'and',
1555 ),
1556 ),
1557 array(
1558 'multi_match' => array(
1559 'query' => $search_text,
1560 'fields' => $search_fields,
1561 /**
1562 * Filter fuzziness for post query
1563 *
1564 * @hook ep_fuzziness_arg
1565 * @param {int} $fuzziness Fuzziness
1566 * @param {array} $prepared_search_fields Search fields
1567 * @param {array} $query_vars Query variables
1568 * @return {int} New fuzziness
1569 */
1570 'fuzziness' => apply_filters( 'ep_fuzziness_arg', 1, $search_fields, $args ),
1571 ),
1572 ),
1573 ),
1574 ),
1575 );
1576 }
1577
1578 /**
1579 * We are using ep_integrate instead of ep_match_all. ep_match_all will be
1580 * supported for legacy code but may be deprecated and removed eventually.
1581 *
1582 * @since 1.3
1583 */
1584
1585 if ( ! empty( $args['s'] ) ) {
1586 add_filter( 'ep_formatted_args_query', [ $this, 'adjust_query_fuzziness' ], 100, 4 );
1587
1588 /**
1589 * Filter formatted Elasticsearch post query (only contains query part)
1590 *
1591 * @hook ep_formatted_args_query
1592 * @param {array} $query Current query
1593 * @param {array} $query_vars Query variables
1594 * @param {string} $search_text Search text
1595 * @param {array} $search_fields Search fields
1596 * @return {array} New query
1597 *
1598 * @since 3.5.5 $search_text and $search_fields parameters added.
1599 */
1600 $formatted_args['query'] = apply_filters(
1601 'ep_formatted_args_query',
1602 $query,
1603 $args,
1604 $search_text,
1605 $search_fields
1606 );
1607 } elseif ( ! empty( $args['ep_match_all'] ) || ! empty( $args['ep_integrate'] ) ) {
1608 $formatted_args['query']['match_all'] = array(
1609 'boost' => 1,
1610 );
1611 }
1612
1613 /**
1614 * Order by 'rand' support
1615 *
1616 * Ref: https://github.com/elastic/elasticsearch/issues/1170
1617 */
1618 if ( ! empty( $args['orderby'] ) ) {
1619 $orderbys = $this->get_orderby_array( $args['orderby'] );
1620 if ( in_array( 'rand', $orderbys, true ) ) {
1621 $formatted_args_query = $formatted_args['query'];
1622 $formatted_args['query'] = [];
1623 $formatted_args['query']['function_score']['query'] = $formatted_args_query;
1624 $formatted_args['query']['function_score']['random_score'] = (object) [];
1625 }
1626 }
1627
1628 /**
1629 * Sticky posts support
1630 */
1631
1632 // Check first if there's sticky posts and show them only in the front page
1633 $sticky_posts = get_option( 'sticky_posts' );
1634 $sticky_posts = ( is_array( $sticky_posts ) && empty( $sticky_posts ) ) ? false : $sticky_posts;
1635
1636 /**
1637 * Filter whether to enable sticky posts for this request
1638 *
1639 * @hook ep_enable_sticky_posts
1640 *
1641 * @param {bool} $allow Allow sticky posts for this request
1642 * @param {array} $args Query variables
1643 * @param {array} $formatted_args EP formatted args
1644 *
1645 * @return {bool} $allow
1646 */
1647 $enable_sticky_posts = apply_filters( 'ep_enable_sticky_posts', is_home(), $args, $formatted_args );
1648
1649 if ( false !== $sticky_posts
1650 && $enable_sticky_posts
1651 && empty( $args['s'] )
1652 && in_array( $args['ignore_sticky_posts'], array( 'false', 0, false ), true ) ) {
1653 $new_sort = [
1654 [
1655 '_score' => [
1656 'order' => 'desc',
1657 ],
1658 ],
1659 ];
1660
1661 $formatted_args['sort'] = array_merge( $new_sort, $formatted_args['sort'] );
1662
1663 $formatted_args_query = $formatted_args['query'];
1664 $formatted_args['query'] = array();
1665 $formatted_args['query']['function_score']['query'] = $formatted_args_query;
1666 $formatted_args['query']['function_score']['functions'] = array(
1667 // add extra weight to sticky posts to show them on top
1668 (object) array(
1669 'filter' => array(
1670 'terms' => array( '_id' => $sticky_posts ),
1671 ),
1672 'weight' => 20,
1673 ),
1674 );
1675 }
1676
1677 /**
1678 * If not set default to post. If search and not set, default to "any".
1679 */
1680 if ( ! empty( $args['post_type'] ) ) {
1681 // should NEVER be "any" but just in case
1682 if ( 'any' !== $args['post_type'] ) {
1683 $post_types = (array) $args['post_type'];
1684 $terms_map_name = 'terms';
1685
1686 $filter['bool']['must'][] = array(
1687 $terms_map_name => array(
1688 'post_type.raw' => array_values( $post_types ),
1689 ),
1690 );
1691
1692 $use_filters = true;
1693 }
1694 } elseif ( empty( $args['s'] ) ) {
1695 $filter['bool']['must'][] = array(
1696 'term' => array(
1697 'post_type.raw' => 'post',
1698 ),
1699 );
1700
1701 $use_filters = true;
1702 }
1703
1704 /**
1705 * Like WP_Query in search context, if no post_status is specified we default to "any". To
1706 * be safe you should ALWAYS specify the post_status parameter UNLIKE with WP_Query.
1707 *
1708 * @since 2.1
1709 */
1710 if ( ! empty( $args['post_status'] ) ) {
1711 // should NEVER be "any" but just in case
1712 if ( 'any' !== $args['post_status'] ) {
1713 $post_status = (array) ( is_string( $args['post_status'] ) ? explode( ',', $args['post_status'] ) : $args['post_status'] );
1714 $post_status = array_map( 'trim', $post_status );
1715 $terms_map_name = 'terms';
1716 if ( count( $post_status ) < 2 ) {
1717 $terms_map_name = 'term';
1718 $post_status = $post_status[0];
1719 }
1720
1721 $filter['bool']['must'][] = array(
1722 $terms_map_name => array(
1723 'post_status' => $post_status,
1724 ),
1725 );
1726
1727 $use_filters = true;
1728 }
1729 } else {
1730 $statuses = get_post_stati( array( 'public' => true ) );
1731
1732 if ( is_admin() ) {
1733 /**
1734 * In the admin we will add protected and private post statuses to the default query
1735 * per WP default behavior.
1736 */
1737 $statuses = array_merge(
1738 $statuses,
1739 get_post_stati(
1740 array(
1741 'protected' => true,
1742 'show_in_admin_all_list' => true,
1743 )
1744 )
1745 );
1746
1747 if ( is_user_logged_in() ) {
1748 $statuses = array_merge( $statuses, get_post_stati( array( 'private' => true ) ) );
1749 }
1750 }
1751
1752 $statuses = array_values( $statuses );
1753
1754 $post_status_filter_type = 'terms';
1755
1756 $filter['bool']['must'][] = array(
1757 $post_status_filter_type => array(
1758 'post_status' => $statuses,
1759 ),
1760 );
1761
1762 $use_filters = true;
1763 }
1764
1765 if ( isset( $args['offset'] ) ) {
1766 $formatted_args['from'] = (int) $args['offset'];
1767 }
1768
1769 if ( isset( $args['paged'] ) && $args['paged'] > 1 ) {
1770 $formatted_args['from'] = $args['posts_per_page'] * ( $args['paged'] - 1 );
1771 }
1772
1773 /**
1774 * Fix negative offset. This happens, for example, on hierarchical post types.
1775 *
1776 * Ref: https://github.com/10up/ElasticPress/issues/2480
1777 */
1778 if ( $formatted_args['from'] < 0 ) {
1779 $formatted_args['from'] = 0;
1780 }
1781
1782 if ( $use_filters ) {
1783 $formatted_args['post_filter'] = $filter;
1784 }
1785
1786 /**
1787 * Support fields.
1788 */
1789 if ( isset( $args['fields'] ) ) {
1790 switch ( $args['fields'] ) {
1791 case 'ids':
1792 $formatted_args['_source'] = array(
1793 'includes' => array(
1794 'post_id',
1795 ),
1796 );
1797 break;
1798
1799 case 'id=>parent':
1800 $formatted_args['_source'] = array(
1801 'includes' => array(
1802 'post_id',
1803 'post_parent',
1804 ),
1805 );
1806 break;
1807 }
1808 }
1809
1810 /**
1811 * Aggregations
1812 */
1813 if ( ! empty( $args['aggs'] ) && is_array( $args['aggs'] ) ) {
1814 // Check if the array indexes are all numeric.
1815 $agg_keys = array_keys( $args['aggs'] );
1816 $agg_num_keys = array_filter( $agg_keys, 'is_int' );
1817 $has_only_num_keys = count( $agg_num_keys ) === count( $args['aggs'] );
1818
1819 if ( $has_only_num_keys ) {
1820 foreach ( $args['aggs'] as $agg ) {
1821 $formatted_args = $this->apply_aggregations( $formatted_args, $agg, $use_filters, $filter );
1822 }
1823 } else {
1824 // Single aggregation.
1825 $formatted_args = $this->apply_aggregations( $formatted_args, $args['aggs'], $use_filters, $filter );
1826 }
1827 }
1828
1829 /**
1830 * Filter formatted Elasticsearch [ost ]query (entire query)
1831 *
1832 * @hook ep_formatted_args
1833 * @param {array} $formatted_args Formatted Elasticsearch query
1834 * @param {array} $query_vars Query variables
1835 * @param {array} $query Query part
1836 * @return {array} New query
1837 */
1838 $formatted_args = apply_filters( 'ep_formatted_args', $formatted_args, $args, $wp_query );
1839
1840 /**
1841 * Filter formatted Elasticsearch [ost ]query (entire query)
1842 *
1843 * @hook ep_post_formatted_args
1844 * @param {array} $formatted_args Formatted Elasticsearch query
1845 * @param {array} $query_vars Query variables
1846 * @param {array} $query Query part
1847 * @return {array} New query
1848 */
1849 $formatted_args = apply_filters( 'ep_post_formatted_args', $formatted_args, $args, $wp_query );
1850
1851 return $formatted_args;
1852 }
1853
1854 /**
1855 * Adjust the fuzziness parameter if needed.
1856 *
1857 * If using fields with type `long`, queries should not have a fuzziness parameter.
1858 *
1859 * @param array $query Current query
1860 * @param array $query_vars Query variables
1861 * @param string $search_text Search text
1862 * @param array $search_fields Search fields
1863 * @return array New query
1864 */
1865 public function adjust_query_fuzziness( $query, $query_vars, $search_text, $search_fields ) {
1866 if ( empty( array_intersect( $search_fields, [ 'ID', 'post_id', 'post_parent' ] ) ) ) {
1867 return $query;
1868 }
1869
1870 if ( ! isset( $query['bool'] ) || ! isset( $query['bool']['should'] ) ) {
1871 return $query;
1872 }
1873
1874 foreach ( $query['bool']['should'] as &$clause ) {
1875 if ( ! isset( $clause['multi_match'] ) ) {
1876 continue;
1877 }
1878
1879 if ( isset( $clause['multi_match']['fuzziness'] ) ) {
1880 unset( $clause['multi_match']['fuzziness'] );
1881 }
1882 }
1883
1884 return $query;
1885 }
1886
1887 /**
1888 * Parse and build out our tax query.
1889 *
1890 * @access protected
1891 *
1892 * @param array $query Tax query
1893 * @return array
1894 */
1895 protected function parse_tax_query( $query ) {
1896 $tax_query = [
1897 'tax_filter' => [],
1898 'tax_must_not_filter' => [],
1899 ];
1900 $relation = '';
1901
1902 foreach ( $query as $tax_queries ) {
1903 // If we have a nested tax query, recurse through that
1904 if ( is_array( $tax_queries ) && empty( $tax_queries['taxonomy'] ) ) {
1905 $result = $this->parse_tax_query( $tax_queries );
1906 $relation = ( ! empty( $tax_queries['relation'] ) ) ? strtolower( $tax_queries['relation'] ) : 'and';
1907 $filter_type = 'and' === $relation ? 'must' : 'should';
1908
1909 // Set the proper filter type and must_not filter, as needed
1910 if ( ! empty( $result['tax_must_not_filter'] ) ) {
1911 $tax_query['tax_filter'][] = [
1912 'bool' => [
1913 $filter_type => $result['tax_filter'],
1914 'must_not' => $result['tax_must_not_filter'],
1915 ],
1916 ];
1917 } else {
1918 $tax_query['tax_filter'][] = [
1919 'bool' => [
1920 $filter_type => $result['tax_filter'],
1921 ],
1922 ];
1923 }
1924 }
1925
1926 // Parse each individual tax query part
1927 $single_tax_query = $tax_queries;
1928 if ( ! empty( $single_tax_query['taxonomy'] ) ) {
1929 $terms = isset( $single_tax_query['terms'] ) ? (array) $single_tax_query['terms'] : array();
1930 $field = ( ! empty( $single_tax_query['field'] ) ) ? $single_tax_query['field'] : 'term_id';
1931
1932 if ( 'name' === $field ) {
1933 $field = 'name.raw';
1934 }
1935
1936 // Set up our terms object
1937 $terms_obj = array(
1938 'terms.' . $single_tax_query['taxonomy'] . '.' . $field => array_values( array_filter( $terms ) ),
1939 );
1940
1941 $operator = ( ! empty( $single_tax_query['operator'] ) ) ? strtolower( $single_tax_query['operator'] ) : 'in';
1942
1943 switch ( $operator ) {
1944 case 'exists':
1945 /**
1946 * add support for "EXISTS" operator
1947 *
1948 * @since 2.5
1949 */
1950 $tax_query['tax_filter'][]['bool'] = array(
1951 'must' => array(
1952 array(
1953 'exists' => array(
1954 'field' => key( $terms_obj ),
1955 ),
1956 ),
1957 ),
1958 );
1959
1960 break;
1961 case 'not exists':
1962 /**
1963 * add support for "NOT EXISTS" operator
1964 *
1965 * @since 2.5
1966 */
1967 $tax_query['tax_filter'][]['bool'] = array(
1968 'must_not' => array(
1969 array(
1970 'exists' => array(
1971 'field' => key( $terms_obj ),
1972 ),
1973 ),
1974 ),
1975 );
1976
1977 break;
1978 case 'not in':
1979 /**
1980 * add support for "NOT IN" operator
1981 *
1982 * @since 2.1
1983 */
1984 // If "NOT IN" than it should filter as must_not
1985 $tax_query['tax_must_not_filter'][]['terms'] = $terms_obj;
1986
1987 break;
1988 case 'and':
1989 /**
1990 * add support for "and" operator
1991 *
1992 * @since 2.4
1993 */
1994 $and_nest = array(
1995 'bool' => array(
1996 'must' => array(),
1997 ),
1998 );
1999
2000 foreach ( $terms as $term ) {
2001 $and_nest['bool']['must'][] = array(
2002 'terms' => array(
2003 'terms.' . $single_tax_query['taxonomy'] . '.' . $field => (array) $term,
2004 ),
2005 );
2006 }
2007
2008 $tax_query['tax_filter'][] = $and_nest;
2009
2010 break;
2011 case 'in':
2012 default:
2013 /**
2014 * Default to IN operator
2015 */
2016 // Add the tax query filter
2017 $tax_query['tax_filter'][]['terms'] = $terms_obj;
2018
2019 break;
2020 }
2021 }
2022 }
2023
2024 return $tax_query;
2025 }
2026
2027 /**
2028 * Parse an 'order' query variable and cast it to ASC or DESC as necessary.
2029 *
2030 * @since 1.1
2031 * @access protected
2032 *
2033 * @param string $order The 'order' query variable.
2034 * @return string The sanitized 'order' query variable.
2035 */
2036 protected function parse_order( $order ) {
2037 // Core will always set sort order to DESC for any invalid value,
2038 // so we can't do any automated testing of this function.
2039 // @codeCoverageIgnoreStart
2040 if ( ! is_string( $order ) || empty( $order ) ) {
2041 return 'desc';
2042 }
2043 // @codeCoverageIgnoreEnd
2044
2045 if ( 'ASC' === strtoupper( $order ) ) {
2046 return 'asc';
2047 } else {
2048 return 'desc';
2049 }
2050 }
2051
2052 /**
2053 * Convert the alias to a properly-prefixed sort value.
2054 *
2055 * @since 1.1
2056 * @access protected
2057 *
2058 * @param string $orderbys Alias or path for the field to order by.
2059 * @param string $default_order Default order direction
2060 * @param array $args Query args
2061 * @return array
2062 */
2063 protected function parse_orderby( $orderbys, $default_order, $args ) {
2064 $orderbys = $this->get_orderby_array( $orderbys );
2065
2066 $sort = [];
2067
2068 foreach ( $orderbys as $key => $value ) {
2069 if ( is_string( $key ) ) {
2070 $orderby_clause = $key;
2071 $order = $value;
2072 } else {
2073 $orderby_clause = $value;
2074 $order = $default_order;
2075 }
2076
2077 if ( ! empty( $orderby_clause ) && 'rand' !== $orderby_clause ) {
2078 if ( 'relevance' === $orderby_clause ) {
2079 $sort[] = array(
2080 '_score' => array(
2081 'order' => $order,
2082 ),
2083 );
2084 } elseif ( 'date' === $orderby_clause ) {
2085 $sort[] = array(
2086 'post_date' => array(
2087 'order' => $order,
2088 ),
2089 );
2090 } elseif ( 'type' === $orderby_clause ) {
2091 $sort[] = array(
2092 'post_type.raw' => array(
2093 'order' => $order,
2094 ),
2095 );
2096 } elseif ( 'modified' === $orderby_clause ) {
2097 $sort[] = array(
2098 'post_modified' => array(
2099 'order' => $order,
2100 ),
2101 );
2102 } elseif ( 'name' === $orderby_clause ) {
2103 $sort[] = array(
2104 'post_' . $orderby_clause . '.raw' => array(
2105 'order' => $order,
2106 ),
2107 );
2108 } elseif ( 'title' === $orderby_clause ) {
2109 $sort[] = array(
2110 'post_' . $orderby_clause . '.sortable' => array(
2111 'order' => $order,
2112 ),
2113 );
2114 } elseif ( 'meta_value' === $orderby_clause ) {
2115 if ( ! empty( $args['meta_key'] ) ) {
2116 $sort[] = array(
2117 'meta.' . $args['meta_key'] . '.raw' => array(
2118 'order' => $order,
2119 ),
2120 );
2121 }
2122 } elseif ( 'meta_value_num' === $orderby_clause ) {
2123 if ( ! empty( $args['meta_key'] ) ) {
2124 $sort[] = array(
2125 'meta.' . $args['meta_key'] . '.long' => array(
2126 'order' => $order,
2127 ),
2128 );
2129 }
2130 } else {
2131 $sort[] = array(
2132 $orderby_clause => array(
2133 'order' => $order,
2134 ),
2135 );
2136 }
2137 }
2138 }
2139
2140 return $sort;
2141 }
2142
2143 /**
2144 * Get Order by args Array
2145 *
2146 * @param string|array $orderbys Order by string or array
2147 * @since 2.1
2148 * @return array
2149 */
2150 protected function get_orderby_array( $orderbys ) {
2151 if ( ! is_array( $orderbys ) ) {
2152 $orderbys = explode( ' ', $orderbys );
2153 }
2154
2155 return $orderbys;
2156 }
2157
2158 /**
2159 * Given a mapping content, try to determine the version used.
2160 *
2161 * @since 3.6.3
2162 *
2163 * @param array $mapping Mapping content.
2164 * @param string $index Index name
2165 * @return string Version of the mapping being used.
2166 */
2167 protected function determine_mapping_version_based_on_existing( $mapping, $index ) {
2168 if ( isset( $mapping[ $index ]['mappings']['post']['_meta']['mapping_version'] ) ) {
2169 return $mapping[ $index ]['mappings']['post']['_meta']['mapping_version'];
2170 }
2171 if ( isset( $mapping[ $index ]['mappings']['_meta']['mapping_version'] ) ) {
2172 return $mapping[ $index ]['mappings']['_meta']['mapping_version'];
2173 }
2174
2175 /**
2176 * Check for 7-0 mapping.
2177 * If mapping has a `post` type, it can't be ES 7, as mapping types were removed in that release.
2178 *
2179 * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/removal-of-types.html
2180 */
2181 if ( ! isset( $mapping[ $index ]['mappings']['post'] ) ) {
2182 return '7-0.php';
2183 }
2184
2185 $post_mapping = $mapping[ $index ]['mappings']['post'];
2186
2187 /**
2188 * Starting at this point, our tests rely on the post_title.fields.sortable field.
2189 * As this field is present in all our mappings, if this field is not present in
2190 * the mapping, this is a custom mapping.
2191 *
2192 * To have this code working with custom mappings, use the `ep_post_mapping_version_determined` filter.
2193 */
2194 if ( ! isset( $post_mapping['properties']['post_title']['fields']['sortable'] ) ) {
2195 return 'unknown';
2196 }
2197
2198 $post_title_sortable = $post_mapping['properties']['post_title']['fields']['sortable'];
2199
2200 /**
2201 * Check for 5-2 mapping.
2202 * Normalizers on keyword fields were only made available in ES 5.2
2203 *
2204 * @see https://www.elastic.co/guide/en/elasticsearch/reference/5.2/release-notes-5.2.0.html
2205 */
2206 if ( isset( $post_title_sortable['normalizer'] ) ) {
2207 return '5-2.php';
2208 }
2209
2210 /**
2211 * Check for 5-0 mapping.
2212 * `keyword` fields were only made available in ES 5.0
2213 *
2214 * @see https://www.elastic.co/guide/en/elasticsearch/reference/5.0/release-notes-5.0.0.html
2215 */
2216 if ( 'keyword' === $post_title_sortable['type'] ) {
2217 return '5-0.php';
2218 }
2219
2220 /**
2221 * Check for pre-5-0 mapping.
2222 * `string` fields were deprecated in ES 5.0 in favor of text/keyword
2223 *
2224 * @see https://www.elastic.co/guide/en/elasticsearch/reference/5.0/release-notes-5.0.0.html
2225 */
2226 if ( 'string' === $post_title_sortable['type'] ) {
2227 return 'pre-5-0.php';
2228 }
2229
2230 return 'unknown';
2231 }
2232
2233 /**
2234 * Given ES args, add aggregations to it.
2235 *
2236 * @since 4.1.0
2237 * @param array $formatted_args Formatted Elasticsearch query.
2238 * @param array $agg Aggregation data.
2239 * @param boolean $use_filters Whether filters should be used or not.
2240 * @param array $filter Filters defined so far.
2241 * @return array Formatted Elasticsearch query with the aggregation added.
2242 */
2243 protected function apply_aggregations( $formatted_args, $agg, $use_filters, $filter ) {
2244 if ( empty( $agg['aggs'] ) ) {
2245 return $formatted_args;
2246 }
2247
2248 // Add a name to the aggregation if it was passed through
2249 $agg_name = ( ! empty( $agg['name'] ) ) ? $agg['name'] : 'aggregation_name';
2250
2251 // Add/use the filter if warranted
2252 if ( isset( $agg['use-filter'] ) && false !== $agg['use-filter'] && $use_filters ) {
2253
2254 // If a filter is being used, use it on the aggregation as well to receive relevant information to the query
2255 $formatted_args['aggs'][ $agg_name ]['filter'] = $filter;
2256 $formatted_args['aggs'][ $agg_name ]['aggs'] = $agg['aggs'];
2257 } else {
2258 $formatted_args['aggs'][ $agg_name ] = $agg['aggs'];
2259 }
2260
2261 return $formatted_args;
2262 }
2263 }
2264