labels = [ 'plural' => esc_html__( 'Terms', 'elasticpress' ), 'singular' => esc_html__( 'Term', 'elasticpress' ), ]; $this->sync_manager = new SyncManager( $this->slug ); $this->query_integration = new QueryIntegration( $this->slug ); } /** * Format query vars into ES query * * @param array $query_vars WP_Term_Query args. * @since 3.1 * @return array */ public function format_args( $query_vars ) { $query_vars = $this->sanitize_query_vars( $query_vars ); $formatted_args = [ 'from' => $this->parse_from( $query_vars ), 'size' => $this->parse_size( $query_vars ), ]; $formatted_args = $this->maybe_orderby( $formatted_args, $query_vars ); $filters = $this->parse_filters( $query_vars ); if ( ! empty( $filters ) ) { $formatted_args['post_filter'] = $filters; } $formatted_args = $this->maybe_set_search_fields( $formatted_args, $query_vars ); $formatted_args = $this->maybe_set_fields( $formatted_args, $query_vars ); /** * Filter full Elasticsearch query for Terms indexable * * @hook ep_term_formatted_args * @param {array} $query Elasticsearch query * @param {array} $query_vars Query variables * @since 3.4 * @return {array} New query */ return apply_filters( 'ep_term_formatted_args', $formatted_args, $query_vars ); } /** * Generate the mapping array * * @since 3.6.0 * @return array */ public function generate_mapping() { $es_version = Elasticsearch::factory()->get_elasticsearch_version(); if ( empty( $es_version ) ) { $es_version = apply_filters( 'ep_fallback_elasticsearch_version', '2.0' ); } $es_version = (string) $es_version; $mapping_file = '7-0.php'; if ( version_compare( $es_version, '7.0', '<' ) ) { $mapping_file = 'initial.php'; } /** * Filter mapping file for Terms indexable * * @hook ep_term_mapping_file * @param {string} $file File name * @since 3.4 * @return {string} New file name */ $mapping = require apply_filters( 'ep_term_mapping_file', __DIR__ . '/../../../mappings/term/' . $mapping_file ); /** * Filter full Elasticsearch query for Terms indexable * * @hook ep_term_mapping * @param {array} $mapping Elasticsearch mapping * @since 3.4 * @return {array} New mapping */ $mapping = apply_filters( 'ep_term_mapping', $mapping ); return $mapping; } /** * Prepare a term document for indexing * * @param int $term_id Term ID * @since 3.1 * @return bool|array */ public function prepare_document( $term_id ) { $term = get_term( $term_id ); if ( ! $term || ! is_a( $term, 'WP_Term' ) ) { return false; } $term_args = [ 'term_id' => $term->term_id, 'ID' => $term->term_id, 'name' => $term->name, 'slug' => $term->slug, 'term_group' => $term->group, 'term_taxonomy_id' => $term->term_taxonomy_id, 'taxonomy' => $term->taxonomy, 'description' => $term->description, 'parent' => $term->parent, 'count' => $term->count, 'meta' => $this->prepare_meta_types( $this->prepare_meta( $term->term_id ) ), 'hierarchy' => $this->prepare_term_hierarchy( $term->term_id, $term->taxonomy ), 'object_ids' => $this->prepare_object_ids( $term->term_id, $term->taxonomy ), ]; /** * Filter term fields pre-sync * * @hook ep_term_sync_args * @param {array} $term_args Current term fields * @param {int} $term_id Term ID * @since 3.4 * @return {array} New fields */ $term_args = apply_filters( 'ep_term_sync_args', $term_args, $term_id ); return $term_args; } /** * Query DB for terms * * @param array $args Query arguments * @since 3.1 * @return array */ public function query_db( $args ) { $defaults = [ 'number' => $this->get_bulk_items_per_page(), 'offset' => 0, 'orderby' => 'id', 'order' => 'desc', 'taxonomy' => $this->get_indexable_taxonomies(), 'hide_empty' => false, 'hierarchical' => false, 'update_term_meta_cache' => false, 'cache_results' => false, 'ep_indexing_advanced_pagination' => true, ]; if ( isset( $args['per_page'] ) ) { $args['number'] = $args['per_page']; } if ( isset( $args['include'] ) ) { $args['include'] = $args['include']; } if ( isset( $args['exclude'] ) ) { $args['exclude'] = $args['exclude']; } /** * Filter database arguments for term query * * @hook ep_term_query_db_args * @param {array} $args Query arguments based to WP_Term_Query * @since 3.4 * @return {array} New arguments */ $args = apply_filters( 'ep_term_query_db_args', wp_parse_args( $args, $defaults ) ); $all_query_args = $args; unset( $all_query_args['number'] ); unset( $all_query_args['offset'] ); unset( $all_query_args['fields'] ); if ( isset( $args['include'] ) || 0 < $args['offset'] ) { // Disable advanced pagination. Not useful if only indexing specific IDs. $args['ep_indexing_advanced_pagination'] = false; } // Explicitly set the orderby to ID to prevent accidental modifications by other code. add_filter( 'terms_clauses', [ $this, 'set_orderby' ], 9999, 3 ); // Enforce the following query args during advanced pagination to ensure things work correctly. if ( $args['ep_indexing_advanced_pagination'] ) { $args = array_merge( $args, [ 'suppress_filters' => false, 'orderby' => 'ID', 'order' => 'DESC', 'paged' => 1, 'offset' => 0, ] ); add_filter( 'terms_clauses', [ $this, 'bulk_indexing_filter_terms_where' ], 9999, 3 ); $query = new WP_Term_Query( $args ); $total_objects = $this->get_total_objects_for_query( $args ); remove_filter( 'terms_clauses', [ $this, 'bulk_indexing_filter_terms_where' ], 9999, 3 ); } else { /** * Filter database arguments for term count query * * @hook ep_term_all_query_db_args * @param {array} $args Query arguments based to `wp_count_terms()` * @since 3.4 * @return {array} New arguments */ $total_objects = wp_count_terms( apply_filters( 'ep_term_all_query_db_args', $all_query_args, $args ) ); $total_objects = ! is_wp_error( $total_objects ) ? (int) $total_objects : 0; if ( ! empty( $args['offset'] ) ) { if ( (int) $args['offset'] >= $total_objects ) { $total_objects = 0; } } $query = new WP_Term_Query( $args ); } remove_filter( 'terms_clauses', [ $this, 'set_orderby' ], 9999, 3 ); if ( is_array( $query->terms ) ) { array_walk( $query->terms, array( $this, 'remap_terms' ) ); } return [ 'objects' => $query->terms, 'total_objects' => $total_objects, ]; } /** * Filters the WHERE clause of the SQL query used for bulk indexing terms by modifying it to include a range of * comment IDs based on advanced pagination parameters. * * @param array $clauses Associative array of the clauses for the query. * @param array $taxonomies An array of taxonomy names. * @param array $args An array of term query arguments. * * @return array The modified SQL WHERE clauses. */ public function bulk_indexing_filter_terms_where( $clauses, $taxonomies, $args ) { $using_advanced_pagination = $args['ep_indexing_advanced_pagination'] ?? false; if ( $using_advanced_pagination ) { $requested_upper_limit_id = $args['ep_indexing_upper_limit_object_id'] ?? PHP_INT_MAX; $requested_lower_limit_object_id = $args['ep_indexing_lower_limit_object_id'] ?? 0; $last_processed_id = $args['ep_indexing_last_processed_object_id'] ?? null; // On the first loopthrough we begin with the requested upper limit ID. Afterwards, use the last processed ID to paginate. $upper_limit_range_object_id = $requested_upper_limit_id; if ( is_numeric( $last_processed_id ) ) { $upper_limit_range_object_id = $last_processed_id - 1; } // Sanitize. Abort if unexpected data at this point. if ( ! is_numeric( $upper_limit_range_object_id ) || ! is_numeric( $requested_lower_limit_object_id ) ) { return $clauses; } $range = [ 'upper_limit' => "t.term_id <= {$upper_limit_range_object_id}", 'lower_limit' => "t.term_id >= {$requested_lower_limit_object_id}", ]; // Skip the end range if it's unnecessary. $skip_ending_range = 0 === $requested_lower_limit_object_id; $where = $clauses['where']; $where = $skip_ending_range ? " {$range['upper_limit']} AND {$where}" : " {$range['upper_limit']} AND {$range['lower_limit']} AND {$where}"; $clauses['where'] = $where; } return $clauses; } /** * Get the total number of terms for a given query. * * @param array $query_args The query args. * @return int The total number of terms. */ protected function get_total_objects_for_query( $query_args ) { static $object_counts = []; // Reset the pagination-related args for optimal caching. $normalized_query_args = array_merge( $query_args, [ 'offset' => 0, 'paged' => 1, 'posts_per_page' => 1, 'no_found_rows' => false, 'ep_indexing_last_processed_object_id' => null, ] ); $cache_key = md5( get_current_blog_id() . wp_json_encode( $normalized_query_args ) ); if ( ! isset( $object_counts[ $cache_key ] ) ) { $object_counts[ $cache_key ] = wp_count_terms( $normalized_query_args ); } return $object_counts[ $cache_key ]; } /** * Returns indexable taxonomies for the current site * * @since 3.1 * @return mixed|void */ public function get_indexable_taxonomies() { $taxonomies = get_taxonomies( [], 'objects' ); $public_taxonomies = []; foreach ( $taxonomies as $taxonomy ) { if ( $taxonomy->public || $taxonomy->publicly_queryable ) { $public_taxonomies[] = $taxonomy->name; } } /** * Filter indexable taxonomies for Terms indexable * * @hook ep_indexable_taxonomies * @param {array} $public_taxonomies Taxonomies * @since 3.4 * @return {array} New taxonomies array */ return apply_filters( 'ep_indexable_taxonomies', $public_taxonomies ); } /** * Rebuild our term object to match the fields we need. * * In particular, result of WP_Term_Query does not * include an "id" field, which our index command * expects. * * @param object $value Term object * @since 3.1 * @return void Returns by reference */ public function remap_terms( &$value ) { $value = (object) array( 'ID' => $value->term_id, 'term_id' => $value->term_id, 'name' => $value->name, 'slug' => $value->slug, 'term_group' => $value->term_group, 'term_taxonomy_id' => $value->term_taxonomy_id, 'taxonomy' => $value->taxonomy, 'description' => $value->description, 'parent' => $value->parent, 'count' => $value->count, ); } /** * Prepare meta to send to ES * * @param int $term_id Term ID * @since 3.1 * @return array */ public function prepare_meta( $term_id ) { $meta = (array) get_term_meta( $term_id ); if ( empty( $meta ) ) { return []; } $prepared_meta = []; /** * Filter index-able private meta * * Allows for specifying private meta keys that may be indexed in the same manner as public meta keys. * * @since 3.4 * @hook ep_prepare_term_meta_allowed_protected_keys * @param {array} $allowed_protected_keys Array of index-able private meta keys. * @param {int} $term_id Term ID. * @return {array} New meta keys */ $allowed_protected_keys = apply_filters( 'ep_prepare_term_meta_allowed_protected_keys', [], $term_id ); /** * Filter non-indexed public meta * * Allows for specifying public meta keys that should be excluded from the ElasticPress index. * * @since 3.4 * @hook ep_prepare_term_meta_excluded_public_keys * @param {array} $public_keys Array of public meta keys to exclude from index. * @param {int} $term_id Term ID. * @return {array} New keys */ $excluded_public_keys = apply_filters( 'ep_prepare_term_meta_excluded_public_keys', [ 'session_tokens', ], $term_id ); foreach ( $meta as $key => $value ) { $allow_index = false; if ( is_protected_meta( $key ) ) { if ( true === $allowed_protected_keys || in_array( $key, $allowed_protected_keys, true ) ) { $allow_index = true; } } elseif ( true !== $excluded_public_keys && ! in_array( $key, $excluded_public_keys, true ) ) { $allow_index = true; } /** * Filter kill switch for any term meta * * @since 3.4 * @hook ep_prepare_term_meta_whitelist_key * @param {boolean} $index_key Whether to index key or not * @param {string} $key Key name * @param {int} $term_id Term ID. * @return {boolean} New index value */ if ( true === $allow_index || apply_filters( 'ep_prepare_term_meta_whitelist_key', false, $key, $term_id ) ) { $prepared_meta[ $key ] = maybe_unserialize( $value ); } } return $prepared_meta; } /** * Prepare term hierarchy to send to ES * * @param int $term_id Term ID. * @param string $taxonomy Term taxonomy. * @since 3.1 * @return array */ public function prepare_term_hierarchy( $term_id, $taxonomy ) { $hierarchy = []; $children = get_term_children( $term_id, $taxonomy ); $ancestors = get_ancestors( $term_id, $taxonomy, 'taxonomy' ); if ( ! empty( $children ) && ! is_wp_error( $children ) ) { $hierarchy['children']['terms'] = $children; $children_count = 0; foreach ( $children as $child_term_id ) { $child_term = get_term( $child_term_id ); $children_count += (int) $child_term->count; } $hierarchy['children']['count'] = $children_count; } else { $hierarchy['children']['terms'] = 0; $hierarchy['children']['count'] = 0; } if ( ! empty( $ancestors ) ) { $hierarchy['ancestors']['terms'] = $ancestors; } else { $hierarchy['ancestors']['terms'] = 0; } return $hierarchy; } /** * Prepare object IDs to send to ES * * @param int $term_id Term ID. * @param string $taxonomy Term taxonomy. * @since 3.1 * @return array */ public function prepare_object_ids( $term_id, $taxonomy ) { $ids = []; $object_ids = get_objects_in_term( [ $term_id ], [ $taxonomy ] ); if ( ! empty( $object_ids ) && ! is_wp_error( $object_ids ) ) { $ids['value'] = array_map( 'absint', array_values( $object_ids ) ); } else { $ids['value'] = 0; } return $ids; } /** * Parse an 'order' query variable and cast it to ASC or DESC as necessary. * * @access protected * * @param string $order The 'order' query variable. * @since 3.1 * @return string The sanitized 'order' query variable. */ protected function parse_order( $order ) { if ( ! is_string( $order ) || empty( $order ) ) { return 'desc'; } if ( 'ASC' === strtoupper( $order ) ) { return 'asc'; } else { return 'desc'; } } /** * Convert the alias to a properly-prefixed sort value. * * @access protected * * @param string $orderby Alias or path for the field to order by. * @param string $order Order direction * @param array $args Query args * @since 3.1 * @return array */ protected function parse_orderby( $orderby, $order, $args ) { $sort = []; if ( empty( $orderby ) ) { return $sort; } $from_to = [ 'slug' => 'slug.raw', 'id' => 'term_id', 'description' => 'description.sortable', ]; if ( in_array( $orderby, [ 'meta_value', 'meta_value_num' ], true ) ) { if ( empty( $args['meta_key'] ) ) { return $sort; } else { $from_to['meta_value'] = 'meta.' . $args['meta_key'] . '.value'; $from_to['meta_value_num'] = 'meta.' . $args['meta_key'] . '.long'; } } if ( 'name' === $orderby ) { $es_version = Elasticsearch::factory()->get_elasticsearch_version(); $from_to['name'] = version_compare( (string) $es_version, '7.0', '<' ) ? 'name.raw' : 'name.sortable'; } $orderby = $from_to[ $orderby ] ?? $orderby; $sort[] = array( $orderby => array( 'order' => $order, ), ); return $sort; } /** * Sanitize WP_Term_Query arguments to be used to create the ES query. * * @since 5.1.0 * @param array $query_vars WP_Term_Query arguments * @return array */ protected function sanitize_query_vars( $query_vars ) { if ( ! empty( $query_vars['get'] ) && 'all' === $query_vars['get'] ) { $query_vars['childless'] = false; $query_vars['child_of'] = 0; $query_vars['hide_empty'] = false; $query_vars['hierarchical'] = false; $query_vars['pad_counts'] = false; } $query_vars['taxonomy'] = ( ! empty( $query_vars['taxonomy'] ) ) ? (array) $query_vars['taxonomy'] : []; return $query_vars; } /** * Parse the `from` clause of the ES Query. * * @since 5.1.0 * @param array $query_vars WP_Term_Query arguments * @return int */ protected function parse_from( $query_vars ) { return ( isset( $query_vars['offset'] ) ) ? (int) $query_vars['offset'] : 0; } /** * Parse the `size` clause of the ES Query. * * @since 5.1.0 * @param array $query_vars WP_Term_Query arguments * @return int */ protected function parse_size( $query_vars ) { if ( ! empty( $query_vars['number'] ) ) { $number = (int) $query_vars['number']; } else { /** * Set the maximum results window size. * * The request will return a HTTP 500 Internal Error if the size of the * request is larger than the [index.max_result_window] parameter in ES. * See the scroll api for a more efficient way to request large data sets. * * @return int The max results window size. * * @since 2.3.0 */ $number = apply_filters( 'ep_max_results_window', 10000 ); } return $number; } /** * Parse the order of results in the ES query. * * @since 5.1.0 * @param array $formatted_args Formatted Elasticsearch query * @param array $query_vars WP_Term_Query arguments * @return array */ protected function maybe_orderby( $formatted_args, $query_vars ) { // Set sort order, default is 'ASC'. if ( ! empty( $query_vars['order'] ) ) { $order = $this->parse_order( $query_vars['order'] ); } else { $order = 'desc'; } // Set orderby, default is 'name'. if ( empty( $query_vars['orderby'] ) ) { $query_vars['orderby'] = 'name'; } // Set sort type. $formatted_args['sort'] = $this->parse_orderby( $query_vars['orderby'], $order, $query_vars ); return $formatted_args; } /** * Based on WP_Term_Query arguments, parses the various filters that could be applied into the ES query. * * @since 5.1.0 * @param array $query_vars WP_Term_Query arguments * @return array */ protected function parse_filters( $query_vars ) { $filters = [ 'taxonomy' => $this->parse_taxonomy( $query_vars ), 'object_ids' => $this->parse_object_ids( $query_vars ), 'include' => $this->parse_include( $query_vars ), 'exclude' => $this->parse_exclude( $query_vars ), 'exclude_tree' => $this->parse_exclude_tree( $query_vars ), 'name' => $this->parse_name( $query_vars ), 'slug' => $this->parse_slug( $query_vars ), 'term_taxonomy_id' => $this->parse_term_taxonomy_id( $query_vars ), 'hierarchical_hide_empty' => $this->parse_hierarchical_hide_empty( $query_vars ), 'child_of' => $this->parse_child_of( $query_vars ), 'parent' => $this->parse_parent( $query_vars ), 'childless' => $this->parse_childless( $query_vars ), 'meta_query' => $this->parse_meta_queries( $query_vars ), ]; $filters = array_values( array_filter( $filters ) ); if ( ! empty( $filters ) ) { $filters = [ 'bool' => [ 'must' => $filters, ], ]; } return $filters; } /** * Parse the `taxonomy` WP Term Query arg and transform it into an ES query clause. * * @since 5.1.0 * @param array $query_vars WP_Term_Query arguments * @return array */ protected function parse_taxonomy( $query_vars ) { if ( empty( $query_vars['taxonomy'] ) ) { return []; } if ( count( $query_vars['taxonomy'] ) < 2 ) { return [ 'term' => [ 'taxonomy.raw' => $query_vars['taxonomy'][0], ], ]; } return [ 'terms' => [ 'taxonomy.raw' => $query_vars['taxonomy'], ], ]; } /** * Parse the `object_ids` WP Term Query arg and transform it into an ES query clause. * * @since 5.1.0 * @param array $query_vars WP_Term_Query arguments * @return array */ protected function parse_object_ids( $query_vars ) { if ( empty( $query_vars['object_ids'] ) ) { return []; } return [ 'bool' => [ 'must' => [ 'terms' => [ 'object_ids.value' => (array) $query_vars['object_ids'], ], ], ], ]; } /** * Parse the `include` WP Term Query arg and transform it into an ES query clause. * * @since 5.1.0 * @param array $query_vars WP_Term_Query arguments * @return array */ protected function parse_include( $query_vars ) { if ( empty( $query_vars['include'] ) ) { return []; } return [ 'bool' => [ 'must' => [ 'terms' => [ 'term_id' => array_values( (array) $query_vars['include'] ), ], ], ], ]; } /** * Parse the `exclude` WP Term Query arg and transform it into an ES query clause. * * @since 5.1.0 * @param array $query_vars WP_Term_Query arguments * @return array */ protected function parse_exclude( $query_vars ) { if ( ! empty( $query_vars['include'] ) || empty( $query_vars['exclude'] ) ) { return []; } return [ 'bool' => [ 'must_not' => [ 'terms' => [ 'term_id' => array_values( (array) $query_vars['exclude'] ), ], ], ], ]; } /** * Parse the `exclude_tree` WP Term Query arg and transform it into an ES query clause. * * @since 5.1.0 * @param array $query_vars WP_Term_Query arguments * @return array */ protected function parse_exclude_tree( $query_vars ) { if ( ! empty( $query_vars['include'] ) || empty( $query_vars['exclude_tree'] ) ) { return []; } return [ 'bool' => [ 'must_not' => [ [ 'terms' => [ 'term_id' => array_values( (array) $query_vars['exclude_tree'] ), ], ], [ 'terms' => [ 'parent' => array_values( (array) $query_vars['exclude_tree'] ), ], ], ], ], ]; } /** * Parse the `name` WP Term Query arg and transform it into an ES query clause. * * @since 5.1.0 * @param array $query_vars WP_Term_Query arguments * @return array */ protected function parse_name( $query_vars ) { if ( empty( $query_vars['name'] ) ) { return []; } return [ 'terms' => [ 'name.raw' => (array) $query_vars['name'], ], ]; } /** * Parse the `slug` WP Term Query arg and transform it into an ES query clause. * * @since 5.1.0 * @param array $query_vars WP_Term_Query arguments * @return array */ protected function parse_slug( $query_vars ) { if ( empty( $query_vars['slug'] ) ) { return []; } $query_vars['slug'] = (array) $query_vars['slug']; $query_vars['slug'] = array_map( 'sanitize_title', $query_vars['slug'] ); return [ 'terms' => [ 'slug.raw' => (array) $query_vars['slug'], ], ]; } /** * Parse the `term_taxonomy_id` WP Term Query arg and transform it into an ES query clause. * * @since 5.1.0 * @param array $query_vars WP_Term_Query arguments * @return array */ protected function parse_term_taxonomy_id( $query_vars ) { if ( empty( $query_vars['term_taxonomy_id'] ) ) { return []; } return [ 'bool' => [ 'must' => [ 'terms' => [ 'term_taxonomy_id' => array_values( (array) $query_vars['term_taxonomy_id'] ), ], ], ], ]; } /** * Parse the `hide_empty` and `hierarchical` WP Term Query args and transform them into ES query clauses. * * `hierarchical` needs to work in conjunction with `hide_empty`, as per WP docs: * > `hierarchical`: Whether to include terms that have non-empty descendants (even if $hide_empty is set to true). * * In summary: * - hide_empty AND hierarchical: count > 1 OR hierarchy.children > 1 * - hide_empty AND NOT hierarchical: count > 1 (ignore hierarchy.children) * - NOT hide_empty (AND hierarchical): there is no need to limit the query * * @see https://developer.wordpress.org/reference/classes/WP_Term_Query/__construct/ * @since 5.1.0 * @param array $query_vars WP_Term_Query arguments * @return array */ protected function parse_hierarchical_hide_empty( $query_vars ) { $hide_empty = isset( $query_vars['hide_empty'] ) ? $query_vars['hide_empty'] : ''; if ( ! $hide_empty ) { return []; } $hierarchical = isset( $query_vars['hierarchical'] ) ? $query_vars['hierarchical'] : ''; if ( ! $hierarchical ) { return [ 'range' => [ 'count' => [ 'gte' => 1, ], ], ]; } return [ 'bool' => [ 'should' => [ [ 'range' => [ 'count' => [ 'gte' => 1, ], ], ], [ 'range' => [ 'hierarchy.children.count' => [ 'gte' => 1, ], ], ], ], ], ]; } /** * Parse the `child_of` WP Term Query arg and transform it into an ES query clause. * * @since 5.1.0 * @param array $query_vars WP_Term_Query arguments * @return array */ protected function parse_child_of( $query_vars ) { if ( empty( $query_vars['child_of'] ) || count( $query_vars['taxonomy'] ) > 1 ) { return []; } return [ 'bool' => [ 'must' => [ 'match_phrase' => [ 'hierarchy.ancestors.terms' => (int) $query_vars['child_of'], ], ], ], ]; } /** * Parse the `parent` WP Term Query arg and transform it into an ES query clause. * * @since 5.1.0 * @param array $query_vars WP_Term_Query arguments * @return array */ protected function parse_parent( $query_vars ) { if ( ! isset( $query_vars['parent'] ) || '' === $query_vars['parent'] ) { return []; } return [ 'bool' => [ 'must' => [ 'term' => [ 'parent' => (int) $query_vars['parent'], ], ], ], ]; } /** * Parse the `childless` WP Term Query arg and transform it into an ES query clause. * * @since 5.1.0 * @param array $query_vars WP_Term_Query arguments * @return array */ protected function parse_childless( $query_vars ) { if ( empty( $query_vars['childless'] ) ) { return []; } return [ 'bool' => [ 'must' => [ 'term' => [ 'hierarchy.children.terms' => 0, ], ], ], ]; } /** * Parse WP Term Query meta queries and transform them into ES query clauses. * * @since 5.1.0 * @param array $query_vars WP_Term_Query arguments * @return array */ protected function parse_meta_queries( $query_vars ) { $meta_queries = []; /** * Support `meta_key`, `meta_value`, and `meta_compare` query args */ if ( ! empty( $query_vars['meta_key'] ) ) { $meta_query_array = [ 'key' => $query_vars['meta_key'], ]; if ( isset( $query_vars['meta_value'] ) && '' !== $query_vars['meta_value'] ) { $meta_query_array['value'] = $query_vars['meta_value']; } if ( isset( $query_vars['meta_compare'] ) ) { $meta_query_array['compare'] = $query_vars['meta_compare']; } $meta_queries[] = $meta_query_array; } /** * Support 'meta_query' query var. */ if ( ! empty( $query_vars['meta_query'] ) ) { $meta_queries = array_merge( $meta_queries, $query_vars['meta_query'] ); } if ( ! empty( $meta_queries ) ) { $built_meta_queries = $this->build_meta_query( $meta_queries ); if ( $built_meta_queries ) { return $built_meta_queries; } } return []; } /** * If in a search context, using `name__like`, or `description__like` set search fields, otherwise query everything. * * @since 5.1.0 * @param array $formatted_args Formatted Elasticsearch query * @param array $query_vars WP_Term_Query arguments * @return array */ protected function maybe_set_search_fields( $formatted_args, $query_vars ) { if ( empty( $query_vars['search'] ) && empty( $query_vars['name__like'] ) && empty( $query_vars['description__like'] ) ) { $formatted_args['query']['match_all'] = [ 'boost' => 1, ]; return $formatted_args; } $search = ! empty( $query_vars['search'] ) ? $query_vars['search'] : ''; $search_fields = []; if ( ! empty( $query_vars['name__like'] ) ) { $search = $query_vars['name__like']; $search_fields[] = 'name'; } if ( ! empty( $query_vars['description__like'] ) ) { $search = $query_vars['description__like']; $search_fields[] = 'description'; } /** * Allow for search field specification */ if ( ! empty( $query_vars['search_fields'] ) ) { $search_fields = $query_vars['search_fields']; } if ( ! empty( $search_fields ) ) { $prepared_search_fields = []; if ( ! empty( $search_fields['meta'] ) ) { $metas = (array) $search_fields['meta']; foreach ( $metas as $meta ) { $prepared_search_fields[] = 'meta.' . $meta . '.value'; } unset( $search_fields['meta'] ); } $prepared_search_fields = array_merge( $search_fields, $prepared_search_fields ); } else { $prepared_search_fields = [ 'name', 'slug', 'taxonomy', 'description', ]; } /** * Filter fields to search on Term query * * @hook ep_term_search_fields * @param {array} $search_fields Search fields * @param {array} $query_vars Query variables * @since 3.4 * @return {array} New search fields */ $prepared_search_fields = apply_filters( 'ep_term_search_fields', $prepared_search_fields, $query_vars ); $search_algorithm = $this->get_search_algorithm( $search, $prepared_search_fields, $query_vars ); $formatted_args['query'] = $search_algorithm->get_query( 'term', $search, $prepared_search_fields, $query_vars ); return $formatted_args; } /** * If needed set the `fields` ES query clause. * * @since 5.1.0 * @param array $formatted_args Formatted Elasticsearch query * @param array $query_vars WP_Term_Query arguments * @return array */ protected function maybe_set_fields( $formatted_args, $query_vars ) { if ( ! isset( $query_vars['fields'] ) ) { return $formatted_args; } switch ( $query_vars['fields'] ) { case 'ids': $formatted_args['_source'] = [ 'includes' => [ 'term_id', ], ]; break; case 'id=>name': $formatted_args['_source'] = [ 'includes' => [ 'term_id', 'name', ], ]; break; case 'id=>parent': $formatted_args['_source'] = [ 'includes' => [ 'term_id', 'parent', ], ]; break; case 'id=>slug': $formatted_args['_source'] = [ 'includes' => [ 'term_id', 'slug', ], ]; break; case 'names': $formatted_args['_source'] = [ 'includes' => [ 'name', ], ]; break; case 'tt_ids': $formatted_args['_source'] = [ 'includes' => [ 'term_taxonomy_id', ], ]; break; } return $formatted_args; } /** * Sets the ORDER BY clause for term queries to order terms by their term_id. * * @param array $clauses The SQL clauses array to modify. * @return array The modified SQL clauses array with the ORDER BY clause set to term_id. * * @since 5.2.0 */ public function set_orderby( $clauses ): array { $clauses['orderby'] = 'ORDER BY t.term_id'; return $clauses; } }