PluginProbe
ElasticPress / 4.2.0
ElasticPress v4.2.0
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.0, at includes/classes/Indexable/Post/Post.php

2,263 lines 62.9 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
1284 *
1285 * @since 2.1
1286 */
1287 if ( ! empty( $args['meta_key'] ) ) {
1288 if ( ! empty( $args['meta_value'] ) ) {
1289 $meta_value = $args['meta_value'];
1290 } elseif ( ! empty( $args['meta_value_num'] ) ) {
1291 $meta_value = $args['meta_value_num'];
1292 }
1293
1294 if ( ! empty( $meta_value ) ) {
1295 $meta_queries[] = array(
1296 'key' => $args['meta_key'],
1297 'value' => $meta_value,
1298 );
1299 }
1300 }
1301
1302 /**
1303 * Todo: Support meta_type
1304 */
1305
1306 /**
1307 * 'meta_query' arg support.
1308 *
1309 * Relation supports 'AND' and 'OR'. 'AND' is the default. For each individual query, the
1310 * following 'compare' values are supported: =, !=, EXISTS, NOT EXISTS. '=' is the default.
1311 *
1312 * @since 1.3
1313 */
1314 if ( ! empty( $args['meta_query'] ) ) {
1315 $meta_queries = array_merge( $meta_queries, $args['meta_query'] );
1316 }
1317
1318 if ( ! empty( $meta_queries ) ) {
1319
1320 $relation = 'must';
1321 if ( ! empty( $args['meta_query'] ) && ! empty( $args['meta_query']['relation'] ) && 'or' === strtolower( $args['meta_query']['relation'] ) ) {
1322 $relation = 'should';
1323 }
1324
1325 // get meta query filter
1326 $meta_filter = $this->build_meta_query( $meta_queries );
1327
1328 if ( ! empty( $meta_filter ) ) {
1329 $filter['bool']['must'][] = $meta_filter;
1330
1331 $use_filters = true;
1332 }
1333 }
1334
1335 /**
1336 * Allow for search field specification
1337 *
1338 * @since 1.0
1339 */
1340 if ( ! empty( $args['search_fields'] ) ) {
1341 $search_field_args = $args['search_fields'];
1342 $search_fields = [];
1343
1344 if ( ! empty( $search_field_args['taxonomies'] ) ) {
1345 $taxes = (array) $search_field_args['taxonomies'];
1346
1347 foreach ( $taxes as $tax ) {
1348 $search_fields[] = 'terms.' . $tax . '.name';
1349 }
1350
1351 unset( $search_field_args['taxonomies'] );
1352 }
1353
1354 if ( ! empty( $search_field_args['meta'] ) ) {
1355 $metas = (array) $search_field_args['meta'];
1356
1357 foreach ( $metas as $meta ) {
1358 $search_fields[] = 'meta.' . $meta . '.value';
1359 }
1360
1361 unset( $search_field_args['meta'] );
1362 }
1363
1364 if ( in_array( 'author_name', $search_field_args, true ) ) {
1365 $search_fields[] = 'post_author.login';
1366
1367 $author_name_index = array_search( 'author_name', $search_field_args, true );
1368 unset( $search_field_args[ $author_name_index ] );
1369 }
1370
1371 $search_fields = array_merge( $search_field_args, $search_fields );
1372 } else {
1373 $search_fields = array(
1374 'post_title',
1375 'post_excerpt',
1376 'post_content',
1377 );
1378 }
1379
1380 /**
1381 * Filter default post search fields
1382 *
1383 * If you are using the weighting engine, this filter should not be used.
1384 * Instead, you should use the ep_weighting_configuration_for_search filter.
1385 *
1386 * @hook ep_search_fields
1387 * @param {array} $search_fields Default search fields
1388 * @param {array} $args WP Query arguments
1389 * @return {array} New defaults
1390 */
1391 $search_fields = apply_filters( 'ep_search_fields', $search_fields, $args );
1392
1393 $default_algorithm_version = '4.0';
1394 if ( defined( 'EP_IS_NETWORK' ) && EP_IS_NETWORK ) {
1395 $search_algorithm_version_option = get_site_option( 'ep_search_algorithm_version', $default_algorithm_version );
1396 } else {
1397 $search_algorithm_version_option = get_option( 'ep_search_algorithm_version', $default_algorithm_version );
1398 }
1399
1400 /**
1401 * Filter the algorithm version to be used.
1402 *
1403 * @since 3.5
1404 * @hook ep_search_algorithm_version
1405 * @param {string} $search_algorithm_version Algorithm version.
1406 * @return {string} New algorithm version
1407 */
1408 $search_algorithm_version = apply_filters( 'ep_search_algorithm_version', $search_algorithm_version_option );
1409
1410 $search_text = ( ! empty( $args['s'] ) ) ? $args['s'] : '';
1411
1412 if ( '4.0' === $search_algorithm_version ) {
1413 $query = array(
1414 'bool' => array(
1415 'should' => array(
1416 array(
1417 'multi_match' => array(
1418 'query' => $search_text,
1419 'type' => 'phrase',
1420 'fields' => $search_fields,
1421 /**
1422 * Filter boost for post match phrase query
1423 *
1424 * @hook ep_match_phrase_boost
1425 * @param {int} $boost Phrase boost
1426 * @param {array} $prepared_search_fields Search fields
1427 * @param {array} $query_vars Query variables
1428 * @return {int} New phrase boost
1429 */
1430 'boost' => apply_filters( 'ep_match_phrase_boost', 3, $search_fields, $args ),
1431 ),
1432 ),
1433 array(
1434 'multi_match' => array(
1435 'query' => $search_text,
1436 'fields' => $search_fields,
1437 /**
1438 * Filter boost for post match query
1439 *
1440 * @hook ep_match_boost
1441 * @param {int} $boost Boost
1442 * @param {array} $prepared_search_fields Search fields
1443 * @param {array} $query_vars Query variables
1444 * @return {int} New boost
1445 */
1446 'boost' => apply_filters( 'ep_match_boost', 1, $search_fields, $args ),
1447 /**
1448 * Filter fuzziness for post match query
1449 *
1450 * @hook ep_match_fuzziness
1451 * @since 4.0.0
1452 * @param {string|int} $fuzziness Fuzziness
1453 * @param {array} $prepared_search_fields Search fields
1454 * @param {array} $query_vars Query variables
1455 * @return {string} New boost
1456 */
1457 'fuzziness' => apply_filters( 'ep_match_fuzziness', 'auto', $search_fields, $args ),
1458 'operator' => 'and',
1459 ),
1460 ),
1461 array(
1462 'multi_match' => [
1463 'query' => $search_text,
1464 'type' => 'cross_fields',
1465 'fields' => $search_fields,
1466 /**
1467 * Filter boost for post match query
1468 *
1469 * @hook ep_match_cross_fields_boost
1470 * @since 4.0.0
1471 * @param {int} $boost Boost
1472 * @param {array} $prepared_search_fields Search fields
1473 * @param {array} $query_vars Query variables
1474 * @return {int} New boost
1475 */
1476 'boost' => apply_filters( 'ep_match_cross_fields_boost', 1, $search_fields, $args ),
1477 'analyzer' => 'standard',
1478 'tie_breaker' => 0.5,
1479 'operator' => 'and',
1480 ],
1481 ),
1482 ),
1483 ),
1484 );
1485 } elseif ( '3.5' === $search_algorithm_version ) {
1486 $query = array(
1487 'bool' => array(
1488 'should' => array(
1489 array(
1490 'multi_match' => array(
1491 'query' => $search_text,
1492 'type' => 'phrase',
1493 'fields' => $search_fields,
1494 /**
1495 * Filter boost for post match phrase query
1496 *
1497 * @hook ep_match_phrase_boost
1498 * @param {int} $boost Phrase boost
1499 * @param {array} $prepared_search_fields Search fields
1500 * @param {array} $query_vars Query variables
1501 * @return {int} New phrase boost
1502 */
1503 'boost' => apply_filters( 'ep_match_phrase_boost', 3, $search_fields, $args ),
1504 ),
1505 ),
1506 array(
1507 'multi_match' => array(
1508 'query' => $search_text,
1509 'fields' => $search_fields,
1510 'type' => 'phrase',
1511 'slop' => 5,
1512 ),
1513 ),
1514 ),
1515 ),
1516 );
1517 } else {
1518 $query = array(
1519 'bool' => array(
1520 'should' => array(
1521 array(
1522 'multi_match' => array(
1523 'query' => $search_text,
1524 'type' => 'phrase',
1525 'fields' => $search_fields,
1526 /**
1527 * Filter boost for post match phrase query
1528 *
1529 * @hook ep_match_phrase_boost
1530 * @param {int} $boost Phrase boost
1531 * @param {array} $prepared_search_fields Search fields
1532 * @param {array} $query_vars Query variables
1533 * @return {int} New phrase boost
1534 */
1535 'boost' => apply_filters( 'ep_match_phrase_boost', 4, $search_fields, $args ),
1536 ),
1537 ),
1538 array(
1539 'multi_match' => array(
1540 'query' => $search_text,
1541 'fields' => $search_fields,
1542 /**
1543 * Filter boost for post match query
1544 *
1545 * @hook ep_match_boost
1546 * @param {int} $boost Boost
1547 * @param {array} $prepared_search_fields Search fields
1548 * @param {array} $query_vars Query variables
1549 * @return {int} New boost
1550 */
1551 'boost' => apply_filters( 'ep_match_boost', 2, $search_fields, $args ),
1552 'fuzziness' => 0,
1553 'operator' => 'and',
1554 ),
1555 ),
1556 array(
1557 'multi_match' => array(
1558 'query' => $search_text,
1559 'fields' => $search_fields,
1560 /**
1561 * Filter fuzziness for post query
1562 *
1563 * @hook ep_fuzziness_arg
1564 * @param {int} $fuzziness Fuzziness
1565 * @param {array} $prepared_search_fields Search fields
1566 * @param {array} $query_vars Query variables
1567 * @return {int} New fuzziness
1568 */
1569 'fuzziness' => apply_filters( 'ep_fuzziness_arg', 1, $search_fields, $args ),
1570 ),
1571 ),
1572 ),
1573 ),
1574 );
1575 }
1576
1577 /**
1578 * We are using ep_integrate instead of ep_match_all. ep_match_all will be
1579 * supported for legacy code but may be deprecated and removed eventually.
1580 *
1581 * @since 1.3
1582 */
1583
1584 if ( ! empty( $args['s'] ) ) {
1585 add_filter( 'ep_formatted_args_query', [ $this, 'adjust_query_fuzziness' ], 100, 4 );
1586
1587 /**
1588 * Filter formatted Elasticsearch post query (only contains query part)
1589 *
1590 * @hook ep_formatted_args_query
1591 * @param {array} $query Current query
1592 * @param {array} $query_vars Query variables
1593 * @param {string} $search_text Search text
1594 * @param {array} $search_fields Search fields
1595 * @return {array} New query
1596 *
1597 * @since 3.5.5 $search_text and $search_fields parameters added.
1598 */
1599 $formatted_args['query'] = apply_filters(
1600 'ep_formatted_args_query',
1601 $query,
1602 $args,
1603 $search_text,
1604 $search_fields
1605 );
1606 } elseif ( ! empty( $args['ep_match_all'] ) || ! empty( $args['ep_integrate'] ) ) {
1607 $formatted_args['query']['match_all'] = array(
1608 'boost' => 1,
1609 );
1610 }
1611
1612 /**
1613 * Order by 'rand' support
1614 *
1615 * Ref: https://github.com/elastic/elasticsearch/issues/1170
1616 */
1617 if ( ! empty( $args['orderby'] ) ) {
1618 $orderbys = $this->get_orderby_array( $args['orderby'] );
1619 if ( in_array( 'rand', $orderbys, true ) ) {
1620 $formatted_args_query = $formatted_args['query'];
1621 $formatted_args['query'] = [];
1622 $formatted_args['query']['function_score']['query'] = $formatted_args_query;
1623 $formatted_args['query']['function_score']['random_score'] = (object) [];
1624 }
1625 }
1626
1627 /**
1628 * Sticky posts support
1629 */
1630
1631 // Check first if there's sticky posts and show them only in the front page
1632 $sticky_posts = get_option( 'sticky_posts' );
1633 $sticky_posts = ( is_array( $sticky_posts ) && empty( $sticky_posts ) ) ? false : $sticky_posts;
1634
1635 /**
1636 * Filter whether to enable sticky posts for this request
1637 *
1638 * @hook ep_enable_sticky_posts
1639 *
1640 * @param {bool} $allow Allow sticky posts for this request
1641 * @param {array} $args Query variables
1642 * @param {array} $formatted_args EP formatted args
1643 *
1644 * @return {bool} $allow
1645 */
1646 $enable_sticky_posts = apply_filters( 'ep_enable_sticky_posts', is_home(), $args, $formatted_args );
1647
1648 if ( false !== $sticky_posts
1649 && $enable_sticky_posts
1650 && empty( $args['s'] )
1651 && in_array( $args['ignore_sticky_posts'], array( 'false', 0, false ), true ) ) {
1652 $new_sort = [
1653 [
1654 '_score' => [
1655 'order' => 'desc',
1656 ],
1657 ],
1658 ];
1659
1660 $formatted_args['sort'] = array_merge( $new_sort, $formatted_args['sort'] );
1661
1662 $formatted_args_query = $formatted_args['query'];
1663 $formatted_args['query'] = array();
1664 $formatted_args['query']['function_score']['query'] = $formatted_args_query;
1665 $formatted_args['query']['function_score']['functions'] = array(
1666 // add extra weight to sticky posts to show them on top
1667 (object) array(
1668 'filter' => array(
1669 'terms' => array( '_id' => $sticky_posts ),
1670 ),
1671 'weight' => 20,
1672 ),
1673 );
1674 }
1675
1676 /**
1677 * If not set default to post. If search and not set, default to "any".
1678 */
1679 if ( ! empty( $args['post_type'] ) ) {
1680 // should NEVER be "any" but just in case
1681 if ( 'any' !== $args['post_type'] ) {
1682 $post_types = (array) $args['post_type'];
1683 $terms_map_name = 'terms';
1684
1685 $filter['bool']['must'][] = array(
1686 $terms_map_name => array(
1687 'post_type.raw' => array_values( $post_types ),
1688 ),
1689 );
1690
1691 $use_filters = true;
1692 }
1693 } elseif ( empty( $args['s'] ) ) {
1694 $filter['bool']['must'][] = array(
1695 'term' => array(
1696 'post_type.raw' => 'post',
1697 ),
1698 );
1699
1700 $use_filters = true;
1701 }
1702
1703 /**
1704 * Like WP_Query in search context, if no post_status is specified we default to "any". To
1705 * be safe you should ALWAYS specify the post_status parameter UNLIKE with WP_Query.
1706 *
1707 * @since 2.1
1708 */
1709 if ( ! empty( $args['post_status'] ) ) {
1710 // should NEVER be "any" but just in case
1711 if ( 'any' !== $args['post_status'] ) {
1712 $post_status = (array) ( is_string( $args['post_status'] ) ? explode( ',', $args['post_status'] ) : $args['post_status'] );
1713 $post_status = array_map( 'trim', $post_status );
1714 $terms_map_name = 'terms';
1715 if ( count( $post_status ) < 2 ) {
1716 $terms_map_name = 'term';
1717 $post_status = $post_status[0];
1718 }
1719
1720 $filter['bool']['must'][] = array(
1721 $terms_map_name => array(
1722 'post_status' => $post_status,
1723 ),
1724 );
1725
1726 $use_filters = true;
1727 }
1728 } else {
1729 $statuses = get_post_stati( array( 'public' => true ) );
1730
1731 if ( is_admin() ) {
1732 /**
1733 * In the admin we will add protected and private post statuses to the default query
1734 * per WP default behavior.
1735 */
1736 $statuses = array_merge(
1737 $statuses,
1738 get_post_stati(
1739 array(
1740 'protected' => true,
1741 'show_in_admin_all_list' => true,
1742 )
1743 )
1744 );
1745
1746 if ( is_user_logged_in() ) {
1747 $statuses = array_merge( $statuses, get_post_stati( array( 'private' => true ) ) );
1748 }
1749 }
1750
1751 $statuses = array_values( $statuses );
1752
1753 $post_status_filter_type = 'terms';
1754
1755 $filter['bool']['must'][] = array(
1756 $post_status_filter_type => array(
1757 'post_status' => $statuses,
1758 ),
1759 );
1760
1761 $use_filters = true;
1762 }
1763
1764 if ( isset( $args['offset'] ) ) {
1765 $formatted_args['from'] = (int) $args['offset'];
1766 }
1767
1768 if ( isset( $args['paged'] ) && $args['paged'] > 1 ) {
1769 $formatted_args['from'] = $args['posts_per_page'] * ( $args['paged'] - 1 );
1770 }
1771
1772 /**
1773 * Fix negative offset. This happens, for example, on hierarchical post types.
1774 *
1775 * Ref: https://github.com/10up/ElasticPress/issues/2480
1776 */
1777 if ( $formatted_args['from'] < 0 ) {
1778 $formatted_args['from'] = 0;
1779 }
1780
1781 if ( $use_filters ) {
1782 $formatted_args['post_filter'] = $filter;
1783 }
1784
1785 /**
1786 * Support fields.
1787 */
1788 if ( isset( $args['fields'] ) ) {
1789 switch ( $args['fields'] ) {
1790 case 'ids':
1791 $formatted_args['_source'] = array(
1792 'includes' => array(
1793 'post_id',
1794 ),
1795 );
1796 break;
1797
1798 case 'id=>parent':
1799 $formatted_args['_source'] = array(
1800 'includes' => array(
1801 'post_id',
1802 'post_parent',
1803 ),
1804 );
1805 break;
1806 }
1807 }
1808
1809 /**
1810 * Aggregations
1811 */
1812 if ( ! empty( $args['aggs'] ) && is_array( $args['aggs'] ) ) {
1813 // Check if the array indexes are all numeric.
1814 $agg_keys = array_keys( $args['aggs'] );
1815 $agg_num_keys = array_filter( $agg_keys, 'is_int' );
1816 $has_only_num_keys = count( $agg_num_keys ) === count( $args['aggs'] );
1817
1818 if ( $has_only_num_keys ) {
1819 foreach ( $args['aggs'] as $agg ) {
1820 $formatted_args = $this->apply_aggregations( $formatted_args, $agg, $use_filters, $filter );
1821 }
1822 } else {
1823 // Single aggregation.
1824 $formatted_args = $this->apply_aggregations( $formatted_args, $args['aggs'], $use_filters, $filter );
1825 }
1826 }
1827
1828 /**
1829 * Filter formatted Elasticsearch [ost ]query (entire query)
1830 *
1831 * @hook ep_formatted_args
1832 * @param {array} $formatted_args Formatted Elasticsearch query
1833 * @param {array} $query_vars Query variables
1834 * @param {array} $query Query part
1835 * @return {array} New query
1836 */
1837 $formatted_args = apply_filters( 'ep_formatted_args', $formatted_args, $args, $wp_query );
1838
1839 /**
1840 * Filter formatted Elasticsearch [ost ]query (entire query)
1841 *
1842 * @hook ep_post_formatted_args
1843 * @param {array} $formatted_args Formatted Elasticsearch query
1844 * @param {array} $query_vars Query variables
1845 * @param {array} $query Query part
1846 * @return {array} New query
1847 */
1848 $formatted_args = apply_filters( 'ep_post_formatted_args', $formatted_args, $args, $wp_query );
1849
1850 return $formatted_args;
1851 }
1852
1853 /**
1854 * Adjust the fuzziness parameter if needed.
1855 *
1856 * If using fields with type `long`, queries should not have a fuzziness parameter.
1857 *
1858 * @param array $query Current query
1859 * @param array $query_vars Query variables
1860 * @param string $search_text Search text
1861 * @param array $search_fields Search fields
1862 * @return array New query
1863 */
1864 public function adjust_query_fuzziness( $query, $query_vars, $search_text, $search_fields ) {
1865 if ( empty( array_intersect( $search_fields, [ 'ID', 'post_id', 'post_parent' ] ) ) ) {
1866 return $query;
1867 }
1868
1869 if ( ! isset( $query['bool'] ) || ! isset( $query['bool']['should'] ) ) {
1870 return $query;
1871 }
1872
1873 foreach ( $query['bool']['should'] as &$clause ) {
1874 if ( ! isset( $clause['multi_match'] ) ) {
1875 continue;
1876 }
1877
1878 if ( isset( $clause['multi_match']['fuzziness'] ) ) {
1879 unset( $clause['multi_match']['fuzziness'] );
1880 }
1881 }
1882
1883 return $query;
1884 }
1885
1886 /**
1887 * Parse and build out our tax query.
1888 *
1889 * @access protected
1890 *
1891 * @param array $query Tax query
1892 * @return array
1893 */
1894 protected function parse_tax_query( $query ) {
1895 $tax_query = [
1896 'tax_filter' => [],
1897 'tax_must_not_filter' => [],
1898 ];
1899 $relation = '';
1900
1901 foreach ( $query as $tax_queries ) {
1902 // If we have a nested tax query, recurse through that
1903 if ( is_array( $tax_queries ) && empty( $tax_queries['taxonomy'] ) ) {
1904 $result = $this->parse_tax_query( $tax_queries );
1905 $relation = ( ! empty( $tax_queries['relation'] ) ) ? strtolower( $tax_queries['relation'] ) : 'and';
1906 $filter_type = 'and' === $relation ? 'must' : 'should';
1907
1908 // Set the proper filter type and must_not filter, as needed
1909 if ( ! empty( $result['tax_must_not_filter'] ) ) {
1910 $tax_query['tax_filter'][] = [
1911 'bool' => [
1912 $filter_type => $result['tax_filter'],
1913 'must_not' => $result['tax_must_not_filter'],
1914 ],
1915 ];
1916 } else {
1917 $tax_query['tax_filter'][] = [
1918 'bool' => [
1919 $filter_type => $result['tax_filter'],
1920 ],
1921 ];
1922 }
1923 }
1924
1925 // Parse each individual tax query part
1926 $single_tax_query = $tax_queries;
1927 if ( ! empty( $single_tax_query['taxonomy'] ) ) {
1928 $terms = isset( $single_tax_query['terms'] ) ? (array) $single_tax_query['terms'] : array();
1929 $field = ( ! empty( $single_tax_query['field'] ) ) ? $single_tax_query['field'] : 'term_id';
1930
1931 if ( 'name' === $field ) {
1932 $field = 'name.raw';
1933 }
1934
1935 // Set up our terms object
1936 $terms_obj = array(
1937 'terms.' . $single_tax_query['taxonomy'] . '.' . $field => array_values( array_filter( $terms ) ),
1938 );
1939
1940 $operator = ( ! empty( $single_tax_query['operator'] ) ) ? strtolower( $single_tax_query['operator'] ) : 'in';
1941
1942 switch ( $operator ) {
1943 case 'exists':
1944 /**
1945 * add support for "EXISTS" operator
1946 *
1947 * @since 2.5
1948 */
1949 $tax_query['tax_filter'][]['bool'] = array(
1950 'must' => array(
1951 array(
1952 'exists' => array(
1953 'field' => key( $terms_obj ),
1954 ),
1955 ),
1956 ),
1957 );
1958
1959 break;
1960 case 'not exists':
1961 /**
1962 * add support for "NOT EXISTS" operator
1963 *
1964 * @since 2.5
1965 */
1966 $tax_query['tax_filter'][]['bool'] = array(
1967 'must_not' => array(
1968 array(
1969 'exists' => array(
1970 'field' => key( $terms_obj ),
1971 ),
1972 ),
1973 ),
1974 );
1975
1976 break;
1977 case 'not in':
1978 /**
1979 * add support for "NOT IN" operator
1980 *
1981 * @since 2.1
1982 */
1983 // If "NOT IN" than it should filter as must_not
1984 $tax_query['tax_must_not_filter'][]['terms'] = $terms_obj;
1985
1986 break;
1987 case 'and':
1988 /**
1989 * add support for "and" operator
1990 *
1991 * @since 2.4
1992 */
1993 $and_nest = array(
1994 'bool' => array(
1995 'must' => array(),
1996 ),
1997 );
1998
1999 foreach ( $terms as $term ) {
2000 $and_nest['bool']['must'][] = array(
2001 'terms' => array(
2002 'terms.' . $single_tax_query['taxonomy'] . '.' . $field => (array) $term,
2003 ),
2004 );
2005 }
2006
2007 $tax_query['tax_filter'][] = $and_nest;
2008
2009 break;
2010 case 'in':
2011 default:
2012 /**
2013 * Default to IN operator
2014 */
2015 // Add the tax query filter
2016 $tax_query['tax_filter'][]['terms'] = $terms_obj;
2017
2018 break;
2019 }
2020 }
2021 }
2022
2023 return $tax_query;
2024 }
2025
2026 /**
2027 * Parse an 'order' query variable and cast it to ASC or DESC as necessary.
2028 *
2029 * @since 1.1
2030 * @access protected
2031 *
2032 * @param string $order The 'order' query variable.
2033 * @return string The sanitized 'order' query variable.
2034 */
2035 protected function parse_order( $order ) {
2036 // Core will always set sort order to DESC for any invalid value,
2037 // so we can't do any automated testing of this function.
2038 // @codeCoverageIgnoreStart
2039 if ( ! is_string( $order ) || empty( $order ) ) {
2040 return 'desc';
2041 }
2042 // @codeCoverageIgnoreEnd
2043
2044 if ( 'ASC' === strtoupper( $order ) ) {
2045 return 'asc';
2046 } else {
2047 return 'desc';
2048 }
2049 }
2050
2051 /**
2052 * Convert the alias to a properly-prefixed sort value.
2053 *
2054 * @since 1.1
2055 * @access protected
2056 *
2057 * @param string $orderbys Alias or path for the field to order by.
2058 * @param string $default_order Default order direction
2059 * @param array $args Query args
2060 * @return array
2061 */
2062 protected function parse_orderby( $orderbys, $default_order, $args ) {
2063 $orderbys = $this->get_orderby_array( $orderbys );
2064
2065 $sort = [];
2066
2067 foreach ( $orderbys as $key => $value ) {
2068 if ( is_string( $key ) ) {
2069 $orderby_clause = $key;
2070 $order = $value;
2071 } else {
2072 $orderby_clause = $value;
2073 $order = $default_order;
2074 }
2075
2076 if ( ! empty( $orderby_clause ) && 'rand' !== $orderby_clause ) {
2077 if ( 'relevance' === $orderby_clause ) {
2078 $sort[] = array(
2079 '_score' => array(
2080 'order' => $order,
2081 ),
2082 );
2083 } elseif ( 'date' === $orderby_clause ) {
2084 $sort[] = array(
2085 'post_date' => array(
2086 'order' => $order,
2087 ),
2088 );
2089 } elseif ( 'type' === $orderby_clause ) {
2090 $sort[] = array(
2091 'post_type.raw' => array(
2092 'order' => $order,
2093 ),
2094 );
2095 } elseif ( 'modified' === $orderby_clause ) {
2096 $sort[] = array(
2097 'post_modified' => array(
2098 'order' => $order,
2099 ),
2100 );
2101 } elseif ( 'name' === $orderby_clause ) {
2102 $sort[] = array(
2103 'post_' . $orderby_clause . '.raw' => array(
2104 'order' => $order,
2105 ),
2106 );
2107 } elseif ( 'title' === $orderby_clause ) {
2108 $sort[] = array(
2109 'post_' . $orderby_clause . '.sortable' => array(
2110 'order' => $order,
2111 ),
2112 );
2113 } elseif ( 'meta_value' === $orderby_clause ) {
2114 if ( ! empty( $args['meta_key'] ) ) {
2115 $sort[] = array(
2116 'meta.' . $args['meta_key'] . '.raw' => array(
2117 'order' => $order,
2118 ),
2119 );
2120 }
2121 } elseif ( 'meta_value_num' === $orderby_clause ) {
2122 if ( ! empty( $args['meta_key'] ) ) {
2123 $sort[] = array(
2124 'meta.' . $args['meta_key'] . '.long' => array(
2125 'order' => $order,
2126 ),
2127 );
2128 }
2129 } else {
2130 $sort[] = array(
2131 $orderby_clause => array(
2132 'order' => $order,
2133 ),
2134 );
2135 }
2136 }
2137 }
2138
2139 return $sort;
2140 }
2141
2142 /**
2143 * Get Order by args Array
2144 *
2145 * @param string|array $orderbys Order by string or array
2146 * @since 2.1
2147 * @return array
2148 */
2149 protected function get_orderby_array( $orderbys ) {
2150 if ( ! is_array( $orderbys ) ) {
2151 $orderbys = explode( ' ', $orderbys );
2152 }
2153
2154 return $orderbys;
2155 }
2156
2157 /**
2158 * Given a mapping content, try to determine the version used.
2159 *
2160 * @since 3.6.3
2161 *
2162 * @param array $mapping Mapping content.
2163 * @param string $index Index name
2164 * @return string Version of the mapping being used.
2165 */
2166 protected function determine_mapping_version_based_on_existing( $mapping, $index ) {
2167 if ( isset( $mapping[ $index ]['mappings']['post']['_meta']['mapping_version'] ) ) {
2168 return $mapping[ $index ]['mappings']['post']['_meta']['mapping_version'];
2169 }
2170 if ( isset( $mapping[ $index ]['mappings']['_meta']['mapping_version'] ) ) {
2171 return $mapping[ $index ]['mappings']['_meta']['mapping_version'];
2172 }
2173
2174 /**
2175 * Check for 7-0 mapping.
2176 * If mapping has a `post` type, it can't be ES 7, as mapping types were removed in that release.
2177 *
2178 * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/removal-of-types.html
2179 */
2180 if ( ! isset( $mapping[ $index ]['mappings']['post'] ) ) {
2181 return '7-0.php';
2182 }
2183
2184 $post_mapping = $mapping[ $index ]['mappings']['post'];
2185
2186 /**
2187 * Starting at this point, our tests rely on the post_title.fields.sortable field.
2188 * As this field is present in all our mappings, if this field is not present in
2189 * the mapping, this is a custom mapping.
2190 *
2191 * To have this code working with custom mappings, use the `ep_post_mapping_version_determined` filter.
2192 */
2193 if ( ! isset( $post_mapping['properties']['post_title']['fields']['sortable'] ) ) {
2194 return 'unknown';
2195 }
2196
2197 $post_title_sortable = $post_mapping['properties']['post_title']['fields']['sortable'];
2198
2199 /**
2200 * Check for 5-2 mapping.
2201 * Normalizers on keyword fields were only made available in ES 5.2
2202 *
2203 * @see https://www.elastic.co/guide/en/elasticsearch/reference/5.2/release-notes-5.2.0.html
2204 */
2205 if ( isset( $post_title_sortable['normalizer'] ) ) {
2206 return '5-2.php';
2207 }
2208
2209 /**
2210 * Check for 5-0 mapping.
2211 * `keyword` fields were only made available in ES 5.0
2212 *
2213 * @see https://www.elastic.co/guide/en/elasticsearch/reference/5.0/release-notes-5.0.0.html
2214 */
2215 if ( 'keyword' === $post_title_sortable['type'] ) {
2216 return '5-0.php';
2217 }
2218
2219 /**
2220 * Check for pre-5-0 mapping.
2221 * `string` fields were deprecated in ES 5.0 in favor of text/keyword
2222 *
2223 * @see https://www.elastic.co/guide/en/elasticsearch/reference/5.0/release-notes-5.0.0.html
2224 */
2225 if ( 'string' === $post_title_sortable['type'] ) {
2226 return 'pre-5-0.php';
2227 }
2228
2229 return 'unknown';
2230 }
2231
2232 /**
2233 * Given ES args, add aggregations to it.
2234 *
2235 * @since 4.1.0
2236 * @param array $formatted_args Formatted Elasticsearch query.
2237 * @param array $agg Aggregation data.
2238 * @param boolean $use_filters Whether filters should be used or not.
2239 * @param array $filter Filters defined so far.
2240 * @return array Formatted Elasticsearch query with the aggregation added.
2241 */
2242 protected function apply_aggregations( $formatted_args, $agg, $use_filters, $filter ) {
2243 if ( empty( $agg['aggs'] ) ) {
2244 return $formatted_args;
2245 }
2246
2247 // Add a name to the aggregation if it was passed through
2248 $agg_name = ( ! empty( $agg['name'] ) ) ? $agg['name'] : 'aggregation_name';
2249
2250 // Add/use the filter if warranted
2251 if ( isset( $agg['use-filter'] ) && false !== $agg['use-filter'] && $use_filters ) {
2252
2253 // If a filter is being used, use it on the aggregation as well to receive relevant information to the query
2254 $formatted_args['aggs'][ $agg_name ]['filter'] = $filter;
2255 $formatted_args['aggs'][ $agg_name ]['aggs'] = $agg['aggs'];
2256 } else {
2257 $formatted_args['aggs'][ $agg_name ] = $agg['aggs'];
2258 }
2259
2260 return $formatted_args;
2261 }
2262 }
2263