PluginProbe
ElasticPress / 5.3.5
ElasticPress v5.3.5
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
← All changes | includes/classes/Indexable/Term/Term.php +810 -485 4.4.05.3.5 View file →
@@ -7,11 +7,11 @@
7 7 */
8 8
9 9 namespace ElasticPress\Indexable\Term;
10 10
11 -use ElasticPress\Indexable as Indexable;
12 -use ElasticPress\Elasticsearch as Elasticsearch;
13 -use \WP_Term_Query as WP_Term_Query;
11 +use WP_Term_Query;
12 +use ElasticPress\Elasticsearch;
13 +use ElasticPress\Indexable;
14 14
15 15 if ( ! defined( 'ABSPATH' ) ) {
16 16 // @codeCoverageIgnoreStart
17 17 exit; // Exit if accessed directly.
@@ -31,13 +31,23 @@
31 31 */
32 32 public $slug = 'term';
33 33
34 34 /**
35 - * Create indexable and initialize dependencies
35 + * Flag to indicate if the indexable has support for
36 + * `id_range` pagination method during a sync.
36 37 *
37 - * @since 3.1
38 + * @var boolean
39 + * @since 5.2.0
38 40 */
39 - public function __construct() {
41 + public $support_indexing_advanced_pagination = true;
42 +
43 + /**
44 + * Instantiate the indexable SyncManager and QueryIntegration, the main responsibles for the WP integration.
45 + *
46 + * @since 4.5.0
47 + * @return void
48 + */
49 + public function setup() {
40 50 $this->labels = [
41 51 'plural' => esc_html__( 'Terms', 'elasticpress' ),
42 52 'singular' => esc_html__( 'Term', 'elasticpress' ),
43 53 ];
@@ -53,460 +63,26 @@
53 63 * @since 3.1
54 64 * @return array
55 65 */
56 66 public function format_args( $query_vars ) {
57 - /**
58 - * Support `number` query var
59 - */
60 - if ( ! empty( $query_vars['number'] ) ) {
61 - $number = (int) $query_vars['number'];
62 - } else {
63 - /**
64 - * Set the maximum results window size.
65 - *
66 - * The request will return a HTTP 500 Internal Error if the size of the
67 - * request is larger than the [index.max_result_window] parameter in ES.
68 - * See the scroll api for a more efficient way to request large data sets.
69 - *
70 - * @return int The max results window size.
71 - *
72 - * @since 2.3.0
73 - */
74 - $number = apply_filters( 'ep_max_results_window', 10000 );
75 - }
67 + $query_vars = $this->sanitize_query_vars( $query_vars );
76 68
77 69 $formatted_args = [
78 - 'from' => 0,
79 - 'size' => $number,
70 + 'from' => $this->parse_from( $query_vars ),
71 + 'size' => $this->parse_size( $query_vars ),
80 72 ];
81 73
82 - /**
83 - * Support `offset` query var
84 - */
85 - if ( isset( $query_vars['offset'] ) ) {
86 - $formatted_args['from'] = (int) $query_vars['offset'];
87 - }
74 + $formatted_args = $this->maybe_orderby( $formatted_args, $query_vars );
88 75
89 - /**
90 - * Support `order` and `orderby` query vars
91 - */
92 -
93 - // Set sort order, default is 'ASC'.
94 - if ( ! empty( $query_vars['order'] ) ) {
95 - $order = $this->parse_order( $query_vars['order'] );
96 - } else {
97 - $order = 'desc';
76 + $filters = $this->parse_filters( $query_vars );
77 + if ( ! empty( $filters ) ) {
78 + $formatted_args['post_filter'] = $filters;
98 79 }
99 80
100 - // Set orderby, default is 'name'.
101 - if ( empty( $query_vars['orderby'] ) ) {
102 - $query_vars['orderby'] = 'name';
103 - }
81 + $formatted_args = $this->maybe_set_search_fields( $formatted_args, $query_vars );
82 + $formatted_args = $this->maybe_set_fields( $formatted_args, $query_vars );
104 83
105 - // Set sort type.
106 - $formatted_args['sort'] = $this->parse_orderby( $query_vars['orderby'], $order, $query_vars );
107 -
108 - $filter = [
109 - 'bool' => [
110 - 'must' => [],
111 - ],
112 - ];
113 -
114 - $use_filters = false;
115 -
116 84 /**
117 - * Support `taxonomy` query var
118 - */
119 - $taxonomy = [];
120 - if ( ! empty( $query_vars['taxonomy'] ) ) {
121 - $taxonomy = (array) $query_vars['taxonomy'];
122 - $terms_map_name = 'terms';
123 -
124 - if ( count( $taxonomy ) < 2 ) {
125 - $terms_map_name = 'term';
126 - $taxonomy = $taxonomy[0];
127 - }
128 -
129 - $filter['bool']['must'][] = [
130 - $terms_map_name => [
131 - 'taxonomy.raw' => $taxonomy,
132 - ],
133 - ];
134 -
135 - $use_filters = true;
136 - }
137 -
138 - /**
139 - * Support `object_ids` query var
140 - */
141 - if ( ! empty( $query_vars['object_ids'] ) ) {
142 - $filter['bool']['must'][]['bool']['must'][] = [
143 - 'terms' => [
144 - 'object_ids.value' => (array) $query_vars['object_ids'],
145 - ],
146 - ];
147 -
148 - $use_filters = true;
149 - }
150 -
151 - /**
152 - * Support `get` query var
153 - */
154 - if ( ! empty( $query_vars['get'] ) && 'all' === $query_vars['get'] ) {
155 - $query_vars['childless'] = false;
156 - $query_vars['child_of'] = 0;
157 - $query_vars['hide_empty'] = false;
158 - $query_vars['hierarchical'] = false;
159 - $query_vars['pad_counts'] = false;
160 - }
161 -
162 - /**
163 - * Support `include` query var
164 - */
165 - if ( ! empty( $query_vars['include'] ) ) {
166 - $filter['bool']['must'][]['bool']['must'] = [
167 - 'terms' => [
168 - 'term_id' => array_values( (array) $query_vars['include'] ),
169 - ],
170 - ];
171 -
172 - $use_filters = true;
173 - }
174 -
175 - /**
176 - * Support `exclude` query var
177 - */
178 - if ( empty( $query_vars['include'] ) && ! empty( $query_vars['exclude'] ) ) {
179 - $filter['bool']['must'][]['bool']['must_not'] = [
180 - 'terms' => [
181 - 'term_id' => array_values( (array) $query_vars['exclude'] ),
182 - ],
183 - ];
184 -
185 - $use_filters = true;
186 - }
187 -
188 - /**
189 - * Support `exclude_tree` query var
190 - */
191 - if ( empty( $query_vars['include'] ) && ! empty( $query_vars['exclude_tree'] ) ) {
192 - $filter['bool']['must'][]['bool']['must_not'] = [
193 - 'terms' => [
194 - 'term_id' => array_values( (array) $query_vars['exclude_tree'] ),
195 - ],
196 - ];
197 -
198 - $filter['bool']['must'][]['bool']['must_not'] = [
199 - 'terms' => [
200 - 'parent' => array_values( (array) $query_vars['exclude_tree'] ),
201 - ],
202 - ];
203 -
204 - $use_filters = true;
205 - }
206 -
207 - /**
208 - * Support `name` query var
209 - */
210 - if ( ! empty( $query_vars['name'] ) ) {
211 - $filter['bool']['must'][] = [
212 - 'terms' => [
213 - 'name.raw' => (array) $query_vars['name'],
214 - ],
215 - ];
216 -
217 - $use_filters = true;
218 - }
219 -
220 - /**
221 - * Support `slug` query var
222 - */
223 - if ( ! empty( $query_vars['slug'] ) ) {
224 - if ( ! is_array( $query_vars['slug'] ) ) {
225 - $query_vars['slug'] = array( $query_vars['slug'] );
226 - }
227 -
228 - $query_vars['slug'] = array_map( 'sanitize_title', $query_vars['slug'] );
229 -
230 - $filter['bool']['must'][] = [
231 - 'terms' => [
232 - 'slug.raw' => (array) $query_vars['slug'],
233 - ],
234 - ];
235 -
236 - $use_filters = true;
237 - }
238 -
239 - /**
240 - * Support `term_taxonomy_id` query var
241 - */
242 - if ( ! empty( $query_vars['term_taxonomy_id'] ) ) {
243 - $filter['bool']['must'][]['bool']['must'] = [
244 - 'terms' => [
245 - 'term_taxonomy_id' => array_values( (array) $query_vars['term_taxonomy_id'] ),
246 - ],
247 - ];
248 -
249 - $use_filters = true;
250 - }
251 -
252 - /**
253 - * Support `hierarchical` and `hide_empty` query var
254 - *
255 - * `hierarchical` needs to work in conjunction with `hide_empty`, as per WP docs:
256 - * > `hierarchical`: Whether to include terms that have non-empty descendants (even if $hide_empty is set to true).
257 - *
258 - * In summary:
259 - * - hide_empty AND hierarchical: count > 1 OR hierarchy.children > 1
260 - * - hide_empty AND NOT hierarchical: count > 1 (ignore hierarchy.children)
261 - * - NOT hide_empty (AND hierarchical): there is no need to limit the query
262 - *
263 - * @see https://developer.wordpress.org/reference/classes/WP_Term_Query/__construct/
264 - */
265 - $hide_empty = isset( $query_vars['hide_empty'] ) ? $query_vars['hide_empty'] : '';
266 - if ( $hide_empty ) {
267 - $hierarchical = isset( $query_vars['hierarchical'] ) ? $query_vars['hierarchical'] : '';
268 -
269 - if ( $hierarchical ) {
270 - $filter_clause = [ 'bool' => [ 'should' => [] ] ];
271 -
272 - $filter_clause['bool']['should'][] = [
273 - 'range' => [
274 - 'count' => [
275 - 'gte' => 1,
276 - ],
277 - ],
278 - ];
279 -
280 - $filter_clause['bool']['should'][] = [
281 - 'range' => [
282 - 'hierarchy.children.count' => [
283 - 'gte' => 1,
284 - ],
285 - ],
286 - ];
287 -
288 - $filter['bool']['must'][] = $filter_clause;
289 -
290 - $use_filters = true;
291 - } else {
292 - $filter['bool']['must'][] = [
293 - 'range' => [
294 - 'count' => [
295 - 'gte' => 1,
296 - ],
297 - ],
298 - ];
299 - }
300 -
301 - $use_filters = true;
302 - }
303 -
304 - /**
305 - * Support `search`, `name__like` and `description__like` query_vars
306 - */
307 - if ( ! empty( $query_vars['search'] ) || ! empty( $query_vars['name__like'] ) || ! empty( $query_vars['description__like'] ) ) {
308 -
309 - $search = ! empty( $query_vars['search'] ) ? $query_vars['search'] : '';
310 - $search_fields = [];
311 -
312 - if ( ! empty( $query_vars['name__like'] ) ) {
313 - $search = $query_vars['name__like'];
314 - $search_fields[] = 'name';
315 - }
316 -
317 - if ( ! empty( $query_vars['description__like'] ) ) {
318 - $search = $query_vars['description__like'];
319 - $search_fields[] = 'description';
320 - }
321 -
322 - /**
323 - * Allow for search field specification
324 - */
325 - if ( ! empty( $query_vars['search_fields'] ) ) {
326 - $search_fields = $query_vars['search_fields'];
327 - }
328 -
329 - if ( ! empty( $search_fields ) ) {
330 - $prepared_search_fields = [];
331 -
332 - if ( ! empty( $search_fields['meta'] ) ) {
333 - $metas = (array) $search_fields['meta'];
334 -
335 - foreach ( $metas as $meta ) {
336 - $prepared_search_fields[] = 'meta.' . $meta . '.value';
337 - }
338 -
339 - unset( $search_fields['meta'] );
340 - }
341 -
342 - $prepared_search_fields = array_merge( $search_fields, $prepared_search_fields );
343 - } else {
344 - $prepared_search_fields = [
345 - 'name',
346 - 'slug',
347 - 'taxonomy',
348 - 'description',
349 - ];
350 - }
351 -
352 - /**
353 - * Filter fields to search on Term query
354 - *
355 - * @hook ep_term_search_fields
356 - * @param {array} $search_fields Search fields
357 - * @param {array} $query_vars Query variables
358 - * @since 3.4
359 - * @return {array} New search fields
360 - */
361 - $prepared_search_fields = apply_filters( 'ep_term_search_fields', $prepared_search_fields, $query_vars );
362 -
363 - $search_algorithm = $this->get_search_algorithm( $search, $prepared_search_fields, $query_vars );
364 - $formatted_args['query'] = $search_algorithm->get_query( 'term', $search, $prepared_search_fields, $query_vars );
365 - } else {
366 - $formatted_args['query']['match_all'] = [
367 - 'boost' => 1,
368 - ];
369 - }
370 -
371 - /**
372 - * Support `child_of` query var.
373 - */
374 - if ( ! empty( $query_vars['child_of'] ) && ( is_string( $taxonomy ) || count( $taxonomy ) < 2 ) ) {
375 - $filter['bool']['must'][]['bool']['must'][] = [
376 - 'match_phrase' => [
377 - 'hierarchy.ancestors.terms' => (int) $query_vars['child_of'],
378 - ],
379 - ];
380 -
381 - $use_filters = true;
382 - }
383 -
384 - /**
385 - * Support `parent` query var.
386 - */
387 - if ( isset( $query_vars['parent'] ) && '' !== $query_vars['parent'] ) {
388 - $filter['bool']['must'][]['bool']['must'] = [
389 - 'term' => [
390 - 'parent' => (int) $query_vars['parent'],
391 - ],
392 - ];
393 -
394 - $use_filters = true;
395 - }
396 -
397 - /**
398 - * Support `childless` query var.
399 - */
400 - if ( ! empty( $query_vars['childless'] ) ) {
401 - $filter['bool']['must'][]['bool']['must'] = [
402 - 'term' => [
403 - 'hierarchy.children.terms' => 0,
404 - ],
405 - ];
406 -
407 - $use_filters = true;
408 - }
409 -
410 - $meta_queries = [];
411 -
412 - /**
413 - * Support `meta_key`, `meta_value`, and `meta_compare` query args
414 - */
415 - if ( ! empty( $query_vars['meta_key'] ) ) {
416 - $meta_query_array = [
417 - 'key' => $query_vars['meta_key'],
418 - ];
419 -
420 - if ( isset( $query_vars['meta_value'] ) && '' !== $query_vars['meta_value'] ) {
421 - $meta_query_array['value'] = $query_vars['meta_value'];
422 - }
423 -
424 - if ( isset( $query_vars['meta_compare'] ) ) {
425 - $meta_query_array['compare'] = $query_vars['meta_compare'];
426 - }
427 -
428 - $meta_queries[] = $meta_query_array;
429 - }
430 -
431 - /**
432 - * Support 'meta_query' query var.
433 - */
434 - if ( ! empty( $query_vars['meta_query'] ) ) {
435 - $meta_queries = array_merge( $meta_queries, $query_vars['meta_query'] );
436 - }
437 -
438 - if ( ! empty( $meta_queries ) ) {
439 - $built_meta_queries = $this->build_meta_query( $meta_queries );
440 -
441 - if ( $built_meta_queries ) {
442 - $filter['bool']['must'][] = $built_meta_queries;
443 - $use_filters = true;
444 - }
445 - }
446 -
447 - /**
448 - * Support `fields` query var.
449 - */
450 - if ( isset( $query_vars['fields'] ) ) {
451 - switch ( $query_vars['fields'] ) {
452 - case 'ids':
453 - $formatted_args['_source'] = [
454 - 'includes' => [
455 - 'term_id',
456 - ],
457 - ];
458 - break;
459 -
460 - case 'id=>name':
461 - $formatted_args['_source'] = [
462 - 'includes' => [
463 - 'term_id',
464 - 'name',
465 - ],
466 - ];
467 - break;
468 -
469 - case 'id=>parent':
470 - $formatted_args['_source'] = [
471 - 'includes' => [
472 - 'term_id',
473 - 'parent',
474 - ],
475 - ];
476 - break;
477 -
478 - case 'id=>slug':
479 - $formatted_args['_source'] = [
480 - 'includes' => [
481 - 'term_id',
482 - 'slug',
483 - ],
484 - ];
485 - break;
486 -
487 - case 'names':
488 - $formatted_args['_source'] = [
489 - 'includes' => [
490 - 'name',
491 - ],
492 - ];
493 - break;
494 - case 'tt_ids':
495 - $formatted_args['_source'] = [
496 - 'includes' => [
497 - 'term_taxonomy_id',
498 - ],
499 - ];
500 - break;
501 - }
502 - }
503 -
504 - if ( $use_filters ) {
505 - $formatted_args['post_filter'] = $filter;
506 - }
507 -
508 - /**
509 85 * Filter full Elasticsearch query for Terms indexable
510 86 *
511 87 * @hook ep_term_formatted_args
512 88 * @param {array} $query Elasticsearch query
@@ -528,15 +104,14 @@
528 104
529 105 if ( empty( $es_version ) ) {
530 106 $es_version = apply_filters( 'ep_fallback_elasticsearch_version', '2.0' );
531 107 }
108 + $es_version = (string) $es_version;
532 109
533 - $mapping_file = 'initial.php';
110 + $mapping_file = '7-0.php';
534 111
535 - if ( version_compare( $es_version, '5.0', '<' ) ) {
536 - $mapping_file = 'pre-5-0.php';
537 - } elseif ( version_compare( $es_version, '7.0', '>=' ) ) {
538 - $mapping_file = '7-0.php';
112 + if ( version_compare( $es_version, '7.0', '<' ) ) {
113 + $mapping_file = 'initial.php';
539 114 }
540 115
541 116 /**
542 117 * Filter mapping file for Terms indexable
@@ -612,16 +187,19 @@
612 187 * @since 3.1
613 188 * @return array
614 189 */
615 190 public function query_db( $args ) {
616 -
617 191 $defaults = [
618 - 'number' => $this->get_bulk_items_per_page(),
619 - 'offset' => 0,
620 - 'orderby' => 'id',
621 - 'order' => 'desc',
622 - 'taxonomy' => $this->get_indexable_taxonomies(),
623 - 'hide_empty' => false,
192 + 'number' => $this->get_bulk_items_per_page(),
193 + 'offset' => 0,
194 + 'orderby' => 'id',
195 + 'order' => 'desc',
196 + 'taxonomy' => $this->get_indexable_taxonomies(),
197 + 'hide_empty' => false,
198 + 'hierarchical' => false,
199 + 'update_term_meta_cache' => false,
200 + 'cache_results' => false,
201 + 'ep_indexing_advanced_pagination' => true,
624 202 ];
625 203
626 204 if ( isset( $args['per_page'] ) ) {
627 205 $args['number'] = $args['per_page'];
@@ -626,8 +204,16 @@
626 204 if ( isset( $args['per_page'] ) ) {
627 205 $args['number'] = $args['per_page'];
628 206 }
629 207
208 + if ( isset( $args['include'] ) ) {
209 + $args['include'] = $args['include'];
210 + }
211 +
212 + if ( isset( $args['exclude'] ) ) {
213 + $args['exclude'] = $args['exclude'];
214 + }
215 +
630 216 /**
631 217 * Filter database arguments for term query
632 218 *
633 219 * @hook ep_term_query_db_args
@@ -642,33 +228,57 @@
642 228 unset( $all_query_args['number'] );
643 229 unset( $all_query_args['offset'] );
644 230 unset( $all_query_args['fields'] );
645 231
646 - /**
647 - * This just seems so inefficient.
648 - *
649 - * @todo Better way to do this?
650 - */
232 + if ( isset( $args['include'] ) || 0 < $args['offset'] ) {
233 + // Disable advanced pagination. Not useful if only indexing specific IDs.
234 + $args['ep_indexing_advanced_pagination'] = false;
235 + }
651 236
652 - /**
653 - * Filter database arguments for term count query
654 - *
655 - * @hook ep_term_all_query_db_args
656 - * @param {array} $args Query arguments based to WP_Term_Query
657 - * @since 3.4
658 - * @return {array} New arguments
659 - */
660 - $all_query = new WP_Term_Query( apply_filters( 'ep_term_all_query_db_args', $all_query_args, $args ) );
237 + // Explicitly set the orderby to ID to prevent accidental modifications by other code.
238 + add_filter( 'terms_clauses', [ $this, 'set_orderby' ], 9999, 3 );
661 239
662 - $total_objects = count( $all_query->terms );
240 + // Enforce the following query args during advanced pagination to ensure things work correctly.
241 + if ( $args['ep_indexing_advanced_pagination'] ) {
242 + $args = array_merge(
243 + $args,
244 + [
245 + 'suppress_filters' => false,
246 + 'orderby' => 'ID',
247 + 'order' => 'DESC',
248 + 'paged' => 1,
249 + 'offset' => 0,
250 + ]
251 + );
252 + add_filter( 'terms_clauses', [ $this, 'bulk_indexing_filter_terms_where' ], 9999, 3 );
663 253
664 - if ( ! empty( $args['offset'] ) ) {
665 - if ( (int) $args['offset'] >= $total_objects ) {
666 - $total_objects = 0;
254 + $query = new WP_Term_Query( $args );
255 + $total_objects = $this->get_total_objects_for_query( $args );
256 +
257 + remove_filter( 'terms_clauses', [ $this, 'bulk_indexing_filter_terms_where' ], 9999, 3 );
258 + } else {
259 +
260 + /**
261 + * Filter database arguments for term count query
262 + *
263 + * @hook ep_term_all_query_db_args
264 + * @param {array} $args Query arguments based to `wp_count_terms()`
265 + * @since 3.4
266 + * @return {array} New arguments
267 + */
268 + $total_objects = wp_count_terms( apply_filters( 'ep_term_all_query_db_args', $all_query_args, $args ) );
269 + $total_objects = ! is_wp_error( $total_objects ) ? (int) $total_objects : 0;
270 +
271 + if ( ! empty( $args['offset'] ) ) {
272 + if ( (int) $args['offset'] >= $total_objects ) {
273 + $total_objects = 0;
274 + }
667 275 }
276 +
277 + $query = new WP_Term_Query( $args );
668 278 }
669 279
670 - $query = new WP_Term_Query( $args );
280 + remove_filter( 'terms_clauses', [ $this, 'set_orderby' ], 9999, 3 );
671 281
672 282 if ( is_array( $query->terms ) ) {
673 283 array_walk( $query->terms, array( $this, 'remap_terms' ) );
674 284 }
@@ -679,8 +289,82 @@
679 289 ];
680 290 }
681 291
682 292 /**
293 + * Filters the WHERE clause of the SQL query used for bulk indexing terms by modifying it to include a range of
294 + * comment IDs based on advanced pagination parameters.
295 + *
296 + * @param array $clauses Associative array of the clauses for the query.
297 + * @param array $taxonomies An array of taxonomy names.
298 + * @param array $args An array of term query arguments.
299 + *
300 + * @return array The modified SQL WHERE clauses.
301 + */
302 + public function bulk_indexing_filter_terms_where( $clauses, $taxonomies, $args ) {
303 + $using_advanced_pagination = $args['ep_indexing_advanced_pagination'] ?? false;
304 +
305 + if ( $using_advanced_pagination ) {
306 + $requested_upper_limit_id = $args['ep_indexing_upper_limit_object_id'] ?? PHP_INT_MAX;
307 + $requested_lower_limit_object_id = $args['ep_indexing_lower_limit_object_id'] ?? 0;
308 + $last_processed_id = $args['ep_indexing_last_processed_object_id'] ?? null;
309 +
310 + // On the first loopthrough we begin with the requested upper limit ID. Afterwards, use the last processed ID to paginate.
311 + $upper_limit_range_object_id = $requested_upper_limit_id;
312 + if ( is_numeric( $last_processed_id ) ) {
313 + $upper_limit_range_object_id = $last_processed_id - 1;
314 + }
315 +
316 + // Sanitize. Abort if unexpected data at this point.
317 + if ( ! is_numeric( $upper_limit_range_object_id ) || ! is_numeric( $requested_lower_limit_object_id ) ) {
318 + return $clauses;
319 + }
320 +
321 + $range = [
322 + 'upper_limit' => "t.term_id <= {$upper_limit_range_object_id}",
323 + 'lower_limit' => "t.term_id >= {$requested_lower_limit_object_id}",
324 + ];
325 +
326 + // Skip the end range if it's unnecessary.
327 + $skip_ending_range = 0 === $requested_lower_limit_object_id;
328 + $where = $clauses['where'];
329 + $where = $skip_ending_range ? " {$range['upper_limit']} AND {$where}" : " {$range['upper_limit']} AND {$range['lower_limit']} AND {$where}";
330 +
331 + $clauses['where'] = $where;
332 + }
333 +
334 + return $clauses;
335 + }
336 +
337 + /**
338 + * Get the total number of terms for a given query.
339 + *
340 + * @param array $query_args The query args.
341 + * @return int The total number of terms.
342 + */
343 + protected function get_total_objects_for_query( $query_args ) {
344 + static $object_counts = [];
345 +
346 + // Reset the pagination-related args for optimal caching.
347 + $normalized_query_args = array_merge(
348 + $query_args,
349 + [
350 + 'offset' => 0,
351 + 'paged' => 1,
352 + 'posts_per_page' => 1,
353 + 'no_found_rows' => false,
354 + 'ep_indexing_last_processed_object_id' => null,
355 + ]
356 + );
357 +
358 + $cache_key = md5( get_current_blog_id() . wp_json_encode( $normalized_query_args ) );
359 + if ( ! isset( $object_counts[ $cache_key ] ) ) {
360 + $object_counts[ $cache_key ] = wp_count_terms( $normalized_query_args );
361 + }
362 +
363 + return $object_counts[ $cache_key ];
364 + }
365 +
366 + /**
683 367 * Returns indexable taxonomies for the current site
684 368 *
685 369 * @since 3.1
686 370 * @return mixed|void
@@ -788,13 +472,11 @@
788 472
789 473 if ( true === $allowed_protected_keys || in_array( $key, $allowed_protected_keys, true ) ) {
790 474 $allow_index = true;
791 475 }
792 - } else {
476 + } elseif ( true !== $excluded_public_keys && ! in_array( $key, $excluded_public_keys, true ) ) {
793 477
794 - if ( true !== $excluded_public_keys && ! in_array( $key, $excluded_public_keys, true ) ) {
795 - $allow_index = true;
796 - }
478 + $allow_index = true;
797 479 }
798 480
799 481 /**
800 482 * Filter kill switch for any term meta
@@ -927,9 +609,9 @@
927 609 }
928 610
929 611 if ( 'name' === $orderby ) {
930 612 $es_version = Elasticsearch::factory()->get_elasticsearch_version();
931 - $from_to['name'] = version_compare( $es_version, '7.0', '<' ) ? 'name.raw' : 'name.sortable';
613 + $from_to['name'] = version_compare( (string) $es_version, '7.0', '<' ) ? 'name.raw' : 'name.sortable';
932 614 }
933 615
934 616 $orderby = $from_to[ $orderby ] ?? $orderby;
935 617
@@ -941,5 +623,648 @@
941 623
942 624 return $sort;
943 625 }
944 626
627 + /**
628 + * Sanitize WP_Term_Query arguments to be used to create the ES query.
629 + *
630 + * @since 5.1.0
631 + * @param array $query_vars WP_Term_Query arguments
632 + * @return array
633 + */
634 + protected function sanitize_query_vars( $query_vars ) {
635 + if ( ! empty( $query_vars['get'] ) && 'all' === $query_vars['get'] ) {
636 + $query_vars['childless'] = false;
637 + $query_vars['child_of'] = 0;
638 + $query_vars['hide_empty'] = false;
639 + $query_vars['hierarchical'] = false;
640 + $query_vars['pad_counts'] = false;
641 + }
642 +
643 + $query_vars['taxonomy'] = ( ! empty( $query_vars['taxonomy'] ) ) ?
644 + (array) $query_vars['taxonomy'] :
645 + [];
646 +
647 + return $query_vars;
648 + }
649 +
650 + /**
651 + * Parse the `from` clause of the ES Query.
652 + *
653 + * @since 5.1.0
654 + * @param array $query_vars WP_Term_Query arguments
655 + * @return int
656 + */
657 + protected function parse_from( $query_vars ) {
658 + return ( isset( $query_vars['offset'] ) ) ? (int) $query_vars['offset'] : 0;
659 + }
660 +
661 + /**
662 + * Parse the `size` clause of the ES Query.
663 + *
664 + * @since 5.1.0
665 + * @param array $query_vars WP_Term_Query arguments
666 + * @return int
667 + */
668 + protected function parse_size( $query_vars ) {
669 + if ( ! empty( $query_vars['number'] ) ) {
670 + $number = (int) $query_vars['number'];
671 + } else {
672 + /**
673 + * Set the maximum results window size.
674 + *
675 + * The request will return a HTTP 500 Internal Error if the size of the
676 + * request is larger than the [index.max_result_window] parameter in ES.
677 + * See the scroll api for a more efficient way to request large data sets.
678 + *
679 + * @return int The max results window size.
680 + *
681 + * @since 2.3.0
682 + */
683 + $number = apply_filters( 'ep_max_results_window', 10000 );
684 + }
685 +
686 + return $number;
687 + }
688 +
689 + /**
690 + * Parse the order of results in the ES query.
691 + *
692 + * @since 5.1.0
693 + * @param array $formatted_args Formatted Elasticsearch query
694 + * @param array $query_vars WP_Term_Query arguments
695 + * @return array
696 + */
697 + protected function maybe_orderby( $formatted_args, $query_vars ) {
698 + // Set sort order, default is 'ASC'.
699 + if ( ! empty( $query_vars['order'] ) ) {
700 + $order = $this->parse_order( $query_vars['order'] );
701 + } else {
702 + $order = 'desc';
703 + }
704 +
705 + // Set orderby, default is 'name'.
706 + if ( empty( $query_vars['orderby'] ) ) {
707 + $query_vars['orderby'] = 'name';
708 + }
709 +
710 + // Set sort type.
711 + $formatted_args['sort'] = $this->parse_orderby( $query_vars['orderby'], $order, $query_vars );
712 +
713 + return $formatted_args;
714 + }
715 +
716 + /**
717 + * Based on WP_Term_Query arguments, parses the various filters that could be applied into the ES query.
718 + *
719 + * @since 5.1.0
720 + * @param array $query_vars WP_Term_Query arguments
721 + * @return array
722 + */
723 + protected function parse_filters( $query_vars ) {
724 + $filters = [
725 + 'taxonomy' => $this->parse_taxonomy( $query_vars ),
726 + 'object_ids' => $this->parse_object_ids( $query_vars ),
727 + 'include' => $this->parse_include( $query_vars ),
728 + 'exclude' => $this->parse_exclude( $query_vars ),
729 + 'exclude_tree' => $this->parse_exclude_tree( $query_vars ),
730 + 'name' => $this->parse_name( $query_vars ),
731 + 'slug' => $this->parse_slug( $query_vars ),
732 + 'term_taxonomy_id' => $this->parse_term_taxonomy_id( $query_vars ),
733 + 'hierarchical_hide_empty' => $this->parse_hierarchical_hide_empty( $query_vars ),
734 + 'child_of' => $this->parse_child_of( $query_vars ),
735 + 'parent' => $this->parse_parent( $query_vars ),
736 + 'childless' => $this->parse_childless( $query_vars ),
737 + 'meta_query' => $this->parse_meta_queries( $query_vars ),
738 + ];
739 +
740 + $filters = array_values( array_filter( $filters ) );
741 +
742 + if ( ! empty( $filters ) ) {
743 + $filters = [
744 + 'bool' => [
745 + 'must' => $filters,
746 + ],
747 + ];
748 + }
749 +
750 + return $filters;
751 + }
752 +
753 + /**
754 + * Parse the `taxonomy` WP Term Query arg and transform it into an ES query clause.
755 + *
756 + * @since 5.1.0
757 + * @param array $query_vars WP_Term_Query arguments
758 + * @return array
759 + */
760 + protected function parse_taxonomy( $query_vars ) {
761 + if ( empty( $query_vars['taxonomy'] ) ) {
762 + return [];
763 + }
764 +
765 + if ( count( $query_vars['taxonomy'] ) < 2 ) {
766 + return [
767 + 'term' => [
768 + 'taxonomy.raw' => $query_vars['taxonomy'][0],
769 + ],
770 + ];
771 + }
772 +
773 + return [
774 + 'terms' => [
775 + 'taxonomy.raw' => $query_vars['taxonomy'],
776 + ],
777 + ];
778 + }
779 +
780 + /**
781 + * Parse the `object_ids` WP Term Query arg and transform it into an ES query clause.
782 + *
783 + * @since 5.1.0
784 + * @param array $query_vars WP_Term_Query arguments
785 + * @return array
786 + */
787 + protected function parse_object_ids( $query_vars ) {
788 + if ( empty( $query_vars['object_ids'] ) ) {
789 + return [];
790 + }
791 +
792 + return [
793 + 'bool' => [
794 + 'must' => [
795 + 'terms' => [
796 + 'object_ids.value' => (array) $query_vars['object_ids'],
797 + ],
798 + ],
799 + ],
800 + ];
801 + }
802 +
803 + /**
804 + * Parse the `include` WP Term Query arg and transform it into an ES query clause.
805 + *
806 + * @since 5.1.0
807 + * @param array $query_vars WP_Term_Query arguments
808 + * @return array
809 + */
810 + protected function parse_include( $query_vars ) {
811 + if ( empty( $query_vars['include'] ) ) {
812 + return [];
813 + }
814 +
815 + return [
816 + 'bool' => [
817 + 'must' => [
818 + 'terms' => [
819 + 'term_id' => array_values( (array) $query_vars['include'] ),
820 + ],
821 + ],
822 + ],
823 + ];
824 + }
825 +
826 + /**
827 + * Parse the `exclude` WP Term Query arg and transform it into an ES query clause.
828 + *
829 + * @since 5.1.0
830 + * @param array $query_vars WP_Term_Query arguments
831 + * @return array
832 + */
833 + protected function parse_exclude( $query_vars ) {
834 + if ( ! empty( $query_vars['include'] ) || empty( $query_vars['exclude'] ) ) {
835 + return [];
836 + }
837 +
838 + return [
839 + 'bool' => [
840 + 'must_not' => [
841 + 'terms' => [
842 + 'term_id' => array_values( (array) $query_vars['exclude'] ),
843 + ],
844 + ],
845 + ],
846 + ];
847 + }
848 +
849 + /**
850 + * Parse the `exclude_tree` WP Term Query arg and transform it into an ES query clause.
851 + *
852 + * @since 5.1.0
853 + * @param array $query_vars WP_Term_Query arguments
854 + * @return array
855 + */
856 + protected function parse_exclude_tree( $query_vars ) {
857 + if ( ! empty( $query_vars['include'] ) || empty( $query_vars['exclude_tree'] ) ) {
858 + return [];
859 + }
860 +
861 + return [
862 + 'bool' => [
863 + 'must_not' => [
864 + [
865 + 'terms' => [
866 + 'term_id' => array_values( (array) $query_vars['exclude_tree'] ),
867 + ],
868 + ],
869 + [
870 + 'terms' => [
871 + 'parent' => array_values( (array) $query_vars['exclude_tree'] ),
872 + ],
873 + ],
874 + ],
875 + ],
876 + ];
877 + }
878 +
879 + /**
880 + * Parse the `name` WP Term Query arg and transform it into an ES query clause.
881 + *
882 + * @since 5.1.0
883 + * @param array $query_vars WP_Term_Query arguments
884 + * @return array
885 + */
886 + protected function parse_name( $query_vars ) {
887 + if ( empty( $query_vars['name'] ) ) {
888 + return [];
889 + }
890 +
891 + return [
892 + 'terms' => [
893 + 'name.raw' => (array) $query_vars['name'],
894 + ],
895 + ];
896 + }
897 +
898 + /**
899 + * Parse the `slug` WP Term Query arg and transform it into an ES query clause.
900 + *
901 + * @since 5.1.0
902 + * @param array $query_vars WP_Term_Query arguments
903 + * @return array
904 + */
905 + protected function parse_slug( $query_vars ) {
906 + if ( empty( $query_vars['slug'] ) ) {
907 + return [];
908 + }
909 +
910 + $query_vars['slug'] = (array) $query_vars['slug'];
911 + $query_vars['slug'] = array_map( 'sanitize_title', $query_vars['slug'] );
912 +
913 + return [
914 + 'terms' => [
915 + 'slug.raw' => (array) $query_vars['slug'],
916 + ],
917 + ];
918 + }
919 +
920 + /**
921 + * Parse the `term_taxonomy_id` WP Term Query arg and transform it into an ES query clause.
922 + *
923 + * @since 5.1.0
924 + * @param array $query_vars WP_Term_Query arguments
925 + * @return array
926 + */
927 + protected function parse_term_taxonomy_id( $query_vars ) {
928 + if ( empty( $query_vars['term_taxonomy_id'] ) ) {
929 + return [];
930 + }
931 +
932 + return [
933 + 'bool' => [
934 + 'must' => [
935 + 'terms' => [
936 + 'term_taxonomy_id' => array_values( (array) $query_vars['term_taxonomy_id'] ),
937 + ],
938 + ],
939 + ],
940 + ];
941 + }
942 +
943 + /**
944 + * Parse the `hide_empty` and `hierarchical` WP Term Query args and transform them into ES query clauses.
945 + *
946 + * `hierarchical` needs to work in conjunction with `hide_empty`, as per WP docs:
947 + * > `hierarchical`: Whether to include terms that have non-empty descendants (even if $hide_empty is set to true).
948 + *
949 + * In summary:
950 + * - hide_empty AND hierarchical: count > 1 OR hierarchy.children > 1
951 + * - hide_empty AND NOT hierarchical: count > 1 (ignore hierarchy.children)
952 + * - NOT hide_empty (AND hierarchical): there is no need to limit the query
953 + *
954 + * @see https://developer.wordpress.org/reference/classes/WP_Term_Query/__construct/
955 + * @since 5.1.0
956 + * @param array $query_vars WP_Term_Query arguments
957 + * @return array
958 + */
959 + protected function parse_hierarchical_hide_empty( $query_vars ) {
960 + $hide_empty = isset( $query_vars['hide_empty'] ) ? $query_vars['hide_empty'] : '';
961 + if ( ! $hide_empty ) {
962 + return [];
963 + }
964 +
965 + $hierarchical = isset( $query_vars['hierarchical'] ) ? $query_vars['hierarchical'] : '';
966 + if ( ! $hierarchical ) {
967 + return [
968 + 'range' => [
969 + 'count' => [
970 + 'gte' => 1,
971 + ],
972 + ],
973 + ];
974 + }
975 +
976 + return [
977 + 'bool' => [
978 + 'should' => [
979 + [
980 + 'range' => [
981 + 'count' => [
982 + 'gte' => 1,
983 + ],
984 + ],
985 + ],
986 + [
987 + 'range' => [
988 + 'hierarchy.children.count' => [
989 + 'gte' => 1,
990 + ],
991 + ],
992 + ],
993 + ],
994 + ],
995 + ];
996 + }
997 +
998 + /**
999 + * Parse the `child_of` WP Term Query arg and transform it into an ES query clause.
1000 + *
1001 + * @since 5.1.0
1002 + * @param array $query_vars WP_Term_Query arguments
1003 + * @return array
1004 + */
1005 + protected function parse_child_of( $query_vars ) {
1006 + if ( empty( $query_vars['child_of'] ) || count( $query_vars['taxonomy'] ) > 1 ) {
1007 + return [];
1008 + }
1009 +
1010 + return [
1011 + 'bool' => [
1012 + 'must' => [
1013 + 'match_phrase' => [
1014 + 'hierarchy.ancestors.terms' => (int) $query_vars['child_of'],
1015 + ],
1016 + ],
1017 + ],
1018 + ];
1019 + }
1020 +
1021 + /**
1022 + * Parse the `parent` WP Term Query arg and transform it into an ES query clause.
1023 + *
1024 + * @since 5.1.0
1025 + * @param array $query_vars WP_Term_Query arguments
1026 + * @return array
1027 + */
1028 + protected function parse_parent( $query_vars ) {
1029 + if ( ! isset( $query_vars['parent'] ) || '' === $query_vars['parent'] ) {
1030 + return [];
1031 + }
1032 +
1033 + return [
1034 + 'bool' => [
1035 + 'must' => [
1036 + 'term' => [
1037 + 'parent' => (int) $query_vars['parent'],
1038 + ],
1039 + ],
1040 + ],
1041 + ];
1042 + }
1043 +
1044 + /**
1045 + * Parse the `childless` WP Term Query arg and transform it into an ES query clause.
1046 + *
1047 + * @since 5.1.0
1048 + * @param array $query_vars WP_Term_Query arguments
1049 + * @return array
1050 + */
1051 + protected function parse_childless( $query_vars ) {
1052 + if ( empty( $query_vars['childless'] ) ) {
1053 + return [];
1054 + }
1055 +
1056 + return [
1057 + 'bool' => [
1058 + 'must' => [
1059 + 'term' => [
1060 + 'hierarchy.children.terms' => 0,
1061 + ],
1062 + ],
1063 + ],
1064 + ];
1065 + }
1066 +
1067 + /**
1068 + * Parse WP Term Query meta queries and transform them into ES query clauses.
1069 + *
1070 + * @since 5.1.0
1071 + * @param array $query_vars WP_Term_Query arguments
1072 + * @return array
1073 + */
1074 + protected function parse_meta_queries( $query_vars ) {
1075 + $meta_queries = [];
1076 + /**
1077 + * Support `meta_key`, `meta_value`, and `meta_compare` query args
1078 + */
1079 + if ( ! empty( $query_vars['meta_key'] ) ) {
1080 + $meta_query_array = [
1081 + 'key' => $query_vars['meta_key'],
1082 + ];
1083 +
1084 + if ( isset( $query_vars['meta_value'] ) && '' !== $query_vars['meta_value'] ) {
1085 + $meta_query_array['value'] = $query_vars['meta_value'];
1086 + }
1087 +
1088 + if ( isset( $query_vars['meta_compare'] ) ) {
1089 + $meta_query_array['compare'] = $query_vars['meta_compare'];
1090 + }
1091 +
1092 + $meta_queries[] = $meta_query_array;
1093 + }
1094 +
1095 + /**
1096 + * Support 'meta_query' query var.
1097 + */
1098 + if ( ! empty( $query_vars['meta_query'] ) ) {
1099 + $meta_queries = array_merge( $meta_queries, $query_vars['meta_query'] );
1100 + }
1101 +
1102 + if ( ! empty( $meta_queries ) ) {
1103 + $built_meta_queries = $this->build_meta_query( $meta_queries );
1104 +
1105 + if ( $built_meta_queries ) {
1106 + return $built_meta_queries;
1107 + }
1108 + }
1109 +
1110 + return [];
1111 + }
1112 +
1113 + /**
1114 + * If in a search context, using `name__like`, or `description__like` set search fields, otherwise query everything.
1115 + *
1116 + * @since 5.1.0
1117 + * @param array $formatted_args Formatted Elasticsearch query
1118 + * @param array $query_vars WP_Term_Query arguments
1119 + * @return array
1120 + */
1121 + protected function maybe_set_search_fields( $formatted_args, $query_vars ) {
1122 + if ( empty( $query_vars['search'] ) && empty( $query_vars['name__like'] ) && empty( $query_vars['description__like'] ) ) {
1123 + $formatted_args['query']['match_all'] = [
1124 + 'boost' => 1,
1125 + ];
1126 +
1127 + return $formatted_args;
1128 + }
1129 +
1130 + $search = ! empty( $query_vars['search'] ) ? $query_vars['search'] : '';
1131 + $search_fields = [];
1132 +
1133 + if ( ! empty( $query_vars['name__like'] ) ) {
1134 + $search = $query_vars['name__like'];
1135 + $search_fields[] = 'name';
1136 + }
1137 +
1138 + if ( ! empty( $query_vars['description__like'] ) ) {
1139 + $search = $query_vars['description__like'];
1140 + $search_fields[] = 'description';
1141 + }
1142 +
1143 + /**
1144 + * Allow for search field specification
1145 + */
1146 + if ( ! empty( $query_vars['search_fields'] ) ) {
1147 + $search_fields = $query_vars['search_fields'];
1148 + }
1149 +
1150 + if ( ! empty( $search_fields ) ) {
1151 + $prepared_search_fields = [];
1152 +
1153 + if ( ! empty( $search_fields['meta'] ) ) {
1154 + $metas = (array) $search_fields['meta'];
1155 +
1156 + foreach ( $metas as $meta ) {
1157 + $prepared_search_fields[] = 'meta.' . $meta . '.value';
1158 + }
1159 +
1160 + unset( $search_fields['meta'] );
1161 + }
1162 +
1163 + $prepared_search_fields = array_merge( $search_fields, $prepared_search_fields );
1164 + } else {
1165 + $prepared_search_fields = [
1166 + 'name',
1167 + 'slug',
1168 + 'taxonomy',
1169 + 'description',
1170 + ];
1171 + }
1172 +
1173 + /**
1174 + * Filter fields to search on Term query
1175 + *
1176 + * @hook ep_term_search_fields
1177 + * @param {array} $search_fields Search fields
1178 + * @param {array} $query_vars Query variables
1179 + * @since 3.4
1180 + * @return {array} New search fields
1181 + */
1182 + $prepared_search_fields = apply_filters( 'ep_term_search_fields', $prepared_search_fields, $query_vars );
1183 +
1184 + $search_algorithm = $this->get_search_algorithm( $search, $prepared_search_fields, $query_vars );
1185 + $formatted_args['query'] = $search_algorithm->get_query( 'term', $search, $prepared_search_fields, $query_vars );
1186 +
1187 + return $formatted_args;
1188 + }
1189 +
1190 + /**
1191 + * If needed set the `fields` ES query clause.
1192 + *
1193 + * @since 5.1.0
1194 + * @param array $formatted_args Formatted Elasticsearch query
1195 + * @param array $query_vars WP_Term_Query arguments
1196 + * @return array
1197 + */
1198 + protected function maybe_set_fields( $formatted_args, $query_vars ) {
1199 + if ( ! isset( $query_vars['fields'] ) ) {
1200 + return $formatted_args;
1201 + }
1202 +
1203 + switch ( $query_vars['fields'] ) {
1204 + case 'ids':
1205 + $formatted_args['_source'] = [
1206 + 'includes' => [
1207 + 'term_id',
1208 + ],
1209 + ];
1210 + break;
1211 +
1212 + case 'id=>name':
1213 + $formatted_args['_source'] = [
1214 + 'includes' => [
1215 + 'term_id',
1216 + 'name',
1217 + ],
1218 + ];
1219 + break;
1220 +
1221 + case 'id=>parent':
1222 + $formatted_args['_source'] = [
1223 + 'includes' => [
1224 + 'term_id',
1225 + 'parent',
1226 + ],
1227 + ];
1228 + break;
1229 +
1230 + case 'id=>slug':
1231 + $formatted_args['_source'] = [
1232 + 'includes' => [
1233 + 'term_id',
1234 + 'slug',
1235 + ],
1236 + ];
1237 + break;
1238 +
1239 + case 'names':
1240 + $formatted_args['_source'] = [
1241 + 'includes' => [
1242 + 'name',
1243 + ],
1244 + ];
1245 + break;
1246 + case 'tt_ids':
1247 + $formatted_args['_source'] = [
1248 + 'includes' => [
1249 + 'term_taxonomy_id',
1250 + ],
1251 + ];
1252 + break;
1253 + }
1254 +
1255 + return $formatted_args;
1256 + }
1257 +
1258 + /**
1259 + * Sets the ORDER BY clause for term queries to order terms by their term_id.
1260 + *
1261 + * @param array $clauses The SQL clauses array to modify.
1262 + * @return array The modified SQL clauses array with the ORDER BY clause set to term_id.
1263 + *
1264 + * @since 5.2.0
1265 + */
1266 + public function set_orderby( $clauses ): array {
1267 + $clauses['orderby'] = 'ORDER BY t.term_id';
1268 + return $clauses;
1269 + }
945 1270 }