Source: includes/classes/Indexable/Term/Term.php

  1. <?php
  2. /**
  3. * Term indexable
  4. *
  5. * @since 3.1
  6. * @package elasticpress
  7. */
  8. namespace ElasticPress\Indexable\Term;
  9. use WP_Term_Query;
  10. use ElasticPress\Elasticsearch;
  11. use ElasticPress\Indexable;
  12. if ( ! defined( 'ABSPATH' ) ) {
  13. // @codeCoverageIgnoreStart
  14. exit; // Exit if accessed directly.
  15. // @codeCoverageIgnoreEnd
  16. }
  17. /**
  18. * Term indexable class
  19. */
  20. class Term extends Indexable {
  21. /**
  22. * Indexable slug
  23. *
  24. * @var string
  25. * @since 3.1
  26. */
  27. public $slug = 'term';
  28. /**
  29. * Create indexable and initialize dependencies
  30. *
  31. * @since 3.1
  32. */
  33. public function __construct() {
  34. $this->labels = [
  35. 'plural' => esc_html__( 'Terms', 'elasticpress' ),
  36. 'singular' => esc_html__( 'Term', 'elasticpress' ),
  37. ];
  38. }
  39. /**
  40. * Instantiate the indexable SyncManager and QueryIntegration, the main responsibles for the WP integration.
  41. *
  42. * @since 4.5.0
  43. * @return void
  44. */
  45. public function setup() {
  46. $this->sync_manager = new SyncManager( $this->slug );
  47. $this->query_integration = new QueryIntegration( $this->slug );
  48. }
  49. /**
  50. * Format query vars into ES query
  51. *
  52. * @param array $query_vars WP_Term_Query args.
  53. * @since 3.1
  54. * @return array
  55. */
  56. public function format_args( $query_vars ) {
  57. $query_vars = $this->sanitize_query_vars( $query_vars );
  58. $formatted_args = [
  59. 'from' => $this->parse_from( $query_vars ),
  60. 'size' => $this->parse_size( $query_vars ),
  61. ];
  62. $formatted_args = $this->maybe_orderby( $formatted_args, $query_vars );
  63. $filters = $this->parse_filters( $query_vars );
  64. if ( ! empty( $filters ) ) {
  65. $formatted_args['post_filter'] = $filters;
  66. }
  67. $formatted_args = $this->maybe_set_search_fields( $formatted_args, $query_vars );
  68. $formatted_args = $this->maybe_set_fields( $formatted_args, $query_vars );
  69. /**
  70. * Filter full Elasticsearch query for Terms indexable
  71. *
  72. * @hook ep_term_formatted_args
  73. * @param {array} $query Elasticsearch query
  74. * @param {array} $query_vars Query variables
  75. * @since 3.4
  76. * @return {array} New query
  77. */
  78. return apply_filters( 'ep_term_formatted_args', $formatted_args, $query_vars );
  79. }
  80. /**
  81. * Generate the mapping array
  82. *
  83. * @since 3.6.0
  84. * @return array
  85. */
  86. public function generate_mapping() {
  87. $es_version = Elasticsearch::factory()->get_elasticsearch_version();
  88. if ( empty( $es_version ) ) {
  89. $es_version = apply_filters( 'ep_fallback_elasticsearch_version', '2.0' );
  90. }
  91. $es_version = (string) $es_version;
  92. $mapping_file = '7-0.php';
  93. if ( version_compare( $es_version, '7.0', '<' ) ) {
  94. $mapping_file = 'initial.php';
  95. }
  96. /**
  97. * Filter mapping file for Terms indexable
  98. *
  99. * @hook ep_term_mapping_file
  100. * @param {string} $file File name
  101. * @since 3.4
  102. * @return {string} New file name
  103. */
  104. $mapping = require apply_filters( 'ep_term_mapping_file', __DIR__ . '/../../../mappings/term/' . $mapping_file );
  105. /**
  106. * Filter full Elasticsearch query for Terms indexable
  107. *
  108. * @hook ep_term_mapping
  109. * @param {array} $mapping Elasticsearch mapping
  110. * @since 3.4
  111. * @return {array} New mapping
  112. */
  113. $mapping = apply_filters( 'ep_term_mapping', $mapping );
  114. return $mapping;
  115. }
  116. /**
  117. * Prepare a term document for indexing
  118. *
  119. * @param int $term_id Term ID
  120. * @since 3.1
  121. * @return bool|array
  122. */
  123. public function prepare_document( $term_id ) {
  124. $term = get_term( $term_id );
  125. if ( ! $term || ! is_a( $term, 'WP_Term' ) ) {
  126. return false;
  127. }
  128. $term_args = [
  129. 'term_id' => $term->term_id,
  130. 'ID' => $term->term_id,
  131. 'name' => $term->name,
  132. 'slug' => $term->slug,
  133. 'term_group' => $term->group,
  134. 'term_taxonomy_id' => $term->term_taxonomy_id,
  135. 'taxonomy' => $term->taxonomy,
  136. 'description' => $term->description,
  137. 'parent' => $term->parent,
  138. 'count' => $term->count,
  139. 'meta' => $this->prepare_meta_types( $this->prepare_meta( $term->term_id ) ),
  140. 'hierarchy' => $this->prepare_term_hierarchy( $term->term_id, $term->taxonomy ),
  141. 'object_ids' => $this->prepare_object_ids( $term->term_id, $term->taxonomy ),
  142. ];
  143. /**
  144. * Filter term fields pre-sync
  145. *
  146. * @hook ep_term_sync_args
  147. * @param {array} $term_args Current term fields
  148. * @param {int} $term_id Term ID
  149. * @since 3.4
  150. * @return {array} New fields
  151. */
  152. $term_args = apply_filters( 'ep_term_sync_args', $term_args, $term_id );
  153. return $term_args;
  154. }
  155. /**
  156. * Query DB for terms
  157. *
  158. * @param array $args Query arguments
  159. * @since 3.1
  160. * @return array
  161. */
  162. public function query_db( $args ) {
  163. $defaults = [
  164. 'number' => $this->get_bulk_items_per_page(),
  165. 'offset' => 0,
  166. 'orderby' => 'id',
  167. 'order' => 'desc',
  168. 'taxonomy' => $this->get_indexable_taxonomies(),
  169. 'hide_empty' => false,
  170. 'hierarchical' => false,
  171. 'update_term_meta_cache' => false,
  172. 'cache_results' => false,
  173. ];
  174. if ( isset( $args['per_page'] ) ) {
  175. $args['number'] = $args['per_page'];
  176. }
  177. /**
  178. * Filter database arguments for term query
  179. *
  180. * @hook ep_term_query_db_args
  181. * @param {array} $args Query arguments based to WP_Term_Query
  182. * @since 3.4
  183. * @return {array} New arguments
  184. */
  185. $args = apply_filters( 'ep_term_query_db_args', wp_parse_args( $args, $defaults ) );
  186. $all_query_args = $args;
  187. unset( $all_query_args['number'] );
  188. unset( $all_query_args['offset'] );
  189. unset( $all_query_args['fields'] );
  190. /**
  191. * Filter database arguments for term count query
  192. *
  193. * @hook ep_term_all_query_db_args
  194. * @param {array} $args Query arguments based to `wp_count_terms()`
  195. * @since 3.4
  196. * @return {array} New arguments
  197. */
  198. $total_objects = wp_count_terms( apply_filters( 'ep_term_all_query_db_args', $all_query_args, $args ) );
  199. $total_objects = ! is_wp_error( $total_objects ) ? (int) $total_objects : 0;
  200. if ( ! empty( $args['offset'] ) ) {
  201. if ( (int) $args['offset'] >= $total_objects ) {
  202. $total_objects = 0;
  203. }
  204. }
  205. $query = new WP_Term_Query( $args );
  206. if ( is_array( $query->terms ) ) {
  207. array_walk( $query->terms, array( $this, 'remap_terms' ) );
  208. }
  209. return [
  210. 'objects' => $query->terms,
  211. 'total_objects' => $total_objects,
  212. ];
  213. }
  214. /**
  215. * Returns indexable taxonomies for the current site
  216. *
  217. * @since 3.1
  218. * @return mixed|void
  219. */
  220. public function get_indexable_taxonomies() {
  221. $taxonomies = get_taxonomies( [], 'objects' );
  222. $public_taxonomies = [];
  223. foreach ( $taxonomies as $taxonomy ) {
  224. if ( $taxonomy->public || $taxonomy->publicly_queryable ) {
  225. $public_taxonomies[] = $taxonomy->name;
  226. }
  227. }
  228. /**
  229. * Filter indexable taxonomies for Terms indexable
  230. *
  231. * @hook ep_indexable_taxonomies
  232. * @param {array} $public_taxonomies Taxonomies
  233. * @since 3.4
  234. * @return {array} New taxonomies array
  235. */
  236. return apply_filters( 'ep_indexable_taxonomies', $public_taxonomies );
  237. }
  238. /**
  239. * Rebuild our term object to match the fields we need.
  240. *
  241. * In particular, result of WP_Term_Query does not
  242. * include an "id" field, which our index command
  243. * expects.
  244. *
  245. * @param object $value Term object
  246. * @since 3.1
  247. * @return void Returns by reference
  248. */
  249. public function remap_terms( &$value ) {
  250. $value = (object) array(
  251. 'ID' => $value->term_id,
  252. 'term_id' => $value->term_id,
  253. 'name' => $value->name,
  254. 'slug' => $value->slug,
  255. 'term_group' => $value->term_group,
  256. 'term_taxonomy_id' => $value->term_taxonomy_id,
  257. 'taxonomy' => $value->taxonomy,
  258. 'description' => $value->description,
  259. 'parent' => $value->parent,
  260. 'count' => $value->count,
  261. );
  262. }
  263. /**
  264. * Prepare meta to send to ES
  265. *
  266. * @param int $term_id Term ID
  267. * @since 3.1
  268. * @return array
  269. */
  270. public function prepare_meta( $term_id ) {
  271. $meta = (array) get_term_meta( $term_id );
  272. if ( empty( $meta ) ) {
  273. return [];
  274. }
  275. $prepared_meta = [];
  276. /**
  277. * Filter index-able private meta
  278. *
  279. * Allows for specifying private meta keys that may be indexed in the same manner as public meta keys.
  280. *
  281. * @since 3.4
  282. * @hook ep_prepare_term_meta_allowed_protected_keys
  283. * @param {array} $allowed_protected_keys Array of index-able private meta keys.
  284. * @param {int} $term_id Term ID.
  285. * @return {array} New meta keys
  286. */
  287. $allowed_protected_keys = apply_filters( 'ep_prepare_term_meta_allowed_protected_keys', [], $term_id );
  288. /**
  289. * Filter non-indexed public meta
  290. *
  291. * Allows for specifying public meta keys that should be excluded from the ElasticPress index.
  292. *
  293. * @since 3.4
  294. * @hook ep_prepare_term_meta_excluded_public_keys
  295. * @param {array} $public_keys Array of public meta keys to exclude from index.
  296. * @param {int} $term_id Term ID.
  297. * @return {array} New keys
  298. */
  299. $excluded_public_keys = apply_filters(
  300. 'ep_prepare_term_meta_excluded_public_keys',
  301. [
  302. 'session_tokens',
  303. ],
  304. $term_id
  305. );
  306. foreach ( $meta as $key => $value ) {
  307. $allow_index = false;
  308. if ( is_protected_meta( $key ) ) {
  309. if ( true === $allowed_protected_keys || in_array( $key, $allowed_protected_keys, true ) ) {
  310. $allow_index = true;
  311. }
  312. } elseif ( true !== $excluded_public_keys && ! in_array( $key, $excluded_public_keys, true ) ) {
  313. $allow_index = true;
  314. }
  315. /**
  316. * Filter kill switch for any term meta
  317. *
  318. * @since 3.4
  319. * @hook ep_prepare_term_meta_whitelist_key
  320. * @param {boolean} $index_key Whether to index key or not
  321. * @param {string} $key Key name
  322. * @param {int} $term_id Term ID.
  323. * @return {boolean} New index value
  324. */
  325. if ( true === $allow_index || apply_filters( 'ep_prepare_term_meta_whitelist_key', false, $key, $term_id ) ) {
  326. $prepared_meta[ $key ] = maybe_unserialize( $value );
  327. }
  328. }
  329. return $prepared_meta;
  330. }
  331. /**
  332. * Prepare term hierarchy to send to ES
  333. *
  334. * @param int $term_id Term ID.
  335. * @param string $taxonomy Term taxonomy.
  336. * @since 3.1
  337. * @return array
  338. */
  339. public function prepare_term_hierarchy( $term_id, $taxonomy ) {
  340. $hierarchy = [];
  341. $children = get_term_children( $term_id, $taxonomy );
  342. $ancestors = get_ancestors( $term_id, $taxonomy, 'taxonomy' );
  343. if ( ! empty( $children ) && ! is_wp_error( $children ) ) {
  344. $hierarchy['children']['terms'] = $children;
  345. $children_count = 0;
  346. foreach ( $children as $child_term_id ) {
  347. $child_term = get_term( $child_term_id );
  348. $children_count += (int) $child_term->count;
  349. }
  350. $hierarchy['children']['count'] = $children_count;
  351. } else {
  352. $hierarchy['children']['terms'] = 0;
  353. $hierarchy['children']['count'] = 0;
  354. }
  355. if ( ! empty( $ancestors ) ) {
  356. $hierarchy['ancestors']['terms'] = $ancestors;
  357. } else {
  358. $hierarchy['ancestors']['terms'] = 0;
  359. }
  360. return $hierarchy;
  361. }
  362. /**
  363. * Prepare object IDs to send to ES
  364. *
  365. * @param int $term_id Term ID.
  366. * @param string $taxonomy Term taxonomy.
  367. * @since 3.1
  368. * @return array
  369. */
  370. public function prepare_object_ids( $term_id, $taxonomy ) {
  371. $ids = [];
  372. $object_ids = get_objects_in_term( [ $term_id ], [ $taxonomy ] );
  373. if ( ! empty( $object_ids ) && ! is_wp_error( $object_ids ) ) {
  374. $ids['value'] = array_map( 'absint', array_values( $object_ids ) );
  375. } else {
  376. $ids['value'] = 0;
  377. }
  378. return $ids;
  379. }
  380. /**
  381. * Parse an 'order' query variable and cast it to ASC or DESC as necessary.
  382. *
  383. * @access protected
  384. *
  385. * @param string $order The 'order' query variable.
  386. * @since 3.1
  387. * @return string The sanitized 'order' query variable.
  388. */
  389. protected function parse_order( $order ) {
  390. if ( ! is_string( $order ) || empty( $order ) ) {
  391. return 'desc';
  392. }
  393. if ( 'ASC' === strtoupper( $order ) ) {
  394. return 'asc';
  395. } else {
  396. return 'desc';
  397. }
  398. }
  399. /**
  400. * Convert the alias to a properly-prefixed sort value.
  401. *
  402. * @access protected
  403. *
  404. * @param string $orderby Alias or path for the field to order by.
  405. * @param string $order Order direction
  406. * @param array $args Query args
  407. * @since 3.1
  408. * @return array
  409. */
  410. protected function parse_orderby( $orderby, $order, $args ) {
  411. $sort = [];
  412. if ( empty( $orderby ) ) {
  413. return $sort;
  414. }
  415. $from_to = [
  416. 'slug' => 'slug.raw',
  417. 'id' => 'term_id',
  418. 'description' => 'description.sortable',
  419. ];
  420. if ( in_array( $orderby, [ 'meta_value', 'meta_value_num' ], true ) ) {
  421. if ( empty( $args['meta_key'] ) ) {
  422. return $sort;
  423. } else {
  424. $from_to['meta_value'] = 'meta.' . $args['meta_key'] . '.value';
  425. $from_to['meta_value_num'] = 'meta.' . $args['meta_key'] . '.long';
  426. }
  427. }
  428. if ( 'name' === $orderby ) {
  429. $es_version = Elasticsearch::factory()->get_elasticsearch_version();
  430. $from_to['name'] = version_compare( (string) $es_version, '7.0', '<' ) ? 'name.raw' : 'name.sortable';
  431. }
  432. $orderby = $from_to[ $orderby ] ?? $orderby;
  433. $sort[] = array(
  434. $orderby => array(
  435. 'order' => $order,
  436. ),
  437. );
  438. return $sort;
  439. }
  440. /**
  441. * Sanitize WP_Term_Query arguments to be used to create the ES query.
  442. *
  443. * @since 5.1.0
  444. * @param array $query_vars WP_Term_Query arguments
  445. * @return array
  446. */
  447. protected function sanitize_query_vars( $query_vars ) {
  448. if ( ! empty( $query_vars['get'] ) && 'all' === $query_vars['get'] ) {
  449. $query_vars['childless'] = false;
  450. $query_vars['child_of'] = 0;
  451. $query_vars['hide_empty'] = false;
  452. $query_vars['hierarchical'] = false;
  453. $query_vars['pad_counts'] = false;
  454. }
  455. $query_vars['taxonomy'] = ( ! empty( $query_vars['taxonomy'] ) ) ?
  456. (array) $query_vars['taxonomy'] :
  457. [];
  458. return $query_vars;
  459. }
  460. /**
  461. * Parse the `from` clause of the ES Query.
  462. *
  463. * @since 5.1.0
  464. * @param array $query_vars WP_Term_Query arguments
  465. * @return int
  466. */
  467. protected function parse_from( $query_vars ) {
  468. return ( isset( $query_vars['offset'] ) ) ? (int) $query_vars['offset'] : 0;
  469. }
  470. /**
  471. * Parse the `size` clause of the ES Query.
  472. *
  473. * @since 5.1.0
  474. * @param array $query_vars WP_Term_Query arguments
  475. * @return int
  476. */
  477. protected function parse_size( $query_vars ) {
  478. if ( ! empty( $query_vars['number'] ) ) {
  479. $number = (int) $query_vars['number'];
  480. } else {
  481. /**
  482. * Set the maximum results window size.
  483. *
  484. * The request will return a HTTP 500 Internal Error if the size of the
  485. * request is larger than the [index.max_result_window] parameter in ES.
  486. * See the scroll api for a more efficient way to request large data sets.
  487. *
  488. * @return int The max results window size.
  489. *
  490. * @since 2.3.0
  491. */
  492. $number = apply_filters( 'ep_max_results_window', 10000 );
  493. }
  494. return $number;
  495. }
  496. /**
  497. * Parse the order of results in the ES query.
  498. *
  499. * @since 5.1.0
  500. * @param array $formatted_args Formatted Elasticsearch query
  501. * @param array $query_vars WP_Term_Query arguments
  502. * @return array
  503. */
  504. protected function maybe_orderby( $formatted_args, $query_vars ) {
  505. // Set sort order, default is 'ASC'.
  506. if ( ! empty( $query_vars['order'] ) ) {
  507. $order = $this->parse_order( $query_vars['order'] );
  508. } else {
  509. $order = 'desc';
  510. }
  511. // Set orderby, default is 'name'.
  512. if ( empty( $query_vars['orderby'] ) ) {
  513. $query_vars['orderby'] = 'name';
  514. }
  515. // Set sort type.
  516. $formatted_args['sort'] = $this->parse_orderby( $query_vars['orderby'], $order, $query_vars );
  517. return $formatted_args;
  518. }
  519. /**
  520. * Based on WP_Term_Query arguments, parses the various filters that could be applied into the ES query.
  521. *
  522. * @since 5.1.0
  523. * @param array $query_vars WP_Term_Query arguments
  524. * @return array
  525. */
  526. protected function parse_filters( $query_vars ) {
  527. $filters = [
  528. 'taxonomy' => $this->parse_taxonomy( $query_vars ),
  529. 'object_ids' => $this->parse_object_ids( $query_vars ),
  530. 'include' => $this->parse_include( $query_vars ),
  531. 'exclude' => $this->parse_exclude( $query_vars ),
  532. 'exclude_tree' => $this->parse_exclude_tree( $query_vars ),
  533. 'name' => $this->parse_name( $query_vars ),
  534. 'slug' => $this->parse_slug( $query_vars ),
  535. 'term_taxonomy_id' => $this->parse_term_taxonomy_id( $query_vars ),
  536. 'hierarchical_hide_empty' => $this->parse_hierarchical_hide_empty( $query_vars ),
  537. 'child_of' => $this->parse_child_of( $query_vars ),
  538. 'parent' => $this->parse_parent( $query_vars ),
  539. 'childless' => $this->parse_childless( $query_vars ),
  540. 'meta_query' => $this->parse_meta_queries( $query_vars ),
  541. ];
  542. $filters = array_values( array_filter( $filters ) );
  543. if ( ! empty( $filters ) ) {
  544. $filters = [
  545. 'bool' => [
  546. 'must' => $filters,
  547. ],
  548. ];
  549. }
  550. return $filters;
  551. }
  552. /**
  553. * Parse the `taxonomy` WP Term Query arg and transform it into an ES query clause.
  554. *
  555. * @since 5.1.0
  556. * @param array $query_vars WP_Term_Query arguments
  557. * @return array
  558. */
  559. protected function parse_taxonomy( $query_vars ) {
  560. if ( empty( $query_vars['taxonomy'] ) ) {
  561. return [];
  562. }
  563. if ( count( $query_vars['taxonomy'] ) < 2 ) {
  564. return [
  565. 'term' => [
  566. 'taxonomy.raw' => $query_vars['taxonomy'][0],
  567. ],
  568. ];
  569. }
  570. return [
  571. 'terms' => [
  572. 'taxonomy.raw' => $query_vars['taxonomy'],
  573. ],
  574. ];
  575. }
  576. /**
  577. * Parse the `object_ids` WP Term Query arg and transform it into an ES query clause.
  578. *
  579. * @since 5.1.0
  580. * @param array $query_vars WP_Term_Query arguments
  581. * @return array
  582. */
  583. protected function parse_object_ids( $query_vars ) {
  584. if ( empty( $query_vars['object_ids'] ) ) {
  585. return [];
  586. }
  587. return [
  588. 'bool' => [
  589. 'must' => [
  590. 'terms' => [
  591. 'object_ids.value' => (array) $query_vars['object_ids'],
  592. ],
  593. ],
  594. ],
  595. ];
  596. }
  597. /**
  598. * Parse the `include` WP Term Query arg and transform it into an ES query clause.
  599. *
  600. * @since 5.1.0
  601. * @param array $query_vars WP_Term_Query arguments
  602. * @return array
  603. */
  604. protected function parse_include( $query_vars ) {
  605. if ( empty( $query_vars['include'] ) ) {
  606. return [];
  607. }
  608. return [
  609. 'bool' => [
  610. 'must' => [
  611. 'terms' => [
  612. 'term_id' => array_values( (array) $query_vars['include'] ),
  613. ],
  614. ],
  615. ],
  616. ];
  617. }
  618. /**
  619. * Parse the `exclude` WP Term Query arg and transform it into an ES query clause.
  620. *
  621. * @since 5.1.0
  622. * @param array $query_vars WP_Term_Query arguments
  623. * @return array
  624. */
  625. protected function parse_exclude( $query_vars ) {
  626. if ( ! empty( $query_vars['include'] ) || empty( $query_vars['exclude'] ) ) {
  627. return [];
  628. }
  629. return [
  630. 'bool' => [
  631. 'must_not' => [
  632. 'terms' => [
  633. 'term_id' => array_values( (array) $query_vars['exclude'] ),
  634. ],
  635. ],
  636. ],
  637. ];
  638. }
  639. /**
  640. * Parse the `exclude_tree` WP Term Query arg and transform it into an ES query clause.
  641. *
  642. * @since 5.1.0
  643. * @param array $query_vars WP_Term_Query arguments
  644. * @return array
  645. */
  646. protected function parse_exclude_tree( $query_vars ) {
  647. if ( ! empty( $query_vars['include'] ) || empty( $query_vars['exclude_tree'] ) ) {
  648. return [];
  649. }
  650. return [
  651. 'bool' => [
  652. 'must_not' => [
  653. [
  654. 'terms' => [
  655. 'term_id' => array_values( (array) $query_vars['exclude_tree'] ),
  656. ],
  657. ],
  658. [
  659. 'terms' => [
  660. 'parent' => array_values( (array) $query_vars['exclude_tree'] ),
  661. ],
  662. ],
  663. ],
  664. ],
  665. ];
  666. }
  667. /**
  668. * Parse the `name` WP Term Query arg and transform it into an ES query clause.
  669. *
  670. * @since 5.1.0
  671. * @param array $query_vars WP_Term_Query arguments
  672. * @return array
  673. */
  674. protected function parse_name( $query_vars ) {
  675. if ( empty( $query_vars['name'] ) ) {
  676. return [];
  677. }
  678. return [
  679. 'terms' => [
  680. 'name.raw' => (array) $query_vars['name'],
  681. ],
  682. ];
  683. }
  684. /**
  685. * Parse the `slug` WP Term Query arg and transform it into an ES query clause.
  686. *
  687. * @since 5.1.0
  688. * @param array $query_vars WP_Term_Query arguments
  689. * @return array
  690. */
  691. protected function parse_slug( $query_vars ) {
  692. if ( empty( $query_vars['slug'] ) ) {
  693. return [];
  694. }
  695. $query_vars['slug'] = (array) $query_vars['slug'];
  696. $query_vars['slug'] = array_map( 'sanitize_title', $query_vars['slug'] );
  697. return [
  698. 'terms' => [
  699. 'slug.raw' => (array) $query_vars['slug'],
  700. ],
  701. ];
  702. }
  703. /**
  704. * Parse the `term_taxonomy_id` WP Term Query arg and transform it into an ES query clause.
  705. *
  706. * @since 5.1.0
  707. * @param array $query_vars WP_Term_Query arguments
  708. * @return array
  709. */
  710. protected function parse_term_taxonomy_id( $query_vars ) {
  711. if ( empty( $query_vars['term_taxonomy_id'] ) ) {
  712. return [];
  713. }
  714. return [
  715. 'bool' => [
  716. 'must' => [
  717. 'terms' => [
  718. 'term_taxonomy_id' => array_values( (array) $query_vars['term_taxonomy_id'] ),
  719. ],
  720. ],
  721. ],
  722. ];
  723. }
  724. /**
  725. * Parse the `hide_empty` and `hierarchical` WP Term Query args and transform them into ES query clauses.
  726. *
  727. * `hierarchical` needs to work in conjunction with `hide_empty`, as per WP docs:
  728. * > `hierarchical`: Whether to include terms that have non-empty descendants (even if $hide_empty is set to true).
  729. *
  730. * In summary:
  731. * - hide_empty AND hierarchical: count > 1 OR hierarchy.children > 1
  732. * - hide_empty AND NOT hierarchical: count > 1 (ignore hierarchy.children)
  733. * - NOT hide_empty (AND hierarchical): there is no need to limit the query
  734. *
  735. * @see https://developer.wordpress.org/reference/classes/WP_Term_Query/__construct/
  736. * @since 5.1.0
  737. * @param array $query_vars WP_Term_Query arguments
  738. * @return array
  739. */
  740. protected function parse_hierarchical_hide_empty( $query_vars ) {
  741. $hide_empty = isset( $query_vars['hide_empty'] ) ? $query_vars['hide_empty'] : '';
  742. if ( ! $hide_empty ) {
  743. return [];
  744. }
  745. $hierarchical = isset( $query_vars['hierarchical'] ) ? $query_vars['hierarchical'] : '';
  746. if ( ! $hierarchical ) {
  747. return [
  748. 'range' => [
  749. 'count' => [
  750. 'gte' => 1,
  751. ],
  752. ],
  753. ];
  754. }
  755. return [
  756. 'bool' => [
  757. 'should' => [
  758. [
  759. 'range' => [
  760. 'count' => [
  761. 'gte' => 1,
  762. ],
  763. ],
  764. ],
  765. [
  766. 'range' => [
  767. 'hierarchy.children.count' => [
  768. 'gte' => 1,
  769. ],
  770. ],
  771. ],
  772. ],
  773. ],
  774. ];
  775. }
  776. /**
  777. * Parse the `child_of` WP Term Query arg and transform it into an ES query clause.
  778. *
  779. * @since 5.1.0
  780. * @param array $query_vars WP_Term_Query arguments
  781. * @return array
  782. */
  783. protected function parse_child_of( $query_vars ) {
  784. if ( empty( $query_vars['child_of'] ) || count( $query_vars['taxonomy'] ) > 1 ) {
  785. return [];
  786. }
  787. return [
  788. 'bool' => [
  789. 'must' => [
  790. 'match_phrase' => [
  791. 'hierarchy.ancestors.terms' => (int) $query_vars['child_of'],
  792. ],
  793. ],
  794. ],
  795. ];
  796. }
  797. /**
  798. * Parse the `parent` WP Term Query arg and transform it into an ES query clause.
  799. *
  800. * @since 5.1.0
  801. * @param array $query_vars WP_Term_Query arguments
  802. * @return array
  803. */
  804. protected function parse_parent( $query_vars ) {
  805. if ( ! isset( $query_vars['parent'] ) || '' === $query_vars['parent'] ) {
  806. return [];
  807. }
  808. return [
  809. 'bool' => [
  810. 'must' => [
  811. 'term' => [
  812. 'parent' => (int) $query_vars['parent'],
  813. ],
  814. ],
  815. ],
  816. ];
  817. }
  818. /**
  819. * Parse the `childless` WP Term Query arg and transform it into an ES query clause.
  820. *
  821. * @since 5.1.0
  822. * @param array $query_vars WP_Term_Query arguments
  823. * @return array
  824. */
  825. protected function parse_childless( $query_vars ) {
  826. if ( empty( $query_vars['childless'] ) ) {
  827. return [];
  828. }
  829. return [
  830. 'bool' => [
  831. 'must' => [
  832. 'term' => [
  833. 'hierarchy.children.terms' => 0,
  834. ],
  835. ],
  836. ],
  837. ];
  838. }
  839. /**
  840. * Parse WP Term Query meta queries and transform them into ES query clauses.
  841. *
  842. * @since 5.1.0
  843. * @param array $query_vars WP_Term_Query arguments
  844. * @return array
  845. */
  846. protected function parse_meta_queries( $query_vars ) {
  847. $meta_queries = [];
  848. /**
  849. * Support `meta_key`, `meta_value`, and `meta_compare` query args
  850. */
  851. if ( ! empty( $query_vars['meta_key'] ) ) {
  852. $meta_query_array = [
  853. 'key' => $query_vars['meta_key'],
  854. ];
  855. if ( isset( $query_vars['meta_value'] ) && '' !== $query_vars['meta_value'] ) {
  856. $meta_query_array['value'] = $query_vars['meta_value'];
  857. }
  858. if ( isset( $query_vars['meta_compare'] ) ) {
  859. $meta_query_array['compare'] = $query_vars['meta_compare'];
  860. }
  861. $meta_queries[] = $meta_query_array;
  862. }
  863. /**
  864. * Support 'meta_query' query var.
  865. */
  866. if ( ! empty( $query_vars['meta_query'] ) ) {
  867. $meta_queries = array_merge( $meta_queries, $query_vars['meta_query'] );
  868. }
  869. if ( ! empty( $meta_queries ) ) {
  870. $built_meta_queries = $this->build_meta_query( $meta_queries );
  871. if ( $built_meta_queries ) {
  872. return $built_meta_queries;
  873. }
  874. }
  875. return [];
  876. }
  877. /**
  878. * If in a search context, using `name__like`, or `description__like` set search fields, otherwise query everything.
  879. *
  880. * @since 5.1.0
  881. * @param array $formatted_args Formatted Elasticsearch query
  882. * @param array $query_vars WP_Term_Query arguments
  883. * @return array
  884. */
  885. protected function maybe_set_search_fields( $formatted_args, $query_vars ) {
  886. if ( empty( $query_vars['search'] ) && empty( $query_vars['name__like'] ) && empty( $query_vars['description__like'] ) ) {
  887. $formatted_args['query']['match_all'] = [
  888. 'boost' => 1,
  889. ];
  890. return $formatted_args;
  891. }
  892. $search = ! empty( $query_vars['search'] ) ? $query_vars['search'] : '';
  893. $search_fields = [];
  894. if ( ! empty( $query_vars['name__like'] ) ) {
  895. $search = $query_vars['name__like'];
  896. $search_fields[] = 'name';
  897. }
  898. if ( ! empty( $query_vars['description__like'] ) ) {
  899. $search = $query_vars['description__like'];
  900. $search_fields[] = 'description';
  901. }
  902. /**
  903. * Allow for search field specification
  904. */
  905. if ( ! empty( $query_vars['search_fields'] ) ) {
  906. $search_fields = $query_vars['search_fields'];
  907. }
  908. if ( ! empty( $search_fields ) ) {
  909. $prepared_search_fields = [];
  910. if ( ! empty( $search_fields['meta'] ) ) {
  911. $metas = (array) $search_fields['meta'];
  912. foreach ( $metas as $meta ) {
  913. $prepared_search_fields[] = 'meta.' . $meta . '.value';
  914. }
  915. unset( $search_fields['meta'] );
  916. }
  917. $prepared_search_fields = array_merge( $search_fields, $prepared_search_fields );
  918. } else {
  919. $prepared_search_fields = [
  920. 'name',
  921. 'slug',
  922. 'taxonomy',
  923. 'description',
  924. ];
  925. }
  926. /**
  927. * Filter fields to search on Term query
  928. *
  929. * @hook ep_term_search_fields
  930. * @param {array} $search_fields Search fields
  931. * @param {array} $query_vars Query variables
  932. * @since 3.4
  933. * @return {array} New search fields
  934. */
  935. $prepared_search_fields = apply_filters( 'ep_term_search_fields', $prepared_search_fields, $query_vars );
  936. $search_algorithm = $this->get_search_algorithm( $search, $prepared_search_fields, $query_vars );
  937. $formatted_args['query'] = $search_algorithm->get_query( 'term', $search, $prepared_search_fields, $query_vars );
  938. return $formatted_args;
  939. }
  940. /**
  941. * If needed set the `fields` ES query clause.
  942. *
  943. * @since 5.1.0
  944. * @param array $formatted_args Formatted Elasticsearch query
  945. * @param array $query_vars WP_Term_Query arguments
  946. * @return array
  947. */
  948. protected function maybe_set_fields( $formatted_args, $query_vars ) {
  949. if ( ! isset( $query_vars['fields'] ) ) {
  950. return $formatted_args;
  951. }
  952. switch ( $query_vars['fields'] ) {
  953. case 'ids':
  954. $formatted_args['_source'] = [
  955. 'includes' => [
  956. 'term_id',
  957. ],
  958. ];
  959. break;
  960. case 'id=>name':
  961. $formatted_args['_source'] = [
  962. 'includes' => [
  963. 'term_id',
  964. 'name',
  965. ],
  966. ];
  967. break;
  968. case 'id=>parent':
  969. $formatted_args['_source'] = [
  970. 'includes' => [
  971. 'term_id',
  972. 'parent',
  973. ],
  974. ];
  975. break;
  976. case 'id=>slug':
  977. $formatted_args['_source'] = [
  978. 'includes' => [
  979. 'term_id',
  980. 'slug',
  981. ],
  982. ];
  983. break;
  984. case 'names':
  985. $formatted_args['_source'] = [
  986. 'includes' => [
  987. 'name',
  988. ],
  989. ];
  990. break;
  991. case 'tt_ids':
  992. $formatted_args['_source'] = [
  993. 'includes' => [
  994. 'term_taxonomy_id',
  995. ],
  996. ];
  997. break;
  998. }
  999. return $formatted_args;
  1000. }
  1001. }