| 1 |
<?php |
| 2 |
/** |
| 3 |
* User indexable |
| 4 |
* |
| 5 |
* @since 3.0 |
| 6 |
* @package elasticpress |
| 7 |
*/ |
| 8 |
|
| 9 |
namespace ElasticPress\Indexable\Post; |
| 10 |
|
| 11 |
use \WP_Query; |
| 12 |
use \WP_User; |
| 13 |
use ElasticPress\Elasticsearch; |
| 14 |
use ElasticPress\Indexable; |
| 15 |
|
| 16 |
if ( ! defined( 'ABSPATH' ) ) { |
| 17 |
// @codeCoverageIgnoreStart |
| 18 |
exit; // Exit if accessed directly. |
| 19 |
// @codeCoverageIgnoreEnd |
| 20 |
} |
| 21 |
|
| 22 |
/** |
| 23 |
* Post indexable class |
| 24 |
*/ |
| 25 |
class Post extends Indexable { |
| 26 |
|
| 27 |
/** |
| 28 |
* Indexable slug used for identification |
| 29 |
* |
| 30 |
* @var string |
| 31 |
* @since 3.0 |
| 32 |
*/ |
| 33 |
public $slug = 'post'; |
| 34 |
|
| 35 |
/** |
| 36 |
* Flag to indicate if the indexable has support for |
| 37 |
* `id_range` pagination method during a sync. |
| 38 |
* |
| 39 |
* @var boolean |
| 40 |
* @since 4.1.0 |
| 41 |
*/ |
| 42 |
public $support_indexing_advanced_pagination = true; |
| 43 |
|
| 44 |
/** |
| 45 |
* Create indexable and initialize dependencies |
| 46 |
* |
| 47 |
* @since 3.0 |
| 48 |
*/ |
| 49 |
public function __construct() { |
| 50 |
$this->labels = [ |
| 51 |
'plural' => esc_html__( 'Posts', 'elasticpress' ), |
| 52 |
'singular' => esc_html__( 'Post', 'elasticpress' ), |
| 53 |
]; |
| 54 |
|
| 55 |
$this->sync_manager = new SyncManager( $this->slug ); |
| 56 |
$this->query_integration = new QueryIntegration( $this->slug ); |
| 57 |
} |
| 58 |
|
| 59 |
/** |
| 60 |
* Query database for posts |
| 61 |
* |
| 62 |
* @param array $args Query DB args |
| 63 |
* @since 3.0 |
| 64 |
* @return array |
| 65 |
*/ |
| 66 |
public function query_db( $args ) { |
| 67 |
$defaults = [ |
| 68 |
'posts_per_page' => $this->get_bulk_items_per_page(), |
| 69 |
'post_type' => $this->get_indexable_post_types(), |
| 70 |
'post_status' => $this->get_indexable_post_status(), |
| 71 |
'offset' => 0, |
| 72 |
'ignore_sticky_posts' => true, |
| 73 |
'orderby' => 'ID', |
| 74 |
'order' => 'desc', |
| 75 |
'no_found_rows' => false, |
| 76 |
'ep_indexing_advanced_pagination' => true, |
| 77 |
'has_password' => false, |
| 78 |
]; |
| 79 |
|
| 80 |
if ( isset( $args['per_page'] ) ) { |
| 81 |
$args['posts_per_page'] = $args['per_page']; |
| 82 |
} |
| 83 |
|
| 84 |
if ( isset( $args['include'] ) ) { |
| 85 |
$args['post__in'] = $args['include']; |
| 86 |
} |
| 87 |
|
| 88 |
if ( isset( $args['exclude'] ) ) { |
| 89 |
$args['post__not_in'] = $args['exclude']; |
| 90 |
} |
| 91 |
|
| 92 |
/** |
| 93 |
* Filter arguments used to query posts from database |
| 94 |
* |
| 95 |
* @hook ep_post_query_db_args |
| 96 |
* @param {array} $args Database arguments |
| 97 |
* @return {array} New arguments |
| 98 |
*/ |
| 99 |
$args = apply_filters( 'ep_index_posts_args', apply_filters( 'ep_post_query_db_args', wp_parse_args( $args, $defaults ) ) ); |
| 100 |
|
| 101 |
if ( isset( $args['post__in'] ) || 0 < $args['offset'] ) { |
| 102 |
// Disable advanced pagination. Not useful if only indexing specific IDs. |
| 103 |
$args['ep_indexing_advanced_pagination'] = false; |
| 104 |
} |
| 105 |
|
| 106 |
// Enforce the following query args during advanced pagination to ensure things work correctly. |
| 107 |
if ( $args['ep_indexing_advanced_pagination'] ) { |
| 108 |
$args = array_merge( |
| 109 |
$args, |
| 110 |
[ |
| 111 |
'suppress_filters' => false, |
| 112 |
'orderby' => 'ID', |
| 113 |
'order' => 'DESC', |
| 114 |
'paged' => 1, |
| 115 |
'offset' => 0, |
| 116 |
'no_found_rows' => true, |
| 117 |
] |
| 118 |
); |
| 119 |
add_filter( 'posts_where', array( $this, 'bulk_indexing_filter_posts_where' ), 9999, 2 ); |
| 120 |
|
| 121 |
$query = new WP_Query( $args ); |
| 122 |
$total_objects = $this->get_total_objects_for_query( $args ); |
| 123 |
|
| 124 |
remove_filter( 'posts_where', array( $this, 'bulk_indexing_filter_posts_where' ), 9999, 2 ); |
| 125 |
} else { |
| 126 |
$query = new WP_Query( $args ); |
| 127 |
$total_objects = $query->found_posts; |
| 128 |
} |
| 129 |
|
| 130 |
return [ |
| 131 |
'objects' => $query->posts, |
| 132 |
'total_objects' => $total_objects, |
| 133 |
]; |
| 134 |
} |
| 135 |
|
| 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 |
*/ |
| 143 |
public function bulk_indexing_filter_posts_where( $where, $query ) { |
| 144 |
$using_advanced_pagination = $query->get( 'ep_indexing_advanced_pagination', false ); |
| 145 |
|
| 146 |
if ( $using_advanced_pagination ) { |
| 147 |
$requested_upper_limit_id = $query->get( 'ep_indexing_upper_limit_object_id', PHP_INT_MAX ); |
| 148 |
$requested_lower_limit_post_id = $query->get( 'ep_indexing_lower_limit_object_id', 0 ); |
| 149 |
$last_processed_id = $query->get( 'ep_indexing_last_processed_object_id', null ); |
| 150 |
|
| 151 |
// On the first loopthrough we begin with the requested upper limit ID. Afterwards, use the last processed ID to paginate. |
| 152 |
$upper_limit_range_post_id = $requested_upper_limit_id; |
| 153 |
if ( is_numeric( $last_processed_id ) ) { |
| 154 |
$upper_limit_range_post_id = $last_processed_id - 1; |
| 155 |
} |
| 156 |
|
| 157 |
// Sanitize. Abort if unexpected data at this point. |
| 158 |
if ( ! is_numeric( $upper_limit_range_post_id ) || ! is_numeric( $requested_lower_limit_post_id ) ) { |
| 159 |
return $where; |
| 160 |
} |
| 161 |
|
| 162 |
$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}", |
| 165 |
]; |
| 166 |
|
| 167 |
// Skip the end range if it's unnecessary. |
| 168 |
$skip_ending_range = 0 === $requested_lower_limit_post_id; |
| 169 |
$where = $skip_ending_range ? "AND {$range['upper_limit']} {$where}" : "AND {$range['upper_limit']} AND {$range['lower_limit']} {$where}"; |
| 170 |
} |
| 171 |
|
| 172 |
return $where; |
| 173 |
} |
| 174 |
|
| 175 |
/** |
| 176 |
* Get SQL_CALC_FOUND_ROWS for a specific query based on it's args. |
| 177 |
* |
| 178 |
* @param array $query_args The query args. |
| 179 |
* @return int The query result's found_posts. |
| 180 |
*/ |
| 181 |
protected function get_total_objects_for_query( $query_args ) { |
| 182 |
static $object_counts = []; |
| 183 |
|
| 184 |
// Reset the pagination-related args for optimal caching. |
| 185 |
$normalized_query_args = array_merge( |
| 186 |
$query_args, |
| 187 |
[ |
| 188 |
'offset' => 0, |
| 189 |
'paged' => 1, |
| 190 |
'posts_per_page' => 1, |
| 191 |
'no_found_rows' => false, |
| 192 |
'ep_indexing_last_processed_object_id' => null, |
| 193 |
] |
| 194 |
); |
| 195 |
|
| 196 |
$cache_key = md5( get_current_blog_id() . wp_json_encode( $normalized_query_args ) ); |
| 197 |
|
| 198 |
if ( ! isset( $object_counts[ $cache_key ] ) ) { |
| 199 |
$object_counts[ $cache_key ] = ( new WP_Query( $normalized_query_args ) )->found_posts; |
| 200 |
} |
| 201 |
|
| 202 |
if ( 0 === $object_counts[ $cache_key ] ) { |
| 203 |
// Do a DB count to make sure the query didn't just die and return 0. |
| 204 |
$db_post_count = $this->get_total_objects_for_query_from_db( $normalized_query_args ); |
| 205 |
|
| 206 |
if ( $db_post_count !== $object_counts[ $cache_key ] ) { |
| 207 |
$object_counts[ $cache_key ] = $db_post_count; |
| 208 |
} |
| 209 |
} |
| 210 |
|
| 211 |
return $object_counts[ $cache_key ]; |
| 212 |
} |
| 213 |
|
| 214 |
/** |
| 215 |
* Get total posts from DB for a specific query based on it's args. |
| 216 |
* |
| 217 |
* @param array $query_args The query args. |
| 218 |
* @since 4.0.0 |
| 219 |
* @return int The total posts. |
| 220 |
*/ |
| 221 |
protected function get_total_objects_for_query_from_db( $query_args ) { |
| 222 |
global $wpdb; |
| 223 |
|
| 224 |
$post_count = 0; |
| 225 |
|
| 226 |
if ( ! isset( $query_args['post_type'] ) || isset( $query_args['ep_indexing_upper_limit_object_id'] ) |
| 227 |
|| isset( $query_args['ep_indexing_lower_limit_object_id'] ) ) { |
| 228 |
return $post_count; |
| 229 |
} |
| 230 |
|
| 231 |
foreach ( $query_args['post_type'] as $post_type ) { |
| 232 |
$post_counts_by_post_status = wp_count_posts( $post_type ); |
| 233 |
foreach ( $post_counts_by_post_status as $post_status => $post_status_count ) { |
| 234 |
if ( ! in_array( $post_status, $query_args['post_status'], true ) ) { |
| 235 |
continue; |
| 236 |
} |
| 237 |
$post_count += $post_status_count; |
| 238 |
} |
| 239 |
} |
| 240 |
|
| 241 |
/** |
| 242 |
* As `wp_count_posts` will also count posts with password, we need to remove |
| 243 |
* them from the final count if they will not be used. |
| 244 |
* |
| 245 |
* The if below will pass if `has_password` is false but not null. |
| 246 |
*/ |
| 247 |
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 != ''" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery |
| 249 |
|
| 250 |
$post_count -= $posts_with_password; |
| 251 |
} |
| 252 |
|
| 253 |
return $post_count; |
| 254 |
} |
| 255 |
|
| 256 |
/** |
| 257 |
* Returns indexable post types for the current site |
| 258 |
* |
| 259 |
* @since 0.9 |
| 260 |
* @return mixed|void |
| 261 |
*/ |
| 262 |
public function get_indexable_post_types() { |
| 263 |
$post_types = get_post_types( array( 'public' => true ) ); |
| 264 |
|
| 265 |
/** |
| 266 |
* Remove attachments by default |
| 267 |
* |
| 268 |
* @since 3.0 |
| 269 |
*/ |
| 270 |
unset( $post_types['attachment'] ); |
| 271 |
|
| 272 |
/** |
| 273 |
* Filter indexable post types |
| 274 |
* |
| 275 |
* @hook ep_indexable_post_types |
| 276 |
* @param {array} $post_types Indexable post types |
| 277 |
* @return {array} New post types |
| 278 |
*/ |
| 279 |
return apply_filters( 'ep_indexable_post_types', $post_types ); |
| 280 |
} |
| 281 |
|
| 282 |
/** |
| 283 |
* Return indexable post_status for the current site |
| 284 |
* |
| 285 |
* @since 1.3 |
| 286 |
* @return array |
| 287 |
*/ |
| 288 |
public function get_indexable_post_status() { |
| 289 |
/** |
| 290 |
* Filter indexable post statuses |
| 291 |
* |
| 292 |
* @hook ep_indexable_post_status |
| 293 |
* @param {array} $post_statuses Indexable post statuses |
| 294 |
* @return {array} New post statuses |
| 295 |
*/ |
| 296 |
return apply_filters( 'ep_indexable_post_status', array( 'publish' ) ); |
| 297 |
} |
| 298 |
|
| 299 |
/** |
| 300 |
* Determine required mapping file |
| 301 |
* |
| 302 |
* @since 3.6.2 |
| 303 |
* @return string |
| 304 |
*/ |
| 305 |
public function get_mapping_name() { |
| 306 |
$es_version = Elasticsearch::factory()->get_elasticsearch_version(); |
| 307 |
|
| 308 |
if ( empty( $es_version ) ) { |
| 309 |
/** |
| 310 |
* Filter fallback Elasticsearch version |
| 311 |
* |
| 312 |
* @hook ep_fallback_elasticsearch_version |
| 313 |
* @param {string} $version Fall back Elasticsearch version |
| 314 |
* @return {string} New version |
| 315 |
*/ |
| 316 |
$es_version = apply_filters( 'ep_fallback_elasticsearch_version', '2.0' ); |
| 317 |
} |
| 318 |
$es_version = (string) $es_version; |
| 319 |
|
| 320 |
$mapping_file = '5-2.php'; |
| 321 |
|
| 322 |
if ( ! $es_version || version_compare( $es_version, '5.0' ) < 0 ) { |
| 323 |
$mapping_file = 'pre-5-0.php'; |
| 324 |
} elseif ( version_compare( $es_version, '5.0', '>=' ) && version_compare( $es_version, '5.2', '<' ) ) { |
| 325 |
$mapping_file = '5-0.php'; |
| 326 |
} elseif ( version_compare( $es_version, '5.2', '>=' ) && version_compare( $es_version, '7.0', '<' ) ) { |
| 327 |
$mapping_file = '5-2.php'; |
| 328 |
} elseif ( version_compare( $es_version, '7.0', '>=' ) ) { |
| 329 |
$mapping_file = '7-0.php'; |
| 330 |
} |
| 331 |
|
| 332 |
return apply_filters( 'ep_post_mapping_version', $mapping_file ); |
| 333 |
} |
| 334 |
|
| 335 |
/** |
| 336 |
* Generate the mapping array |
| 337 |
* |
| 338 |
* @since 4.1.0 |
| 339 |
* @return array |
| 340 |
*/ |
| 341 |
public function generate_mapping() { |
| 342 |
$mapping_file = $this->get_mapping_name(); |
| 343 |
|
| 344 |
/** |
| 345 |
* Filter post indexable mapping file |
| 346 |
* |
| 347 |
* @hook ep_post_mapping_file |
| 348 |
* @param {string} $file Path to file |
| 349 |
* @return {string} New file path |
| 350 |
*/ |
| 351 |
$mapping = require apply_filters( 'ep_post_mapping_file', __DIR__ . '/../../../mappings/post/' . $mapping_file ); |
| 352 |
|
| 353 |
/** |
| 354 |
* Filter post indexable mapping |
| 355 |
* |
| 356 |
* @hook ep_post_mapping |
| 357 |
* @param {array} $mapping Mapping |
| 358 |
* @return {array} New mapping |
| 359 |
*/ |
| 360 |
$mapping = apply_filters( 'ep_post_mapping', $mapping ); |
| 361 |
|
| 362 |
delete_transient( 'ep_post_mapping_version' ); |
| 363 |
|
| 364 |
return $mapping; |
| 365 |
} |
| 366 |
|
| 367 |
/** |
| 368 |
* Determine version of mapping currently on the post index. |
| 369 |
* |
| 370 |
* @since 3.6.2 |
| 371 |
* @return string|WP_Error|false $version |
| 372 |
*/ |
| 373 |
public function determine_mapping_version() { |
| 374 |
$version = get_transient( 'ep_post_mapping_version' ); |
| 375 |
|
| 376 |
if ( empty( $version ) ) { |
| 377 |
$index = $this->get_index_name(); |
| 378 |
$mapping = Elasticsearch::factory()->get_mapping( $index ); |
| 379 |
|
| 380 |
if ( empty( $mapping ) ) { |
| 381 |
return new \WP_Error( 'ep_failed_mapping_version', esc_html__( 'Error while fetching the mapping version.', 'elasticpress' ) ); |
| 382 |
} |
| 383 |
|
| 384 |
if ( ! isset( $mapping[ $index ] ) ) { |
| 385 |
return false; |
| 386 |
} |
| 387 |
|
| 388 |
$version = $this->determine_mapping_version_based_on_existing( $mapping, $index ); |
| 389 |
|
| 390 |
set_transient( |
| 391 |
'ep_post_mapping_version', |
| 392 |
$version, |
| 393 |
/** |
| 394 |
* Filter the post mapping version cache expiration. |
| 395 |
* |
| 396 |
* @hook ep_post_mapping_version_cache_expiration |
| 397 |
* @since 3.6.5 |
| 398 |
* @param {int} $version Time in seconds for the transient expiration |
| 399 |
* @return {int} New time |
| 400 |
*/ |
| 401 |
apply_filters( 'ep_post_mapping_version_cache_expiration', DAY_IN_SECONDS ) |
| 402 |
); |
| 403 |
} |
| 404 |
|
| 405 |
/** |
| 406 |
* Filter the mapping version for posts. |
| 407 |
* |
| 408 |
* @hook ep_post_mapping_version_determined |
| 409 |
* @since 3.6.2 |
| 410 |
* @param {string} $version Determined version string |
| 411 |
* @return {string} New version string |
| 412 |
*/ |
| 413 |
return apply_filters( 'ep_post_mapping_version_determined', $version ); |
| 414 |
} |
| 415 |
|
| 416 |
/** |
| 417 |
* Prepare a post for syncing |
| 418 |
* |
| 419 |
* @param int $post_id Post ID. |
| 420 |
* @since 0.9.1 |
| 421 |
* @return bool|array |
| 422 |
*/ |
| 423 |
public function prepare_document( $post_id ) { |
| 424 |
global $post; |
| 425 |
$post = get_post( $post_id ); |
| 426 |
setup_postdata( $post ); |
| 427 |
|
| 428 |
if ( empty( $post ) ) { |
| 429 |
return false; |
| 430 |
} |
| 431 |
|
| 432 |
$user = get_userdata( $post->post_author ); |
| 433 |
|
| 434 |
if ( $user instanceof WP_User ) { |
| 435 |
$user_data = array( |
| 436 |
'raw' => $user->user_login, |
| 437 |
'login' => $user->user_login, |
| 438 |
'display_name' => $user->display_name, |
| 439 |
'id' => $user->ID, |
| 440 |
); |
| 441 |
} else { |
| 442 |
$user_data = array( |
| 443 |
'raw' => '', |
| 444 |
'login' => '', |
| 445 |
'display_name' => '', |
| 446 |
'id' => '', |
| 447 |
); |
| 448 |
} |
| 449 |
|
| 450 |
$post_date = $post->post_date; |
| 451 |
$post_date_gmt = $post->post_date_gmt; |
| 452 |
$post_modified = $post->post_modified; |
| 453 |
$post_modified_gmt = $post->post_modified_gmt; |
| 454 |
$comment_count = absint( $post->comment_count ); |
| 455 |
$comment_status = $post->comment_status; |
| 456 |
$ping_status = $post->ping_status; |
| 457 |
$menu_order = (int) $post->menu_order; |
| 458 |
|
| 459 |
/** |
| 460 |
* Filter to ignore invalid dates |
| 461 |
* |
| 462 |
* @hook ep_ignore_invalid_dates |
| 463 |
* @param {bool} $ignore True to ignore |
| 464 |
* @param {int} $post_id Post ID |
| 465 |
* @param {WP_Post} $post Post object |
| 466 |
* @return {bool} New ignore value |
| 467 |
*/ |
| 468 |
if ( apply_filters( 'ep_ignore_invalid_dates', true, $post_id, $post ) ) { |
| 469 |
if ( ! strtotime( $post_date ) || '0000-00-00 00:00:00' === $post_date ) { |
| 470 |
$post_date = null; |
| 471 |
} |
| 472 |
|
| 473 |
if ( ! strtotime( $post_date_gmt ) || '0000-00-00 00:00:00' === $post_date_gmt ) { |
| 474 |
$post_date_gmt = null; |
| 475 |
} |
| 476 |
|
| 477 |
if ( ! strtotime( $post_modified ) || '0000-00-00 00:00:00' === $post_modified ) { |
| 478 |
$post_modified = null; |
| 479 |
} |
| 480 |
|
| 481 |
if ( ! strtotime( $post_modified_gmt ) || '0000-00-00 00:00:00' === $post_modified_gmt ) { |
| 482 |
$post_modified_gmt = null; |
| 483 |
} |
| 484 |
} |
| 485 |
|
| 486 |
// To prevent infinite loop, we don't queue when updated_postmeta. |
| 487 |
remove_action( 'updated_postmeta', [ $this->sync_manager, 'action_queue_meta_sync' ], 10 ); |
| 488 |
|
| 489 |
/** |
| 490 |
* Filter to allow indexing of filtered post content |
| 491 |
* |
| 492 |
* @hook ep_allow_post_content_filtered_index |
| 493 |
* @param {bool} $ignore True to allow |
| 494 |
* @return {bool} New value |
| 495 |
*/ |
| 496 |
$post_content_filtered_allowed = apply_filters( 'ep_allow_post_content_filtered_index', true ); |
| 497 |
|
| 498 |
$post_args = array( |
| 499 |
'post_id' => $post_id, |
| 500 |
'ID' => $post_id, |
| 501 |
'post_author' => $user_data, |
| 502 |
'post_date' => $post_date, |
| 503 |
'post_date_gmt' => $post_date_gmt, |
| 504 |
'post_title' => $post->post_title, |
| 505 |
'post_excerpt' => $post->post_excerpt, |
| 506 |
'post_content_filtered' => $post_content_filtered_allowed ? apply_filters( 'the_content', $post->post_content ) : '', |
| 507 |
'post_content' => $post->post_content, |
| 508 |
'post_status' => $post->post_status, |
| 509 |
'post_name' => $post->post_name, |
| 510 |
'post_modified' => $post_modified, |
| 511 |
'post_modified_gmt' => $post_modified_gmt, |
| 512 |
'post_parent' => $post->post_parent, |
| 513 |
'post_type' => $post->post_type, |
| 514 |
'post_mime_type' => $post->post_mime_type, |
| 515 |
'permalink' => get_permalink( $post_id ), |
| 516 |
'terms' => $this->prepare_terms( $post ), |
| 517 |
'meta' => $this->prepare_meta_types( $this->prepare_meta( $post ) ), // post_meta removed in 2.4. |
| 518 |
'date_terms' => $this->prepare_date_terms( $post_date ), |
| 519 |
'comment_count' => $comment_count, |
| 520 |
'comment_status' => $comment_status, |
| 521 |
'ping_status' => $ping_status, |
| 522 |
'menu_order' => $menu_order, |
| 523 |
'guid' => $post->guid, |
| 524 |
'thumbnail' => $this->prepare_thumbnail( $post ), |
| 525 |
); |
| 526 |
|
| 527 |
/** |
| 528 |
* Filter sync arguments for a post. For backwards compatibility. |
| 529 |
* |
| 530 |
* @hook ep_post_sync_args |
| 531 |
* @param {array} $post_args Post arguments |
| 532 |
* @param {int} $post_id Post ID |
| 533 |
* @return {array} New arguments |
| 534 |
*/ |
| 535 |
$post_args = apply_filters( 'ep_post_sync_args', $post_args, $post_id ); |
| 536 |
|
| 537 |
/** |
| 538 |
* Filter sync arguments for a post after meta preparation. |
| 539 |
* |
| 540 |
* @hook ep_post_sync_args_post_prepare_meta |
| 541 |
* @param {array} $post_args Post arguments |
| 542 |
* @param {int} $post_id Post ID |
| 543 |
* @return {array} New arguments |
| 544 |
*/ |
| 545 |
$post_args = apply_filters( 'ep_post_sync_args_post_prepare_meta', $post_args, $post_id ); |
| 546 |
|
| 547 |
// Turn back on updated_postmeta hook |
| 548 |
add_action( 'updated_postmeta', [ $this->sync_manager, 'action_queue_meta_sync' ], 10, 4 ); |
| 549 |
|
| 550 |
return $post_args; |
| 551 |
} |
| 552 |
|
| 553 |
/** |
| 554 |
* Prepare thumbnail to send to ES. |
| 555 |
* |
| 556 |
* @param WP_Post $post Post object. |
| 557 |
* @return array|null Thumbnail data. |
| 558 |
*/ |
| 559 |
public function prepare_thumbnail( $post ) { |
| 560 |
$attachment_id = get_post_thumbnail_id( $post ); |
| 561 |
|
| 562 |
if ( ! $attachment_id ) { |
| 563 |
return null; |
| 564 |
} |
| 565 |
|
| 566 |
/** |
| 567 |
* Filters the image size to use when indexing the post thumbnail. |
| 568 |
* |
| 569 |
* Defaults to the `woocommerce_thumbnail` size if WooCommerce is in |
| 570 |
* use. Otherwise the `thumbnail` size is used. |
| 571 |
* |
| 572 |
* @hook ep_thumbnail_image_size |
| 573 |
* @since 4.0.0 |
| 574 |
* @param {string|int[]} $image_size Image size. Can be any registered |
| 575 |
* image size name, or an array of |
| 576 |
* width and height values in pixels |
| 577 |
* (in that order). |
| 578 |
* @param {WP_Post} $post Post being indexed. |
| 579 |
* @return {array} Image size to pass to wp_get_attachment_image_src(). |
| 580 |
*/ |
| 581 |
$image_size = apply_filters( |
| 582 |
'ep_post_thumbnail_image_size', |
| 583 |
function_exists( 'WC' ) ? 'woocommerce_thumbnail' : 'thumbnail', |
| 584 |
$post |
| 585 |
); |
| 586 |
|
| 587 |
$image_src = wp_get_attachment_image_src( $attachment_id, $image_size ); |
| 588 |
$image_alt = trim( wp_strip_all_tags( get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ) ) ); |
| 589 |
|
| 590 |
if ( ! $image_src ) { |
| 591 |
return null; |
| 592 |
} |
| 593 |
|
| 594 |
return [ |
| 595 |
'ID' => $attachment_id, |
| 596 |
'src' => $image_src[0], |
| 597 |
'width' => $image_src[1], |
| 598 |
'height' => $image_src[2], |
| 599 |
'alt' => $image_alt, |
| 600 |
]; |
| 601 |
} |
| 602 |
|
| 603 |
/** |
| 604 |
* Prepare date terms to send to ES. |
| 605 |
* |
| 606 |
* @param string $date_to_prepare Post date |
| 607 |
* @since 0.1.4 |
| 608 |
* @return array |
| 609 |
*/ |
| 610 |
public function prepare_date_terms( $date_to_prepare ) { |
| 611 |
$terms_to_prepare = [ |
| 612 |
'year' => 'Y', |
| 613 |
'month' => 'm', |
| 614 |
'week' => 'W', |
| 615 |
'dayofyear' => 'z', |
| 616 |
'day' => 'd', |
| 617 |
'dayofweek' => 'w', |
| 618 |
'dayofweek_iso' => 'N', |
| 619 |
'hour' => 'H', |
| 620 |
'minute' => 'i', |
| 621 |
'second' => 's', |
| 622 |
'm' => 'Ym', // yearmonth |
| 623 |
]; |
| 624 |
|
| 625 |
// Combine all the date term formats and perform one single call to date_i18n() for performance. |
| 626 |
$date_format = implode( '||', array_values( $terms_to_prepare ) ); |
| 627 |
$combined_dates = explode( '||', date_i18n( $date_format, strtotime( $date_to_prepare ) ) ); |
| 628 |
|
| 629 |
// Then split up the results for individual indexing. |
| 630 |
$date_terms = []; |
| 631 |
foreach ( $terms_to_prepare as $term_name => $date_format ) { |
| 632 |
$index_in_combined_format = array_search( $term_name, array_keys( $terms_to_prepare ), true ); |
| 633 |
$date_terms[ $term_name ] = (int) $combined_dates[ $index_in_combined_format ]; |
| 634 |
} |
| 635 |
|
| 636 |
return $date_terms; |
| 637 |
} |
| 638 |
|
| 639 |
/** |
| 640 |
* Get an array of taxonomies that are indexable for the given post |
| 641 |
* |
| 642 |
* @since 4.0.0 |
| 643 |
* @param WP_Post $post Post object |
| 644 |
* @return array Array of WP_Taxonomy objects that should be indexed |
| 645 |
*/ |
| 646 |
public function get_indexable_post_taxonomies( $post ) { |
| 647 |
$taxonomies = get_object_taxonomies( $post->post_type, 'objects' ); |
| 648 |
$selected_taxonomies = []; |
| 649 |
|
| 650 |
foreach ( $taxonomies as $taxonomy ) { |
| 651 |
if ( $taxonomy->public || $taxonomy->publicly_queryable ) { |
| 652 |
$selected_taxonomies[] = $taxonomy; |
| 653 |
} |
| 654 |
} |
| 655 |
|
| 656 |
/** |
| 657 |
* Filter taxonomies to be synced with post |
| 658 |
* |
| 659 |
* @hook ep_sync_taxonomies |
| 660 |
* @param {array} $selected_taxonomies Selected taxonomies |
| 661 |
* @param {WP_Post} Post object |
| 662 |
* @return {array} New taxonomies |
| 663 |
*/ |
| 664 |
$selected_taxonomies = (array) apply_filters( 'ep_sync_taxonomies', $selected_taxonomies, $post ); |
| 665 |
|
| 666 |
// Important we validate here to ensure there are no invalid taxonomy values returned from the filter, as just one would cause wp_get_object_terms() to fail. |
| 667 |
$validated_taxonomies = []; |
| 668 |
foreach ( $selected_taxonomies as $selected_taxonomy ) { |
| 669 |
// If we get a taxonomy name, we need to convert it to taxonomy object |
| 670 |
if ( ! is_object( $selected_taxonomy ) && taxonomy_exists( (string) $selected_taxonomy ) ) { |
| 671 |
$selected_taxonomy = get_taxonomy( $selected_taxonomy ); |
| 672 |
} |
| 673 |
|
| 674 |
// We check if the $taxonomy object has a valid name property. Backward compatibility since WP_Taxonomy introduced in WP 4.7 |
| 675 |
if ( ! is_a( $selected_taxonomy, '\WP_Taxonomy' ) || ! property_exists( $selected_taxonomy, 'name' ) || ! taxonomy_exists( $selected_taxonomy->name ) ) { |
| 676 |
continue; |
| 677 |
} |
| 678 |
|
| 679 |
$validated_taxonomies[] = $selected_taxonomy; |
| 680 |
} |
| 681 |
|
| 682 |
return $validated_taxonomies; |
| 683 |
} |
| 684 |
|
| 685 |
/** |
| 686 |
* Prepare terms to send to ES. |
| 687 |
* |
| 688 |
* @param WP_Post $post Post object |
| 689 |
* @since 0.1.0 |
| 690 |
* @return array |
| 691 |
*/ |
| 692 |
private function prepare_terms( $post ) { |
| 693 |
$selected_taxonomies = $this->get_indexable_post_taxonomies( $post ); |
| 694 |
|
| 695 |
if ( empty( $selected_taxonomies ) ) { |
| 696 |
return []; |
| 697 |
} |
| 698 |
|
| 699 |
$terms = []; |
| 700 |
|
| 701 |
/** |
| 702 |
* Filter to allow child terms to be indexed |
| 703 |
* |
| 704 |
* @hook ep_sync_terms_allow_hierarchy |
| 705 |
* @param {bool} $allow True means allow |
| 706 |
* @return {bool} New value |
| 707 |
*/ |
| 708 |
$allow_hierarchy = apply_filters( 'ep_sync_terms_allow_hierarchy', true ); |
| 709 |
|
| 710 |
foreach ( $selected_taxonomies as $taxonomy ) { |
| 711 |
$object_terms = get_the_terms( $post->ID, $taxonomy->name ); |
| 712 |
|
| 713 |
if ( ! $object_terms || is_wp_error( $object_terms ) ) { |
| 714 |
continue; |
| 715 |
} |
| 716 |
|
| 717 |
$terms_dic = []; |
| 718 |
|
| 719 |
foreach ( $object_terms as $term ) { |
| 720 |
if ( ! isset( $terms_dic[ $term->term_id ] ) ) { |
| 721 |
$terms_dic[ $term->term_id ] = $this->get_formatted_term( $term, $post->ID ); |
| 722 |
|
| 723 |
if ( $allow_hierarchy ) { |
| 724 |
$terms_dic = $this->get_parent_terms( $terms_dic, $term, $taxonomy->name, $post->ID ); |
| 725 |
} |
| 726 |
} |
| 727 |
} |
| 728 |
$terms[ $taxonomy->name ] = array_values( $terms_dic ); |
| 729 |
} |
| 730 |
|
| 731 |
return $terms; |
| 732 |
} |
| 733 |
|
| 734 |
/** |
| 735 |
* Recursively get all the ancestor terms of the given term |
| 736 |
* |
| 737 |
* @param array $terms Terms array |
| 738 |
* @param WP_Term $term Current term |
| 739 |
* @param string $tax_name Taxonomy |
| 740 |
* @param int $object_id Post ID |
| 741 |
* |
| 742 |
* @return array |
| 743 |
*/ |
| 744 |
private function get_parent_terms( $terms, $term, $tax_name, $object_id ) { |
| 745 |
$parent_term = get_term( $term->parent, $tax_name ); |
| 746 |
if ( ! $parent_term || is_wp_error( $parent_term ) ) { |
| 747 |
return $terms; |
| 748 |
} |
| 749 |
if ( ! isset( $terms[ $parent_term->term_id ] ) ) { |
| 750 |
$terms[ $parent_term->term_id ] = $this->get_formatted_term( $parent_term, $object_id ); |
| 751 |
|
| 752 |
} |
| 753 |
return $this->get_parent_terms( $terms, $parent_term, $tax_name, $object_id ); |
| 754 |
} |
| 755 |
|
| 756 |
/** |
| 757 |
* Given a term, format it to be appended to the post ES document. |
| 758 |
* |
| 759 |
* @since 4.5.0 |
| 760 |
* @param \WP_Term $term Term to be formatted |
| 761 |
* @param int $post_id The post ID |
| 762 |
* @return array |
| 763 |
*/ |
| 764 |
private function get_formatted_term( \WP_Term $term, int $post_id ) : array { |
| 765 |
$formatted_term = [ |
| 766 |
'term_id' => $term->term_id, |
| 767 |
'slug' => $term->slug, |
| 768 |
'name' => $term->name, |
| 769 |
'parent' => $term->parent, |
| 770 |
'term_taxonomy_id' => $term->term_taxonomy_id, |
| 771 |
'term_order' => (int) $this->get_term_order( $term->term_taxonomy_id, $post_id ), |
| 772 |
]; |
| 773 |
|
| 774 |
/** |
| 775 |
* As the name implies, the facet attribute is used to list all terms in facets. |
| 776 |
* As in facets, the term_order associated with a post does not matter, we set it as 0 here. |
| 777 |
* Note that this is set as 0 instead of simply removed to keep backward compatibility. |
| 778 |
*/ |
| 779 |
$term_facet = $formatted_term; |
| 780 |
$term_facet['term_order'] = 0; |
| 781 |
$formatted_term['facet'] = wp_json_encode( $term_facet ); |
| 782 |
|
| 783 |
return $formatted_term; |
| 784 |
} |
| 785 |
|
| 786 |
/** |
| 787 |
* Retreives term order for the object/term_taxonomy_id combination |
| 788 |
* |
| 789 |
* @param int $term_taxonomy_id Term Taxonomy ID |
| 790 |
* @param int $object_id Post ID |
| 791 |
* |
| 792 |
* @return int Term Order |
| 793 |
*/ |
| 794 |
protected function get_term_order( $term_taxonomy_id, $object_id ) { |
| 795 |
global $wpdb; |
| 796 |
|
| 797 |
$cache_key = "{$object_id}_term_order"; |
| 798 |
$term_orders = wp_cache_get( $cache_key ); |
| 799 |
|
| 800 |
if ( false === $term_orders ) { |
| 801 |
$results = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery |
| 802 |
$wpdb->prepare( |
| 803 |
"SELECT term_taxonomy_id, term_order from $wpdb->term_relationships where object_id=%d;", |
| 804 |
$object_id |
| 805 |
), |
| 806 |
ARRAY_A |
| 807 |
); |
| 808 |
|
| 809 |
$term_orders = []; |
| 810 |
|
| 811 |
foreach ( $results as $result ) { |
| 812 |
$term_orders[ $result['term_taxonomy_id'] ] = $result['term_order']; |
| 813 |
} |
| 814 |
|
| 815 |
wp_cache_set( $cache_key, $term_orders ); |
| 816 |
} |
| 817 |
|
| 818 |
return isset( $term_orders[ $term_taxonomy_id ] ) ? (int) $term_orders[ $term_taxonomy_id ] : 0; |
| 819 |
|
| 820 |
} |
| 821 |
|
| 822 |
/** |
| 823 |
* Checks if meta key is allowed |
| 824 |
* |
| 825 |
* @param string $meta_key meta key to check |
| 826 |
* @param WP_Post $post Post object |
| 827 |
* @since 4.3.0 |
| 828 |
* @return boolean |
| 829 |
*/ |
| 830 |
public function is_meta_allowed( $meta_key, $post ) { |
| 831 |
$test_metas = [ |
| 832 |
$meta_key => true, |
| 833 |
]; |
| 834 |
|
| 835 |
$filtered_test_metas = $this->filter_allowed_metas( $test_metas, $post ); |
| 836 |
|
| 837 |
return array_key_exists( $meta_key, $filtered_test_metas ); |
| 838 |
} |
| 839 |
|
| 840 |
/** |
| 841 |
* Filter post meta to only the allowed ones to be send to ES |
| 842 |
* |
| 843 |
* @param array $metas Key => value pairs of post meta |
| 844 |
* @param WP_Post $post Post object |
| 845 |
* @since 4.3.0 |
| 846 |
* @return array |
| 847 |
*/ |
| 848 |
public function filter_allowed_metas( $metas, $post ) { |
| 849 |
$filtered_metas = []; |
| 850 |
|
| 851 |
/** |
| 852 |
* Filter indexable protected meta keys for posts |
| 853 |
* |
| 854 |
* @hook ep_prepare_meta_allowed_protected_keys |
| 855 |
* @param {array} $keys Allowed protected keys |
| 856 |
* @param {WP_Post} $post Post object |
| 857 |
* @since 1.7 |
| 858 |
* @return {array} New keys |
| 859 |
*/ |
| 860 |
$allowed_protected_keys = apply_filters( 'ep_prepare_meta_allowed_protected_keys', [], $post ); |
| 861 |
|
| 862 |
/** |
| 863 |
* Filter public keys to exclude from indexed post |
| 864 |
* |
| 865 |
* @hook ep_prepare_meta_excluded_public_keys |
| 866 |
* @param {array} $keys Excluded protected keys |
| 867 |
* @param {WP_Post} $post Post object |
| 868 |
* @since 1.7 |
| 869 |
* @return {array} New keys |
| 870 |
*/ |
| 871 |
$excluded_public_keys = apply_filters( 'ep_prepare_meta_excluded_public_keys', [], $post ); |
| 872 |
|
| 873 |
foreach ( $metas as $key => $value ) { |
| 874 |
|
| 875 |
$allow_index = false; |
| 876 |
|
| 877 |
if ( is_protected_meta( $key ) ) { |
| 878 |
|
| 879 |
if ( true === $allowed_protected_keys || in_array( $key, $allowed_protected_keys, true ) ) { |
| 880 |
$allow_index = true; |
| 881 |
} |
| 882 |
} else { |
| 883 |
|
| 884 |
if ( true !== $excluded_public_keys && ! in_array( $key, $excluded_public_keys, true ) ) { |
| 885 |
$allow_index = true; |
| 886 |
} |
| 887 |
} |
| 888 |
|
| 889 |
/** |
| 890 |
* Filter force whitelisting a meta key |
| 891 |
* |
| 892 |
* @hook ep_prepare_meta_whitelist_key |
| 893 |
* @param {bool} $whitelist True to whitelist key |
| 894 |
* @param {string} $key Meta key |
| 895 |
* @param {WP_Post} $post Post object |
| 896 |
* @return {bool} New whitelist value |
| 897 |
*/ |
| 898 |
if ( true === $allow_index || apply_filters( 'ep_prepare_meta_whitelist_key', false, $key, $post ) ) { |
| 899 |
$filtered_metas[ $key ] = $value; |
| 900 |
} |
| 901 |
} |
| 902 |
return $filtered_metas; |
| 903 |
} |
| 904 |
|
| 905 |
/** |
| 906 |
* Prepare post meta to send to ES |
| 907 |
* |
| 908 |
* @param WP_Post $post Post object |
| 909 |
* @since 0.1.0 |
| 910 |
* @return array |
| 911 |
*/ |
| 912 |
public function prepare_meta( $post ) { |
| 913 |
/** |
| 914 |
* Filter pre-prepare meta for a post |
| 915 |
* |
| 916 |
* @hook ep_prepare_meta_data |
| 917 |
* @param {array} $meta Meta data |
| 918 |
* @param {WP_Post} $post Post object |
| 919 |
* @return {array} New meta |
| 920 |
*/ |
| 921 |
$meta = apply_filters( 'ep_prepare_meta_data', (array) get_post_meta( $post->ID ), $post ); |
| 922 |
|
| 923 |
if ( empty( $meta ) ) { |
| 924 |
/** |
| 925 |
* Filter final list of prepared meta. |
| 926 |
* |
| 927 |
* @hook ep_prepared_post_meta |
| 928 |
* @param {array} $prepared_meta Prepared meta |
| 929 |
* @param {WP_Post} $post Post object |
| 930 |
* @since 3.4 |
| 931 |
* @return {array} Prepared meta |
| 932 |
*/ |
| 933 |
return apply_filters( 'ep_prepared_post_meta', [], $post ); |
| 934 |
} |
| 935 |
|
| 936 |
$filtered_metas = $this->filter_allowed_metas( $meta, $post ); |
| 937 |
$prepared_meta = []; |
| 938 |
|
| 939 |
foreach ( $filtered_metas as $key => $value ) { |
| 940 |
if ( ! empty( $key ) ) { |
| 941 |
$prepared_meta[ $key ] = maybe_unserialize( $value ); |
| 942 |
} |
| 943 |
} |
| 944 |
|
| 945 |
/** |
| 946 |
* Filter final list of prepared meta. |
| 947 |
* |
| 948 |
* @hook ep_prepared_post_meta |
| 949 |
* @param {array} $prepared_meta Prepared meta |
| 950 |
* @param {WP_Post} $post Post object |
| 951 |
* @since 3.4 |
| 952 |
* @return {array} Prepared meta |
| 953 |
*/ |
| 954 |
return apply_filters( 'ep_prepared_post_meta', $prepared_meta, $post ); |
| 955 |
|
| 956 |
} |
| 957 |
|
| 958 |
/** |
| 959 |
* Format WP query args for ES |
| 960 |
* |
| 961 |
* @param array $args WP_Query arguments. |
| 962 |
* @param WP_Query $wp_query WP_Query object |
| 963 |
* @since 0.9.0 |
| 964 |
* @return array |
| 965 |
*/ |
| 966 |
public function format_args( $args, $wp_query ) { |
| 967 |
$args = $this->sanitize_wp_query_args( $args ); |
| 968 |
|
| 969 |
$formatted_args = [ |
| 970 |
'from' => $this->parse_from( $args ), |
| 971 |
'size' => $this->parse_size( $args ), |
| 972 |
]; |
| 973 |
|
| 974 |
$filters = $this->parse_filters( $args, $wp_query ); |
| 975 |
|
| 976 |
if ( ! empty( $filters ) ) { |
| 977 |
$formatted_args['post_filter'] = $filters; |
| 978 |
} |
| 979 |
|
| 980 |
$formatted_args = $this->maybe_set_search_fields( $formatted_args, $args ); |
| 981 |
$formatted_args = $this->maybe_set_fields( $formatted_args, $args ); |
| 982 |
$formatted_args = $this->maybe_orderby( $formatted_args, $args ); |
| 983 |
$formatted_args = $this->maybe_add_sticky_posts( $formatted_args, $args ); |
| 984 |
$formatted_args = $this->maybe_set_aggs( $formatted_args, $args, $filters ); |
| 985 |
|
| 986 |
/** |
| 987 |
* Filter formatted Elasticsearch [ost ]query (entire query) |
| 988 |
* |
| 989 |
* @hook ep_formatted_args |
| 990 |
* @param {array} $formatted_args Formatted Elasticsearch query |
| 991 |
* @param {array} $query_vars Query variables |
| 992 |
* @param {array} $query Query part |
| 993 |
* @return {array} New query |
| 994 |
*/ |
| 995 |
$formatted_args = apply_filters( 'ep_formatted_args', $formatted_args, $args, $wp_query ); |
| 996 |
|
| 997 |
/** |
| 998 |
* Filter formatted Elasticsearch [ost ]query (entire query) |
| 999 |
* |
| 1000 |
* @hook ep_post_formatted_args |
| 1001 |
* @param {array} $formatted_args Formatted Elasticsearch query |
| 1002 |
* @param {array} $query_vars Query variables |
| 1003 |
* @param {array} $query Query part |
| 1004 |
* @return {array} New query |
| 1005 |
*/ |
| 1006 |
$formatted_args = apply_filters( 'ep_post_formatted_args', $formatted_args, $args, $wp_query ); |
| 1007 |
|
| 1008 |
return $formatted_args; |
| 1009 |
} |
| 1010 |
|
| 1011 |
/** |
| 1012 |
* Adjust the fuzziness parameter if needed. |
| 1013 |
* |
| 1014 |
* If using fields with type `long`, queries should not have a fuzziness parameter. |
| 1015 |
* |
| 1016 |
* @param array $query Current query |
| 1017 |
* @param array $query_vars Query variables |
| 1018 |
* @param string $search_text Search text |
| 1019 |
* @param array $search_fields Search fields |
| 1020 |
* @return array New query |
| 1021 |
*/ |
| 1022 |
public function adjust_query_fuzziness( $query, $query_vars, $search_text, $search_fields ) { |
| 1023 |
if ( empty( array_intersect( $search_fields, [ 'ID', 'post_id', 'post_parent' ] ) ) ) { |
| 1024 |
return $query; |
| 1025 |
} |
| 1026 |
|
| 1027 |
if ( ! isset( $query['bool'] ) || ! isset( $query['bool']['should'] ) ) { |
| 1028 |
return $query; |
| 1029 |
} |
| 1030 |
|
| 1031 |
foreach ( $query['bool']['should'] as &$clause ) { |
| 1032 |
if ( ! isset( $clause['multi_match'] ) ) { |
| 1033 |
continue; |
| 1034 |
} |
| 1035 |
|
| 1036 |
if ( isset( $clause['multi_match']['fuzziness'] ) ) { |
| 1037 |
unset( $clause['multi_match']['fuzziness'] ); |
| 1038 |
} |
| 1039 |
} |
| 1040 |
|
| 1041 |
return $query; |
| 1042 |
} |
| 1043 |
|
| 1044 |
/** |
| 1045 |
* Parse and build out our tax query. |
| 1046 |
* |
| 1047 |
* @access protected |
| 1048 |
* |
| 1049 |
* @param array $query Tax query |
| 1050 |
* @return array |
| 1051 |
*/ |
| 1052 |
protected function parse_tax_query( $query ) { |
| 1053 |
$tax_query = [ |
| 1054 |
'tax_filter' => [], |
| 1055 |
'tax_must_not_filter' => [], |
| 1056 |
]; |
| 1057 |
$relation = ''; |
| 1058 |
|
| 1059 |
foreach ( $query as $tax_queries ) { |
| 1060 |
// If we have a nested tax query, recurse through that |
| 1061 |
if ( is_array( $tax_queries ) && empty( $tax_queries['taxonomy'] ) ) { |
| 1062 |
$result = $this->parse_tax_query( $tax_queries ); |
| 1063 |
$relation = ( ! empty( $tax_queries['relation'] ) ) ? strtolower( $tax_queries['relation'] ) : 'and'; |
| 1064 |
$filter_type = 'and' === $relation ? 'must' : 'should'; |
| 1065 |
|
| 1066 |
// Set the proper filter type and must_not filter, as needed |
| 1067 |
if ( ! empty( $result['tax_must_not_filter'] ) ) { |
| 1068 |
$tax_query['tax_filter'][] = [ |
| 1069 |
'bool' => [ |
| 1070 |
$filter_type => $result['tax_filter'], |
| 1071 |
'must_not' => $result['tax_must_not_filter'], |
| 1072 |
], |
| 1073 |
]; |
| 1074 |
} else { |
| 1075 |
$tax_query['tax_filter'][] = [ |
| 1076 |
'bool' => [ |
| 1077 |
$filter_type => $result['tax_filter'], |
| 1078 |
], |
| 1079 |
]; |
| 1080 |
} |
| 1081 |
} |
| 1082 |
|
| 1083 |
// Parse each individual tax query part |
| 1084 |
$single_tax_query = $tax_queries; |
| 1085 |
if ( ! empty( $single_tax_query['taxonomy'] ) ) { |
| 1086 |
$terms = isset( $single_tax_query['terms'] ) ? (array) $single_tax_query['terms'] : array(); |
| 1087 |
$field = $this->parse_tax_query_field( $single_tax_query['field'] ); |
| 1088 |
|
| 1089 |
if ( 'slug' === $field ) { |
| 1090 |
$terms = array_map( 'sanitize_title', $terms ); |
| 1091 |
} |
| 1092 |
|
| 1093 |
// Set up our terms object |
| 1094 |
$terms_obj = array( |
| 1095 |
'terms.' . $single_tax_query['taxonomy'] . '.' . $field => array_values( array_filter( $terms ) ), |
| 1096 |
); |
| 1097 |
|
| 1098 |
$operator = ( ! empty( $single_tax_query['operator'] ) ) ? strtolower( $single_tax_query['operator'] ) : 'in'; |
| 1099 |
|
| 1100 |
switch ( $operator ) { |
| 1101 |
case 'exists': |
| 1102 |
/** |
| 1103 |
* add support for "EXISTS" operator |
| 1104 |
* |
| 1105 |
* @since 2.5 |
| 1106 |
*/ |
| 1107 |
$tax_query['tax_filter'][]['bool'] = array( |
| 1108 |
'must' => array( |
| 1109 |
array( |
| 1110 |
'exists' => array( |
| 1111 |
'field' => key( $terms_obj ), |
| 1112 |
), |
| 1113 |
), |
| 1114 |
), |
| 1115 |
); |
| 1116 |
|
| 1117 |
break; |
| 1118 |
case 'not exists': |
| 1119 |
/** |
| 1120 |
* add support for "NOT EXISTS" operator |
| 1121 |
* |
| 1122 |
* @since 2.5 |
| 1123 |
*/ |
| 1124 |
$tax_query['tax_filter'][]['bool'] = array( |
| 1125 |
'must_not' => array( |
| 1126 |
array( |
| 1127 |
'exists' => array( |
| 1128 |
'field' => key( $terms_obj ), |
| 1129 |
), |
| 1130 |
), |
| 1131 |
), |
| 1132 |
); |
| 1133 |
|
| 1134 |
break; |
| 1135 |
case 'not in': |
| 1136 |
/** |
| 1137 |
* add support for "NOT IN" operator |
| 1138 |
* |
| 1139 |
* @since 2.1 |
| 1140 |
*/ |
| 1141 |
// If "NOT IN" than it should filter as must_not |
| 1142 |
$tax_query['tax_must_not_filter'][]['terms'] = $terms_obj; |
| 1143 |
|
| 1144 |
break; |
| 1145 |
case 'and': |
| 1146 |
/** |
| 1147 |
* add support for "and" operator |
| 1148 |
* |
| 1149 |
* @since 2.4 |
| 1150 |
*/ |
| 1151 |
$and_nest = array( |
| 1152 |
'bool' => array( |
| 1153 |
'must' => array(), |
| 1154 |
), |
| 1155 |
); |
| 1156 |
|
| 1157 |
foreach ( $terms as $term ) { |
| 1158 |
$and_nest['bool']['must'][] = array( |
| 1159 |
'terms' => array( |
| 1160 |
'terms.' . $single_tax_query['taxonomy'] . '.' . $field => (array) $term, |
| 1161 |
), |
| 1162 |
); |
| 1163 |
} |
| 1164 |
|
| 1165 |
$tax_query['tax_filter'][] = $and_nest; |
| 1166 |
|
| 1167 |
break; |
| 1168 |
case 'in': |
| 1169 |
default: |
| 1170 |
/** |
| 1171 |
* Default to IN operator |
| 1172 |
*/ |
| 1173 |
// Add the tax query filter |
| 1174 |
$tax_query['tax_filter'][]['terms'] = $terms_obj; |
| 1175 |
|
| 1176 |
break; |
| 1177 |
} |
| 1178 |
} |
| 1179 |
} |
| 1180 |
|
| 1181 |
return $tax_query; |
| 1182 |
} |
| 1183 |
|
| 1184 |
/** |
| 1185 |
* Parse an 'order' query variable and cast it to ASC or DESC as necessary. |
| 1186 |
* |
| 1187 |
* @since 1.1 |
| 1188 |
* @access protected |
| 1189 |
* |
| 1190 |
* @param string $order The 'order' query variable. |
| 1191 |
* @return string The sanitized 'order' query variable. |
| 1192 |
*/ |
| 1193 |
protected function parse_order( $order ) { |
| 1194 |
// Core will always set sort order to DESC for any invalid value, |
| 1195 |
// so we can't do any automated testing of this function. |
| 1196 |
// @codeCoverageIgnoreStart |
| 1197 |
if ( ! is_string( $order ) || empty( $order ) ) { |
| 1198 |
return 'desc'; |
| 1199 |
} |
| 1200 |
// @codeCoverageIgnoreEnd |
| 1201 |
|
| 1202 |
if ( 'ASC' === strtoupper( $order ) ) { |
| 1203 |
return 'asc'; |
| 1204 |
} else { |
| 1205 |
return 'desc'; |
| 1206 |
} |
| 1207 |
} |
| 1208 |
|
| 1209 |
/** |
| 1210 |
* Convert the alias to a properly-prefixed sort value. |
| 1211 |
* |
| 1212 |
* @since 1.1 |
| 1213 |
* @access protected |
| 1214 |
* |
| 1215 |
* @param string $orderbys Alias or path for the field to order by. |
| 1216 |
* @param string $default_order Default order direction |
| 1217 |
* @param array $args Query args |
| 1218 |
* @return array |
| 1219 |
*/ |
| 1220 |
protected function parse_orderby( $orderbys, $default_order, $args ) { |
| 1221 |
$orderbys = $this->get_orderby_array( $orderbys ); |
| 1222 |
|
| 1223 |
$from_to = [ |
| 1224 |
'relevance' => '_score', |
| 1225 |
'date' => 'post_date', |
| 1226 |
'type' => 'post_type.raw', |
| 1227 |
'modified' => 'post_modified', |
| 1228 |
'name' => 'post_name.raw', |
| 1229 |
'title' => 'post_title.sortable', |
| 1230 |
]; |
| 1231 |
|
| 1232 |
$sort = []; |
| 1233 |
|
| 1234 |
foreach ( $orderbys as $key => $value ) { |
| 1235 |
if ( is_string( $key ) ) { |
| 1236 |
$orderby_clause = $key; |
| 1237 |
$order = $value; |
| 1238 |
} else { |
| 1239 |
$orderby_clause = $value; |
| 1240 |
$order = $default_order; |
| 1241 |
} |
| 1242 |
|
| 1243 |
if ( empty( $orderby_clause ) || 'rand' === $orderby_clause ) { |
| 1244 |
continue; |
| 1245 |
} |
| 1246 |
|
| 1247 |
/** |
| 1248 |
* If `orderby` is 'none', WordPress will let the database decide on what should be used to order. |
| 1249 |
* It will use the primary key ASC. |
| 1250 |
*/ |
| 1251 |
if ( 'none' === $orderby_clause ) { |
| 1252 |
$orderby_clause = 'ID'; |
| 1253 |
$order = 'asc'; |
| 1254 |
} |
| 1255 |
|
| 1256 |
if ( ! empty( $from_to[ $orderby_clause ] ) ) { |
| 1257 |
$orderby_clause = $from_to[ $orderby_clause ]; |
| 1258 |
} else { |
| 1259 |
$orderby_clause = $this->parse_orderby_meta_fields( $orderby_clause, $args ); |
| 1260 |
} |
| 1261 |
|
| 1262 |
$sort[] = array( |
| 1263 |
$orderby_clause => array( |
| 1264 |
'order' => $order, |
| 1265 |
), |
| 1266 |
); |
| 1267 |
} |
| 1268 |
|
| 1269 |
return $sort; |
| 1270 |
} |
| 1271 |
|
| 1272 |
/** |
| 1273 |
* Try to parse orderby meta fields |
| 1274 |
* |
| 1275 |
* @since 4.6.0 |
| 1276 |
* @param string $orderby_clause Current orderby value |
| 1277 |
* @param array $args Query args |
| 1278 |
* @return string New orderby value |
| 1279 |
*/ |
| 1280 |
protected function parse_orderby_meta_fields( $orderby_clause, $args ) { |
| 1281 |
global $wpdb; |
| 1282 |
|
| 1283 |
$from_to_metatypes = [ |
| 1284 |
'num' => 'long', |
| 1285 |
'numeric' => 'long', |
| 1286 |
'binary' => 'value.sortable', |
| 1287 |
'char' => 'value.sortable', |
| 1288 |
'date' => 'date', |
| 1289 |
'datetime' => 'datetime', |
| 1290 |
'decimal' => 'double', |
| 1291 |
'signed' => 'long', |
| 1292 |
'time' => 'time', |
| 1293 |
'unsigned' => 'long', |
| 1294 |
]; |
| 1295 |
|
| 1296 |
// Code is targeting Elasticsearch directly |
| 1297 |
if ( preg_match( '/^meta\.(.*?)\.(.*)/', $orderby_clause, $match_meta ) ) { |
| 1298 |
return $orderby_clause; |
| 1299 |
} |
| 1300 |
|
| 1301 |
// WordPress meta_value_* compatibility |
| 1302 |
if ( preg_match( '/^meta_value_?(.*)/', $orderby_clause, $match_type ) ) { |
| 1303 |
$meta_type = $from_to_metatypes[ strtolower( $match_type[1] ) ] ?? 'value.sortable'; |
| 1304 |
} |
| 1305 |
|
| 1306 |
if ( ! empty( $args['meta_key'] ) ) { |
| 1307 |
$meta_field = $args['meta_key']; |
| 1308 |
} |
| 1309 |
|
| 1310 |
// Already have everything needed |
| 1311 |
if ( isset( $meta_type ) && isset( $meta_field ) ) { |
| 1312 |
return "meta.{$meta_field}.{$meta_type}"; |
| 1313 |
} |
| 1314 |
|
| 1315 |
// Don't have any other ways to guess |
| 1316 |
if ( empty( $args['meta_query'] ) ) { |
| 1317 |
return $orderby_clause; |
| 1318 |
} |
| 1319 |
|
| 1320 |
$meta_query = new \WP_Meta_Query( $args['meta_query'] ); |
| 1321 |
// Calling get_sql() to populate the WP_Meta_Query->clauses attribute |
| 1322 |
$meta_query->get_sql( 'post', $wpdb->posts, 'ID' ); |
| 1323 |
|
| 1324 |
$clauses = $meta_query->get_clauses(); |
| 1325 |
|
| 1326 |
// If it refers to a named meta_query clause |
| 1327 |
if ( ! empty( $clauses[ $orderby_clause ] ) ) { |
| 1328 |
$meta_field = $clauses[ $orderby_clause ]['key']; |
| 1329 |
$clause_meta_type = strtolower( $clauses[ $orderby_clause ]['type'] ?? $clauses[ $orderby_clause ]['cast'] ); |
| 1330 |
} else { |
| 1331 |
/** |
| 1332 |
* At this point we: |
| 1333 |
* 1. Try to find the meta key in any meta_query clause and use the type WP found |
| 1334 |
* 2. If ordering by `meta_value*`, use the first meta_query clause |
| 1335 |
* 3. Give up and use the orderby clause as is (code could be capturing it later on) |
| 1336 |
*/ |
| 1337 |
$meta_keys_and_types = wp_list_pluck( $clauses, 'cast', 'key' ); |
| 1338 |
if ( isset( $meta_keys_and_types[ $orderby_clause ] ) ) { |
| 1339 |
$meta_field = $orderby_clause; |
| 1340 |
$clause_meta_type = strtolower( $meta_keys_and_types[ $orderby_clause ] ?? $meta_keys_and_types[ $orderby_clause ] ); |
| 1341 |
} elseif ( isset( $meta_type ) ) { |
| 1342 |
$primary_clause = reset( $clauses ); |
| 1343 |
$meta_field = $primary_clause['key']; |
| 1344 |
} else { |
| 1345 |
unset( $meta_type ); |
| 1346 |
unset( $meta_field ); |
| 1347 |
} |
| 1348 |
} |
| 1349 |
|
| 1350 |
if ( ! isset( $meta_type ) && isset( $clause_meta_type ) ) { |
| 1351 |
$meta_type = $from_to_metatypes[ $clause_meta_type ] ?? 'value.sortable'; |
| 1352 |
} |
| 1353 |
|
| 1354 |
if ( isset( $meta_type ) && isset( $meta_field ) ) { |
| 1355 |
$orderby_clause = "meta.{$meta_field}.{$meta_type}"; |
| 1356 |
} |
| 1357 |
|
| 1358 |
return $orderby_clause; |
| 1359 |
} |
| 1360 |
|
| 1361 |
/** |
| 1362 |
* Get Order by args Array |
| 1363 |
* |
| 1364 |
* @param string|array $orderbys Order by string or array |
| 1365 |
* @since 2.1 |
| 1366 |
* @return array |
| 1367 |
*/ |
| 1368 |
protected function get_orderby_array( $orderbys ) { |
| 1369 |
if ( ! is_array( $orderbys ) ) { |
| 1370 |
$orderbys = explode( ' ', $orderbys ); |
| 1371 |
} |
| 1372 |
|
| 1373 |
return $orderbys; |
| 1374 |
} |
| 1375 |
|
| 1376 |
/** |
| 1377 |
* Given a mapping content, try to determine the version used. |
| 1378 |
* |
| 1379 |
* @since 3.6.3 |
| 1380 |
* |
| 1381 |
* @param array $mapping Mapping content. |
| 1382 |
* @param string $index Index name |
| 1383 |
* @return string Version of the mapping being used. |
| 1384 |
*/ |
| 1385 |
protected function determine_mapping_version_based_on_existing( $mapping, $index ) { |
| 1386 |
if ( isset( $mapping[ $index ]['mappings']['post']['_meta']['mapping_version'] ) ) { |
| 1387 |
return $mapping[ $index ]['mappings']['post']['_meta']['mapping_version']; |
| 1388 |
} |
| 1389 |
if ( isset( $mapping[ $index ]['mappings']['_meta']['mapping_version'] ) ) { |
| 1390 |
return $mapping[ $index ]['mappings']['_meta']['mapping_version']; |
| 1391 |
} |
| 1392 |
|
| 1393 |
/** |
| 1394 |
* Check for 7-0 mapping. |
| 1395 |
* If mapping has a `post` type, it can't be ES 7, as mapping types were removed in that release. |
| 1396 |
* |
| 1397 |
* @see https://www.elastic.co/guide/en/elasticsearch/reference/current/removal-of-types.html |
| 1398 |
*/ |
| 1399 |
if ( ! isset( $mapping[ $index ]['mappings']['post'] ) ) { |
| 1400 |
return '7-0.php'; |
| 1401 |
} |
| 1402 |
|
| 1403 |
$post_mapping = $mapping[ $index ]['mappings']['post']; |
| 1404 |
|
| 1405 |
/** |
| 1406 |
* Starting at this point, our tests rely on the post_title.fields.sortable field. |
| 1407 |
* As this field is present in all our mappings, if this field is not present in |
| 1408 |
* the mapping, this is a custom mapping. |
| 1409 |
* |
| 1410 |
* To have this code working with custom mappings, use the `ep_post_mapping_version_determined` filter. |
| 1411 |
*/ |
| 1412 |
if ( ! isset( $post_mapping['properties']['post_title']['fields']['sortable'] ) ) { |
| 1413 |
return 'unknown'; |
| 1414 |
} |
| 1415 |
|
| 1416 |
$post_title_sortable = $post_mapping['properties']['post_title']['fields']['sortable']; |
| 1417 |
|
| 1418 |
/** |
| 1419 |
* Check for 5-2 mapping. |
| 1420 |
* Normalizers on keyword fields were only made available in ES 5.2 |
| 1421 |
* |
| 1422 |
* @see https://www.elastic.co/guide/en/elasticsearch/reference/5.2/release-notes-5.2.0.html |
| 1423 |
*/ |
| 1424 |
if ( isset( $post_title_sortable['normalizer'] ) ) { |
| 1425 |
return '5-2.php'; |
| 1426 |
} |
| 1427 |
|
| 1428 |
/** |
| 1429 |
* Check for 5-0 mapping. |
| 1430 |
* `keyword` fields were only made available in ES 5.0 |
| 1431 |
* |
| 1432 |
* @see https://www.elastic.co/guide/en/elasticsearch/reference/5.0/release-notes-5.0.0.html |
| 1433 |
*/ |
| 1434 |
if ( 'keyword' === $post_title_sortable['type'] ) { |
| 1435 |
return '5-0.php'; |
| 1436 |
} |
| 1437 |
|
| 1438 |
/** |
| 1439 |
* Check for pre-5-0 mapping. |
| 1440 |
* `string` fields were deprecated in ES 5.0 in favor of text/keyword |
| 1441 |
* |
| 1442 |
* @see https://www.elastic.co/guide/en/elasticsearch/reference/5.0/release-notes-5.0.0.html |
| 1443 |
*/ |
| 1444 |
if ( 'string' === $post_title_sortable['type'] ) { |
| 1445 |
return 'pre-5-0.php'; |
| 1446 |
} |
| 1447 |
|
| 1448 |
return 'unknown'; |
| 1449 |
} |
| 1450 |
|
| 1451 |
/** |
| 1452 |
* Given ES args, add aggregations to it. |
| 1453 |
* |
| 1454 |
* @since 4.1.0 |
| 1455 |
* @param array $formatted_args Formatted Elasticsearch query |
| 1456 |
* @param array $agg Aggregation data. |
| 1457 |
* @param boolean $use_filters Whether filters should be used or not. |
| 1458 |
* @param array $filter Filters defined so far. |
| 1459 |
* @return array Formatted Elasticsearch query with the aggregation added. |
| 1460 |
*/ |
| 1461 |
protected function apply_aggregations( $formatted_args, $agg, $use_filters, $filter ) { |
| 1462 |
if ( empty( $agg['aggs'] ) ) { |
| 1463 |
return $formatted_args; |
| 1464 |
} |
| 1465 |
|
| 1466 |
// Add a name to the aggregation if it was passed through |
| 1467 |
$agg_name = ( ! empty( $agg['name'] ) ) ? $agg['name'] : 'aggregation_name'; |
| 1468 |
|
| 1469 |
// Add/use the filter if warranted |
| 1470 |
if ( isset( $agg['use-filter'] ) && false !== $agg['use-filter'] && $use_filters ) { |
| 1471 |
|
| 1472 |
// If a filter is being used, use it on the aggregation as well to receive relevant information to the query |
| 1473 |
$formatted_args['aggs'][ $agg_name ]['filter'] = $filter; |
| 1474 |
$formatted_args['aggs'][ $agg_name ]['aggs'] = $agg['aggs']; |
| 1475 |
} else { |
| 1476 |
$formatted_args['aggs'][ $agg_name ] = $agg['aggs']; |
| 1477 |
} |
| 1478 |
|
| 1479 |
return $formatted_args; |
| 1480 |
} |
| 1481 |
|
| 1482 |
/** |
| 1483 |
* Get the search algorithm that should be used. |
| 1484 |
* |
| 1485 |
* @since 4.3.0 |
| 1486 |
* @param string $search_text Search term(s) |
| 1487 |
* @param array $search_fields Search fields |
| 1488 |
* @param array $query_vars Query vars |
| 1489 |
* @return SearchAlgorithm Instance of search algorithm to be used |
| 1490 |
*/ |
| 1491 |
public function get_search_algorithm( string $search_text, array $search_fields, array $query_vars ) : \ElasticPress\SearchAlgorithm { |
| 1492 |
$search_algorithm_version_option = \ElasticPress\Utils\get_option( 'ep_search_algorithm_version', '4.0' ); |
| 1493 |
|
| 1494 |
/** |
| 1495 |
* Filter the algorithm version to be used. |
| 1496 |
* |
| 1497 |
* @since 3.5 |
| 1498 |
* @hook ep_search_algorithm_version |
| 1499 |
* @param {string} $search_algorithm_version Algorithm version. |
| 1500 |
* @return {string} New algorithm version |
| 1501 |
*/ |
| 1502 |
$search_algorithm = apply_filters( 'ep_search_algorithm_version', $search_algorithm_version_option ); |
| 1503 |
|
| 1504 |
/** |
| 1505 |
* Filter the search algorithm to be used |
| 1506 |
* |
| 1507 |
* @hook ep_{$indexable_slug}_search_algorithm |
| 1508 |
* @since 4.3.0 |
| 1509 |
* @param {string} $search_algorithm Slug of the search algorithm used as fallback |
| 1510 |
* @param {string} $search_term Search term |
| 1511 |
* @param {array} $search_fields Fields to be searched |
| 1512 |
* @param {array} $query_vars Query variables |
| 1513 |
* @return {string} New search algorithm slug |
| 1514 |
*/ |
| 1515 |
$search_algorithm = apply_filters( "ep_{$this->slug}_search_algorithm", $search_algorithm, $search_text, $search_fields, $query_vars ); |
| 1516 |
|
| 1517 |
return \ElasticPress\SearchAlgorithms::factory()->get( $search_algorithm ); |
| 1518 |
} |
| 1519 |
|
| 1520 |
/** |
| 1521 |
* Based on WP_Query arguments, parses the various filters that could be applied into the ES query. |
| 1522 |
* |
| 1523 |
* @since 4.4.0 |
| 1524 |
* @param array $args WP_Query arguments |
| 1525 |
* @param WP_Query $query WP_Query object |
| 1526 |
* @return array |
| 1527 |
*/ |
| 1528 |
protected function parse_filters( $args, $query ) { |
| 1529 |
/** |
| 1530 |
* A note about the order of this array indices: |
| 1531 |
* As previously there was no way to access each part, some snippets might be accessing |
| 1532 |
* these filters by its usual numeric indices (see the array_values() call below.) |
| 1533 |
*/ |
| 1534 |
$filters = [ |
| 1535 |
'tax_query' => $this->parse_tax_queries( $args, $query ), |
| 1536 |
'post_parent' => $this->parse_post_parent( $args ), |
| 1537 |
'post_parent__in' => $this->parse_post_parent__in( $args ), |
| 1538 |
'post_parent__not_in' => $this->parse_post_parent__not_in( $args ), |
| 1539 |
'post__in' => $this->parse_post__in( $args ), |
| 1540 |
'post_name__in' => $this->parse_post_name__in( $args ), |
| 1541 |
'post__not_in' => $this->parse_post__not_in( $args ), |
| 1542 |
'category__not_in' => $this->parse_category__not_in( $args ), |
| 1543 |
'tag__not_in' => $this->parse_tag__not_in( $args ), |
| 1544 |
'author' => $this->parse_author( $args ), |
| 1545 |
'post_mime_type' => $this->parse_post_mime_type( $args ), |
| 1546 |
'date' => $this->parse_date( $args ), |
| 1547 |
'meta_query' => $this->parse_meta_queries( $args ), |
| 1548 |
'post_type' => $this->parse_post_type( $args ), |
| 1549 |
'post_status' => $this->parse_post_status( $args ), |
| 1550 |
]; |
| 1551 |
|
| 1552 |
/** |
| 1553 |
* Filter the ES filters that will be applied to the ES query. |
| 1554 |
* |
| 1555 |
* Although each index of the `$filters` array contains the related WP Query argument, |
| 1556 |
* it will be removed before applied to the ES query. |
| 1557 |
* |
| 1558 |
* @hook ep_post_filters |
| 1559 |
* @param {array} Current filters |
| 1560 |
* @param {array} WP Query args |
| 1561 |
* @param {WP_Query} WP Query object |
| 1562 |
* @return {array} New filters |
| 1563 |
*/ |
| 1564 |
$filters = apply_filters( 'ep_post_filters', $filters, $args, $query ); |
| 1565 |
|
| 1566 |
$filters = array_values( array_filter( $filters ) ); |
| 1567 |
|
| 1568 |
if ( ! empty( $filters ) ) { |
| 1569 |
$filters = [ |
| 1570 |
'bool' => [ |
| 1571 |
'must' => $filters, |
| 1572 |
], |
| 1573 |
]; |
| 1574 |
} |
| 1575 |
|
| 1576 |
return $filters; |
| 1577 |
} |
| 1578 |
|
| 1579 |
/** |
| 1580 |
* Sanitize WP_Query arguments to be used to create the ES query. |
| 1581 |
* |
| 1582 |
* Elasticsearch will error if a terms query contains empty items like an empty string. |
| 1583 |
* |
| 1584 |
* @since 4.4.0 |
| 1585 |
* @param array $args WP_Query arguments |
| 1586 |
* @return array |
| 1587 |
*/ |
| 1588 |
protected function sanitize_wp_query_args( $args ) { |
| 1589 |
$keys_to_sanitize = [ |
| 1590 |
'author__in', |
| 1591 |
'author__not_in', |
| 1592 |
'category__and', |
| 1593 |
'category__in', |
| 1594 |
'category__not_in', |
| 1595 |
'tag__and', |
| 1596 |
'tag__in', |
| 1597 |
'tag__not_in', |
| 1598 |
'tag_slug__and', |
| 1599 |
'tag_slug__in', |
| 1600 |
'post_parent__in', |
| 1601 |
'post_parent__not_in', |
| 1602 |
'post__in', |
| 1603 |
'post__not_in', |
| 1604 |
'post_name__in', |
| 1605 |
]; |
| 1606 |
foreach ( $keys_to_sanitize as $key ) { |
| 1607 |
if ( ! isset( $args[ $key ] ) ) { |
| 1608 |
continue; |
| 1609 |
} |
| 1610 |
$args[ $key ] = array_filter( (array) $args[ $key ] ); |
| 1611 |
} |
| 1612 |
|
| 1613 |
return $args; |
| 1614 |
} |
| 1615 |
|
| 1616 |
/** |
| 1617 |
* Parse the `from` clause of the ES Query. |
| 1618 |
* |
| 1619 |
* @since 4.4.0 |
| 1620 |
* @param array $args WP_Query arguments |
| 1621 |
* @return int |
| 1622 |
*/ |
| 1623 |
protected function parse_from( $args ) { |
| 1624 |
$from = 0; |
| 1625 |
|
| 1626 |
if ( isset( $args['offset'] ) ) { |
| 1627 |
$from = (int) $args['offset']; |
| 1628 |
} |
| 1629 |
|
| 1630 |
if ( isset( $args['paged'] ) && $args['paged'] > 1 ) { |
| 1631 |
$from = $args['posts_per_page'] * ( $args['paged'] - 1 ); |
| 1632 |
} |
| 1633 |
|
| 1634 |
/** |
| 1635 |
* Fix negative offset. This happens, for example, on hierarchical post types. |
| 1636 |
* |
| 1637 |
* Ref: https://github.com/10up/ElasticPress/issues/2480 |
| 1638 |
*/ |
| 1639 |
if ( $from < 0 ) { |
| 1640 |
$from = 0; |
| 1641 |
} |
| 1642 |
|
| 1643 |
return $from; |
| 1644 |
} |
| 1645 |
|
| 1646 |
/** |
| 1647 |
* Parse the `size` clause of the ES Query. |
| 1648 |
* |
| 1649 |
* @since 4.4.0 |
| 1650 |
* @param array $args WP_Query arguments |
| 1651 |
* @return int |
| 1652 |
*/ |
| 1653 |
protected function parse_size( $args ) { |
| 1654 |
if ( empty( $args['posts_per_page'] ) ) { |
| 1655 |
return (int) get_option( 'posts_per_page' ); |
| 1656 |
} |
| 1657 |
|
| 1658 |
$posts_per_page = (int) $args['posts_per_page']; |
| 1659 |
|
| 1660 |
// ES have a maximum size allowed so we have to convert "-1" to a maximum size. |
| 1661 |
if ( -1 === $posts_per_page ) { |
| 1662 |
/** |
| 1663 |
* Filter max result size if set to -1 |
| 1664 |
* |
| 1665 |
* The request will return a HTTP 500 Internal Error if the size of the |
| 1666 |
* request is larger than the [index.max_result_window] parameter in ES. |
| 1667 |
* See the scroll api for a more efficient way to request large data sets. |
| 1668 |
* |
| 1669 |
* @hook ep_max_results_window |
| 1670 |
* @param {int} Max result window |
| 1671 |
* @return {int} New window |
| 1672 |
*/ |
| 1673 |
$posts_per_page = apply_filters( 'ep_max_results_window', 10000 ); |
| 1674 |
} |
| 1675 |
|
| 1676 |
return $posts_per_page; |
| 1677 |
} |
| 1678 |
|
| 1679 |
/** |
| 1680 |
* Parse the order of results in the ES query. It could simply be a `sort` clause or a function score query if using RAND. |
| 1681 |
* |
| 1682 |
* @since 4.4.0 |
| 1683 |
* @param array $formatted_args Formatted Elasticsearch query |
| 1684 |
* @param array $args WP_Query arguments |
| 1685 |
* @return array |
| 1686 |
*/ |
| 1687 |
protected function maybe_orderby( $formatted_args, $args ) { |
| 1688 |
/** |
| 1689 |
* Order and Orderby arguments |
| 1690 |
* |
| 1691 |
* Used for how Elasticsearch will sort results |
| 1692 |
* |
| 1693 |
* @since 1.1 |
| 1694 |
*/ |
| 1695 |
|
| 1696 |
// Set sort order, default is 'desc'. |
| 1697 |
if ( ! empty( $args['order'] ) ) { |
| 1698 |
$order = $this->parse_order( $args['order'] ); |
| 1699 |
} else { |
| 1700 |
$order = 'desc'; |
| 1701 |
} |
| 1702 |
|
| 1703 |
// Default sort for non-searches to date. |
| 1704 |
if ( empty( $args['orderby'] ) && ( ! isset( $args['s'] ) || '' === $args['s'] ) ) { |
| 1705 |
/** |
| 1706 |
* Filter default post query order by |
| 1707 |
* |
| 1708 |
* @hook ep_set_default_sort |
| 1709 |
* @param {string} $sort Default sort |
| 1710 |
* @param {string $order Order direction |
| 1711 |
* @return {string} New default |
| 1712 |
*/ |
| 1713 |
$args['orderby'] = apply_filters( 'ep_set_default_sort', 'date', $order ); |
| 1714 |
} |
| 1715 |
|
| 1716 |
// Set sort type. |
| 1717 |
if ( ! empty( $args['orderby'] ) ) { |
| 1718 |
$formatted_args['sort'] = $this->parse_orderby( $args['orderby'], $order, $args ); |
| 1719 |
} else { |
| 1720 |
// Default sort is to use the score (based on relevance). |
| 1721 |
$default_sort = array( |
| 1722 |
array( |
| 1723 |
'_score' => array( |
| 1724 |
'order' => $order, |
| 1725 |
), |
| 1726 |
), |
| 1727 |
); |
| 1728 |
|
| 1729 |
/** |
| 1730 |
* Filter the ES query order (`sort` clause) |
| 1731 |
* |
| 1732 |
* This filter is used in searches if `orderby` is not set in the WP_Query args. |
| 1733 |
* The default value is: |
| 1734 |
* |
| 1735 |
* $default_sort = array( |
| 1736 |
* array( |
| 1737 |
* '_score' => array( |
| 1738 |
* 'order' => $order, |
| 1739 |
* ), |
| 1740 |
* ), |
| 1741 |
* ); |
| 1742 |
* |
| 1743 |
* @hook ep_set_sort |
| 1744 |
* @since 3.6.3 |
| 1745 |
* @param {array} $sort Default sort. |
| 1746 |
* @param {string} $order Order direction |
| 1747 |
* @return {array} New default |
| 1748 |
*/ |
| 1749 |
$default_sort = apply_filters( 'ep_set_sort', $default_sort, $order ); |
| 1750 |
|
| 1751 |
$formatted_args['sort'] = $default_sort; |
| 1752 |
} |
| 1753 |
|
| 1754 |
/** |
| 1755 |
* Order by 'rand' support |
| 1756 |
* |
| 1757 |
* Ref: https://github.com/elastic/elasticsearch/issues/1170 |
| 1758 |
*/ |
| 1759 |
if ( ! empty( $args['orderby'] ) ) { |
| 1760 |
$orderbys = $this->get_orderby_array( $args['orderby'] ); |
| 1761 |
if ( in_array( 'rand', $orderbys, true ) ) { |
| 1762 |
$formatted_args_query = $formatted_args['query']; |
| 1763 |
$formatted_args['query'] = []; |
| 1764 |
$formatted_args['query']['function_score']['query'] = $formatted_args_query; |
| 1765 |
$formatted_args['query']['function_score']['random_score'] = (object) []; |
| 1766 |
} |
| 1767 |
} |
| 1768 |
|
| 1769 |
return $formatted_args; |
| 1770 |
} |
| 1771 |
|
| 1772 |
/** |
| 1773 |
* Parse all taxonomy queries. |
| 1774 |
* |
| 1775 |
* Although the name may be misleading, it handles the `tax_query` argument. There is a `parse_tax_query` that handles each "small" query. |
| 1776 |
* |
| 1777 |
* @since 4.4.0 |
| 1778 |
* @param array $args WP_Query arguments |
| 1779 |
* @param WP_Query $query WP_Query object |
| 1780 |
* @return array |
| 1781 |
*/ |
| 1782 |
protected function parse_tax_queries( $args, $query ) { |
| 1783 |
/** |
| 1784 |
* Tax Query support |
| 1785 |
* |
| 1786 |
* Support for the tax_query argument of WP_Query. Currently only provides support for the 'AND' relation |
| 1787 |
* between taxonomies. Field only supports slug, term_id, and name defaulting to term_id. |
| 1788 |
* |
| 1789 |
* @use field = slug |
| 1790 |
* terms array |
| 1791 |
* @since 0.9.1 |
| 1792 |
*/ |
| 1793 |
if ( ! empty( $query->tax_query ) && ! empty( $query->tax_query->queries ) ) { |
| 1794 |
$args['tax_query'] = $query->tax_query->queries; |
| 1795 |
} |
| 1796 |
|
| 1797 |
if ( empty( $args['tax_query'] ) ) { |
| 1798 |
return []; |
| 1799 |
} |
| 1800 |
|
| 1801 |
// Main tax_query array for ES. |
| 1802 |
$es_tax_query = []; |
| 1803 |
|
| 1804 |
$tax_queries = $this->parse_tax_query( $args['tax_query'] ); |
| 1805 |
|
| 1806 |
if ( ! empty( $tax_queries['tax_filter'] ) ) { |
| 1807 |
$relation = 'must'; |
| 1808 |
|
| 1809 |
if ( ! empty( $args['tax_query']['relation'] ) && 'or' === strtolower( $args['tax_query']['relation'] ) ) { |
| 1810 |
$relation = 'should'; |
| 1811 |
} |
| 1812 |
|
| 1813 |
$es_tax_query[ $relation ] = $tax_queries['tax_filter']; |
| 1814 |
} |
| 1815 |
|
| 1816 |
if ( ! empty( $tax_queries['tax_must_not_filter'] ) ) { |
| 1817 |
$es_tax_query['must_not'] = $tax_queries['tax_must_not_filter']; |
| 1818 |
} |
| 1819 |
|
| 1820 |
if ( ! empty( $es_tax_query ) ) { |
| 1821 |
return [ 'bool' => $es_tax_query ]; |
| 1822 |
} |
| 1823 |
|
| 1824 |
return []; |
| 1825 |
} |
| 1826 |
|
| 1827 |
/** |
| 1828 |
* Parse the `post_parent` WP Query arg and transform it into an ES query clause. |
| 1829 |
* |
| 1830 |
* @since 4.4.0 |
| 1831 |
* @param array $args WP_Query arguments |
| 1832 |
* @return array |
| 1833 |
*/ |
| 1834 |
protected function parse_post_parent( $args ) { |
| 1835 |
$has_post_parent = isset( $args['post_parent'] ) && ( in_array( $args['post_parent'], [ 0, '0' ], true ) || ! empty( $args['post_parent'] ) ); |
| 1836 |
if ( ! $has_post_parent || 'any' === strtolower( $args['post_parent'] ) ) { |
| 1837 |
return []; |
| 1838 |
} |
| 1839 |
|
| 1840 |
return [ |
| 1841 |
'bool' => [ |
| 1842 |
'must' => [ |
| 1843 |
'term' => [ |
| 1844 |
'post_parent' => (int) $args['post_parent'], |
| 1845 |
], |
| 1846 |
], |
| 1847 |
], |
| 1848 |
]; |
| 1849 |
} |
| 1850 |
|
| 1851 |
/** |
| 1852 |
* Parse the `post_parent__in` WP Query arg and transform it into an ES query clause. |
| 1853 |
* |
| 1854 |
* @since 4.5.0 |
| 1855 |
* @param array $args WP_Query arguments |
| 1856 |
* @return array |
| 1857 |
*/ |
| 1858 |
protected function parse_post_parent__in( $args ) { |
| 1859 |
if ( empty( $args['post_parent__in'] ) ) { |
| 1860 |
return []; |
| 1861 |
} |
| 1862 |
|
| 1863 |
return [ |
| 1864 |
'bool' => [ |
| 1865 |
'must' => [ |
| 1866 |
'terms' => [ |
| 1867 |
'post_parent' => array_values( (array) $args['post_parent__in'] ), |
| 1868 |
], |
| 1869 |
], |
| 1870 |
], |
| 1871 |
]; |
| 1872 |
} |
| 1873 |
|
| 1874 |
/** |
| 1875 |
* Parse the `post_parent__not_in` WP Query arg and transform it into an ES query clause. |
| 1876 |
* |
| 1877 |
* @since 4.5.0 |
| 1878 |
* @param array $args WP_Query arguments |
| 1879 |
* @return array |
| 1880 |
*/ |
| 1881 |
protected function parse_post_parent__not_in( $args ) { |
| 1882 |
if ( empty( $args['post_parent__not_in'] ) ) { |
| 1883 |
return []; |
| 1884 |
} |
| 1885 |
|
| 1886 |
return [ |
| 1887 |
'bool' => [ |
| 1888 |
'must_not' => [ |
| 1889 |
'terms' => [ |
| 1890 |
'post_parent' => array_values( (array) $args['post_parent__not_in'] ), |
| 1891 |
], |
| 1892 |
], |
| 1893 |
], |
| 1894 |
]; |
| 1895 |
} |
| 1896 |
|
| 1897 |
/** |
| 1898 |
* Parse the `post__in` WP Query arg and transform it into an ES query clause. |
| 1899 |
* |
| 1900 |
* @since 4.4.0 |
| 1901 |
* @param array $args WP_Query arguments |
| 1902 |
* @return array |
| 1903 |
*/ |
| 1904 |
protected function parse_post__in( $args ) { |
| 1905 |
if ( empty( $args['post__in'] ) ) { |
| 1906 |
return []; |
| 1907 |
} |
| 1908 |
|
| 1909 |
return [ |
| 1910 |
'bool' => [ |
| 1911 |
'must' => [ |
| 1912 |
'terms' => [ |
| 1913 |
'post_id' => array_values( (array) $args['post__in'] ), |
| 1914 |
], |
| 1915 |
], |
| 1916 |
], |
| 1917 |
]; |
| 1918 |
} |
| 1919 |
|
| 1920 |
/** |
| 1921 |
* Parse the `post_name__in` WP Query arg and transform it into an ES query clause. |
| 1922 |
* |
| 1923 |
* @since 4.4.0 |
| 1924 |
* @param array $args WP_Query arguments |
| 1925 |
* @return array |
| 1926 |
*/ |
| 1927 |
protected function parse_post_name__in( $args ) { |
| 1928 |
if ( empty( $args['post_name__in'] ) ) { |
| 1929 |
return []; |
| 1930 |
} |
| 1931 |
|
| 1932 |
return [ |
| 1933 |
'bool' => [ |
| 1934 |
'must' => [ |
| 1935 |
'terms' => [ |
| 1936 |
'post_name.raw' => array_values( (array) $args['post_name__in'] ), |
| 1937 |
], |
| 1938 |
], |
| 1939 |
], |
| 1940 |
]; |
| 1941 |
} |
| 1942 |
|
| 1943 |
/** |
| 1944 |
* Parse the `post__not_in` WP Query arg and transform it into an ES query clause. |
| 1945 |
* |
| 1946 |
* @since 4.4.0 |
| 1947 |
* @param array $args WP_Query arguments |
| 1948 |
* @return array |
| 1949 |
*/ |
| 1950 |
protected function parse_post__not_in( $args ) { |
| 1951 |
if ( empty( $args['post__not_in'] ) ) { |
| 1952 |
return []; |
| 1953 |
} |
| 1954 |
|
| 1955 |
return [ |
| 1956 |
'bool' => [ |
| 1957 |
'must_not' => [ |
| 1958 |
'terms' => [ |
| 1959 |
'post_id' => array_values( (array) $args['post__not_in'] ), |
| 1960 |
], |
| 1961 |
], |
| 1962 |
], |
| 1963 |
]; |
| 1964 |
} |
| 1965 |
|
| 1966 |
/** |
| 1967 |
* Parse the `category__not_in` WP Query arg and transform it into an ES query clause. |
| 1968 |
* |
| 1969 |
* @since 4.4.0 |
| 1970 |
* @param array $args WP_Query arguments |
| 1971 |
* @return array |
| 1972 |
*/ |
| 1973 |
protected function parse_category__not_in( $args ) { |
| 1974 |
if ( empty( $args['category__not_in'] ) ) { |
| 1975 |
return []; |
| 1976 |
} |
| 1977 |
|
| 1978 |
return [ |
| 1979 |
'bool' => [ |
| 1980 |
'must_not' => [ |
| 1981 |
'terms' => [ |
| 1982 |
'terms.category.term_id' => array_values( (array) $args['category__not_in'] ), |
| 1983 |
], |
| 1984 |
], |
| 1985 |
], |
| 1986 |
]; |
| 1987 |
} |
| 1988 |
|
| 1989 |
/** |
| 1990 |
* Parse the `tag__not_in` WP Query arg and transform it into an ES query clause. |
| 1991 |
* |
| 1992 |
* @since 4.4.0 |
| 1993 |
* @param array $args WP_Query arguments |
| 1994 |
* @return array |
| 1995 |
*/ |
| 1996 |
protected function parse_tag__not_in( $args ) { |
| 1997 |
if ( empty( $args['tag__not_in'] ) ) { |
| 1998 |
return []; |
| 1999 |
} |
| 2000 |
|
| 2001 |
return [ |
| 2002 |
'bool' => [ |
| 2003 |
'must_not' => [ |
| 2004 |
'terms' => [ |
| 2005 |
'terms.post_tag.term_id' => array_values( (array) $args['tag__not_in'] ), |
| 2006 |
], |
| 2007 |
], |
| 2008 |
], |
| 2009 |
]; |
| 2010 |
} |
| 2011 |
|
| 2012 |
/** |
| 2013 |
* Parse the various author-related WP Query args and transform them into ES query clauses. |
| 2014 |
* |
| 2015 |
* @since 4.4.0 |
| 2016 |
* @param array $args WP_Query arguments |
| 2017 |
* @return array |
| 2018 |
*/ |
| 2019 |
protected function parse_author( $args ) { |
| 2020 |
if ( ! empty( $args['author'] ) ) { |
| 2021 |
return [ |
| 2022 |
'term' => [ |
| 2023 |
'post_author.id' => $args['author'], |
| 2024 |
], |
| 2025 |
]; |
| 2026 |
} |
| 2027 |
|
| 2028 |
if ( ! empty( $args['author_name'] ) ) { |
| 2029 |
// Since this was set to use the display name initially, there might be some code that used this feature. |
| 2030 |
// Let's ensure that any query vars coming in using author_name are in fact slugs. |
| 2031 |
// This was changed back in ticket #1622 to use the display name, so we removed the sanitize_user() call. |
| 2032 |
return [ |
| 2033 |
'term' => [ |
| 2034 |
'post_author.display_name' => $args['author_name'], |
| 2035 |
], |
| 2036 |
]; |
| 2037 |
} |
| 2038 |
|
| 2039 |
if ( ! empty( $args['author__in'] ) ) { |
| 2040 |
return [ |
| 2041 |
'bool' => [ |
| 2042 |
'must' => [ |
| 2043 |
'terms' => [ |
| 2044 |
'post_author.id' => array_values( (array) $args['author__in'] ), |
| 2045 |
], |
| 2046 |
], |
| 2047 |
], |
| 2048 |
]; |
| 2049 |
} |
| 2050 |
|
| 2051 |
if ( ! empty( $args['author__not_in'] ) ) { |
| 2052 |
return [ |
| 2053 |
'bool' => [ |
| 2054 |
'must_not' => [ |
| 2055 |
'terms' => [ |
| 2056 |
'post_author.id' => array_values( (array) $args['author__not_in'] ), |
| 2057 |
], |
| 2058 |
], |
| 2059 |
], |
| 2060 |
]; |
| 2061 |
} |
| 2062 |
|
| 2063 |
return []; |
| 2064 |
} |
| 2065 |
|
| 2066 |
/** |
| 2067 |
* Parse the `post_mime_type` WP Query arg and transform it into an ES query clause. |
| 2068 |
* |
| 2069 |
* If we have array, it will be fool text search filter. |
| 2070 |
* If we have string(like filter images in media screen), we will have mime type "image" so need to check it as |
| 2071 |
* regexp filter. |
| 2072 |
* |
| 2073 |
* @since 4.4.0 |
| 2074 |
* @param array $args WP_Query arguments |
| 2075 |
* @return array |
| 2076 |
*/ |
| 2077 |
protected function parse_post_mime_type( $args ) { |
| 2078 |
if ( empty( $args['post_mime_type'] ) ) { |
| 2079 |
return []; |
| 2080 |
} |
| 2081 |
|
| 2082 |
if ( is_array( $args['post_mime_type'] ) ) { |
| 2083 |
|
| 2084 |
$args_post_mime_type = []; |
| 2085 |
|
| 2086 |
foreach ( $args['post_mime_type'] as $mime_type ) { |
| 2087 |
/** |
| 2088 |
* check if matches the MIME type pattern: type/subtype and |
| 2089 |
* leave an empty string as posts, pages and CPTs don't have a MIME type |
| 2090 |
*/ |
| 2091 |
if ( preg_match( '/^[-._a-z0-9]+\/[-._a-z0-9]+$/i', $mime_type ) || empty( $mime_type ) ) { |
| 2092 |
$args_post_mime_type[] = $mime_type; |
| 2093 |
} else { |
| 2094 |
$filtered_mime_type_by_type = wp_match_mime_types( $mime_type, wp_get_mime_types() ); |
| 2095 |
|
| 2096 |
$args_post_mime_type = array_merge( $args_post_mime_type, $filtered_mime_type_by_type[ $mime_type ] ); |
| 2097 |
} |
| 2098 |
} |
| 2099 |
|
| 2100 |
return [ |
| 2101 |
'terms' => [ |
| 2102 |
'post_mime_type' => $args_post_mime_type, |
| 2103 |
], |
| 2104 |
]; |
| 2105 |
} |
| 2106 |
|
| 2107 |
if ( is_string( $args['post_mime_type'] ) ) { |
| 2108 |
return [ |
| 2109 |
'regexp' => array( |
| 2110 |
'post_mime_type' => $args['post_mime_type'] . '.*', |
| 2111 |
), |
| 2112 |
]; |
| 2113 |
} |
| 2114 |
|
| 2115 |
return []; |
| 2116 |
} |
| 2117 |
|
| 2118 |
/** |
| 2119 |
* Parse the various date-related WP Query args and transform them into ES query clauses. |
| 2120 |
* |
| 2121 |
* @since 4.4.0 |
| 2122 |
* @param array $args WP_Query arguments |
| 2123 |
* @return array |
| 2124 |
*/ |
| 2125 |
protected function parse_date( $args ) { |
| 2126 |
$date_filter = DateQuery::simple_es_date_filter( $args ); |
| 2127 |
|
| 2128 |
if ( ! empty( $date_filter ) ) { |
| 2129 |
return $date_filter; |
| 2130 |
} |
| 2131 |
|
| 2132 |
if ( ! empty( $args['date_query'] ) ) { |
| 2133 |
|
| 2134 |
$date_query = new DateQuery( $args['date_query'] ); |
| 2135 |
|
| 2136 |
$date_filter = $date_query->get_es_filter(); |
| 2137 |
|
| 2138 |
if ( array_key_exists( 'and', $date_filter ) ) { |
| 2139 |
return $date_filter['and']; |
| 2140 |
} |
| 2141 |
} |
| 2142 |
} |
| 2143 |
|
| 2144 |
/** |
| 2145 |
* Parse all meta queries. |
| 2146 |
* |
| 2147 |
* Although the name may be misleading, it handles the `meta_query` argument. There is a `build_meta_query` that handles each "small" query. |
| 2148 |
* |
| 2149 |
* @since 4.4.0 |
| 2150 |
* @param array $args WP_Query arguments |
| 2151 |
* @return array |
| 2152 |
*/ |
| 2153 |
protected function parse_meta_queries( $args ) { |
| 2154 |
/** |
| 2155 |
* 'meta_query' arg support. |
| 2156 |
* |
| 2157 |
* Relation supports 'AND' and 'OR'. 'AND' is the default. For each individual query, the |
| 2158 |
* following 'compare' values are supported: =, !=, EXISTS, NOT EXISTS. '=' is the default. |
| 2159 |
* |
| 2160 |
* @since 1.3 |
| 2161 |
*/ |
| 2162 |
$meta_queries = ( ! empty( $args['meta_query'] ) ) ? $args['meta_query'] : []; |
| 2163 |
$meta_queries = ( new \WP_Meta_Query() )->sanitize_query( $meta_queries ); |
| 2164 |
|
| 2165 |
/** |
| 2166 |
* Todo: Support meta_type |
| 2167 |
*/ |
| 2168 |
|
| 2169 |
/** |
| 2170 |
* Support `meta_key`, `meta_value`, `meta_value_num`, and `meta_compare` query args |
| 2171 |
*/ |
| 2172 |
if ( ! empty( $args['meta_key'] ) ) { |
| 2173 |
$meta_query_array = [ |
| 2174 |
'key' => $args['meta_key'], |
| 2175 |
]; |
| 2176 |
|
| 2177 |
if ( isset( $args['meta_value'] ) && '' !== $args['meta_value'] ) { |
| 2178 |
$meta_query_array['value'] = $args['meta_value']; |
| 2179 |
} elseif ( isset( $args['meta_value_num'] ) && '' !== $args['meta_value_num'] ) { |
| 2180 |
$meta_query_array['value'] = $args['meta_value_num']; |
| 2181 |
} |
| 2182 |
|
| 2183 |
if ( isset( $args['meta_compare'] ) ) { |
| 2184 |
$meta_query_array['compare'] = $args['meta_compare']; |
| 2185 |
} |
| 2186 |
|
| 2187 |
if ( ! empty( $meta_queries ) ) { |
| 2188 |
$meta_queries = [ |
| 2189 |
'relation' => 'AND', |
| 2190 |
$meta_query_array, |
| 2191 |
$meta_queries, |
| 2192 |
]; |
| 2193 |
} else { |
| 2194 |
$meta_queries = [ $meta_query_array ]; |
| 2195 |
} |
| 2196 |
} |
| 2197 |
|
| 2198 |
if ( ! empty( $meta_queries ) ) { |
| 2199 |
// get meta query filter |
| 2200 |
$meta_filter = $this->build_meta_query( $meta_queries ); |
| 2201 |
|
| 2202 |
if ( ! empty( $meta_filter ) ) { |
| 2203 |
return $meta_filter; |
| 2204 |
} |
| 2205 |
} |
| 2206 |
|
| 2207 |
return []; |
| 2208 |
} |
| 2209 |
|
| 2210 |
/** |
| 2211 |
* Parse the `post_type` WP Query arg and transform it into an ES query clause. |
| 2212 |
* |
| 2213 |
* @since 4.4.0 |
| 2214 |
* @param array $args WP_Query arguments |
| 2215 |
* @return array |
| 2216 |
*/ |
| 2217 |
protected function parse_post_type( $args ) { |
| 2218 |
/** |
| 2219 |
* If not set default to post. If search and not set, default to "any". |
| 2220 |
*/ |
| 2221 |
if ( ! empty( $args['post_type'] ) ) { |
| 2222 |
// should NEVER be "any" but just in case |
| 2223 |
if ( 'any' !== $args['post_type'] ) { |
| 2224 |
$post_types = (array) $args['post_type']; |
| 2225 |
$terms_map_name = 'terms'; |
| 2226 |
|
| 2227 |
return [ |
| 2228 |
$terms_map_name => [ |
| 2229 |
'post_type.raw' => array_values( $post_types ), |
| 2230 |
], |
| 2231 |
]; |
| 2232 |
} |
| 2233 |
} elseif ( empty( $args['s'] ) ) { |
| 2234 |
return [ |
| 2235 |
'term' => [ |
| 2236 |
'post_type.raw' => 'post', |
| 2237 |
], |
| 2238 |
]; |
| 2239 |
} |
| 2240 |
|
| 2241 |
return []; |
| 2242 |
} |
| 2243 |
|
| 2244 |
/** |
| 2245 |
* Parse the `post_status` WP Query arg and transform it into an ES query clause. |
| 2246 |
* |
| 2247 |
* @since 4.4.0 |
| 2248 |
* @param array $args WP_Query arguments |
| 2249 |
* @return array |
| 2250 |
*/ |
| 2251 |
protected function parse_post_status( $args ) { |
| 2252 |
/** |
| 2253 |
* Like WP_Query in search context, if no post_status is specified we default to "any". To |
| 2254 |
* be safe you should ALWAYS specify the post_status parameter UNLIKE with WP_Query. |
| 2255 |
* |
| 2256 |
* @since 2.1 |
| 2257 |
*/ |
| 2258 |
if ( ! empty( $args['post_status'] ) ) { |
| 2259 |
// should NEVER be "any" but just in case |
| 2260 |
if ( 'any' !== $args['post_status'] ) { |
| 2261 |
$post_status = (array) ( is_string( $args['post_status'] ) ? explode( ',', $args['post_status'] ) : $args['post_status'] ); |
| 2262 |
$post_status = array_map( 'trim', $post_status ); |
| 2263 |
$terms_map_name = 'terms'; |
| 2264 |
if ( count( $post_status ) < 2 ) { |
| 2265 |
$terms_map_name = 'term'; |
| 2266 |
$post_status = $post_status[0]; |
| 2267 |
} |
| 2268 |
|
| 2269 |
return [ |
| 2270 |
$terms_map_name => [ |
| 2271 |
'post_status' => is_array( $post_status ) ? array_values( $post_status ) : $post_status, |
| 2272 |
], |
| 2273 |
]; |
| 2274 |
} |
| 2275 |
} else { |
| 2276 |
$statuses = get_post_stati( array( 'public' => true ) ); |
| 2277 |
|
| 2278 |
if ( is_admin() ) { |
| 2279 |
/** |
| 2280 |
* In the admin we will add protected and private post statuses to the default query |
| 2281 |
* per WP default behavior. |
| 2282 |
*/ |
| 2283 |
$statuses = array_merge( |
| 2284 |
$statuses, |
| 2285 |
get_post_stati( |
| 2286 |
array( |
| 2287 |
'protected' => true, |
| 2288 |
'show_in_admin_all_list' => true, |
| 2289 |
) |
| 2290 |
) |
| 2291 |
); |
| 2292 |
|
| 2293 |
if ( is_user_logged_in() ) { |
| 2294 |
$statuses = array_merge( $statuses, get_post_stati( array( 'private' => true ) ) ); |
| 2295 |
} |
| 2296 |
} |
| 2297 |
|
| 2298 |
$statuses = array_values( $statuses ); |
| 2299 |
|
| 2300 |
$post_status_filter_type = 'terms'; |
| 2301 |
|
| 2302 |
return [ |
| 2303 |
$post_status_filter_type => [ |
| 2304 |
'post_status' => $statuses, |
| 2305 |
], |
| 2306 |
]; |
| 2307 |
} |
| 2308 |
|
| 2309 |
return []; |
| 2310 |
} |
| 2311 |
|
| 2312 |
/** |
| 2313 |
* If in a search context set search fields, otherwise query everything. |
| 2314 |
* |
| 2315 |
* @since 4.4.0 |
| 2316 |
* @param array $formatted_args Formatted Elasticsearch query |
| 2317 |
* @param array $args WP_Query arguments |
| 2318 |
* @return array |
| 2319 |
*/ |
| 2320 |
protected function maybe_set_search_fields( $formatted_args, $args ) { |
| 2321 |
/** |
| 2322 |
* Allow for search field specification |
| 2323 |
* |
| 2324 |
* @since 1.0 |
| 2325 |
*/ |
| 2326 |
if ( ! empty( $args['search_fields'] ) ) { |
| 2327 |
$search_field_args = $args['search_fields']; |
| 2328 |
$search_fields = []; |
| 2329 |
|
| 2330 |
if ( ! empty( $search_field_args['taxonomies'] ) ) { |
| 2331 |
$taxes = (array) $search_field_args['taxonomies']; |
| 2332 |
|
| 2333 |
foreach ( $taxes as $tax ) { |
| 2334 |
$search_fields[] = 'terms.' . $tax . '.name'; |
| 2335 |
} |
| 2336 |
|
| 2337 |
unset( $search_field_args['taxonomies'] ); |
| 2338 |
} |
| 2339 |
|
| 2340 |
if ( ! empty( $search_field_args['meta'] ) ) { |
| 2341 |
$metas = (array) $search_field_args['meta']; |
| 2342 |
|
| 2343 |
foreach ( $metas as $meta ) { |
| 2344 |
$search_fields[] = 'meta.' . $meta . '.value'; |
| 2345 |
} |
| 2346 |
|
| 2347 |
unset( $search_field_args['meta'] ); |
| 2348 |
} |
| 2349 |
|
| 2350 |
if ( in_array( 'author_name', $search_field_args, true ) ) { |
| 2351 |
$search_fields[] = 'post_author.login'; |
| 2352 |
|
| 2353 |
$author_name_index = array_search( 'author_name', $search_field_args, true ); |
| 2354 |
unset( $search_field_args[ $author_name_index ] ); |
| 2355 |
} |
| 2356 |
|
| 2357 |
$search_fields = array_merge( $search_field_args, $search_fields ); |
| 2358 |
} else { |
| 2359 |
$search_fields = array( |
| 2360 |
'post_title', |
| 2361 |
'post_excerpt', |
| 2362 |
'post_content', |
| 2363 |
); |
| 2364 |
} |
| 2365 |
|
| 2366 |
/** |
| 2367 |
* Filter default post search fields |
| 2368 |
* |
| 2369 |
* If you are using the weighting engine, this filter should not be used. |
| 2370 |
* Instead, you should use the ep_weighting_configuration_for_search filter. |
| 2371 |
* |
| 2372 |
* @hook ep_search_fields |
| 2373 |
* @param {array} $search_fields Default search fields |
| 2374 |
* @param {array} $args WP Query arguments |
| 2375 |
* @return {array} New defaults |
| 2376 |
*/ |
| 2377 |
$search_fields = apply_filters( 'ep_search_fields', $search_fields, $args ); |
| 2378 |
|
| 2379 |
$search_text = ( ! empty( $args['s'] ) ) ? $args['s'] : ''; |
| 2380 |
|
| 2381 |
/** |
| 2382 |
* We are using ep_integrate instead of ep_match_all. ep_match_all will be |
| 2383 |
* supported for legacy code but may be deprecated and removed eventually. |
| 2384 |
* |
| 2385 |
* @since 1.3 |
| 2386 |
*/ |
| 2387 |
|
| 2388 |
if ( ! empty( $search_text ) ) { |
| 2389 |
add_filter( 'ep_post_formatted_args_query', [ $this, 'adjust_query_fuzziness' ], 100, 4 ); |
| 2390 |
|
| 2391 |
$search_algorithm = $this->get_search_algorithm( $search_text, $search_fields, $args ); |
| 2392 |
$formatted_args['query'] = $search_algorithm->get_query( 'post', $search_text, $search_fields, $args ); |
| 2393 |
} elseif ( ! empty( $args['ep_match_all'] ) || ! empty( $args['ep_integrate'] ) ) { |
| 2394 |
$formatted_args['query']['match_all'] = array( |
| 2395 |
'boost' => 1, |
| 2396 |
); |
| 2397 |
} |
| 2398 |
|
| 2399 |
return $formatted_args; |
| 2400 |
} |
| 2401 |
|
| 2402 |
/** |
| 2403 |
* If needed bring sticky posts and order them. |
| 2404 |
* |
| 2405 |
* @since 4.4.0 |
| 2406 |
* @param array $formatted_args Formatted Elasticsearch query |
| 2407 |
* @param array $args WP_Query arguments |
| 2408 |
* @return array |
| 2409 |
*/ |
| 2410 |
protected function maybe_add_sticky_posts( $formatted_args, $args ) { |
| 2411 |
/** |
| 2412 |
* Sticky posts support |
| 2413 |
*/ |
| 2414 |
|
| 2415 |
// Check first if there's sticky posts and show them only in the front page |
| 2416 |
$sticky_posts = get_option( 'sticky_posts' ); |
| 2417 |
$sticky_posts = ( is_array( $sticky_posts ) && empty( $sticky_posts ) ) ? false : $sticky_posts; |
| 2418 |
|
| 2419 |
/** |
| 2420 |
* Filter whether to enable sticky posts for this request |
| 2421 |
* |
| 2422 |
* @hook ep_enable_sticky_posts |
| 2423 |
* |
| 2424 |
* @param {bool} $allow Allow sticky posts for this request |
| 2425 |
* @param {array} $args Query variables |
| 2426 |
* @param {array} $formatted_args EP formatted args |
| 2427 |
* |
| 2428 |
* @return {bool} $allow |
| 2429 |
*/ |
| 2430 |
$enable_sticky_posts = apply_filters( 'ep_enable_sticky_posts', is_home(), $args, $formatted_args ); |
| 2431 |
|
| 2432 |
if ( false !== $sticky_posts |
| 2433 |
&& $enable_sticky_posts |
| 2434 |
&& empty( $args['s'] ) |
| 2435 |
&& in_array( $args['ignore_sticky_posts'], array( 'false', 0, false ), true ) ) { |
| 2436 |
$new_sort = [ |
| 2437 |
[ |
| 2438 |
'_score' => [ |
| 2439 |
'order' => 'desc', |
| 2440 |
], |
| 2441 |
], |
| 2442 |
]; |
| 2443 |
|
| 2444 |
$formatted_args['sort'] = array_merge( $new_sort, $formatted_args['sort'] ); |
| 2445 |
|
| 2446 |
$formatted_args_query = $formatted_args['query']; |
| 2447 |
$formatted_args['query'] = array(); |
| 2448 |
$formatted_args['query']['function_score']['query'] = $formatted_args_query; |
| 2449 |
$formatted_args['query']['function_score']['functions'] = array( |
| 2450 |
// add extra weight to sticky posts to show them on top |
| 2451 |
(object) array( |
| 2452 |
'filter' => array( |
| 2453 |
'terms' => array( '_id' => $sticky_posts ), |
| 2454 |
), |
| 2455 |
'weight' => 20, |
| 2456 |
), |
| 2457 |
); |
| 2458 |
} |
| 2459 |
|
| 2460 |
return $formatted_args; |
| 2461 |
} |
| 2462 |
|
| 2463 |
/** |
| 2464 |
* If needed set the `fields` ES query clause. |
| 2465 |
* |
| 2466 |
* @since 4.4.0 |
| 2467 |
* @param array $formatted_args Formatted Elasticsearch query |
| 2468 |
* @param array $args WP_Query arguments |
| 2469 |
* @return array |
| 2470 |
*/ |
| 2471 |
protected function maybe_set_fields( $formatted_args, $args ) { |
| 2472 |
/** |
| 2473 |
* Support fields. |
| 2474 |
*/ |
| 2475 |
if ( isset( $args['fields'] ) ) { |
| 2476 |
switch ( $args['fields'] ) { |
| 2477 |
case 'ids': |
| 2478 |
$formatted_args['_source'] = array( |
| 2479 |
'includes' => array( |
| 2480 |
'post_id', |
| 2481 |
), |
| 2482 |
); |
| 2483 |
break; |
| 2484 |
|
| 2485 |
case 'id=>parent': |
| 2486 |
$formatted_args['_source'] = array( |
| 2487 |
'includes' => array( |
| 2488 |
'post_id', |
| 2489 |
'post_parent', |
| 2490 |
), |
| 2491 |
); |
| 2492 |
break; |
| 2493 |
} |
| 2494 |
} |
| 2495 |
|
| 2496 |
return $formatted_args; |
| 2497 |
} |
| 2498 |
|
| 2499 |
/** |
| 2500 |
* If needed set the `aggs` ES query clause. |
| 2501 |
* |
| 2502 |
* @since 4.4.0 |
| 2503 |
* @param array $formatted_args Formatted Elasticsearch query. |
| 2504 |
* @param array $args WP_Query arguments |
| 2505 |
* @param array $filters Filters to be applied to the ES query |
| 2506 |
* @return array |
| 2507 |
*/ |
| 2508 |
protected function maybe_set_aggs( $formatted_args, $args, $filters ) { |
| 2509 |
/** |
| 2510 |
* Aggregations |
| 2511 |
*/ |
| 2512 |
if ( ! empty( $args['aggs'] ) && is_array( $args['aggs'] ) ) { |
| 2513 |
// Check if the array indexes are all numeric. |
| 2514 |
$agg_keys = array_keys( $args['aggs'] ); |
| 2515 |
$agg_num_keys = array_filter( $agg_keys, 'is_int' ); |
| 2516 |
$has_only_num_keys = count( $agg_num_keys ) === count( $args['aggs'] ); |
| 2517 |
|
| 2518 |
if ( $has_only_num_keys ) { |
| 2519 |
foreach ( $args['aggs'] as $agg ) { |
| 2520 |
$formatted_args = $this->apply_aggregations( $formatted_args, $agg, ! empty( $filters ), $filters ); |
| 2521 |
} |
| 2522 |
} else { |
| 2523 |
// Single aggregation. |
| 2524 |
$formatted_args = $this->apply_aggregations( $formatted_args, $args['aggs'], ! empty( $filters ), $filters ); |
| 2525 |
} |
| 2526 |
} |
| 2527 |
|
| 2528 |
return $formatted_args; |
| 2529 |
} |
| 2530 |
|
| 2531 |
/** |
| 2532 |
* Parse tax query field value. |
| 2533 |
* |
| 2534 |
* @since 4.4.0 |
| 2535 |
* @param string $field Field name |
| 2536 |
* @return string |
| 2537 |
*/ |
| 2538 |
protected function parse_tax_query_field( string $field ) : string { |
| 2539 |
|
| 2540 |
$from_to = [ |
| 2541 |
'name' => 'name.raw', |
| 2542 |
'slug' => 'slug', |
| 2543 |
'term_taxonomy_id' => 'term_taxonomy_id', |
| 2544 |
]; |
| 2545 |
|
| 2546 |
return $from_to[ $field ] ?? 'term_id'; |
| 2547 |
} |
| 2548 |
|
| 2549 |
/** |
| 2550 |
* Return all distinct meta fields in the database. |
| 2551 |
* |
| 2552 |
* @since 4.4.0 |
| 2553 |
* @param bool $force_refresh Whether to use or not a cached value. Default false, use cached. |
| 2554 |
* @return array |
| 2555 |
*/ |
| 2556 |
public function get_distinct_meta_field_keys_db( bool $force_refresh = false ) : array { |
| 2557 |
global $wpdb; |
| 2558 |
|
| 2559 |
/** |
| 2560 |
* Short-circuits the process of getting distinct meta keys from the database. |
| 2561 |
* |
| 2562 |
* Returning a non-null value will effectively short-circuit the function. |
| 2563 |
* |
| 2564 |
* @since 4.4.0 |
| 2565 |
* @hook ep_post_pre_meta_keys_db |
| 2566 |
* @param {null} $meta_keys Distinct meta keys array |
| 2567 |
* @return {null|array} Distinct meta keys array or `null` to keep default behavior |
| 2568 |
*/ |
| 2569 |
$pre_meta_keys = apply_filters( 'ep_post_pre_meta_keys_db', null ); |
| 2570 |
if ( null !== $pre_meta_keys ) { |
| 2571 |
return $pre_meta_keys; |
| 2572 |
} |
| 2573 |
|
| 2574 |
$cache_key = 'ep_meta_field_keys'; |
| 2575 |
|
| 2576 |
if ( ! $force_refresh ) { |
| 2577 |
$cached = get_transient( $cache_key ); |
| 2578 |
if ( false !== $cached ) { |
| 2579 |
$cached = (array) json_decode( (string) $cached ); |
| 2580 |
/* this filter is documented below */ |
| 2581 |
return (array) apply_filters( 'ep_post_meta_keys_db', $cached ); |
| 2582 |
} |
| 2583 |
} |
| 2584 |
|
| 2585 |
/** |
| 2586 |
* To avoid running a too expensive SQL query, we run a query getting all public keys |
| 2587 |
* and only the private keys allowed by the `ep_prepare_meta_allowed_protected_keys` filter. |
| 2588 |
* This query does not order by on purpose, as that also brings a performance penalty. |
| 2589 |
*/ |
| 2590 |
$allowed_protected_keys = apply_filters( 'ep_prepare_meta_allowed_protected_keys', [], new \WP_Post( (object) [] ) ); |
| 2591 |
$allowed_protected_keys_sql = ''; |
| 2592 |
if ( ! empty( $allowed_protected_keys ) ) { |
| 2593 |
$placeholders = implode( ',', array_fill( 0, count( $allowed_protected_keys ), '%s' ) ); |
| 2594 |
$allowed_protected_keys_sql = " OR meta_key IN ( {$placeholders} ) "; |
| 2595 |
} |
| 2596 |
|
| 2597 |
// phpcs:disable WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber |
| 2598 |
$meta_keys = $wpdb->get_col( |
| 2599 |
$wpdb->prepare( |
| 2600 |
"SELECT DISTINCT meta_key |
| 2601 |
FROM {$wpdb->postmeta} |
| 2602 |
WHERE meta_key NOT LIKE %s {$allowed_protected_keys_sql} |
| 2603 |
LIMIT 800", |
| 2604 |
'\_%', |
| 2605 |
...$allowed_protected_keys |
| 2606 |
) |
| 2607 |
); |
| 2608 |
// phpcs:enable WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber |
| 2609 |
|
| 2610 |
sort( $meta_keys ); |
| 2611 |
|
| 2612 |
// Make sure the size of the transient will not be bigger than 1MB |
| 2613 |
do { |
| 2614 |
$transient_size = strlen( wp_json_encode( $meta_keys ) ); |
| 2615 |
if ( $transient_size >= MB_IN_BYTES ) { |
| 2616 |
array_pop( $meta_keys ); |
| 2617 |
} else { |
| 2618 |
break; |
| 2619 |
} |
| 2620 |
} while ( true ); |
| 2621 |
set_transient( $cache_key, wp_json_encode( $meta_keys ), DAY_IN_SECONDS ); |
| 2622 |
|
| 2623 |
/** |
| 2624 |
* Filter the distinct meta keys fetched from the database. |
| 2625 |
* |
| 2626 |
* @since 4.4.0 |
| 2627 |
* @hook ep_post_meta_keys_db |
| 2628 |
* @param {array} $meta_keys Distinct meta keys array |
| 2629 |
* @return {array} New distinct meta keys array |
| 2630 |
*/ |
| 2631 |
return (array) apply_filters( 'ep_post_meta_keys_db', $meta_keys ); |
| 2632 |
} |
| 2633 |
|
| 2634 |
/** |
| 2635 |
* Return all distinct meta fields in the database per post type. |
| 2636 |
* |
| 2637 |
* @since 4.4.0 |
| 2638 |
* @param string $post_type Post type slug |
| 2639 |
* @param bool $force_refresh Whether to use or not a cached value. Default false, use cached. |
| 2640 |
* @return array |
| 2641 |
*/ |
| 2642 |
public function get_distinct_meta_field_keys_db_per_post_type( string $post_type, bool $force_refresh = false ) : array { |
| 2643 |
$allowed_screen = 'status-report' === \ElasticPress\Screen::factory()->get_current_screen(); |
| 2644 |
|
| 2645 |
/** |
| 2646 |
* Filter if the current screen is allowed or not to use the function. |
| 2647 |
* |
| 2648 |
* This method can be too resource intensive, use it with caution. |
| 2649 |
* |
| 2650 |
* @since 4.4.0 |
| 2651 |
* @hook ep_post_meta_keys_db_per_post_type_allowed_screen |
| 2652 |
* @param {bool} $allowed_screen Whether this is an allowed screen or not. |
| 2653 |
* @return {bool} New value of $allowed_screen |
| 2654 |
*/ |
| 2655 |
if ( ! apply_filters( 'ep_post_meta_keys_db_per_post_type_allowed_screen', $allowed_screen ) ) { |
| 2656 |
_doing_it_wrong( |
| 2657 |
__METHOD__, |
| 2658 |
esc_html__( 'This method should not be called outside specific pages. Use the `ep_post_meta_keys_db_per_post_type_allowed_screen` filter if you need to use it in your custom screen.' ), |
| 2659 |
'ElasticPress 4.4.0' |
| 2660 |
); |
| 2661 |
return []; |
| 2662 |
} |
| 2663 |
|
| 2664 |
/** |
| 2665 |
* Short-circuits the process of getting distinct meta keys from the database per post type. |
| 2666 |
* |
| 2667 |
* Returning a non-null value will effectively short-circuit the function. |
| 2668 |
* |
| 2669 |
* @since 4.4.0 |
| 2670 |
* @hook ep_post_pre_meta_keys_db_per_post_type |
| 2671 |
* @param {null} $meta_keys Distinct meta keys array |
| 2672 |
* @param {string} $post_type Post type slug |
| 2673 |
* @return {null|array} Distinct meta keys array or `null` to keep default behavior |
| 2674 |
*/ |
| 2675 |
$pre_meta_keys = apply_filters( 'ep_post_pre_meta_keys_db_per_post_type', null, $post_type ); |
| 2676 |
if ( null !== $pre_meta_keys ) { |
| 2677 |
return $pre_meta_keys; |
| 2678 |
} |
| 2679 |
|
| 2680 |
$cache_key = 'ep_meta_field_keys_' . $post_type; |
| 2681 |
|
| 2682 |
if ( ! $force_refresh ) { |
| 2683 |
$cached = get_transient( $cache_key ); |
| 2684 |
if ( false !== $cached ) { |
| 2685 |
$cached = (array) json_decode( (string) $cached ); |
| 2686 |
/* this filter is documented below */ |
| 2687 |
return (array) apply_filters( 'ep_post_meta_keys_db_per_post_type', $cached, $post_type ); |
| 2688 |
} |
| 2689 |
} |
| 2690 |
|
| 2691 |
$meta_keys = []; |
| 2692 |
$post_ids_batches = $this->get_lazy_post_type_ids( $post_type ); |
| 2693 |
foreach ( $post_ids_batches as $post_ids ) { |
| 2694 |
$new_meta_keys = $this->get_meta_keys_from_post_ids( $post_ids ); |
| 2695 |
|
| 2696 |
$meta_keys = array_unique( array_merge( $meta_keys, $new_meta_keys ) ); |
| 2697 |
} |
| 2698 |
|
| 2699 |
// Make sure the size of the transient will not be bigger than 1MB |
| 2700 |
do { |
| 2701 |
$transient_size = strlen( wp_json_encode( $meta_keys ) ); |
| 2702 |
if ( $transient_size >= MB_IN_BYTES ) { |
| 2703 |
array_pop( $meta_keys ); |
| 2704 |
} else { |
| 2705 |
break; |
| 2706 |
} |
| 2707 |
} while ( true ); |
| 2708 |
set_transient( $cache_key, wp_json_encode( $meta_keys ), DAY_IN_SECONDS ); |
| 2709 |
|
| 2710 |
/** |
| 2711 |
* Filter the distinct meta keys fetched from the database per post type. |
| 2712 |
* |
| 2713 |
* @since 4.4.0 |
| 2714 |
* @hook ep_post_meta_keys_db_per_post_type |
| 2715 |
* @param {array} $meta_keys Distinct meta keys array |
| 2716 |
* @param {string} $post_type Post type slug |
| 2717 |
* @return {array} New distinct meta keys array |
| 2718 |
*/ |
| 2719 |
return (array) apply_filters( 'ep_post_meta_keys_db_per_post_type', $meta_keys, $post_type ); |
| 2720 |
} |
| 2721 |
|
| 2722 |
/** |
| 2723 |
* Return all distinct meta fields in the database per post type. |
| 2724 |
* |
| 2725 |
* @since 4.4.0 |
| 2726 |
* @param string $post_type Post type slug |
| 2727 |
* @param bool $force_refresh Whether to use or not a cached value. Default false, use cached. |
| 2728 |
* @return array |
| 2729 |
*/ |
| 2730 |
public function get_indexable_meta_keys_per_post_type( string $post_type, bool $force_refresh = false ) : array { |
| 2731 |
$mock_post = new \WP_Post( (object) [ 'post_type' => $post_type ] ); |
| 2732 |
$meta_keys = $this->get_distinct_meta_field_keys_db_per_post_type( $post_type, $force_refresh ); |
| 2733 |
|
| 2734 |
$fake_meta_values = array_combine( $meta_keys, array_fill( 0, count( $meta_keys ), 'test-value' ) ); |
| 2735 |
$filtered_meta = apply_filters( 'ep_prepare_meta_data', $fake_meta_values, $mock_post ); |
| 2736 |
|
| 2737 |
return array_filter( |
| 2738 |
array_keys( $filtered_meta ), |
| 2739 |
function ( $meta_key ) use ( $mock_post ) { |
| 2740 |
return $this->is_meta_allowed( $meta_key, $mock_post ); |
| 2741 |
} |
| 2742 |
); |
| 2743 |
} |
| 2744 |
|
| 2745 |
/** |
| 2746 |
* Return the meta keys that will (possibly) be indexed. |
| 2747 |
* |
| 2748 |
* This function gets all the meta keys in the database, creates a fake post without a type and with all the meta fields, |
| 2749 |
* runs the `ep_prepare_meta_data` filter against it and checks if meta keys are allowed or not. |
| 2750 |
* Although it provides a good indicator, it is not 100% correct as developers could create code using the |
| 2751 |
* `ep_prepare_meta_data` filter that would depend on "real" data. |
| 2752 |
* |
| 2753 |
* @since 4.4.0 |
| 2754 |
* @param bool $force_refresh Whether to use or not a cached value. Default false, use cached. |
| 2755 |
* @return array |
| 2756 |
*/ |
| 2757 |
public function get_predicted_indexable_meta_keys( bool $force_refresh = false ) : array { |
| 2758 |
$empty_post = new \WP_Post( (object) [] ); |
| 2759 |
$meta_keys = $this->get_distinct_meta_field_keys_db( $force_refresh ); |
| 2760 |
|
| 2761 |
$fake_meta_values = array_combine( $meta_keys, array_fill( 0, count( $meta_keys ), 'test-value' ) ); |
| 2762 |
$filtered_meta = apply_filters( 'ep_prepare_meta_data', $fake_meta_values, $empty_post ); |
| 2763 |
|
| 2764 |
$all_keys = array_filter( |
| 2765 |
array_keys( $filtered_meta ), |
| 2766 |
function( $meta_key ) use ( $empty_post ) { |
| 2767 |
return $this->is_meta_allowed( $meta_key, $empty_post ); |
| 2768 |
} |
| 2769 |
); |
| 2770 |
|
| 2771 |
sort( $all_keys ); |
| 2772 |
|
| 2773 |
return $all_keys; |
| 2774 |
} |
| 2775 |
|
| 2776 |
/** |
| 2777 |
* Given a post type, *yields* their Post IDs. |
| 2778 |
* |
| 2779 |
* If post IDs are found, this function will return a PHP Generator. To avoid timeout, it will yield 8 groups or 11,000 IDs. |
| 2780 |
* |
| 2781 |
* @since 4.4.0 |
| 2782 |
* @see https://www.php.net/manual/en/language.generators.overview.php |
| 2783 |
* @param string $post_type The post type slug |
| 2784 |
* @return iterator |
| 2785 |
*/ |
| 2786 |
protected function get_lazy_post_type_ids( string $post_type ) { |
| 2787 |
global $wpdb; |
| 2788 |
|
| 2789 |
$total = $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery |
| 2790 |
$wpdb->prepare( |
| 2791 |
"SELECT count(*) FROM {$wpdb->posts} WHERE post_type = %s", |
| 2792 |
$post_type |
| 2793 |
) |
| 2794 |
); |
| 2795 |
|
| 2796 |
if ( ! $total ) { |
| 2797 |
return []; |
| 2798 |
} |
| 2799 |
|
| 2800 |
/** |
| 2801 |
* Filter the number of IDs to be fetched per page to discover distinct meta fields per post type. |
| 2802 |
* |
| 2803 |
* @hook ep_post_meta_by_type_ids_per_page |
| 2804 |
* @since 4.4.0 |
| 2805 |
* @param {int} $per_page Number of IDs |
| 2806 |
* @param {string} $post_type The post type slug |
| 2807 |
* @return {string} New number of IDs |
| 2808 |
*/ |
| 2809 |
$per_page = apply_filters( 'ep_post_meta_by_type_ids_per_page', 11000, $post_type ); |
| 2810 |
|
| 2811 |
$pages = min( ceil( $total / $per_page ), 8 ); |
| 2812 |
|
| 2813 |
/** |
| 2814 |
* Filter the number of times EP will fetch IDs from the database |
| 2815 |
* |
| 2816 |
* @hook ep_post_meta_by_type_number_of_pages |
| 2817 |
* @since 4.4.0 |
| 2818 |
* @param {int} $pages Number of "pages" (not WP post type) |
| 2819 |
* @param {int} $per_page Number of IDs per page |
| 2820 |
* @param {string} $post_type The post type slug |
| 2821 |
* @return {string} New number of pages |
| 2822 |
*/ |
| 2823 |
$pages = apply_filters( 'ep_post_meta_by_type_number_of_pages', $pages, $per_page, $post_type ); |
| 2824 |
|
| 2825 |
for ( $page = 0; $page < $pages; $page++ ) { |
| 2826 |
$start = $per_page * $page; |
| 2827 |
$ids = $wpdb->get_col( // phpcs:ignore WordPress.DB.DirectDatabaseQuery |
| 2828 |
$wpdb->prepare( |
| 2829 |
"SELECT ID FROM {$wpdb->posts} WHERE post_type = %s LIMIT %d, %d", |
| 2830 |
$post_type, |
| 2831 |
$start, |
| 2832 |
$per_page |
| 2833 |
) |
| 2834 |
); |
| 2835 |
yield $ids; |
| 2836 |
} |
| 2837 |
} |
| 2838 |
|
| 2839 |
/** |
| 2840 |
* Given a set of post IDs, return distinct meta keys associated with them. |
| 2841 |
* |
| 2842 |
* @since 4.4.0 |
| 2843 |
* @param array $post_ids Set of post IDs |
| 2844 |
* @return array |
| 2845 |
*/ |
| 2846 |
protected function get_meta_keys_from_post_ids( array $post_ids ) : array { |
| 2847 |
global $wpdb; |
| 2848 |
|
| 2849 |
if ( empty( $post_ids ) ) { |
| 2850 |
return []; |
| 2851 |
} |
| 2852 |
|
| 2853 |
$placeholders = implode( ',', array_fill( 0, count( $post_ids ), '%d' ) ); |
| 2854 |
$meta_keys = $wpdb->get_col( // phpcs:ignore WordPress.DB.DirectDatabaseQuery |
| 2855 |
$wpdb->prepare( |
| 2856 |
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare |
| 2857 |
"SELECT DISTINCT meta_key FROM {$wpdb->postmeta} WHERE post_id IN ( {$placeholders} )", |
| 2858 |
$post_ids |
| 2859 |
) |
| 2860 |
); |
| 2861 |
|
| 2862 |
return $meta_keys; |
| 2863 |
} |
| 2864 |
|
| 2865 |
/** |
| 2866 |
* Add a `term_suggest` field to the mapping. |
| 2867 |
* |
| 2868 |
* This method assumes the `edge_ngram_analyzer` analyzer was already added to the mapping. |
| 2869 |
* |
| 2870 |
* @since 4.5.0 |
| 2871 |
* @param array $mapping The mapping array |
| 2872 |
* @return array |
| 2873 |
*/ |
| 2874 |
public function add_term_suggest_field( array $mapping ) : array { |
| 2875 |
if ( version_compare( (string) Elasticsearch::factory()->get_elasticsearch_version(), '7.0', '<' ) ) { |
| 2876 |
$mapping_properties = &$mapping['mappings']['post']['properties']; |
| 2877 |
} else { |
| 2878 |
$mapping_properties = &$mapping['mappings']['properties']; |
| 2879 |
} |
| 2880 |
|
| 2881 |
$text_type = $mapping_properties['post_content']['type']; |
| 2882 |
|
| 2883 |
$mapping_properties['term_suggest'] = array( |
| 2884 |
'type' => $text_type, |
| 2885 |
'analyzer' => 'edge_ngram_analyzer', |
| 2886 |
'search_analyzer' => 'standard', |
| 2887 |
); |
| 2888 |
|
| 2889 |
return $mapping; |
| 2890 |
} |
| 2891 |
} |
| 2892 |
|