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/Post/Post.php +596 -211 4.4.15.3.5 View file →
@@ -1,7 +1,7 @@
1 1 <?php
2 2 /**
3 - * User indexable
3 + * Post indexable
4 4 *
5 5 * @since 3.0
6 6 * @package elasticpress
7 7 */
@@ -7,12 +7,12 @@
7 7 */
8 8
9 9 namespace ElasticPress\Indexable\Post;
10 10
11 -use ElasticPress\Indexable as Indexable;
12 -use ElasticPress\Elasticsearch as Elasticsearch;
13 -use \WP_Query as WP_Query;
14 -use \WP_User as WP_User;
11 +use WP_Query;
12 +use WP_User;
13 +use ElasticPress\Elasticsearch;
14 +use ElasticPress\Indexable;
15 15
16 16 if ( ! defined( 'ABSPATH' ) ) {
17 17 // @codeCoverageIgnoreStart
18 18 exit; // Exit if accessed directly.
@@ -41,13 +41,14 @@
41 41 */
42 42 public $support_indexing_advanced_pagination = true;
43 43
44 44 /**
45 - * Create indexable and initialize dependencies
45 + * Instantiate the indexable SyncManager and QueryIntegration
46 46 *
47 - * @since 3.0
47 + * @since 5.2.0
48 + * @return void
48 49 */
49 - public function __construct() {
50 + public function setup() {
50 51 $this->labels = [
51 52 'plural' => esc_html__( 'Posts', 'elasticpress' ),
52 53 'singular' => esc_html__( 'Post', 'elasticpress' ),
53 54 ];
@@ -102,8 +103,11 @@
102 103 // Disable advanced pagination. Not useful if only indexing specific IDs.
103 104 $args['ep_indexing_advanced_pagination'] = false;
104 105 }
105 106
107 + // Explicitly set the orderby to ID to prevent accidental modifications by other code.
108 + add_filter( 'posts_orderby', [ $this, 'set_posts_orderby' ], 9999, 2 );
109 +
106 110 // Enforce the following query args during advanced pagination to ensure things work correctly.
107 111 if ( $args['ep_indexing_advanced_pagination'] ) {
108 112 $args = array_merge(
109 113 $args,
@@ -115,19 +119,21 @@
115 119 'offset' => 0,
116 120 'no_found_rows' => true,
117 121 ]
118 122 );
119 - add_filter( 'posts_where', array( $this, 'bulk_indexing_filter_posts_where' ), 9999, 2 );
123 + add_filter( 'posts_where', [ $this, 'bulk_indexing_filter_posts_where' ], 9999, 2 );
120 124
121 125 $query = new WP_Query( $args );
122 126 $total_objects = $this->get_total_objects_for_query( $args );
123 127
124 - remove_filter( 'posts_where', array( $this, 'bulk_indexing_filter_posts_where' ), 9999, 2 );
128 + remove_filter( 'posts_where', [ $this, 'bulk_indexing_filter_posts_where' ], 9999, 2 );
125 129 } else {
126 130 $query = new WP_Query( $args );
127 131 $total_objects = $query->found_posts;
128 132 }
129 133
134 + remove_filter( 'posts_orderby', [ $this, 'set_posts_orderby' ], 9999, 2 );
135 +
130 136 return [
131 137 'objects' => $query->posts,
132 138 'total_objects' => $total_objects,
133 139 ];
@@ -132,16 +138,18 @@
132 138 'total_objects' => $total_objects,
133 139 ];
134 140 }
135 141
136 - /**
137 - * Manipulate the WHERE clause of the bulk indexing query to paginate by ID in order to avoid performance issues with SQL offset.
138 - *
139 - * @param string $where The current $where clause.
140 - * @param WP_Query $query WP_Query object.
141 - * @return string WHERE clause with our pagination added if needed.
142 - */
142 + /**
143 + * Manipulate the WHERE clause of the bulk indexing query to paginate by ID in order to avoid performance issues with SQL offset.
144 + *
145 + * @param string $where The current $where clause.
146 + * @param WP_Query $query WP_Query object.
147 + * @return string WHERE clause with our pagination added if needed.
148 + */
143 149 public function bulk_indexing_filter_posts_where( $where, $query ) {
150 + global $wpdb;
151 +
144 152 $using_advanced_pagination = $query->get( 'ep_indexing_advanced_pagination', false );
145 153
146 154 if ( $using_advanced_pagination ) {
147 155 $requested_upper_limit_id = $query->get( 'ep_indexing_upper_limit_object_id', PHP_INT_MAX );
@@ -159,10 +167,10 @@
159 167 return $where;
160 168 }
161 169
162 170 $range = [
163 - 'upper_limit' => "{$GLOBALS['wpdb']->posts}.ID <= {$upper_limit_range_post_id}",
164 - 'lower_limit' => "{$GLOBALS['wpdb']->posts}.ID >= {$requested_lower_limit_post_id}",
171 + 'upper_limit' => "{$wpdb->posts}.ID <= {$upper_limit_range_post_id}",
172 + 'lower_limit' => "{$wpdb->posts}.ID >= {$requested_lower_limit_post_id}",
165 173 ];
166 174
167 175 // Skip the end range if it's unnecessary.
168 176 $skip_ending_range = 0 === $requested_lower_limit_post_id;
@@ -244,9 +252,9 @@
244 252 *
245 253 * The if below will pass if `has_password` is false but not null.
246 254 */
247 255 if ( isset( $query_args['has_password'] ) && ! $query_args['has_password'] ) {
248 - $posts_with_password = (int) $wpdb->get_var( "SELECT COUNT(1) AS posts_with_password FROM {$wpdb->posts} WHERE post_password != ''" );
256 + $posts_with_password = (int) $wpdb->get_var( "SELECT COUNT(1) AS posts_with_password FROM {$wpdb->posts} WHERE post_password != ''" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery
249 257
250 258 $post_count -= $posts_with_password;
251 259 }
252 260
@@ -314,19 +322,14 @@
314 322 * @return {string} New version
315 323 */
316 324 $es_version = apply_filters( 'ep_fallback_elasticsearch_version', '2.0' );
317 325 }
326 + $es_version = (string) $es_version;
318 327
319 - $mapping_file = '5-2.php';
328 + $mapping_file = '7-0.php';
320 329
321 - if ( ! $es_version || version_compare( $es_version, '5.0' ) < 0 ) {
322 - $mapping_file = 'pre-5-0.php';
323 - } elseif ( version_compare( $es_version, '5.0', '>=' ) && version_compare( $es_version, '5.2', '<' ) ) {
324 - $mapping_file = '5-0.php';
325 - } elseif ( version_compare( $es_version, '5.2', '>=' ) && version_compare( $es_version, '7.0', '<' ) ) {
330 + if ( version_compare( $es_version, '7.0', '<' ) ) {
326 331 $mapping_file = '5-2.php';
327 - } elseif ( version_compare( $es_version, '7.0', '>=' ) ) {
328 - $mapping_file = '7-0.php';
329 332 }
330 333
331 334 return apply_filters( 'ep_post_mapping_version', $mapping_file );
332 335 }
@@ -419,9 +422,11 @@
419 422 * @since 0.9.1
420 423 * @return bool|array
421 424 */
422 425 public function prepare_document( $post_id ) {
426 + global $post;
423 427 $post = get_post( $post_id );
428 + setup_postdata( $post );
424 429
425 430 if ( empty( $post ) ) {
426 431 return false;
427 432 }
@@ -450,9 +455,9 @@
450 455 $post_modified_gmt = $post->post_modified_gmt;
451 456 $comment_count = absint( $post->comment_count );
452 457 $comment_status = $post->comment_status;
453 458 $ping_status = $post->ping_status;
454 - $menu_order = absint( $post->menu_order );
459 + $menu_order = (int) $post->menu_order;
455 460
456 461 /**
457 462 * Filter to ignore invalid dates
458 463 *
@@ -562,11 +567,8 @@
562 567
563 568 /**
564 569 * Filters the image size to use when indexing the post thumbnail.
565 570 *
566 - * Defaults to the `woocommerce_thumbnail` size if WooCommerce is in
567 - * use. Otherwise the `thumbnail` size is used.
568 - *
569 571 * @hook ep_thumbnail_image_size
570 572 * @since 4.0.0
571 573 * @param {string|int[]} $image_size Image size. Can be any registered
572 574 * image size name, or an array of
@@ -576,25 +578,31 @@
576 578 * @return {array} Image size to pass to wp_get_attachment_image_src().
577 579 */
578 580 $image_size = apply_filters(
579 581 'ep_post_thumbnail_image_size',
580 - function_exists( 'WC' ) ? 'woocommerce_thumbnail' : 'thumbnail',
582 + 'medium',
581 583 $post
582 584 );
583 585
584 - $image_src = wp_get_attachment_image_src( $attachment_id, $image_size );
586 + $image = wp_get_attachment_image_src( $attachment_id, $image_size );
585 587 $image_alt = trim( wp_strip_all_tags( get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ) ) );
586 588
587 - if ( ! $image_src ) {
589 + if ( ! $image ) {
588 590 return null;
589 591 }
590 592
593 + list( $src, $width, $height ) = $image;
594 +
595 + $image_meta = wp_get_attachment_metadata( $attachment_id );
596 + $srcset = wp_calculate_image_srcset( [ absint( $width ), absint( $height ) ], $src, $image_meta, $attachment_id );
597 +
591 598 return [
592 599 'ID' => $attachment_id,
593 - 'src' => $image_src[0],
594 - 'width' => $image_src[1],
595 - 'height' => $image_src[2],
600 + 'src' => $src,
601 + 'width' => $width,
602 + 'height' => $height,
596 603 'alt' => $image_alt,
604 + 'srcset' => $srcset,
597 605 ];
598 606 }
599 607
600 608 /**
@@ -599,9 +607,9 @@
599 607
600 608 /**
601 609 * Prepare date terms to send to ES.
602 610 *
603 - * @param string $date_to_prepare Post date
611 + * @param null|string $date_to_prepare Post date
604 612 * @since 0.1.4
605 613 * @return array
606 614 */
607 615 public function prepare_date_terms( $date_to_prepare ) {
@@ -620,9 +628,9 @@
620 628 ];
621 629
622 630 // Combine all the date term formats and perform one single call to date_i18n() for performance.
623 631 $date_format = implode( '||', array_values( $terms_to_prepare ) );
624 - $combined_dates = explode( '||', date_i18n( $date_format, strtotime( $date_to_prepare ) ) );
632 + $combined_dates = explode( '||', date_i18n( $date_format, strtotime( (string) $date_to_prepare ) ) );
625 633
626 634 // Then split up the results for individual indexing.
627 635 $date_terms = [];
628 636 foreach ( $terms_to_prepare as $term_name => $date_format ) {
@@ -714,19 +722,10 @@
714 722 $terms_dic = [];
715 723
716 724 foreach ( $object_terms as $term ) {
717 725 if ( ! isset( $terms_dic[ $term->term_id ] ) ) {
718 - $terms_dic[ $term->term_id ] = array(
719 - 'term_id' => $term->term_id,
720 - 'slug' => $term->slug,
721 - 'name' => $term->name,
722 - 'parent' => $term->parent,
723 - 'term_taxonomy_id' => $term->term_taxonomy_id,
724 - 'term_order' => (int) $this->get_term_order( $term->term_taxonomy_id, $post->ID ),
725 - );
726 + $terms_dic[ $term->term_id ] = $this->get_formatted_term( $term, $post->ID );
726 727
727 - $terms_dic[ $term->term_id ]['facet'] = wp_json_encode( $terms_dic[ $term->term_id ] );
728 -
729 728 if ( $allow_hierarchy ) {
730 729 $terms_dic = $this->get_parent_terms( $terms_dic, $term, $taxonomy->name, $post->ID );
731 730 }
732 731 }
@@ -752,26 +751,47 @@
752 751 if ( ! $parent_term || is_wp_error( $parent_term ) ) {
753 752 return $terms;
754 753 }
755 754 if ( ! isset( $terms[ $parent_term->term_id ] ) ) {
756 - $terms[ $parent_term->term_id ] = array(
757 - 'term_id' => $parent_term->term_id,
758 - 'slug' => $parent_term->slug,
759 - 'name' => $parent_term->name,
760 - 'parent' => $parent_term->parent,
761 - 'term_taxonomy_id' => $parent_term->term_taxonomy_id,
762 - 'term_order' => $this->get_term_order( $parent_term->term_taxonomy_id, $object_id ),
763 - );
755 + $terms[ $parent_term->term_id ] = $this->get_formatted_term( $parent_term, $object_id );
764 756
765 - $terms[ $parent_term->term_id ]['facet'] = wp_json_encode( $terms[ $parent_term->term_id ] );
766 -
767 757 }
768 758 return $this->get_parent_terms( $terms, $parent_term, $tax_name, $object_id );
769 759 }
770 760
771 761 /**
772 - * Retreives term order for the object/term_taxonomy_id combination
762 + * Given a term, format it to be appended to the post ES document.
773 763 *
764 + * @since 4.5.0
765 + * @param \WP_Term $term Term to be formatted
766 + * @param int $post_id The post ID
767 + * @return array
768 + */
769 + private function get_formatted_term( \WP_Term $term, int $post_id ): array {
770 + $formatted_term = [
771 + 'term_id' => $term->term_id,
772 + 'slug' => $term->slug,
773 + 'name' => $term->name,
774 + 'parent' => $term->parent,
775 + 'term_taxonomy_id' => $term->term_taxonomy_id,
776 + 'term_order' => (int) $this->get_term_order( $term->term_taxonomy_id, $post_id ),
777 + ];
778 +
779 + /**
780 + * As the name implies, the facet attribute is used to list all terms in facets.
781 + * As in facets, the term_order associated with a post does not matter, we set it as 0 here.
782 + * Note that this is set as 0 instead of simply removed to keep backward compatibility.
783 + */
784 + $term_facet = $formatted_term;
785 + $term_facet['term_order'] = 0;
786 + $formatted_term['facet'] = wp_json_encode( $term_facet );
787 +
788 + return $formatted_term;
789 + }
790 +
791 + /**
792 + * Retrieves term order for the object/term_taxonomy_id combination
793 + *
774 794 * @param int $term_taxonomy_id Term Taxonomy ID
775 795 * @param int $object_id Post ID
776 796 *
777 797 * @return int Term Order
@@ -782,9 +802,9 @@
782 802 $cache_key = "{$object_id}_term_order";
783 803 $term_orders = wp_cache_get( $cache_key );
784 804
785 805 if ( false === $term_orders ) {
786 - $results = $wpdb->get_results(
806 + $results = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
787 807 $wpdb->prepare(
788 808 "SELECT term_taxonomy_id, term_order from $wpdb->term_relationships where object_id=%d;",
789 809 $object_id
790 810 ),
@@ -800,9 +820,8 @@
800 820 wp_cache_set( $cache_key, $term_orders );
801 821 }
802 822
803 823 return isset( $term_orders[ $term_taxonomy_id ] ) ? (int) $term_orders[ $term_taxonomy_id ] : 0;
804 -
805 824 }
806 825
807 826 /**
808 827 * Checks if meta key is allowed
@@ -832,59 +851,15 @@
832 851 */
833 852 public function filter_allowed_metas( $metas, $post ) {
834 853 $filtered_metas = [];
835 854
836 - /**
837 - * Filter indexable protected meta keys for posts
838 - *
839 - * @hook ep_prepare_meta_allowed_protected_keys
840 - * @param {array} $keys Allowed protected keys
841 - * @param {WP_Post} $post Post object
842 - * @since 1.7
843 - * @return {array} New keys
844 - */
845 - $allowed_protected_keys = apply_filters( 'ep_prepare_meta_allowed_protected_keys', [], $post );
855 + $search = \ElasticPress\Features::factory()->get_registered_feature( 'search' );
856 + if ( $search && ! empty( $search->weighting ) && 'manual' === $search->weighting->get_meta_mode() ) {
857 + $filtered_metas = $this->filter_allowed_metas_manual( $metas, $post );
858 + } else {
859 + $filtered_metas = $this->filter_allowed_metas_auto( $metas, $post );
860 + }
846 861
847 - /**
848 - * Filter public keys to exclude from indexed post
849 - *
850 - * @hook ep_prepare_meta_excluded_public_keys
851 - * @param {array} $keys Excluded protected keys
852 - * @param {WP_Post} $post Post object
853 - * @since 1.7
854 - * @return {array} New keys
855 - */
856 - $excluded_public_keys = apply_filters( 'ep_prepare_meta_excluded_public_keys', [], $post );
857 -
858 - foreach ( $metas as $key => $value ) {
859 -
860 - $allow_index = false;
861 -
862 - if ( is_protected_meta( $key ) ) {
863 -
864 - if ( true === $allowed_protected_keys || in_array( $key, $allowed_protected_keys, true ) ) {
865 - $allow_index = true;
866 - }
867 - } else {
868 -
869 - if ( true !== $excluded_public_keys && ! in_array( $key, $excluded_public_keys, true ) ) {
870 - $allow_index = true;
871 - }
872 - }
873 -
874 - /**
875 - * Filter force whitelisting a meta key
876 - *
877 - * @hook ep_prepare_meta_whitelist_key
878 - * @param {bool} $whitelist True to whitelist key
879 - * @param {string} $key Meta key
880 - * @param {WP_Post} $post Post object
881 - * @return {bool} New whitelist value
882 - */
883 - if ( true === $allow_index || apply_filters( 'ep_prepare_meta_whitelist_key', false, $key, $post ) ) {
884 - $filtered_metas[ $key ] = $value;
885 - }
886 - }
887 862 return $filtered_metas;
888 863 }
889 864
890 865 /**
@@ -921,9 +896,11 @@
921 896 $filtered_metas = $this->filter_allowed_metas( $meta, $post );
922 897 $prepared_meta = [];
923 898
924 899 foreach ( $filtered_metas as $key => $value ) {
925 - $prepared_meta[ $key ] = maybe_unserialize( $value );
900 + if ( ! empty( $key ) ) {
901 + $prepared_meta[ $key ] = maybe_unserialize( $value );
902 + }
926 903 }
927 904
928 905 /**
929 906 * Filter final list of prepared meta.
@@ -934,9 +911,8 @@
934 911 * @since 3.4
935 912 * @return {array} Prepared meta
936 913 */
937 914 return apply_filters( 'ep_prepared_post_meta', $prepared_meta, $post );
938 -
939 915 }
940 916
941 917 /**
942 918 * Format WP query args for ES
@@ -966,26 +942,26 @@
966 942 $formatted_args = $this->maybe_add_sticky_posts( $formatted_args, $args );
967 943 $formatted_args = $this->maybe_set_aggs( $formatted_args, $args, $filters );
968 944
969 945 /**
970 - * Filter formatted Elasticsearch [ost ]query (entire query)
946 + * Filter formatted Elasticsearch query (entire query)
971 947 *
972 948 * @hook ep_formatted_args
973 949 * @param {array} $formatted_args Formatted Elasticsearch query
974 - * @param {array} $query_vars Query variables
975 - * @param {array} $query Query part
976 - * @return {array} New query
950 + * @param {array} $args WP_Query variables
951 + * @param {object} $wp_query WP_Query object
952 + * @return {array} New query
977 953 */
978 954 $formatted_args = apply_filters( 'ep_formatted_args', $formatted_args, $args, $wp_query );
979 955
980 956 /**
981 - * Filter formatted Elasticsearch [ost ]query (entire query)
957 + * Filter formatted Elasticsearch post query (entire query)
982 958 *
983 959 * @hook ep_post_formatted_args
984 960 * @param {array} $formatted_args Formatted Elasticsearch query
985 - * @param {array} $query_vars Query variables
986 - * @param {array} $query Query part
987 - * @return {array} New query
961 + * @param {array} $args WP_Query variables
962 + * @param {object} $wp_query WP_Query object
963 + * @return {array} New query
988 964 */
989 965 $formatted_args = apply_filters( 'ep_post_formatted_args', $formatted_args, $args, $wp_query );
990 966
991 967 return $formatted_args;
@@ -1222,22 +1198,26 @@
1222 1198 $orderby_clause = $value;
1223 1199 $order = $default_order;
1224 1200 }
1225 1201
1226 - if ( empty( $orderby_clause ) || 'rand' === $orderby_clause ) {
1202 + if ( empty( $orderby_clause ) || 'rand' === $orderby_clause || preg_match( '/^rand\((\d+)\)$/i', $orderby_clause ) ) {
1227 1203 continue;
1228 1204 }
1229 1205
1230 - if ( in_array( $orderby_clause, [ 'meta_value', 'meta_value_num' ], true ) ) {
1231 - if ( empty( $args['meta_key'] ) ) {
1232 - continue;
1233 - } else {
1234 - $from_to['meta_value'] = 'meta.' . $args['meta_key'] . '.raw';
1235 - $from_to['meta_value_num'] = 'meta.' . $args['meta_key'] . '.long';
1236 - }
1206 + /**
1207 + * If `orderby` is 'none', WordPress will let the database decide on what should be used to order.
1208 + * It will use the primary key ASC.
1209 + */
1210 + if ( 'none' === $orderby_clause ) {
1211 + $orderby_clause = 'ID';
1212 + $order = 'asc';
1237 1213 }
1238 1214
1239 - $orderby_clause = $from_to[ $orderby_clause ] ?? $orderby_clause;
1215 + if ( ! empty( $from_to[ $orderby_clause ] ) ) {
1216 + $orderby_clause = $from_to[ $orderby_clause ];
1217 + } else {
1218 + $orderby_clause = $this->parse_orderby_meta_fields( $orderby_clause, $args );
1219 + }
1240 1220
1241 1221 $sort[] = array(
1242 1222 $orderby_clause => array(
1243 1223 'order' => $order,
@@ -1248,8 +1228,97 @@
1248 1228 return $sort;
1249 1229 }
1250 1230
1251 1231 /**
1232 + * Try to parse orderby meta fields
1233 + *
1234 + * @since 4.6.0
1235 + * @param string $orderby_clause Current orderby value
1236 + * @param array $args Query args
1237 + * @return string New orderby value
1238 + */
1239 + protected function parse_orderby_meta_fields( $orderby_clause, $args ) {
1240 + global $wpdb;
1241 +
1242 + $from_to_metatypes = [
1243 + 'num' => 'long',
1244 + 'numeric' => 'long',
1245 + 'binary' => 'value.sortable',
1246 + 'char' => 'value.sortable',
1247 + 'date' => 'date',
1248 + 'datetime' => 'datetime',
1249 + 'decimal' => 'double',
1250 + 'signed' => 'long',
1251 + 'time' => 'time',
1252 + 'unsigned' => 'long',
1253 + ];
1254 +
1255 + // Code is targeting Elasticsearch directly
1256 + if ( preg_match( '/^meta\.(.*?)\.(.*)/', $orderby_clause, $match_meta ) ) {
1257 + return $orderby_clause;
1258 + }
1259 +
1260 + // WordPress meta_value_* compatibility
1261 + if ( preg_match( '/^meta_value_?(.*)/', $orderby_clause, $match_type ) ) {
1262 + $meta_type = $from_to_metatypes[ strtolower( $match_type[1] ) ] ?? 'value.sortable';
1263 + }
1264 +
1265 + if ( ! empty( $args['meta_key'] ) ) {
1266 + $meta_field = $args['meta_key'];
1267 + }
1268 +
1269 + // Already have everything needed
1270 + if ( isset( $meta_type ) && isset( $meta_field ) ) {
1271 + return "meta.{$meta_field}.{$meta_type}";
1272 + }
1273 +
1274 + // Don't have any other ways to guess
1275 + if ( empty( $args['meta_query'] ) ) {
1276 + return $orderby_clause;
1277 + }
1278 +
1279 + $meta_query = new \WP_Meta_Query( $args['meta_query'] );
1280 + // Calling get_sql() to populate the WP_Meta_Query->clauses attribute
1281 + $meta_query->get_sql( 'post', $wpdb->posts, 'ID' );
1282 +
1283 + $clauses = $meta_query->get_clauses();
1284 +
1285 + // If it refers to a named meta_query clause
1286 + if ( ! empty( $clauses[ $orderby_clause ] ) ) {
1287 + $meta_field = $clauses[ $orderby_clause ]['key'];
1288 + $clause_meta_type = strtolower( $clauses[ $orderby_clause ]['type'] ?? $clauses[ $orderby_clause ]['cast'] );
1289 + } else {
1290 + /**
1291 + * At this point we:
1292 + * 1. Try to find the meta key in any meta_query clause and use the type WP found
1293 + * 2. If ordering by `meta_value*`, use the first meta_query clause
1294 + * 3. Give up and use the orderby clause as is (code could be capturing it later on)
1295 + */
1296 + $meta_keys_and_types = wp_list_pluck( $clauses, 'cast', 'key' );
1297 + if ( isset( $meta_keys_and_types[ $orderby_clause ] ) ) {
1298 + $meta_field = $orderby_clause;
1299 + $clause_meta_type = strtolower( $meta_keys_and_types[ $orderby_clause ] ?? $meta_keys_and_types[ $orderby_clause ] );
1300 + } elseif ( isset( $meta_type ) ) {
1301 + $primary_clause = reset( $clauses );
1302 + $meta_field = $primary_clause['key'];
1303 + } else {
1304 + unset( $meta_type );
1305 + unset( $meta_field );
1306 + }
1307 + }
1308 +
1309 + if ( ! isset( $meta_type ) && isset( $clause_meta_type ) ) {
1310 + $meta_type = $from_to_metatypes[ $clause_meta_type ] ?? 'value.sortable';
1311 + }
1312 +
1313 + if ( isset( $meta_type ) && isset( $meta_field ) ) {
1314 + $orderby_clause = "meta.{$meta_field}.{$meta_type}";
1315 + }
1316 +
1317 + return $orderby_clause;
1318 + }
1319 +
1320 + /**
1252 1321 * Get Order by args Array
1253 1322 *
1254 1323 * @param string|array $orderbys Order by string or array
1255 1324 * @since 2.1
@@ -1314,28 +1383,8 @@
1314 1383 if ( isset( $post_title_sortable['normalizer'] ) ) {
1315 1384 return '5-2.php';
1316 1385 }
1317 1386
1318 - /**
1319 - * Check for 5-0 mapping.
1320 - * `keyword` fields were only made available in ES 5.0
1321 - *
1322 - * @see https://www.elastic.co/guide/en/elasticsearch/reference/5.0/release-notes-5.0.0.html
1323 - */
1324 - if ( 'keyword' === $post_title_sortable['type'] ) {
1325 - return '5-0.php';
1326 - }
1327 -
1328 - /**
1329 - * Check for pre-5-0 mapping.
1330 - * `string` fields were deprecated in ES 5.0 in favor of text/keyword
1331 - *
1332 - * @see https://www.elastic.co/guide/en/elasticsearch/reference/5.0/release-notes-5.0.0.html
1333 - */
1334 - if ( 'string' === $post_title_sortable['type'] ) {
1335 - return 'pre-5-0.php';
1336 - }
1337 -
1338 1387 return 'unknown';
1339 1388 }
1340 1389
1341 1390 /**
@@ -1377,9 +1426,9 @@
1377 1426 * @param array $search_fields Search fields
1378 1427 * @param array $query_vars Query vars
1379 1428 * @return SearchAlgorithm Instance of search algorithm to be used
1380 1429 */
1381 - public function get_search_algorithm( string $search_text, array $search_fields, array $query_vars ) : \ElasticPress\SearchAlgorithm {
1430 + public function get_search_algorithm( string $search_text, array $search_fields, array $query_vars ): \ElasticPress\SearchAlgorithm {
1382 1431 $search_algorithm_version_option = \ElasticPress\Utils\get_option( 'ep_search_algorithm_version', '4.0' );
1383 1432
1384 1433 /**
1385 1434 * Filter the algorithm version to be used.
@@ -1421,21 +1470,24 @@
1421 1470 * As previously there was no way to access each part, some snippets might be accessing
1422 1471 * these filters by its usual numeric indices (see the array_values() call below.)
1423 1472 */
1424 1473 $filters = [
1425 - 'tax_query' => $this->parse_tax_queries( $args, $query ),
1426 - 'post_parent' => $this->parse_post_parent( $args ),
1427 - 'post__in' => $this->parse_post__in( $args ),
1428 - 'post_name__in' => $this->parse_post_name__in( $args ),
1429 - 'post__not_in' => $this->parse_post__not_in( $args ),
1430 - 'category__not_in' => $this->parse_category__not_in( $args ),
1431 - 'tag__not_in' => $this->parse_tag__not_in( $args ),
1432 - 'author' => $this->parse_author( $args ),
1433 - 'post_mime_type' => $this->parse_post_mime_type( $args ),
1434 - 'date' => $this->parse_date( $args ),
1435 - 'meta_query' => $this->parse_meta_queries( $args ),
1436 - 'post_type' => $this->parse_post_type( $args ),
1437 - 'post_status' => $this->parse_post_status( $args ),
1474 + 'tax_query' => $this->parse_tax_queries( $args, $query ),
1475 + 'post_parent' => $this->parse_post_parent( $args ),
1476 + 'post_parent__in' => $this->parse_post_parent__in( $args ),
1477 + 'post_parent__not_in' => $this->parse_post_parent__not_in( $args ),
1478 + 'post__in' => $this->parse_post__in( $args ),
1479 + 'post_name__in' => $this->parse_post_name__in( $args ),
1480 + 'post__not_in' => $this->parse_post__not_in( $args ),
1481 + 'category__not_in' => $this->parse_category__not_in( $args ),
1482 + 'tag__not_in' => $this->parse_tag__not_in( $args ),
1483 + 'author' => $this->parse_author( $args ),
1484 + 'post_mime_type' => $this->parse_post_mime_type( $args ),
1485 + 'date' => $this->parse_date( $args ),
1486 + 'meta_query' => $this->parse_meta_queries( $args ),
1487 + 'post_type' => $this->parse_post_type( $args ),
1488 + 'post_status' => $this->parse_post_status( $args ),
1489 + 'painless_script' => $this->painless_script_query( $args ),
1438 1490 ];
1439 1491
1440 1492 /**
1441 1493 * Filter the ES filters that will be applied to the ES query.
@@ -1639,19 +1691,33 @@
1639 1691 $formatted_args['sort'] = $default_sort;
1640 1692 }
1641 1693
1642 1694 /**
1643 - * Order by 'rand' support
1695 + * Order by 'rand' and 'Rand(x)' support
1644 1696 *
1645 1697 * Ref: https://github.com/elastic/elasticsearch/issues/1170
1646 1698 */
1647 1699 if ( ! empty( $args['orderby'] ) ) {
1648 - $orderbys = $this->get_orderby_array( $args['orderby'] );
1649 - if ( in_array( 'rand', $orderbys, true ) ) {
1650 - $formatted_args_query = $formatted_args['query'];
1651 - $formatted_args['query'] = [];
1652 - $formatted_args['query']['function_score']['query'] = $formatted_args_query;
1653 - $formatted_args['query']['function_score']['random_score'] = (object) [];
1700 + $orderbys = $this->get_orderby_array( $args['orderby'] );
1701 + $random_orderbys = preg_grep( '/^rand(?:\((\d+)\))?$/i', $orderbys );
1702 +
1703 + if ( ! empty( $random_orderbys ) ) {
1704 + $formatted_args_query = $formatted_args['query'];
1705 + $formatted_args['query'] = [];
1706 + $formatted_args['query']['function_score']['query'] = $formatted_args_query;
1707 +
1708 + // Get the first random orderby found.
1709 + $random_orderby = reset( $random_orderbys );
1710 +
1711 + // check if rand with seed.
1712 + if ( preg_match( '/^rand\((\d+)\)$/i', $random_orderby, $matches ) ) {
1713 + $formatted_args['query']['function_score']['random_score'] = (object) [
1714 + 'seed' => (int) $matches[1],
1715 + 'field' => '_seq_no',
1716 + ];
1717 + } else {
1718 + $formatted_args['query']['function_score']['random_score'] = (object) [];
1719 + }
1654 1720 }
1655 1721 }
1656 1722
1657 1723 return $formatted_args;
@@ -1736,8 +1802,54 @@
1736 1802 ];
1737 1803 }
1738 1804
1739 1805 /**
1806 + * Parse the `post_parent__in` WP Query arg and transform it into an ES query clause.
1807 + *
1808 + * @since 4.5.0
1809 + * @param array $args WP_Query arguments
1810 + * @return array
1811 + */
1812 + protected function parse_post_parent__in( $args ) {
1813 + if ( empty( $args['post_parent__in'] ) ) {
1814 + return [];
1815 + }
1816 +
1817 + return [
1818 + 'bool' => [
1819 + 'must' => [
1820 + 'terms' => [
1821 + 'post_parent' => array_values( (array) $args['post_parent__in'] ),
1822 + ],
1823 + ],
1824 + ],
1825 + ];
1826 + }
1827 +
1828 + /**
1829 + * Parse the `post_parent__not_in` WP Query arg and transform it into an ES query clause.
1830 + *
1831 + * @since 4.5.0
1832 + * @param array $args WP_Query arguments
1833 + * @return array
1834 + */
1835 + protected function parse_post_parent__not_in( $args ) {
1836 + if ( empty( $args['post_parent__not_in'] ) ) {
1837 + return [];
1838 + }
1839 +
1840 + return [
1841 + 'bool' => [
1842 + 'must_not' => [
1843 + 'terms' => [
1844 + 'post_parent' => array_values( (array) $args['post_parent__not_in'] ),
1845 + ],
1846 + ],
1847 + ],
1848 + ];
1849 + }
1850 +
1851 + /**
1740 1852 * Parse the `post__in` WP Query arg and transform it into an ES query clause.
1741 1853 *
1742 1854 * @since 4.4.0
1743 1855 * @param array $args WP_Query arguments
@@ -1797,9 +1909,9 @@
1797 1909 return [
1798 1910 'bool' => [
1799 1911 'must_not' => [
1800 1912 'terms' => [
1801 - 'post_id' => (array) $args['post__not_in'],
1913 + 'post_id' => array_values( (array) $args['post__not_in'] ),
1802 1914 ],
1803 1915 ],
1804 1916 ],
1805 1917 ];
@@ -1934,9 +2046,12 @@
1934 2046 $args_post_mime_type[] = $mime_type;
1935 2047 } else {
1936 2048 $filtered_mime_type_by_type = wp_match_mime_types( $mime_type, wp_get_mime_types() );
1937 2049
1938 - $args_post_mime_type = array_merge( $args_post_mime_type, $filtered_mime_type_by_type[ $mime_type ] );
2050 + $args_post_mime_type = array_merge(
2051 + $args_post_mime_type,
2052 + $filtered_mime_type_by_type[ $mime_type ] ?? [ $mime_type ]
2053 + );
1939 2054 }
1940 2055 }
1941 2056
1942 2057 return [
@@ -1978,8 +2093,10 @@
1978 2093 $date_filter = $date_query->get_es_filter();
1979 2094
1980 2095 if ( array_key_exists( 'and', $date_filter ) ) {
1981 2096 return $date_filter['and'];
2097 + } elseif ( array_key_exists( 'or', $date_filter ) ) {
2098 + return $date_filter['or'];
1982 2099 }
1983 2100 }
1984 2101 }
1985 2102
@@ -2001,8 +2118,9 @@
2001 2118 *
2002 2119 * @since 1.3
2003 2120 */
2004 2121 $meta_queries = ( ! empty( $args['meta_query'] ) ) ? $args['meta_query'] : [];
2122 + $meta_queries = ( new \WP_Meta_Query() )->sanitize_query( $meta_queries );
2005 2123
2006 2124 /**
2007 2125 * Todo: Support meta_type
2008 2126 */
@@ -2108,35 +2226,14 @@
2108 2226 }
2109 2227
2110 2228 return [
2111 2229 $terms_map_name => [
2112 - 'post_status' => $post_status,
2230 + 'post_status' => is_array( $post_status ) ? array_values( $post_status ) : $post_status,
2113 2231 ],
2114 2232 ];
2115 2233 }
2116 - } else {
2234 + } elseif ( ! is_admin() ) {
2117 2235 $statuses = get_post_stati( array( 'public' => true ) );
2118 -
2119 - if ( is_admin() ) {
2120 - /**
2121 - * In the admin we will add protected and private post statuses to the default query
2122 - * per WP default behavior.
2123 - */
2124 - $statuses = array_merge(
2125 - $statuses,
2126 - get_post_stati(
2127 - array(
2128 - 'protected' => true,
2129 - 'show_in_admin_all_list' => true,
2130 - )
2131 - )
2132 - );
2133 -
2134 - if ( is_user_logged_in() ) {
2135 - $statuses = array_merge( $statuses, get_post_stati( array( 'private' => true ) ) );
2136 - }
2137 - }
2138 -
2139 2236 $statuses = array_values( $statuses );
2140 2237
2141 2238 $post_status_filter_type = 'terms';
2142 2239
@@ -2150,8 +2247,55 @@
2150 2247 return [];
2151 2248 }
2152 2249
2153 2250 /**
2251 + * Parse the `painless_script` WP Query arg and transform it into an ES query clause.
2252 + *
2253 + * @since 5.3.0
2254 + * @param array $args WP_Query arguments
2255 + * @return array
2256 + */
2257 + protected function painless_script_query( $args ) {
2258 + $scripts = $args['painless_script'] ?? [];
2259 +
2260 + if ( empty( $scripts ) ) {
2261 + return [];
2262 + }
2263 +
2264 + $normalized_scripts = array_map(
2265 + function ( $script_config ) {
2266 + return is_string( $script_config )
2267 + ? [ 'source' => $script_config ]
2268 + : $script_config;
2269 + },
2270 + $scripts
2271 + );
2272 +
2273 + $normalized_scripts = array_filter( $normalized_scripts );
2274 +
2275 + $filters = [];
2276 + foreach ( $normalized_scripts as $script_config ) {
2277 + $script_query = [ 'source' => $script_config['source'] ];
2278 +
2279 + if ( ! empty( $script_config['params'] ) && is_array( $script_config['params'] ) ) {
2280 + $script_query['params'] = $script_config['params'];
2281 + }
2282 +
2283 + $filters['bool']['must'][] = [
2284 + 'bool' => [
2285 + 'filter' => [
2286 + 'script' => [
2287 + 'script' => $script_query,
2288 + ],
2289 + ],
2290 + ],
2291 + ];
2292 + }
2293 +
2294 + return $filters;
2295 + }
2296 +
2297 + /**
2154 2298 * If in a search context set search fields, otherwise query everything.
2155 2299 *
2156 2300 * @since 4.4.0
2157 2301 * @param array $formatted_args Formatted Elasticsearch query
@@ -2375,9 +2519,9 @@
2375 2519 * @since 4.4.0
2376 2520 * @param string $field Field name
2377 2521 * @return string
2378 2522 */
2379 - protected function parse_tax_query_field( string $field ) : string {
2523 + protected function parse_tax_query_field( string $field ): string {
2380 2524
2381 2525 $from_to = [
2382 2526 'name' => 'name.raw',
2383 2527 'slug' => 'slug',
@@ -2387,8 +2531,143 @@
2387 2531 return $from_to[ $field ] ?? 'term_id';
2388 2532 }
2389 2533
2390 2534 /**
2535 + * Filter a list of meta keys down to those chosen by the user or
2536 + * allowed via a hook.
2537 + *
2538 + * This function is used when manual management of metadata fields is
2539 + * enabled. This is the default behaviour as of 5.0.0 and controlled by the
2540 + * `ep_meta_mode` filter.
2541 + *
2542 + * @param array $metas Key => value pairs of post meta
2543 + * @param WP_Post $post Post object
2544 + * @since 5.0.0
2545 + * @return array
2546 + */
2547 + protected function filter_allowed_metas_manual( $metas, $post ) {
2548 + $filtered_metas = [];
2549 + $search_feature = \ElasticPress\Features::factory()->get_registered_feature( 'search' );
2550 +
2551 + if ( empty( $post->post_type ) ) {
2552 + return $filtered_metas;
2553 + }
2554 +
2555 + $weighting = $search_feature->weighting->get_weighting_configuration_with_defaults();
2556 + $is_searchable = in_array( $search_feature, $search_feature->get_searchable_post_types(), true );
2557 + if ( empty( $weighting[ $post->post_type ] ) && $is_searchable ) {
2558 + return $filtered_metas;
2559 + }
2560 +
2561 + /** This filter is documented in includes/classes/Indexable/Post/Post.php */
2562 + $allowed_protected_keys = apply_filters( 'ep_prepare_meta_allowed_protected_keys', [], $post );
2563 +
2564 + $selected_keys = [];
2565 + if ( ! empty( $weighting[ $post->post_type ] ) ) {
2566 + $selected_keys = array_map(
2567 + function ( $field ) {
2568 + if ( false === strpos( $field, 'meta.' ) ) {
2569 + return null;
2570 + }
2571 + $field_name_parts = explode( '.', $field );
2572 + return $field_name_parts[1];
2573 + },
2574 + array_keys( $weighting[ $post->post_type ] )
2575 + );
2576 + $selected_keys = array_filter( $selected_keys );
2577 + }
2578 +
2579 + /**
2580 + * Filter indexable meta keys for posts
2581 + *
2582 + * @hook ep_prepare_meta_allowed_keys
2583 + * @param {array} $keys Allowed keys
2584 + * @param {WP_Post} $post Post object
2585 + * @since 5.0.0
2586 + * @return {array} New keys
2587 + */
2588 + $allowed_keys = apply_filters( 'ep_prepare_meta_allowed_keys', array_merge( $allowed_protected_keys, $selected_keys ), $post );
2589 +
2590 + foreach ( $metas as $key => $value ) {
2591 + if ( ! in_array( $key, $allowed_keys, true ) ) {
2592 + continue;
2593 + }
2594 +
2595 + $filtered_metas[ $key ] = $value;
2596 + }
2597 +
2598 + return $filtered_metas;
2599 + }
2600 +
2601 + /**
2602 + * Filter a list of meta keys down to public keys or protected keys
2603 + * allowed via a hook.
2604 + *
2605 + * This function is used to filter meta keys when ElasticPress is in
2606 + * network mode or when the meta mode is set to `auto` via the
2607 + * `ep_meta_mode` hook. This was the default behaviour prior to 5.0.0.
2608 + *
2609 + * @param array $metas Key => value pairs of post meta
2610 + * @param WP_Post $post Post object
2611 + * @since 5.0.0
2612 + * @return array
2613 + */
2614 + protected function filter_allowed_metas_auto( $metas, $post ) {
2615 + $filtered_metas = [];
2616 +
2617 + /**
2618 + * Filter indexable protected meta keys for posts
2619 + *
2620 + * @hook ep_prepare_meta_allowed_protected_keys
2621 + * @param {array} $keys Allowed protected keys
2622 + * @param {WP_Post} $post Post object
2623 + * @since 1.7
2624 + * @return {array} New keys
2625 + */
2626 + $allowed_protected_keys = apply_filters( 'ep_prepare_meta_allowed_protected_keys', [], $post );
2627 +
2628 + /**
2629 + * Filter public keys to exclude from indexed post
2630 + *
2631 + * @hook ep_prepare_meta_excluded_public_keys
2632 + * @param {array} $keys Excluded protected keys
2633 + * @param {WP_Post} $post Post object
2634 + * @since 1.7
2635 + * @return {array} New keys
2636 + */
2637 + $excluded_public_keys = apply_filters( 'ep_prepare_meta_excluded_public_keys', [], $post );
2638 +
2639 + foreach ( $metas as $key => $value ) {
2640 +
2641 + $allow_index = false;
2642 +
2643 + if ( is_protected_meta( $key ) ) {
2644 +
2645 + if ( true === $allowed_protected_keys || in_array( $key, $allowed_protected_keys, true ) ) {
2646 + $allow_index = true;
2647 + }
2648 + } elseif ( true !== $excluded_public_keys && ! in_array( $key, $excluded_public_keys, true ) ) {
2649 +
2650 + $allow_index = true;
2651 + }
2652 +
2653 + /**
2654 + * Filter force whitelisting a meta key
2655 + *
2656 + * @hook ep_prepare_meta_whitelist_key
2657 + * @param {bool} $whitelist True to whitelist key
2658 + * @param {string} $key Meta key
2659 + * @param {WP_Post} $post Post object
2660 + * @return {bool} New whitelist value
2661 + */
2662 + if ( true === $allow_index || apply_filters( 'ep_prepare_meta_whitelist_key', false, $key, $post ) ) {
2663 + $filtered_metas[ $key ] = $value;
2664 + }
2665 + }
2666 + return $filtered_metas;
2667 + }
2668 +
2669 + /**
2391 2670 * Return all distinct meta fields in the database.
2392 2671 *
2393 2672 * @since 4.4.0
2394 2673 * @param bool $force_refresh Whether to use or not a cached value. Default false, use cached.
@@ -2393,9 +2672,9 @@
2393 2672 * @since 4.4.0
2394 2673 * @param bool $force_refresh Whether to use or not a cached value. Default false, use cached.
2395 2674 * @return array
2396 2675 */
2397 - public function get_distinct_meta_field_keys_db( bool $force_refresh = false ) : array {
2676 + public function get_distinct_meta_field_keys_db( bool $force_refresh = false ): array {
2398 2677 global $wpdb;
2399 2678
2400 2679 /**
2401 2680 * Short-circuits the process of getting distinct meta keys from the database.
@@ -2434,10 +2713,10 @@
2434 2713 $placeholders = implode( ',', array_fill( 0, count( $allowed_protected_keys ), '%s' ) );
2435 2714 $allowed_protected_keys_sql = " OR meta_key IN ( {$placeholders} ) ";
2436 2715 }
2437 2716
2717 + // phpcs:disable WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
2438 2718 $meta_keys = $wpdb->get_col(
2439 - // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
2440 2719 $wpdb->prepare(
2441 2720 "SELECT DISTINCT meta_key
2442 2721 FROM {$wpdb->postmeta}
2443 2722 WHERE meta_key NOT LIKE %s {$allowed_protected_keys_sql}
@@ -2444,10 +2723,11 @@
2444 2723 LIMIT 800",
2445 2724 '\_%',
2446 2725 ...$allowed_protected_keys
2447 2726 )
2448 - // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
2449 2727 );
2728 + // phpcs:enable WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
2729 +
2450 2730 sort( $meta_keys );
2451 2731
2452 2732 // Make sure the size of the transient will not be bigger than 1MB
2453 2733 do {
@@ -2478,9 +2758,9 @@
2478 2758 * @param string $post_type Post type slug
2479 2759 * @param bool $force_refresh Whether to use or not a cached value. Default false, use cached.
2480 2760 * @return array
2481 2761 */
2482 - public function get_distinct_meta_field_keys_db_per_post_type( string $post_type, bool $force_refresh = false ) : array {
2762 + public function get_distinct_meta_field_keys_db_per_post_type( string $post_type, bool $force_refresh = false ): array {
2483 2763 $allowed_screen = 'status-report' === \ElasticPress\Screen::factory()->get_current_screen();
2484 2764
2485 2765 /**
2486 2766 * Filter if the current screen is allowed or not to use the function.
@@ -2566,9 +2846,9 @@
2566 2846 * @param string $post_type Post type slug
2567 2847 * @param bool $force_refresh Whether to use or not a cached value. Default false, use cached.
2568 2848 * @return array
2569 2849 */
2570 - public function get_indexable_meta_keys_per_post_type( string $post_type, bool $force_refresh = false ) : array {
2850 + public function get_indexable_meta_keys_per_post_type( string $post_type, bool $force_refresh = false ): array {
2571 2851 $mock_post = new \WP_Post( (object) [ 'post_type' => $post_type ] );
2572 2852 $meta_keys = $this->get_distinct_meta_field_keys_db_per_post_type( $post_type, $force_refresh );
2573 2853
2574 2854 $fake_meta_values = array_combine( $meta_keys, array_fill( 0, count( $meta_keys ), 'test-value' ) );
@@ -2593,18 +2873,21 @@
2593 2873 * @since 4.4.0
2594 2874 * @param bool $force_refresh Whether to use or not a cached value. Default false, use cached.
2595 2875 * @return array
2596 2876 */
2597 - public function get_predicted_indexable_meta_keys( bool $force_refresh = false ) : array {
2877 + public function get_predicted_indexable_meta_keys( bool $force_refresh = false ): array {
2598 2878 $empty_post = new \WP_Post( (object) [] );
2599 2879 $meta_keys = $this->get_distinct_meta_field_keys_db( $force_refresh );
2600 2880
2601 - $fake_meta_values = array_combine( $meta_keys, array_fill( 0, count( $meta_keys ), 'test-value' ) );
2881 + $fake_meta_values = array_combine(
2882 + $meta_keys,
2883 + array_fill( 0, count( $meta_keys ), $this->get_test_meta_value() )
2884 + );
2602 2885 $filtered_meta = apply_filters( 'ep_prepare_meta_data', $fake_meta_values, $empty_post );
2603 2886
2604 2887 $all_keys = array_filter(
2605 2888 array_keys( $filtered_meta ),
2606 - function( $meta_key ) use ( $empty_post ) {
2889 + function ( $meta_key ) use ( $empty_post ) {
2607 2890 return $this->is_meta_allowed( $meta_key, $empty_post );
2608 2891 }
2609 2892 );
2610 2893
@@ -2613,8 +2896,26 @@
2613 2896 return $all_keys;
2614 2897 }
2615 2898
2616 2899 /**
2900 + * Return the value used to fill meta fields while predicting indexable content.
2901 + *
2902 + * @since 5.1.0
2903 + * @return string
2904 + */
2905 + public function get_test_meta_value(): string {
2906 + /**
2907 + * Filter the value used to fill meta fields while predicting indexable content.
2908 + *
2909 + * @hook ep_post_test_meta_value
2910 + * @since 5.1.0
2911 + * @param {string} $test_meta_value The test meta value. Default: test-value
2912 + * @return {string} New test meta value
2913 + */
2914 + return (string) apply_filters( 'ep_post_test_meta_value', 'test-value' );
2915 + }
2916 +
2917 + /**
2617 2918 * Given a post type, *yields* their Post IDs.
2618 2919 *
2619 2920 * If post IDs are found, this function will return a PHP Generator. To avoid timeout, it will yield 8 groups or 11,000 IDs.
2620 2921 *
@@ -2625,9 +2926,14 @@
2625 2926 */
2626 2927 protected function get_lazy_post_type_ids( string $post_type ) {
2627 2928 global $wpdb;
2628 2929
2629 - $total = $wpdb->get_var( $wpdb->prepare( "SELECT count(*) FROM {$wpdb->posts} WHERE post_type = %s", $post_type ) );
2930 + $total = $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
2931 + $wpdb->prepare(
2932 + "SELECT count(*) FROM {$wpdb->posts} WHERE post_type = %s",
2933 + $post_type
2934 + )
2935 + );
2630 2936
2631 2937 if ( ! $total ) {
2632 2938 return [];
2633 2939 }
@@ -2658,9 +2964,9 @@
2658 2964 $pages = apply_filters( 'ep_post_meta_by_type_number_of_pages', $pages, $per_page, $post_type );
2659 2965
2660 2966 for ( $page = 0; $page < $pages; $page++ ) {
2661 2967 $start = $per_page * $page;
2662 - $ids = $wpdb->get_col(
2968 + $ids = $wpdb->get_col( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
2663 2969 $wpdb->prepare(
2664 2970 "SELECT ID FROM {$wpdb->posts} WHERE post_type = %s LIMIT %d, %d",
2665 2971 $post_type,
2666 2972 $start,
@@ -2677,9 +2983,9 @@
2677 2983 * @since 4.4.0
2678 2984 * @param array $post_ids Set of post IDs
2679 2985 * @return array
2680 2986 */
2681 - protected function get_meta_keys_from_post_ids( array $post_ids ) : array {
2987 + protected function get_meta_keys_from_post_ids( array $post_ids ): array {
2682 2988 global $wpdb;
2683 2989
2684 2990 if ( empty( $post_ids ) ) {
2685 2991 return [];
@@ -2685,9 +2991,9 @@
2685 2991 return [];
2686 2992 }
2687 2993
2688 2994 $placeholders = implode( ',', array_fill( 0, count( $post_ids ), '%d' ) );
2689 - $meta_keys = $wpdb->get_col(
2995 + $meta_keys = $wpdb->get_col( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
2690 2996 $wpdb->prepare(
2691 2997 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
2692 2998 "SELECT DISTINCT meta_key FROM {$wpdb->postmeta} WHERE post_id IN ( {$placeholders} )",
2693 2999 $post_ids
@@ -2694,6 +3000,85 @@
2694 3000 )
2695 3001 );
2696 3002
2697 3003 return $meta_keys;
3004 + }
3005 +
3006 + /**
3007 + * Add a `term_suggest` field to the mapping.
3008 + *
3009 + * This method assumes the `edge_ngram_analyzer` analyzer was already added to the mapping.
3010 + *
3011 + * @since 4.5.0
3012 + * @param array $mapping The mapping array
3013 + * @return array
3014 + */
3015 + public function add_term_suggest_field( array $mapping ): array {
3016 + if ( version_compare( (string) Elasticsearch::factory()->get_elasticsearch_version(), '7.0', '<' ) ) {
3017 + $mapping_properties = &$mapping['mappings']['post']['properties'];
3018 + } else {
3019 + $mapping_properties = &$mapping['mappings']['properties'];
3020 + }
3021 +
3022 + $text_type = $mapping_properties['post_content']['type'];
3023 +
3024 + $mapping_properties['term_suggest'] = array(
3025 + 'type' => $text_type,
3026 + 'analyzer' => 'edge_ngram_analyzer',
3027 + 'search_analyzer' => 'standard',
3028 + );
3029 +
3030 + return $mapping;
3031 + }
3032 +
3033 + /**
3034 + * Return all meta data added to the Weighting Dashboard plus all allowed keys via code.
3035 + *
3036 + * @since 5.1.4
3037 + * @return array
3038 + */
3039 + public function get_all_allowed_metas_manual(): array {
3040 + $post_types = \ElasticPress\Indexables::factory()->get( 'post' )->get_indexable_post_types();
3041 + $search_feature = \ElasticPress\Features::factory()->get_registered_feature( 'search' );
3042 + $weighting = $search_feature->weighting->get_weighting_configuration_with_defaults();
3043 + $fake_post = new \WP_Post( new \stdClass() );
3044 +
3045 + $all_allowed_metas = [];
3046 + foreach ( $post_types as $post_type ) {
3047 + $fake_post->post_type = $post_type;
3048 + $allowed_protected_keys = apply_filters( 'ep_prepare_meta_allowed_protected_keys', [], $fake_post );
3049 +
3050 + $selected_keys = [];
3051 + if ( ! empty( $weighting[ $post_type ] ) ) {
3052 + $selected_keys = array_map(
3053 + function ( $field ) {
3054 + if ( false === strpos( $field, 'meta.' ) ) {
3055 + return null;
3056 + }
3057 + $field_name_parts = explode( '.', $field );
3058 + return $field_name_parts[1];
3059 + },
3060 + array_keys( $weighting[ $post_type ] )
3061 + );
3062 + $selected_keys = array_filter( $selected_keys );
3063 + }
3064 +
3065 + $allowed_keys = apply_filters( 'ep_prepare_meta_allowed_keys', array_merge( $allowed_protected_keys, $selected_keys ), $fake_post );
3066 +
3067 + $all_allowed_metas = array_merge( $all_allowed_metas, $allowed_keys );
3068 + }
3069 +
3070 + return array_unique( $all_allowed_metas );
3071 + }
3072 +
3073 + /**
3074 + * Sets the ORDER BY clause to sort posts by post ID in descending order.
3075 + *
3076 + * @return string The modified order by clause.
3077 + *
3078 + * @since 5.2.0
3079 + */
3080 + public function set_posts_orderby(): string {
3081 + global $wpdb;
3082 + return "{$wpdb->posts}.ID DESC";
2698 3083 }
2699 3084 }