PluginProbe
ElasticPress / 4.2.0
ElasticPress v4.2.0
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
elasticpress / includes / classes / Indexable.php

Indexable.php in ElasticPress 4.2.0, at includes/classes/Indexable.php

1,160 lines 31.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Indexable abstract class.
4 *
5 * An indexable is a type of "data" in WP e.g. post type, term, user, etc.
6 *
7 * @since 3.0
8 * @package elasticpress
9 */
10
11 namespace ElasticPress;
12
13 use ElasticPress\Elasticsearch as Elasticsearch;
14 use ElasticPress\SyncManager as SyncManager;
15 use ElasticPress\QueryIntegration as QueryIntegration;
16
17 if ( ! defined( 'ABSPATH' ) ) {
18 exit; // Exit if accessed directly.
19 }
20
21 /**
22 * An indexable is essentially a document type that can be indexed
23 * and queried against
24 *
25 * @since 3.0
26 */
27 abstract class Indexable {
28
29 /**
30 * Declaring an Indexable global means it won't have an index for each blog in
31 * the network. Instead it will just have one index. There will also be no
32 * network alias.
33 *
34 * @var boolean
35 * @since 3.0
36 */
37 public $global = false;
38
39 /**
40 * Instance of SyncManager. This should handle automated syncing of indexable
41 * objects.
42 *
43 * @var SyncManager
44 * @since 3.0
45 */
46 public $sync_manager;
47
48 /**
49 * Instance of QueryIntegration. This should handle integrating with a default
50 * WP query.
51 *
52 * @var QueryIntegration
53 * @since 3.0
54 */
55 public $query_integration;
56
57 /**
58 * Flag to indicate if the indexable has support for
59 * `id_range` pagination method during a sync.
60 *
61 * @var boolean
62 * @since 4.1.0
63 */
64 public $support_indexing_advanced_pagination = false;
65
66 /**
67 * Get number of bulk items to index per page
68 *
69 * @since 3.0
70 * @return int
71 */
72 public function get_bulk_items_per_page() {
73 /**
74 * Filter bulk items to sync per batch
75 *
76 * @hook ep_bulk_items_per_page
77 * @param {int} $number Number of items per batch
78 * @param {Indexable} $indexable Current indexable
79 * @return {int} New number of items
80 * @since 3.0
81 */
82 return apply_filters( 'ep_bulk_items_per_page', 350, $this );
83 }
84
85 /**
86 * Get the name of the index. Each indexable needs a unique index name
87 *
88 * @param int $blog_id `null` means current blog.
89 * @since 3.0
90 * @return string
91 */
92 public function get_index_name( $blog_id = null ) {
93 if ( $this->global ) {
94 $site_url = network_site_url();
95
96 if ( ! empty( $site_url ) ) {
97 $index_name = preg_replace( '#https?://(www\.)?#i', '', $site_url );
98 $index_name = preg_replace( '#[^\w]#', '', $index_name ) . '-' . $this->slug;
99 } else {
100 $index_name = false;
101 }
102 } else {
103 if ( ! $blog_id ) {
104 $blog_id = get_current_blog_id();
105 }
106
107 $site_url = get_site_url( $blog_id );
108
109 if ( ! empty( $site_url ) ) {
110 $index_name = preg_replace( '#https?://(www\.)?#i', '', $site_url );
111 $index_name = preg_replace( '#[^\w]#', '', $index_name ) . '-' . $this->slug . '-' . $blog_id;
112 } else {
113 $index_name = false;
114 }
115 }
116
117 $prefix = Utils\get_index_prefix();
118
119 if ( ! empty( $prefix ) ) {
120 $index_name = $prefix . '-' . $index_name;
121 }
122
123 $index_name = strtolower( $index_name );
124
125 /**
126 * Filter index name
127 *
128 * @hook ep_index_name
129 * @param {string} $index_name Name of index
130 * @param {int} $blog_id Blog ID
131 * @param {Indexable} $indexable Current indexable
132 * @return {string} Index name
133 * @since 3.0
134 */
135 return apply_filters( 'ep_index_name', $index_name, $blog_id, $this );
136 }
137
138 /**
139 * Get unique indexable network alias
140 *
141 * @since 3.0
142 * @return string
143 */
144 public function get_network_alias() {
145 $url = network_site_url();
146 $slug = preg_replace( '#https?://(www\.)?#i', '', $url );
147 $slug = preg_replace( '#[^\w]#', '', $slug );
148
149 $alias = $slug . '-' . $this->slug . '-global';
150
151 $prefix = Utils\get_index_prefix();
152
153 if ( ! empty( $prefix ) ) {
154 $alias = $prefix . '-' . $alias;
155 }
156
157 /**
158 * Filter global/network Elasticsearch alias
159 *
160 * @hook ep_global_alias
161 * @param {string} $number Current alias
162 * @return {string} New alias
163 */
164 return apply_filters( 'ep_global_alias', $alias );
165 }
166
167 /**
168 * Delete unique indexable network alias
169 *
170 * @since 3.0
171 * @return boolean
172 */
173 public function delete_network_alias() {
174 return Elasticsearch::factory()->delete_network_alias( $this->get_network_alias() );
175 }
176
177 /**
178 * Create unique indexable network alias
179 *
180 * @param array $indexes Array of indexes.
181 * @since 3.0
182 * @return boolean
183 */
184 public function create_network_alias( $indexes ) {
185 return Elasticsearch::factory()->create_network_alias( $indexes, $this->get_network_alias() );
186 }
187
188 /**
189 * Delete an object within the indexable
190 *
191 * @param int $object_id Object to delete.
192 * @param boolean $blocking Whether to issue blocking HTTP request or not.
193 * @since 3.0
194 * @return boolean
195 */
196 public function delete( $object_id, $blocking = true ) {
197 /**
198 * Fires before object deletion
199 *
200 * @hook ep_delete_{indexable_slug}
201 * @param {int} $object_id ID of object being deleted
202 * @param {string} $indexable_slug The slug of the indexable type that is being deleted
203 */
204 do_action( 'ep_delete_' . $this->slug, $object_id, $this->slug );
205
206 return Elasticsearch::factory()->delete_document( $this->get_index_name(), $this->slug, $object_id, $blocking );
207 }
208
209 /**
210 * Get an object within the indexable
211 *
212 * @param int $object_id Object to get.
213 * @since 3.0
214 * @return boolean|array
215 */
216 public function get( $object_id ) {
217 return Elasticsearch::factory()->get_document( $this->get_index_name(), $this->slug, $object_id );
218 }
219
220 /**
221 * Get objects within the indexable
222 *
223 * @param int $object_ids Array of object ids to get.
224 * @since 3.6.0
225 * @return boolean|array
226 */
227 public function multi_get( $object_ids ) {
228 return Elasticsearch::factory()->get_documents( $this->get_index_name(), $this->slug, $object_ids );
229 }
230
231 /**
232 * Delete an index within the indexable
233 *
234 * @param int $blog_id `null` means current blog.
235 * @since 3.0
236 * @return boolean
237 */
238 public function delete_index( $blog_id = null ) {
239 return Elasticsearch::factory()->delete_index( $this->get_index_name( $blog_id ) );
240 }
241
242 /**
243 * Index an object within the indexable. This calls prepare_document
244 *
245 * @param int $object_id Object to index.
246 * @param boolean $blocking Blocking HTTP request or not.
247 * @since 3.0
248 * @return boolean
249 */
250 public function index( $object_id, $blocking = false ) {
251 $document = $this->prepare_document( $object_id );
252
253 if ( false === $document ) {
254 return false;
255 }
256
257 /**
258 * Conditionally kill indexing on a specific object
259 *
260 * @hook ep_{indexable_slug}_index_kill
261 * @param {bool} $kill True to not index
262 * @param {int} $object_id Id of object to index
263 * @since 3.0
264 * @return {bool} New kill value
265 */
266 if ( apply_filters( 'ep_' . $this->slug . '_index_kill', false, $object_id ) ) {
267 return false;
268 }
269
270 /**
271 * Filter document before index
272 *
273 * @hook ep_pre_index_{indexable_slug}
274 * @param {array} $document Document to index
275 * @return {array} New document
276 * @since 3.0
277 */
278 $document = apply_filters( 'ep_pre_index_' . $this->slug, $document );
279
280 $return = Elasticsearch::factory()->index_document( $this->get_index_name(), $this->slug, $document, $blocking );
281
282 /**
283 * Fires after document is indexed
284 *
285 * @hook ep_after_index_{indexable_slug}
286 * @param {array} $document Document to index
287 * @param {array|boolean} $return ES response on success, false on failure
288 * @since 3.0
289 */
290 do_action( 'ep_after_index_' . $this->slug, $document, $return );
291
292 return $return;
293 }
294
295 /**
296 * Determine if indexable index exists
297 *
298 * @param int $blog_id Blog to check index for.
299 * @since 3.0
300 * @return boolean
301 */
302 public function index_exists( $blog_id = null ) {
303 return Elasticsearch::factory()->index_exists( $this->get_index_name( $blog_id ) );
304 }
305
306 /**
307 * Bulk index objects. This calls prepare_document on each object
308 *
309 * @param array $object_ids Array of object IDs.
310 * @since 3.0
311 * @return WP_Error|array
312 */
313 public function bulk_index( $object_ids ) {
314 $body = '';
315
316 foreach ( $object_ids as $object_id ) {
317 $action_args = array(
318 'index' => array(
319 '_id' => absint( $object_id ),
320 ),
321 );
322
323 $document = $this->prepare_document( $object_id );
324
325 /**
326 * Conditionally kill indexing on a specific object
327 *
328 * @hook ep_bulk_index_action_args
329 * @param {array} $action_args Bulk action arguments
330 * @param {array} $document Document to index
331 * @since 3.0
332 * @return {array} New action args
333 */
334 $body .= wp_json_encode( apply_filters( 'ep_bulk_index_action_args', $action_args, $document ) ) . "\n";
335 $body .= addcslashes( wp_json_encode( $document ), "\n" );
336
337 $body .= "\n\n";
338 }
339
340 $result = Elasticsearch::factory()->bulk_index( $this->get_index_name(), $this->slug, $body );
341
342 /**
343 * Perform actions after a bulk indexing is completed
344 *
345 * @hook ep_after_bulk_index
346 * @param {array} $object_ids List of object ids attempted to be indexed
347 * @param {string} $slug Current indexable slug
348 * @param {array|bool} $result Result of the Elasticsearch query. False on error.
349 */
350 do_action( 'ep_after_bulk_index', $object_ids, $this->slug, $result );
351
352 return $result;
353 }
354
355 /**
356 * Bulk index objects but with a dynamic size of queue.
357 *
358 * @since 4.0.0
359 * @param array $object_ids Array of object IDs.
360 * @return array[WP_Error|array] The return of each request made.
361 */
362 public function bulk_index_dynamically( $object_ids ) {
363 $documents = [];
364
365 foreach ( $object_ids as $object_id ) {
366 $action_args = array(
367 'index' => array(
368 '_id' => absint( $object_id ),
369 ),
370 );
371
372 $document = $this->prepare_document( $object_id );
373
374 /**
375 * Conditionally kill indexing on a specific object
376 *
377 * @hook ep_bulk_index_action_args
378 * @param {array} $action_args Bulk action arguments
379 * @param {array} $document Document to index
380 * @since 3.0
381 * @return {array} New action args
382 */
383 $document_str = wp_json_encode( apply_filters( 'ep_bulk_index_action_args', $action_args, $document ) ) . "\n";
384 $document_str .= addcslashes( wp_json_encode( $document ), "\n" );
385 $document_str .= "\n\n";
386
387 $documents[] = $document_str;
388 }
389
390 $results = $this->send_bulk_index_request( $documents );
391
392 /**
393 * Perform actions after a dynamic bulk indexing is completed
394 *
395 * @hook ep_after_bulk_index_dynamically
396 * @since 4.0.0
397 * @param {array} $object_ids List of object ids attempted to be indexed
398 * @param {string} $slug Current indexable slug
399 * @param {array|bool} $result Result of the Elasticsearch query. False on error.
400 */
401 do_action( 'ep_after_bulk_index_dynamically', $object_ids, $this->slug, $results );
402
403 return $results;
404 }
405
406 /**
407 * Bulk index documents through several requests with dynamic size.
408 *
409 * @param array $documents The documents to be sent to Elasticsearch (already formatted.)
410 * @return array[WP_Error|array]
411 */
412 protected function send_bulk_index_request( $documents ) {
413 static $min_buffer_size, $max_buffer_size, $current_buffer_size, $incremental_step;
414
415 if ( ! $min_buffer_size ) {
416 /**
417 * Filter the minimum buffer size for dynamic bulk index requests.
418 *
419 * @hook ep_dynamic_bulk_min_buffer_size
420 * @since 4.0.0
421 * @param {int} $min_buffer_size Min buffer size for dynamic bulk index (in bytes.)
422 * @return {int} New size.
423 */
424 $min_buffer_size = apply_filters( 'ep_dynamic_bulk_min_buffer_size', MB_IN_BYTES / 2 );
425 }
426
427 if ( ! $max_buffer_size ) {
428 /**
429 * Filter the max buffer size for dynamic bulk index requests.
430 *
431 * @hook ep_dynamic_bulk_max_buffer_size
432 * @since 4.0.0
433 * @param {int} $max_buffer_size Max buffer size for dynamic bulk index (in bytes.)
434 * @return {int} New size.
435 */
436 $max_buffer_size = apply_filters( 'ep_dynamic_bulk_max_buffer_size', 150 * MB_IN_BYTES );
437 }
438
439 if ( ! $incremental_step ) {
440 /**
441 * Filter the number of bytes the current buffer size should be incremented in case of success.
442 *
443 * @hook ep_dynamic_bulk_incremental_step
444 * @since 4.0.0
445 * @param {int} $incremental_step Number of bytes to add to the current buffer size.
446 * @return {int} New incremental step.
447 */
448 $incremental_step = apply_filters( 'ep_dynamic_bulk_incremental_step', MB_IN_BYTES / 2 );
449 }
450
451 /**
452 * Perform actions before a new batch of documents is processed.
453 *
454 * @hook ep_before_send_dynamic_bulk_requests
455 * @since 4.0.0
456 * @param {array} $documents Array of documents to be sent to Elasticsearch.
457 */
458 do_action( 'ep_before_send_dynamic_bulk_requests', $documents );
459
460 if ( ! $current_buffer_size ) {
461 $current_buffer_size = $min_buffer_size;
462 }
463
464 $results = [];
465
466 $body = [];
467
468 $requests = 0;
469
470 /*
471 * This script will use two main arrays: $body and $documents, being $body the
472 * documents to be sent in the next request and $documents the list of docs to be indexed.
473 * The do-while loop will stop if all documents are sent or if a request fails even sending
474 * a buffer as small as possible.
475 */
476 do {
477 $next_document = array_shift( $documents );
478
479 // If the next document alone takes the entire current buffer size,
480 // 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 ) {
482 array_unshift( $documents, $next_document );
483 } else {
484 if ( mb_strlen( $next_document ) > $max_buffer_size ) {
485 /**
486 * Perform actions when a post is bigger than the max buffer size.
487 *
488 * @hook ep_dynamic_bulk_post_too_big
489 * @since 4.0.0
490 * @param {string} $document JSON string of the post detected as too big.
491 */
492 do_action( 'ep_dynamic_bulk_post_too_big', $next_document );
493 $results[] = new \WP_Error( 'ep_too_big_request_skipped', 'Indexable too big. Request not sent.' );
494 continue;
495 }
496 $body[] = $next_document;
497 if ( mb_strlen( implode( '', $body ) ) < $current_buffer_size && ! empty( $documents ) ) {
498 continue;
499 }
500 if ( mb_strlen( implode( '', $body ) ) > $max_buffer_size ) {
501 // The last document added to body made it too big, so let's give it back.
502 array_unshift( $documents, array_pop( $body ) );
503 }
504 }
505
506 // Try the request.
507 timer_start();
508 $result = Elasticsearch::factory()->bulk_index( $this->get_index_name(), $this->slug, implode( '', $body ) );
509 $request_time = timer_stop();
510 $requests++;
511
512 /**
513 * Perform actions before a new batch of documents is processed.
514 *
515 * @hook ep_after_send_dynamic_bulk_request
516 * @since 4.0.0
517 * @param {WP_Error|array} $result Result of the request.
518 * @param {array} $body Array of documents sent to Elasticsearch.
519 * @param {array} $documents Array of documents to be sent to Elasticsearch.
520 * @param {int} $min_buffer_size Min buffer size for dynamic bulk index (in bytes.)
521 * @param {int} $max_buffer_size Max buffer size for dynamic bulk index (in bytes.)
522 * @param {int} $current_buffer_size Current buffer size for dynamic bulk index (in bytes.)
523 * @param {int} $request_time Total time of the request.
524 */
525 do_action( 'ep_after_send_dynamic_bulk_request', $result, $body, $documents, $min_buffer_size, $max_buffer_size, $current_buffer_size, $request_time );
526
527 // It failed, possibly adjust the buffer size and try again.
528 if ( is_wp_error( $result ) ) {
529 // Too many requests, wait and try again.
530 if ( 429 === $result->get_error_code() ) {
531 sleep( 2 );
532 }
533
534 // If the error is not a "Request too big" then we really fail this batch of documents.
535 if ( 413 !== $result->get_error_code() ) {
536 $results[] = $result;
537 continue;
538 }
539
540 if ( count( $body ) === 1 ) {
541 $max_buffer_size = min( $max_buffer_size, mb_strlen( implode( '', $body ) ) );
542 $results[] = $result;
543 $body = [];
544 continue;
545 }
546
547 // As the buffer is as small as possible, return the error.
548 if ( mb_strlen( implode( '', $body ) ) === $min_buffer_size ) {
549 $results[] = $result;
550 continue;
551 }
552
553 // 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 ) );
555
556 $max_buffer_size = count( $body ) ?
557 max( $min_buffer_size, mb_strlen( implode( '', $body ) ) ) :
558 $min_buffer_size;
559
560 $current_buffer_size = $max_buffer_size;
561 continue;
562 }
563
564 // 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 ) {
566 $current_buffer_size = min( ( $current_buffer_size + $incremental_step ), $max_buffer_size );
567 }
568
569 $results[] = $result;
570
571 $body = [];
572 } while ( ! empty( $documents ) );
573
574 /**
575 * Perform actions after a batch of documents was processed.
576 *
577 * @hook ep_after_send_dynamic_bulk_requests
578 * @since 4.0.0
579 * @param {array} $results Array of results sent.
580 * @param {int} $requests Number of all requests sent.
581 */
582 do_action( 'ep_after_send_dynamic_bulk_requests', $results, $requests );
583
584 return $results;
585 }
586
587 /**
588 * Query Elasticsearch for documents
589 *
590 * @param array $formatted_args Formatted es query arguments.
591 * @param array $query_args WP_Query args.
592 * @param string $index Index(es) to query. Comma separate for multiple. Defaults to current.
593 * @param mixed $query_object Could be WP_Query, WP_User_Query, etc.
594 * @since 3.0
595 * @return array
596 */
597 public function query_es( $formatted_args, $query_args, $index = null, $query_object = null ) {
598 if ( null === $index ) {
599 $index = $this->get_index_name();
600 }
601
602 return Elasticsearch::factory()->query( $index, $this->slug, $formatted_args, $query_args, $query_object );
603 }
604
605 /**
606 * Check to see if we should allow elasticpress to override this query
607 *
608 * @param \WP_Query|\WP_User_Query|\WP_Term_Query $query WP_Query or WP_User_Query or WP_Term_Query instance
609 * @return bool
610 * @since 3.0
611 */
612 public function elasticpress_enabled( $query ) {
613 $enabled = false;
614
615 if ( ! empty( $query->query_vars['ep_integrate'] ) ) {
616 $enabled = true;
617 }
618
619 /**
620 * Determine if ElasticPress should integrate with a query
621 *
622 * @hook ep_elasticpress_enabled
623 * @param {bool} $enabled Whether to integrate with Elasticsearch or not
624 * @param {WP_Query} $query WP_Query to evaluate
625 * @return {bool} Enabled value
626 */
627 $enabled = apply_filters( 'ep_elasticpress_enabled', $enabled, $query );
628
629 if ( isset( $query->query_vars['ep_integrate'] ) && ! filter_var( $query->query_vars['ep_integrate'], FILTER_VALIDATE_BOOLEAN ) ) {
630 $enabled = false;
631 }
632
633 return $enabled;
634 }
635
636 /**
637 * Prepare meta type values to send to ES
638 *
639 * @param array $meta Array of meta.
640 * @since 3.0
641 * @return array
642 */
643 public function prepare_meta_types( $meta ) {
644
645 $prepared_meta = [];
646
647 foreach ( $meta as $meta_key => $meta_values ) {
648 if ( ! is_array( $meta_values ) ) {
649 $meta_values = array( $meta_values );
650 }
651
652 $prepared_meta[ $meta_key ] = array_map( array( $this, 'prepare_meta_value_types' ), $meta_values );
653 }
654
655 return $prepared_meta;
656
657 }
658
659 /**
660 * Prepare meta types for meta value
661 *
662 * @param mixed $meta_value Meta value to prepare.
663 * @since 3.0
664 * @return array
665 */
666 public function prepare_meta_value_types( $meta_value ) {
667
668 $max_java_int_value = PHP_INT_MAX;
669
670 $meta_types = [];
671
672 if ( is_array( $meta_value ) || is_object( $meta_value ) ) {
673 $meta_value = serialize( $meta_value ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
674 }
675
676 $meta_types['value'] = $meta_value;
677 $meta_types['raw'] = $meta_value;
678
679 if ( is_numeric( $meta_value ) ) {
680 $long = intval( $meta_value );
681
682 if ( $max_java_int_value < $long ) {
683 $long = $max_java_int_value;
684 }
685
686 $double = floatval( $meta_value );
687
688 if ( ! is_finite( $double ) ) {
689 $double = 0;
690 }
691
692 $meta_types['long'] = $long;
693 $meta_types['double'] = $double;
694 }
695
696 $meta_types['boolean'] = filter_var( $meta_value, FILTER_VALIDATE_BOOLEAN );
697
698 $meta_types = $this->prepare_date_meta_values( $meta_types, $meta_value );
699
700 return $meta_types;
701 }
702
703 /**
704 * Checks if a meta_value is a valid date and prepare extra meta-data.
705 *
706 * @param array $meta_types Array of currently prepared data
707 * @param string $meta_value Meta value to prepare.
708 *
709 * @return array
710 */
711 public function prepare_date_meta_values( $meta_types, $meta_value ) {
712
713 if ( empty( $meta_value ) || ! is_string( $meta_value ) ) {
714 return $meta_types;
715 }
716
717 $meta_types['date'] = '1970-01-01';
718 $meta_types['datetime'] = '1970-01-01 00:00:01';
719 $meta_types['time'] = '00:00:01';
720
721 // is this is a recognizable date format?
722 $new_date = date_create( $meta_value, \wp_timezone() );
723 if ( $new_date ) {
724 $timestamp = $new_date->getTimestamp();
725
726 // PHP allows DateTime to build dates with the non-existing year 0000, and this causes
727 // issues when integrating into stricter systems. This is by design:
728 // https://bugs.php.net/bug.php?id=60288
729 if ( false !== $timestamp && '0000' !== $new_date->format( 'Y' ) ) {
730 $meta_types['date'] = $new_date->format( 'Y-m-d' );
731 $meta_types['datetime'] = $new_date->format( 'Y-m-d H:i:s' );
732 $meta_types['time'] = $new_date->format( 'H:i:s' );
733 }
734 }
735
736 return $meta_types;
737 }
738
739 /**
740 * Build Elasticsearch filter query for WP meta_query
741 *
742 * @since 2.2
743 * @param array $meta_queries Array of queries
744 * @return array
745 */
746 public function build_meta_query( $meta_queries ) {
747 $meta_filter = [];
748
749 $outer_relation = 'must';
750 if ( ! empty( $meta_queries['relation'] ) && 'or' === strtolower( $meta_queries['relation'] ) ) {
751 $outer_relation = 'should';
752 }
753
754 $meta_query_type_mapping = [
755 'numeric' => 'long',
756 'binary' => 'raw',
757 'char' => 'raw',
758 'date' => 'date',
759 'datetime' => 'datetime',
760 'decimal' => 'double',
761 'signed' => 'long',
762 'time' => 'time',
763 'unsigned' => 'long',
764 ];
765
766 foreach ( $meta_queries as $single_meta_query ) {
767
768 /**
769 * There is a strange case where meta_query looks like this:
770 * array(
771 * "something" => array(
772 * array(
773 * 'key' => ...
774 * ...
775 * )
776 * )
777 * )
778 *
779 * Somehow WordPress (WooCommerce) handles that case so we need to as well.
780 *
781 * @since 2.1
782 */
783 if ( is_array( $single_meta_query ) && empty( $single_meta_query['key'] ) ) {
784 reset( $single_meta_query );
785 $first_key = key( $single_meta_query );
786
787 if ( is_array( $single_meta_query[ $first_key ] ) ) {
788 $single_meta_query = $single_meta_query[ $first_key ];
789 }
790 }
791
792 if ( ! empty( $single_meta_query['key'] ) ) {
793
794 $terms_obj = false;
795
796 $compare = '=';
797 if ( ! empty( $single_meta_query['compare'] ) ) {
798 $compare = strtolower( $single_meta_query['compare'] );
799 }
800
801 $type = null;
802 if ( ! empty( $single_meta_query['type'] ) ) {
803 $type = strtolower( $single_meta_query['type'] );
804 }
805
806 // Comparisons need to look at different paths
807 if ( in_array( $compare, array( 'exists', 'not exists' ), true ) ) {
808 $meta_key_path = 'meta.' . $single_meta_query['key'];
809 } elseif ( in_array( $compare, array( '=', '!=' ), true ) && ! $type ) {
810 $meta_key_path = 'meta.' . $single_meta_query['key'] . '.raw';
811 } elseif ( in_array( $compare, array( 'like', 'not like' ), true ) ) {
812 $meta_key_path = 'meta.' . $single_meta_query['key'] . '.value';
813 } elseif ( $type && isset( $meta_query_type_mapping[ $type ] ) ) {
814 // Map specific meta field types to different Elasticsearch core types
815 $meta_key_path = 'meta.' . $single_meta_query['key'] . '.' . $meta_query_type_mapping[ $type ];
816 } elseif ( in_array( $compare, array( '>=', '<=', '>', '<', 'between', 'not between' ), true ) ) {
817 $meta_key_path = 'meta.' . $single_meta_query['key'] . '.double';
818 } else {
819 $meta_key_path = 'meta.' . $single_meta_query['key'] . '.raw';
820 }
821
822 switch ( $compare ) {
823 case 'not in':
824 case '!=':
825 if ( isset( $single_meta_query['value'] ) ) {
826 $terms_obj = array(
827 'bool' => array(
828 'must_not' => array(
829 array(
830 'terms' => array(
831 $meta_key_path => (array) $single_meta_query['value'],
832 ),
833 ),
834 ),
835 ),
836 );
837 }
838
839 break;
840 case 'exists':
841 $terms_obj = array(
842 'exists' => array(
843 'field' => $meta_key_path,
844 ),
845 );
846
847 break;
848 case 'not exists':
849 $terms_obj = array(
850 'bool' => array(
851 'must_not' => array(
852 array(
853 'exists' => array(
854 'field' => $meta_key_path,
855 ),
856 ),
857 ),
858 ),
859 );
860
861 break;
862 case '>=':
863 if ( isset( $single_meta_query['value'] ) ) {
864 $terms_obj = array(
865 'bool' => array(
866 'must' => array(
867 array(
868 'range' => array(
869 $meta_key_path => array(
870 'gte' => $single_meta_query['value'],
871 ),
872 ),
873 ),
874 ),
875 ),
876 );
877 }
878
879 break;
880 case 'between':
881 if ( isset( $single_meta_query['value'] ) && is_array( $single_meta_query['value'] ) && 2 === count( $single_meta_query['value'] ) ) {
882 $terms_obj = array(
883 'bool' => array(
884 'must' => array(
885 array(
886 'range' => array(
887 $meta_key_path => array(
888 'gte' => $single_meta_query['value'][0],
889 ),
890 ),
891 ),
892 array(
893 'range' => array(
894 $meta_key_path => array(
895 'lte' => $single_meta_query['value'][1],
896 ),
897 ),
898 ),
899 ),
900 ),
901 );
902 }
903
904 break;
905 case 'not between':
906 if ( isset( $single_meta_query['value'] ) && is_array( $single_meta_query['value'] ) && 2 === count( $single_meta_query['value'] ) ) {
907 $terms_obj = array(
908 'bool' => array(
909 'should' => array(
910 array(
911 'range' => array(
912 $meta_key_path => array(
913 'lte' => $single_meta_query['value'][0],
914 ),
915 ),
916 ),
917 array(
918 'range' => array(
919 $meta_key_path => array(
920 'gte' => $single_meta_query['value'][1],
921 ),
922 ),
923 ),
924 ),
925 ),
926 );
927 }
928
929 break;
930 case '<=':
931 if ( isset( $single_meta_query['value'] ) ) {
932 $terms_obj = array(
933 'bool' => array(
934 'must' => array(
935 array(
936 'range' => array(
937 $meta_key_path => array(
938 'lte' => $single_meta_query['value'],
939 ),
940 ),
941 ),
942 ),
943 ),
944 );
945 }
946
947 break;
948 case '>':
949 if ( isset( $single_meta_query['value'] ) ) {
950 $terms_obj = array(
951 'bool' => array(
952 'must' => array(
953 array(
954 'range' => array(
955 $meta_key_path => array(
956 'gt' => $single_meta_query['value'],
957 ),
958 ),
959 ),
960 ),
961 ),
962 );
963 }
964
965 break;
966 case '<':
967 if ( isset( $single_meta_query['value'] ) ) {
968 $terms_obj = array(
969 'bool' => array(
970 'must' => array(
971 array(
972 'range' => array(
973 $meta_key_path => array(
974 'lt' => $single_meta_query['value'],
975 ),
976 ),
977 ),
978 ),
979 ),
980 );
981 }
982
983 break;
984 case 'like':
985 if ( isset( $single_meta_query['value'] ) ) {
986 $terms_obj = array(
987 'match_phrase' => array(
988 $meta_key_path => $single_meta_query['value'],
989 ),
990 );
991 }
992 break;
993 case 'not like':
994 if ( isset( $single_meta_query['value'] ) ) {
995 $terms_obj = array(
996 'bool' => array(
997 'must_not' => array(
998 array(
999 'match_phrase' => array(
1000 $meta_key_path => $single_meta_query['value'],
1001 ),
1002 ),
1003 ),
1004 ),
1005 );
1006 }
1007 break;
1008 case '=':
1009 default:
1010 if ( isset( $single_meta_query['value'] ) ) {
1011 $terms_obj = array(
1012 'terms' => array(
1013 $meta_key_path => (array) $single_meta_query['value'],
1014 ),
1015 );
1016 }
1017
1018 break;
1019 }
1020
1021 // Add the meta query filter
1022 if ( false !== $terms_obj ) {
1023 $meta_filter[] = $terms_obj;
1024 }
1025 } elseif ( is_array( $single_meta_query ) && isset( $single_meta_query[0] ) && is_array( $single_meta_query[0] ) ) {
1026 /**
1027 * Handle multidimensional array. Something like:
1028 *
1029 * 'meta_query' => array(
1030 * 'relation' => 'AND',
1031 * array(
1032 * 'key' => 'meta_key_1',
1033 * 'value' => '1',
1034 * ),
1035 * array(
1036 * 'relation' => 'OR',
1037 * array(
1038 * 'key' => 'meta_key_2',
1039 * 'value' => '2',
1040 * ),
1041 * array(
1042 * 'key' => 'meta_key_3',
1043 * 'value' => '4',
1044 * ),
1045 * ),
1046 * ),
1047 */
1048 $inner_relation = 'must';
1049 if ( ! empty( $single_meta_query['relation'] ) && 'or' === strtolower( $single_meta_query['relation'] ) ) {
1050 $inner_relation = 'should';
1051 }
1052
1053 $meta_filter[] = array(
1054 'bool' => array(
1055 $inner_relation => $this->build_meta_query( $single_meta_query ),
1056 ),
1057 );
1058 }
1059 }
1060
1061 if ( ! empty( $meta_filter ) ) {
1062 return [
1063 'bool' => [
1064 $outer_relation => $meta_filter,
1065 ],
1066 ];
1067 } else {
1068 return false;
1069 }
1070 }
1071
1072 /**
1073 * Get the indexable mapping.
1074 *
1075 * @since 3.6.0
1076 * @return boolean|array
1077 */
1078 public function get_mapping() {
1079 return Elasticsearch::factory()->get_mapping( $this->get_index_name() );
1080 }
1081
1082 /**
1083 * Compare the mapping generated by the plugin and the mapping stored in Elasticsearch.
1084 *
1085 * @todo properly implement the check.
1086 *
1087 * @since 3.6.0
1088 * @return bool|WP_Error
1089 */
1090 public function compare_mappings() {
1091 if ( ! method_exists( $this, 'generate_mapping' ) ) {
1092 return new \WP_Error( 'ep_generate_mapping_not_implemented' );
1093 }
1094
1095 $new_mapping = $this->generate_mapping();
1096 $stored_mapping = $this->get_mapping();
1097
1098 return ( (string) $new_mapping['settings']['index.number_of_shards'] === $stored_mapping[ $this->get_index_name() ]['settings']['index']['number_of_shards'] );
1099 }
1100
1101 /**
1102 * Utilitary function to check if the indexable is being fully reindexed, i.e.,
1103 * the index was deleted, a new mapping was sent and content is being reindexed.
1104 *
1105 * @param int|null $blog_id Blog ID
1106 * @return boolean
1107 */
1108 public function is_full_reindexing( $blog_id = null ) {
1109 if ( $this->global ) {
1110 $blog_id = null;
1111 } elseif ( ! $blog_id ) {
1112 $blog_id = get_current_blog_id();
1113 }
1114
1115 return \ElasticPress\IndexHelper::factory()->is_full_reindexing( $this->slug, $blog_id );
1116 }
1117
1118 /**
1119 * Send mapping to Elasticsearch
1120 *
1121 * @return boolean
1122 */
1123 public function put_mapping() {
1124 $mapping = $this->generate_mapping();
1125
1126 return Elasticsearch::factory()->put_mapping( $this->get_index_name(), $mapping );
1127 }
1128
1129 /**
1130 * Must implement a method that given an object ID, returns a formatted Elasticsearch
1131 * document
1132 *
1133 * @param int $object_id Object to prepare.
1134 * @return array
1135 */
1136 abstract public function prepare_document( $object_id );
1137
1138 /**
1139 * Must implement a method that queries MySQL for objects and returns them
1140 * in a standardized format. This is necessary so we can genericize the index
1141 * process across indexables.
1142 *
1143 * @param array $args Array to query DB against.
1144 * @return boolean
1145 */
1146 abstract public function query_db( $args );
1147
1148 /**
1149 * Shim function for backwards-compatibility on custom Indexables.
1150 *
1151 * @since 4.1.0
1152 * @return array
1153 */
1154 public function generate_mapping() {
1155 _doing_it_wrong( __METHOD__, 'The Indexable class should not call generate_mapping() directly.', 'ElasticPress 4.0' );
1156
1157 return [];
1158 }
1159 }
1160