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 +805 -489 4.7.15.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,18 +31,15 @@
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() {
40 - $this->labels = [
41 - 'plural' => esc_html__( 'Terms', 'elasticpress' ),
42 - 'singular' => esc_html__( 'Term', 'elasticpress' ),
43 - ];
44 - }
41 + public $support_indexing_advanced_pagination = true;
45 42
46 43 /**
47 44 * Instantiate the indexable SyncManager and QueryIntegration, the main responsibles for the WP integration.
48 45 *
@@ -49,8 +46,13 @@
49 46 * @since 4.5.0
50 47 * @return void
51 48 */
52 49 public function setup() {
50 + $this->labels = [
51 + 'plural' => esc_html__( 'Terms', 'elasticpress' ),
52 + 'singular' => esc_html__( 'Term', 'elasticpress' ),
53 + ];
54 +
53 55 $this->sync_manager = new SyncManager( $this->slug );
54 56 $this->query_integration = new QueryIntegration( $this->slug );
55 57 }
56 58
@@ -61,460 +63,26 @@
61 63 * @since 3.1
62 64 * @return array
63 65 */
64 66 public function format_args( $query_vars ) {
65 - /**
66 - * Support `number` query var
67 - */
68 - if ( ! empty( $query_vars['number'] ) ) {
69 - $number = (int) $query_vars['number'];
70 - } else {
71 - /**
72 - * Set the maximum results window size.
73 - *
74 - * The request will return a HTTP 500 Internal Error if the size of the
75 - * request is larger than the [index.max_result_window] parameter in ES.
76 - * See the scroll api for a more efficient way to request large data sets.
77 - *
78 - * @return int The max results window size.
79 - *
80 - * @since 2.3.0
81 - */
82 - $number = apply_filters( 'ep_max_results_window', 10000 );
83 - }
67 + $query_vars = $this->sanitize_query_vars( $query_vars );
84 68
85 69 $formatted_args = [
86 - 'from' => 0,
87 - 'size' => $number,
70 + 'from' => $this->parse_from( $query_vars ),
71 + 'size' => $this->parse_size( $query_vars ),
88 72 ];
89 73
90 - /**
91 - * Support `offset` query var
92 - */
93 - if ( isset( $query_vars['offset'] ) ) {
94 - $formatted_args['from'] = (int) $query_vars['offset'];
95 - }
74 + $formatted_args = $this->maybe_orderby( $formatted_args, $query_vars );
96 75
97 - /**
98 - * Support `order` and `orderby` query vars
99 - */
100 -
101 - // Set sort order, default is 'ASC'.
102 - if ( ! empty( $query_vars['order'] ) ) {
103 - $order = $this->parse_order( $query_vars['order'] );
104 - } else {
105 - $order = 'desc';
76 + $filters = $this->parse_filters( $query_vars );
77 + if ( ! empty( $filters ) ) {
78 + $formatted_args['post_filter'] = $filters;
106 79 }
107 80
108 - // Set orderby, default is 'name'.
109 - if ( empty( $query_vars['orderby'] ) ) {
110 - $query_vars['orderby'] = 'name';
111 - }
81 + $formatted_args = $this->maybe_set_search_fields( $formatted_args, $query_vars );
82 + $formatted_args = $this->maybe_set_fields( $formatted_args, $query_vars );
112 83
113 - // Set sort type.
114 - $formatted_args['sort'] = $this->parse_orderby( $query_vars['orderby'], $order, $query_vars );
115 -
116 - $filter = [
117 - 'bool' => [
118 - 'must' => [],
119 - ],
120 - ];
121 -
122 - $use_filters = false;
123 -
124 84 /**
125 - * Support `taxonomy` query var
126 - */
127 - $taxonomy = [];
128 - if ( ! empty( $query_vars['taxonomy'] ) ) {
129 - $taxonomy = (array) $query_vars['taxonomy'];
130 - $terms_map_name = 'terms';
131 -
132 - if ( count( $taxonomy ) < 2 ) {
133 - $terms_map_name = 'term';
134 - $taxonomy = $taxonomy[0];
135 - }
136 -
137 - $filter['bool']['must'][] = [
138 - $terms_map_name => [
139 - 'taxonomy.raw' => $taxonomy,
140 - ],
141 - ];
142 -
143 - $use_filters = true;
144 - }
145 -
146 - /**
147 - * Support `object_ids` query var
148 - */
149 - if ( ! empty( $query_vars['object_ids'] ) ) {
150 - $filter['bool']['must'][]['bool']['must'][] = [
151 - 'terms' => [
152 - 'object_ids.value' => (array) $query_vars['object_ids'],
153 - ],
154 - ];
155 -
156 - $use_filters = true;
157 - }
158 -
159 - /**
160 - * Support `get` query var
161 - */
162 - if ( ! empty( $query_vars['get'] ) && 'all' === $query_vars['get'] ) {
163 - $query_vars['childless'] = false;
164 - $query_vars['child_of'] = 0;
165 - $query_vars['hide_empty'] = false;
166 - $query_vars['hierarchical'] = false;
167 - $query_vars['pad_counts'] = false;
168 - }
169 -
170 - /**
171 - * Support `include` query var
172 - */
173 - if ( ! empty( $query_vars['include'] ) ) {
174 - $filter['bool']['must'][]['bool']['must'] = [
175 - 'terms' => [
176 - 'term_id' => array_values( (array) $query_vars['include'] ),
177 - ],
178 - ];
179 -
180 - $use_filters = true;
181 - }
182 -
183 - /**
184 - * Support `exclude` query var
185 - */
186 - if ( empty( $query_vars['include'] ) && ! empty( $query_vars['exclude'] ) ) {
187 - $filter['bool']['must'][]['bool']['must_not'] = [
188 - 'terms' => [
189 - 'term_id' => array_values( (array) $query_vars['exclude'] ),
190 - ],
191 - ];
192 -
193 - $use_filters = true;
194 - }
195 -
196 - /**
197 - * Support `exclude_tree` query var
198 - */
199 - if ( empty( $query_vars['include'] ) && ! empty( $query_vars['exclude_tree'] ) ) {
200 - $filter['bool']['must'][]['bool']['must_not'] = [
201 - 'terms' => [
202 - 'term_id' => array_values( (array) $query_vars['exclude_tree'] ),
203 - ],
204 - ];
205 -
206 - $filter['bool']['must'][]['bool']['must_not'] = [
207 - 'terms' => [
208 - 'parent' => array_values( (array) $query_vars['exclude_tree'] ),
209 - ],
210 - ];
211 -
212 - $use_filters = true;
213 - }
214 -
215 - /**
216 - * Support `name` query var
217 - */
218 - if ( ! empty( $query_vars['name'] ) ) {
219 - $filter['bool']['must'][] = [
220 - 'terms' => [
221 - 'name.raw' => (array) $query_vars['name'],
222 - ],
223 - ];
224 -
225 - $use_filters = true;
226 - }
227 -
228 - /**
229 - * Support `slug` query var
230 - */
231 - if ( ! empty( $query_vars['slug'] ) ) {
232 - if ( ! is_array( $query_vars['slug'] ) ) {
233 - $query_vars['slug'] = array( $query_vars['slug'] );
234 - }
235 -
236 - $query_vars['slug'] = array_map( 'sanitize_title', $query_vars['slug'] );
237 -
238 - $filter['bool']['must'][] = [
239 - 'terms' => [
240 - 'slug.raw' => (array) $query_vars['slug'],
241 - ],
242 - ];
243 -
244 - $use_filters = true;
245 - }
246 -
247 - /**
248 - * Support `term_taxonomy_id` query var
249 - */
250 - if ( ! empty( $query_vars['term_taxonomy_id'] ) ) {
251 - $filter['bool']['must'][]['bool']['must'] = [
252 - 'terms' => [
253 - 'term_taxonomy_id' => array_values( (array) $query_vars['term_taxonomy_id'] ),
254 - ],
255 - ];
256 -
257 - $use_filters = true;
258 - }
259 -
260 - /**
261 - * Support `hierarchical` and `hide_empty` query var
262 - *
263 - * `hierarchical` needs to work in conjunction with `hide_empty`, as per WP docs:
264 - * > `hierarchical`: Whether to include terms that have non-empty descendants (even if $hide_empty is set to true).
265 - *
266 - * In summary:
267 - * - hide_empty AND hierarchical: count > 1 OR hierarchy.children > 1
268 - * - hide_empty AND NOT hierarchical: count > 1 (ignore hierarchy.children)
269 - * - NOT hide_empty (AND hierarchical): there is no need to limit the query
270 - *
271 - * @see https://developer.wordpress.org/reference/classes/WP_Term_Query/__construct/
272 - */
273 - $hide_empty = isset( $query_vars['hide_empty'] ) ? $query_vars['hide_empty'] : '';
274 - if ( $hide_empty ) {
275 - $hierarchical = isset( $query_vars['hierarchical'] ) ? $query_vars['hierarchical'] : '';
276 -
277 - if ( $hierarchical ) {
278 - $filter_clause = [ 'bool' => [ 'should' => [] ] ];
279 -
280 - $filter_clause['bool']['should'][] = [
281 - 'range' => [
282 - 'count' => [
283 - 'gte' => 1,
284 - ],
285 - ],
286 - ];
287 -
288 - $filter_clause['bool']['should'][] = [
289 - 'range' => [
290 - 'hierarchy.children.count' => [
291 - 'gte' => 1,
292 - ],
293 - ],
294 - ];
295 -
296 - $filter['bool']['must'][] = $filter_clause;
297 -
298 - $use_filters = true;
299 - } else {
300 - $filter['bool']['must'][] = [
301 - 'range' => [
302 - 'count' => [
303 - 'gte' => 1,
304 - ],
305 - ],
306 - ];
307 - }
308 -
309 - $use_filters = true;
310 - }
311 -
312 - /**
313 - * Support `search`, `name__like` and `description__like` query_vars
314 - */
315 - if ( ! empty( $query_vars['search'] ) || ! empty( $query_vars['name__like'] ) || ! empty( $query_vars['description__like'] ) ) {
316 -
317 - $search = ! empty( $query_vars['search'] ) ? $query_vars['search'] : '';
318 - $search_fields = [];
319 -
320 - if ( ! empty( $query_vars['name__like'] ) ) {
321 - $search = $query_vars['name__like'];
322 - $search_fields[] = 'name';
323 - }
324 -
325 - if ( ! empty( $query_vars['description__like'] ) ) {
326 - $search = $query_vars['description__like'];
327 - $search_fields[] = 'description';
328 - }
329 -
330 - /**
331 - * Allow for search field specification
332 - */
333 - if ( ! empty( $query_vars['search_fields'] ) ) {
334 - $search_fields = $query_vars['search_fields'];
335 - }
336 -
337 - if ( ! empty( $search_fields ) ) {
338 - $prepared_search_fields = [];
339 -
340 - if ( ! empty( $search_fields['meta'] ) ) {
341 - $metas = (array) $search_fields['meta'];
342 -
343 - foreach ( $metas as $meta ) {
344 - $prepared_search_fields[] = 'meta.' . $meta . '.value';
345 - }
346 -
347 - unset( $search_fields['meta'] );
348 - }
349 -
350 - $prepared_search_fields = array_merge( $search_fields, $prepared_search_fields );
351 - } else {
352 - $prepared_search_fields = [
353 - 'name',
354 - 'slug',
355 - 'taxonomy',
356 - 'description',
357 - ];
358 - }
359 -
360 - /**
361 - * Filter fields to search on Term query
362 - *
363 - * @hook ep_term_search_fields
364 - * @param {array} $search_fields Search fields
365 - * @param {array} $query_vars Query variables
366 - * @since 3.4
367 - * @return {array} New search fields
368 - */
369 - $prepared_search_fields = apply_filters( 'ep_term_search_fields', $prepared_search_fields, $query_vars );
370 -
371 - $search_algorithm = $this->get_search_algorithm( $search, $prepared_search_fields, $query_vars );
372 - $formatted_args['query'] = $search_algorithm->get_query( 'term', $search, $prepared_search_fields, $query_vars );
373 - } else {
374 - $formatted_args['query']['match_all'] = [
375 - 'boost' => 1,
376 - ];
377 - }
378 -
379 - /**
380 - * Support `child_of` query var.
381 - */
382 - if ( ! empty( $query_vars['child_of'] ) && ( is_string( $taxonomy ) || count( $taxonomy ) < 2 ) ) {
383 - $filter['bool']['must'][]['bool']['must'][] = [
384 - 'match_phrase' => [
385 - 'hierarchy.ancestors.terms' => (int) $query_vars['child_of'],
386 - ],
387 - ];
388 -
389 - $use_filters = true;
390 - }
391 -
392 - /**
393 - * Support `parent` query var.
394 - */
395 - if ( isset( $query_vars['parent'] ) && '' !== $query_vars['parent'] ) {
396 - $filter['bool']['must'][]['bool']['must'] = [
397 - 'term' => [
398 - 'parent' => (int) $query_vars['parent'],
399 - ],
400 - ];
401 -
402 - $use_filters = true;
403 - }
404 -
405 - /**
406 - * Support `childless` query var.
407 - */
408 - if ( ! empty( $query_vars['childless'] ) ) {
409 - $filter['bool']['must'][]['bool']['must'] = [
410 - 'term' => [
411 - 'hierarchy.children.terms' => 0,
412 - ],
413 - ];
414 -
415 - $use_filters = true;
416 - }
417 -
418 - $meta_queries = [];
419 -
420 - /**
421 - * Support `meta_key`, `meta_value`, and `meta_compare` query args
422 - */
423 - if ( ! empty( $query_vars['meta_key'] ) ) {
424 - $meta_query_array = [
425 - 'key' => $query_vars['meta_key'],
426 - ];
427 -
428 - if ( isset( $query_vars['meta_value'] ) && '' !== $query_vars['meta_value'] ) {
429 - $meta_query_array['value'] = $query_vars['meta_value'];
430 - }
431 -
432 - if ( isset( $query_vars['meta_compare'] ) ) {
433 - $meta_query_array['compare'] = $query_vars['meta_compare'];
434 - }
435 -
436 - $meta_queries[] = $meta_query_array;
437 - }
438 -
439 - /**
440 - * Support 'meta_query' query var.
441 - */
442 - if ( ! empty( $query_vars['meta_query'] ) ) {
443 - $meta_queries = array_merge( $meta_queries, $query_vars['meta_query'] );
444 - }
445 -
446 - if ( ! empty( $meta_queries ) ) {
447 - $built_meta_queries = $this->build_meta_query( $meta_queries );
448 -
449 - if ( $built_meta_queries ) {
450 - $filter['bool']['must'][] = $built_meta_queries;
451 - $use_filters = true;
452 - }
453 - }
454 -
455 - /**
456 - * Support `fields` query var.
457 - */
458 - if ( isset( $query_vars['fields'] ) ) {
459 - switch ( $query_vars['fields'] ) {
460 - case 'ids':
461 - $formatted_args['_source'] = [
462 - 'includes' => [
463 - 'term_id',
464 - ],
465 - ];
466 - break;
467 -
468 - case 'id=>name':
469 - $formatted_args['_source'] = [
470 - 'includes' => [
471 - 'term_id',
472 - 'name',
473 - ],
474 - ];
475 - break;
476 -
477 - case 'id=>parent':
478 - $formatted_args['_source'] = [
479 - 'includes' => [
480 - 'term_id',
481 - 'parent',
482 - ],
483 - ];
484 - break;
485 -
486 - case 'id=>slug':
487 - $formatted_args['_source'] = [
488 - 'includes' => [
489 - 'term_id',
490 - 'slug',
491 - ],
492 - ];
493 - break;
494 -
495 - case 'names':
496 - $formatted_args['_source'] = [
497 - 'includes' => [
498 - 'name',
499 - ],
500 - ];
501 - break;
502 - case 'tt_ids':
503 - $formatted_args['_source'] = [
504 - 'includes' => [
505 - 'term_taxonomy_id',
506 - ],
507 - ];
508 - break;
509 - }
510 - }
511 -
512 - if ( $use_filters ) {
513 - $formatted_args['post_filter'] = $filter;
514 - }
515 -
516 - /**
517 85 * Filter full Elasticsearch query for Terms indexable
518 86 *
519 87 * @hook ep_term_formatted_args
520 88 * @param {array} $query Elasticsearch query
@@ -538,14 +106,12 @@
538 106 $es_version = apply_filters( 'ep_fallback_elasticsearch_version', '2.0' );
539 107 }
540 108 $es_version = (string) $es_version;
541 109
542 - $mapping_file = 'initial.php';
110 + $mapping_file = '7-0.php';
543 111
544 - if ( version_compare( $es_version, '5.0', '<' ) ) {
545 - $mapping_file = 'pre-5-0.php';
546 - } elseif ( version_compare( $es_version, '7.0', '>=' ) ) {
547 - $mapping_file = '7-0.php';
112 + if ( version_compare( $es_version, '7.0', '<' ) ) {
113 + $mapping_file = 'initial.php';
548 114 }
549 115
550 116 /**
551 117 * Filter mapping file for Terms indexable
@@ -621,16 +187,19 @@
621 187 * @since 3.1
622 188 * @return array
623 189 */
624 190 public function query_db( $args ) {
625 -
626 191 $defaults = [
627 - 'number' => $this->get_bulk_items_per_page(),
628 - 'offset' => 0,
629 - 'orderby' => 'id',
630 - 'order' => 'desc',
631 - 'taxonomy' => $this->get_indexable_taxonomies(),
632 - '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,
633 202 ];
634 203
635 204 if ( isset( $args['per_page'] ) ) {
636 205 $args['number'] = $args['per_page'];
@@ -635,8 +204,16 @@
635 204 if ( isset( $args['per_page'] ) ) {
636 205 $args['number'] = $args['per_page'];
637 206 }
638 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 +
639 216 /**
640 217 * Filter database arguments for term query
641 218 *
642 219 * @hook ep_term_query_db_args
@@ -651,33 +228,57 @@
651 228 unset( $all_query_args['number'] );
652 229 unset( $all_query_args['offset'] );
653 230 unset( $all_query_args['fields'] );
654 231
655 - /**
656 - * This just seems so inefficient.
657 - *
658 - * @todo Better way to do this?
659 - */
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 + }
660 236
661 - /**
662 - * Filter database arguments for term count query
663 - *
664 - * @hook ep_term_all_query_db_args
665 - * @param {array} $args Query arguments based to WP_Term_Query
666 - * @since 3.4
667 - * @return {array} New arguments
668 - */
669 - $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 );
670 239
671 - $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 );
672 253
673 - if ( ! empty( $args['offset'] ) ) {
674 - if ( (int) $args['offset'] >= $total_objects ) {
675 - $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 + }
676 275 }
276 +
277 + $query = new WP_Term_Query( $args );
677 278 }
678 279
679 - $query = new WP_Term_Query( $args );
280 + remove_filter( 'terms_clauses', [ $this, 'set_orderby' ], 9999, 3 );
680 281
681 282 if ( is_array( $query->terms ) ) {
682 283 array_walk( $query->terms, array( $this, 'remap_terms' ) );
683 284 }
@@ -688,8 +289,82 @@
688 289 ];
689 290 }
690 291
691 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 + /**
692 367 * Returns indexable taxonomies for the current site
693 368 *
694 369 * @since 3.1
695 370 * @return mixed|void
@@ -797,13 +472,11 @@
797 472
798 473 if ( true === $allowed_protected_keys || in_array( $key, $allowed_protected_keys, true ) ) {
799 474 $allow_index = true;
800 475 }
801 - } else {
476 + } elseif ( true !== $excluded_public_keys && ! in_array( $key, $excluded_public_keys, true ) ) {
802 477
803 - if ( true !== $excluded_public_keys && ! in_array( $key, $excluded_public_keys, true ) ) {
804 - $allow_index = true;
805 - }
478 + $allow_index = true;
806 479 }
807 480
808 481 /**
809 482 * Filter kill switch for any term meta
@@ -950,5 +623,648 @@
950 623
951 624 return $sort;
952 625 }
953 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 + }
954 1270 }