PluginProbe
ElasticPress / 5.3.5
ElasticPress v5.3.5
5.3.5 5.3.4 3.6.5 3.6.6 4.0.0 4.0.1 4.1.0 4.2.0 4.2.1 4.2.2 4.3.0 4.3.1 4.4.0 4.4.1 4.5.0 4.5.1 4.5.2 4.6.0 4.6.1 4.7.0 4.7.1 4.7.2 5.0.0 5.0.1 5.0.2 All 108 releases
← All changes | includes/classes/Indexable.php +186 -68 4.3.05.3.5 View file →
@@ -9,11 +9,10 @@
9 9 */
10 10
11 11 namespace ElasticPress;
12 12
13 -use ElasticPress\Elasticsearch as Elasticsearch;
14 -use ElasticPress\SyncManager as SyncManager;
15 -use ElasticPress\QueryIntegration as QueryIntegration;
13 +use ElasticPress\Elasticsearch;
14 +use ElasticPress\SyncManager;
16 15
17 16 if ( ! defined( 'ABSPATH' ) ) {
18 17 exit; // Exit if accessed directly.
19 18 }
@@ -48,9 +47,9 @@
48 47 /**
49 48 * Instance of QueryIntegration. This should handle integrating with a default
50 49 * WP query.
51 50 *
52 - * @var QueryIntegration
51 + * @var object
53 52 * @since 3.0
54 53 */
55 54 public $query_integration;
56 55
@@ -63,8 +62,27 @@
63 62 */
64 63 public $support_indexing_advanced_pagination = false;
65 64
66 65 /**
66 + * Indexable slug
67 + *
68 + * @since 4.5.0
69 + * @var string
70 + */
71 + public $slug = '';
72 +
73 + /**
74 + * Indexable labels
75 + *
76 + * @since 4.5.0
77 + * @var array
78 + */
79 + public $labels = [
80 + 'plural' => '',
81 + 'singular' => '',
82 + ];
83 +
84 + /**
67 85 * Get number of bulk items to index per page
68 86 *
69 87 * @since 3.0
70 88 * @return int
@@ -244,12 +262,16 @@
244 262 *
245 263 * @param int $object_id Object to index.
246 264 * @param boolean $blocking Blocking HTTP request or not.
247 265 * @since 3.0
248 - * @return boolean
266 + * @return object|boolean
249 267 */
250 268 public function index( $object_id, $blocking = false ) {
251 - $document = $this->prepare_document( $object_id );
269 + try {
270 + $document = $this->prepare_document( $object_id );
271 + } catch ( \Throwable $th ) {
272 + return false;
273 + }
252 274
253 275 if ( false === $document ) {
254 276 return false;
255 277 }
@@ -256,8 +278,9 @@
256 278
257 279 /**
258 280 * Conditionally kill indexing on a specific object
259 281 *
282 + * @deprecated 5.3.3 Use ep_{indexable_slug}_sync_kill instead
260 283 * @hook ep_{indexable_slug}_index_kill
261 284 * @param {bool} $kill True to not index
262 285 * @param {int} $object_id Id of object to index
263 286 * @since 3.0
@@ -262,13 +285,26 @@
262 285 * @param {int} $object_id Id of object to index
263 286 * @since 3.0
264 287 * @return {bool} New kill value
265 288 */
266 - if ( apply_filters( 'ep_' . $this->slug . '_index_kill', false, $object_id ) ) {
289 + if ( apply_filters_deprecated( 'ep_' . $this->slug . '_index_kill', [ false, $object_id ], 'ElasticPress 5.3.3', 'ep_' . $this->slug . '_sync_kill' ) ) {
267 290 return false;
268 291 }
269 292
270 293 /**
294 + * Conditionally kill indexing for an object.
295 + *
296 + * @hook ep_{$this->slug}_sync_kill
297 + * @param {bool} $kill True means dont sync
298 + * @param {int} $object_id Object ID
299 + * @return {bool} New value
300 + */
301 + $ep_indexable_sync_kill = apply_filters( 'ep_' . $this->slug . '_sync_kill', false, $object_id );
302 + if ( $ep_indexable_sync_kill ) {
303 + return false;
304 + }
305 +
306 + /**
271 307 * Filter document before index
272 308 *
273 309 * @hook ep_pre_index_{indexable_slug}
274 310 * @param {array} $document Document to index
@@ -283,9 +319,9 @@
283 319 * Fires after document is indexed
284 320 *
285 321 * @hook ep_after_index_{indexable_slug}
286 322 * @param {array} $document Document to index
287 - * @param {array|boolean} $return ES response on success, false on failure
323 + * @param {object|boolean} $return ES response on success, false on failure
288 324 * @since 3.0
289 325 */
290 326 do_action( 'ep_after_index_' . $this->slug, $document, $return );
291 327
@@ -312,8 +348,10 @@
312 348 */
313 349 public function bulk_index( $object_ids ) {
314 350 $body = '';
315 351
352 + $non_es_errors = [];
353 +
316 354 foreach ( $object_ids as $object_id ) {
317 355 $action_args = array(
318 356 'index' => array(
319 357 '_id' => absint( $object_id ),
@@ -319,9 +357,22 @@
319 357 '_id' => absint( $object_id ),
320 358 ),
321 359 );
322 360
323 - $document = $this->prepare_document( $object_id );
361 + try {
362 + $document = $this->prepare_document( $object_id );
363 + } catch ( \Throwable $th ) {
364 + $non_es_errors[] = [
365 + 'index' => [
366 + '_id' => absint( $object_id ),
367 + 'error' => [
368 + 'type' => 'prepare_document_error',
369 + 'reason' => $th->getMessage(),
370 + ],
371 + ],
372 + ];
373 + continue;
374 + }
324 375
325 376 /**
326 377 * Conditionally kill indexing on a specific object
327 378 *
@@ -338,8 +389,13 @@
338 389 }
339 390
340 391 $result = Elasticsearch::factory()->bulk_index( $this->get_index_name(), $this->slug, $body );
341 392
393 + if ( ! empty( $non_es_errors ) ) {
394 + $result['errors'] = true;
395 + $result['items'] = isset( $result['items'] ) ? array_merge( $result['items'], $non_es_errors ) : $non_es_errors;
396 + }
397 +
342 398 /**
343 399 * Perform actions after a bulk indexing is completed
344 400 *
345 401 * @hook ep_after_bulk_index
@@ -361,8 +417,10 @@
361 417 */
362 418 public function bulk_index_dynamically( $object_ids ) {
363 419 $documents = [];
364 420
421 + $non_es_errors = [];
422 +
365 423 foreach ( $object_ids as $object_id ) {
366 424 $action_args = array(
367 425 'index' => array(
368 426 '_id' => absint( $object_id ),
@@ -368,10 +426,27 @@
368 426 '_id' => absint( $object_id ),
369 427 ),
370 428 );
371 429
372 - $document = $this->prepare_document( $object_id );
430 + try {
431 + $document = $this->prepare_document( $object_id );
432 + } catch ( \Throwable $th ) {
433 + $non_es_errors[] = [
434 + 'index' => [
435 + '_id' => absint( $object_id ),
436 + 'error' => [
437 + 'type' => 'prepare_document_error',
438 + 'reason' => $th->getMessage(),
439 + ],
440 + ],
441 + ];
442 + continue;
443 + }
373 444
445 + if ( empty( $document ) ) {
446 + continue;
447 + }
448 +
374 449 /**
375 450 * Conditionally kill indexing on a specific object
376 451 *
377 452 * @hook ep_bulk_index_action_args
@@ -386,10 +461,35 @@
386 461
387 462 $documents[] = $document_str;
388 463 }
389 464
465 + if ( empty( $documents ) ) {
466 + return ( ! empty( $non_es_errors ) ? [
467 + [
468 + 'errors' => true,
469 + 'items' => $non_es_errors,
470 + ],
471 + ] : [
472 + new \WP_Error(
473 + 'ep_bulk_index_no_documents',
474 + esc_html__( 'It was not possible to create a body request with the document IDs provided.', 'elasticpress' ),
475 + $object_ids
476 + ),
477 + ] );
478 + }
479 +
390 480 $results = $this->send_bulk_index_request( $documents );
391 481
482 + if ( ! empty( $non_es_errors ) ) {
483 + $results = [
484 + [
485 + 'errors' => true,
486 + 'items' => $non_es_errors,
487 + ],
488 + ...$results,
489 + ];
490 + }
491 +
392 492 /**
393 493 * Perform actions after a dynamic bulk indexing is completed
394 494 *
395 495 * @hook ep_after_bulk_index_dynamically
@@ -462,12 +562,12 @@
462 562 }
463 563
464 564 $results = [];
465 565
466 - $body = [];
566 + $body = [];
567 + $current_body_size = 0;
568 + $requests = 0;
467 569
468 - $requests = 0;
469 -
470 570 /*
471 571 * This script will use two main arrays: $body and $documents, being $body the
472 572 * documents to be sent in the next request and $documents the list of docs to be indexed.
473 573 * The do-while loop will stop if all documents are sent or if a request fails even sending
@@ -473,16 +573,18 @@
473 573 * The do-while loop will stop if all documents are sent or if a request fails even sending
474 574 * a buffer as small as possible.
475 575 */
476 576 do {
477 - $next_document = array_shift( $documents );
577 + $next_document = array_shift( $documents );
578 + $next_document_size = mb_strlen( $next_document );
579 + $has_buffered_documents = count( $body ) > 0;
478 580
479 581 // If the next document alone takes the entire current buffer size,
480 582 // let's add it back to the pipe and send what we have first
481 - if ( mb_strlen( $next_document ) > $current_buffer_size && count( $body ) > 0 ) {
583 + if ( $next_document_size > $current_buffer_size && $has_buffered_documents ) {
482 584 array_unshift( $documents, $next_document );
483 585 } else {
484 - if ( mb_strlen( $next_document ) > $max_buffer_size ) {
586 + if ( $next_document_size > $max_buffer_size ) {
485 587 /**
486 588 * Perform actions when a post is bigger than the max buffer size.
487 589 *
488 590 * @hook ep_dynamic_bulk_post_too_big
@@ -492,15 +594,22 @@
492 594 do_action( 'ep_dynamic_bulk_post_too_big', $next_document );
493 595 $results[] = new \WP_Error( 'ep_too_big_request_skipped', 'Indexable too big. Request not sent.' );
494 596 continue;
495 597 }
496 - $body[] = $next_document;
497 - if ( mb_strlen( implode( '', $body ) ) < $current_buffer_size && ! empty( $documents ) ) {
598 +
599 + $body[] = $next_document;
600 + $current_body_size += $next_document_size;
601 +
602 + $can_add_more_documents = ( $current_body_size < $current_buffer_size && ! empty( $documents ) );
603 + if ( $can_add_more_documents ) {
498 604 continue;
499 605 }
500 - if ( mb_strlen( implode( '', $body ) ) > $max_buffer_size ) {
606 +
607 + if ( $current_body_size > $max_buffer_size ) {
501 608 // The last document added to body made it too big, so let's give it back.
502 - array_unshift( $documents, array_pop( $body ) );
609 + $removed_document = array_pop( $body );
610 + $current_body_size -= mb_strlen( $removed_document );
611 + array_unshift( $documents, $removed_document );
503 612 }
504 613 }
505 614
506 615 // Try the request.
@@ -506,9 +615,9 @@
506 615 // Try the request.
507 616 timer_start();
508 617 $result = Elasticsearch::factory()->bulk_index( $this->get_index_name(), $this->slug, implode( '', $body ) );
509 618 $request_time = timer_stop();
510 - $requests++;
619 + ++$requests;
511 620
512 621 /**
513 622 * Perform actions before a new batch of documents is processed.
514 623 *
@@ -525,37 +634,42 @@
525 634 do_action( 'ep_after_send_dynamic_bulk_request', $result, $body, $documents, $min_buffer_size, $max_buffer_size, $current_buffer_size, $request_time );
526 635
527 636 // It failed, possibly adjust the buffer size and try again.
528 637 if ( is_wp_error( $result ) ) {
638 + $error_code = $result->get_error_code();
639 +
529 640 // Too many requests, wait and try again.
530 - if ( 429 === $result->get_error_code() ) {
641 + if ( 429 === $error_code ) {
531 642 sleep( 2 );
532 643 }
533 644
534 645 // If the error is not a "Request too big" then we really fail this batch of documents.
535 - if ( 413 !== $result->get_error_code() ) {
646 + if ( 413 !== $error_code ) {
536 647 $results[] = $result;
537 648 continue;
538 649 }
539 650
540 651 if ( count( $body ) === 1 ) {
541 - $max_buffer_size = min( $max_buffer_size, mb_strlen( implode( '', $body ) ) );
542 - $results[] = $result;
543 - $body = [];
652 + $max_buffer_size = min( $max_buffer_size, $current_body_size );
653 + $results[] = $result;
654 + $body = [];
655 + $current_body_size = 0;
544 656 continue;
545 657 }
546 658
547 659 // As the buffer is as small as possible, return the error.
548 - if ( mb_strlen( implode( '', $body ) ) === $min_buffer_size ) {
660 + if ( $current_body_size === $min_buffer_size ) {
549 661 $results[] = $result;
550 662 continue;
551 663 }
552 664
553 665 // We have a too big buffer. Remove one doc from the body, and set both max and current as its size.
554 - array_unshift( $documents, array_pop( $body ) );
666 + $removed_document = array_pop( $body );
667 + $current_body_size -= mb_strlen( $removed_document );
668 + array_unshift( $documents, $removed_document );
555 669
556 670 $max_buffer_size = count( $body ) ?
557 - max( $min_buffer_size, mb_strlen( implode( '', $body ) ) ) :
671 + max( $min_buffer_size, $current_body_size ) :
558 672 $min_buffer_size;
559 673
560 674 $current_buffer_size = $max_buffer_size;
561 675 continue;
@@ -561,15 +675,16 @@
561 675 continue;
562 676 }
563 677
564 678 // Things worked so we can try to bump the buffer size.
565 - if ( $current_buffer_size < $max_buffer_size && mb_strlen( implode( '', $body ) ) > $current_buffer_size ) {
679 + if ( $current_buffer_size < $max_buffer_size && $current_body_size > $current_buffer_size ) {
566 680 $current_buffer_size = min( ( $current_buffer_size + $incremental_step ), $max_buffer_size );
567 681 }
568 682
569 683 $results[] = $result;
570 684
571 - $body = [];
685 + $body = [];
686 + $current_body_size = 0;
572 687 } while ( ! empty( $documents ) );
573 688
574 689 /**
575 690 * Perform actions after a batch of documents was processed.
@@ -652,9 +767,8 @@
652 767 $prepared_meta[ $meta_key ] = array_map( array( $this, 'prepare_meta_value_types' ), $meta_values );
653 768 }
654 769
655 770 return $prepared_meta;
656 -
657 771 }
658 772
659 773 /**
660 774 * Prepare meta types for meta value
@@ -777,33 +891,8 @@
777 891 'unsigned' => 'long',
778 892 ];
779 893
780 894 foreach ( $meta_queries as $single_meta_query ) {
781 -
782 - /**
783 - * There is a strange case where meta_query looks like this:
784 - * array(
785 - * "something" => array(
786 - * array(
787 - * 'key' => ...
788 - * ...
789 - * )
790 - * )
791 - * )
792 - *
793 - * Somehow WordPress (WooCommerce) handles that case so we need to as well.
794 - *
795 - * @since 2.1
796 - */
797 - if ( is_array( $single_meta_query ) && empty( $single_meta_query['key'] ) ) {
798 - reset( $single_meta_query );
799 - $first_key = key( $single_meta_query );
800 -
801 - if ( is_array( $single_meta_query[ $first_key ] ) ) {
802 - $single_meta_query = $single_meta_query[ $first_key ];
803 - }
804 - }
805 -
806 895 if ( ! empty( $single_meta_query['key'] ) ) {
807 896
808 897 $terms_obj = false;
809 898
@@ -1037,9 +1126,9 @@
1037 1126 // Add the meta query filter
1038 1127 if ( false !== $terms_obj ) {
1039 1128 $meta_filter[] = $terms_obj;
1040 1129 }
1041 - } elseif ( is_array( $single_meta_query ) && isset( $single_meta_query[0] ) && is_array( $single_meta_query[0] ) ) {
1130 + } elseif ( is_array( $single_meta_query ) ) {
1042 1131 /**
1043 1132 * Handle multidimensional array. Something like:
1044 1133 *
1045 1134 * 'meta_query' => array(
@@ -1114,9 +1203,9 @@
1114 1203 return ( (string) $new_mapping['settings']['index.number_of_shards'] === $stored_mapping[ $this->get_index_name() ]['settings']['index']['number_of_shards'] );
1115 1204 }
1116 1205
1117 1206 /**
1118 - * Utilitary function to check if the indexable is being fully reindexed, i.e.,
1207 + * Utility function to check if the indexable is being fully reindexed, i.e.,
1119 1208 * the index was deleted, a new mapping was sent and content is being reindexed.
1120 1209 *
1121 1210 * @param int|null $blog_id Blog ID
1122 1211 * @return boolean
@@ -1133,14 +1222,15 @@
1133 1222
1134 1223 /**
1135 1224 * Send mapping to Elasticsearch
1136 1225 *
1137 - * @return boolean
1226 + * @param string $return_type Desired return type. Can be either 'bool' or 'raw'
1227 + * @return bool|WP_Error
1138 1228 */
1139 - public function put_mapping() {
1229 + public function put_mapping( $return_type = 'bool' ) {
1140 1230 $mapping = $this->generate_mapping();
1141 1231
1142 - return Elasticsearch::factory()->put_mapping( $this->get_index_name(), $mapping );
1232 + return Elasticsearch::factory()->put_mapping( $this->get_index_name(), $mapping, $return_type );
1143 1233 }
1144 1234
1145 1235 /**
1146 1236 * Must implement a method that given an object ID, returns a formatted Elasticsearch
@@ -1156,9 +1246,9 @@
1156 1246 * in a standardized format. This is necessary so we can genericize the index
1157 1247 * process across indexables.
1158 1248 *
1159 1249 * @param array $args Array to query DB against.
1160 - * @return boolean
1250 + * @return array
1161 1251 */
1162 1252 abstract public function query_db( $args );
1163 1253
1164 1254 /**
@@ -1181,9 +1271,9 @@
1181 1271 * @param array $search_fields Search fields
1182 1272 * @param array $query_vars Query vars
1183 1273 * @return SearchAlgorithm Instance of search algorithm to be used
1184 1274 */
1185 - public function get_search_algorithm( string $search_text, array $search_fields, array $query_vars ) : \ElasticPress\SearchAlgorithm {
1275 + public function get_search_algorithm( string $search_text, array $search_fields, array $query_vars ): \ElasticPress\SearchAlgorithm {
1186 1276 /**
1187 1277 * Filter the search algorithm to be used
1188 1278 *
1189 1279 * @hook ep_{$indexable_slug}_search_algorithm
@@ -1204,22 +1294,23 @@
1204 1294 *
1205 1295 * @since 4.3.0
1206 1296 * @param null|int $blog_id (Optional) The blog ID. Sending `null` will use the current blog ID.
1207 1297 * @return array
1298 + * @throws \Exception An exception if meta fields are not available.
1208 1299 */
1209 1300 public function get_distinct_meta_field_keys( $blog_id = null ) {
1210 1301 $mapping = $this->get_mapping();
1211 1302
1212 1303 try {
1213 - if ( version_compare( Elasticsearch::factory()->get_elasticsearch_version(), '7.0', '<' ) ) {
1214 - $meta_fields = $mapping[ $this->get_index_name( $blog_id ) ]['mappings']['post']['properties']['meta']['properties'];
1304 + if ( version_compare( (string) Elasticsearch::factory()->get_elasticsearch_version(), '7.0', '<' ) ) {
1305 + $meta_fields = (array) $mapping[ $this->get_index_name( $blog_id ) ]['mappings']['post']['properties']['meta']['properties'];
1215 1306 } else {
1216 - $meta_fields = $mapping[ $this->get_index_name( $blog_id ) ]['mappings']['properties']['meta']['properties'];
1307 + $meta_fields = (array) $mapping[ $this->get_index_name( $blog_id ) ]['mappings']['properties']['meta']['properties'];
1217 1308 }
1218 1309 $meta_keys = array_values( array_keys( $meta_fields ) );
1219 1310 sort( $meta_keys );
1220 1311 } catch ( \Throwable $th ) {
1221 - return new \Exception( 'Meta fields not available.', 0 );
1312 + throw new \Exception( 'Meta fields not available.', 0 );
1222 1313 }
1223 1314
1224 1315 return $meta_keys;
1225 1316 }
@@ -1269,6 +1360,33 @@
1269 1360 $values[] = $es_bucket['key'];
1270 1361 }
1271 1362
1272 1363 return $values;
1364 + }
1365 +
1366 + /**
1367 + * Should instantiate the indexable SyncManager and QueryIntegration, the main responsibles for the WP integration.
1368 + *
1369 + * @since 4.5.0
1370 + */
1371 + public function setup() {}
1372 +
1373 + /**
1374 + * Given a mapping, add the ngram analyzer to it
1375 + *
1376 + * @since 4.5.0
1377 + * @param array $mapping The mapping
1378 + * @return array
1379 + */
1380 + public function add_ngram_analyzer( array $mapping ): array {
1381 + $mapping['settings']['analysis']['analyzer']['edge_ngram_analyzer'] = array(
1382 + 'type' => 'custom',
1383 + 'tokenizer' => 'standard',
1384 + 'filter' => array(
1385 + 'lowercase',
1386 + 'edge_ngram',
1387 + ),
1388 + );
1389 +
1390 + return $mapping;
1273 1391 }
1274 1392 }