PluginProbe
ElasticPress / 5.0.2
ElasticPress v5.0.2
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 / IndexHelper.php

IndexHelper.php in ElasticPress 5.0.2, at includes/classes/IndexHelper.php

1,555 lines 45.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Index Helper
4 *
5 * NOTE: As explained in the doc linked below, the dashboard sync exits after each output()
6 * call, to respond to the AJAX request. That means this script will be called several times
7 * while syncing via dashboard, relying on the index_meta to pick it up where it stopped.
8 *
9 * @since 4.0.0
10 * @see https://elasticpress.zendesk.com/hc/en-us/articles/16672117103501-Sync-Process
11 * @package elasticpress
12 */
13
14 namespace ElasticPress;
15
16 use ElasticPress\Utils;
17
18 /**
19 * Index Helper Class.
20 *
21 * @since 4.0.0
22 */
23 class IndexHelper {
24 /**
25 * Array to hold all the index sync information.
26 *
27 * @since 4.0.0
28 * @var array|bool
29 */
30 protected $index_meta = false;
31
32 /**
33 * Arguments to be used during the index process.
34 *
35 * @var array
36 */
37 protected $args = [];
38
39 /**
40 * Queried objects of the current sync item in the stack.
41 *
42 * @since 4.0.0
43 * @var array
44 */
45 protected $current_query = [];
46
47 /**
48 * Holds temporary wp_actions when indexing with pagination
49 *
50 * @since 4.0.0
51 * @var array
52 */
53 private $temporary_wp_actions = [];
54
55 /**
56 * Initialize class.
57 *
58 * @since 4.0.0
59 */
60 public function setup() {
61 $this->index_meta = Utils\get_indexing_status();
62 }
63
64 /**
65 * Method to index everything.
66 *
67 * @since 4.0.0
68 * @param array $args Arguments.
69 */
70 public function full_index( $args ) {
71 register_shutdown_function( [ $this, 'handle_index_error' ] );
72 add_filter( 'wp_php_error_message', [ $this, 'wp_handle_index_error' ], 10, 2 );
73
74 $this->index_meta = Utils\get_indexing_status();
75
76 /**
77 * Filter the sync arguments
78 *
79 * @since 4.5.0
80 * @hook ep_sync_args
81 * @param {array} $args Sync arguments
82 * @param {array} $index_meta Current index meta
83 * @return {array} New sync arguments
84 */
85 $this->args = apply_filters( 'ep_sync_args', $args, $this->index_meta );
86
87 if ( false === $this->index_meta ) {
88 $this->maybe_apply_feature_settings();
89 $this->build_index_meta();
90 }
91
92 // For the dashboard, this will be called and exit the script until the queue is empty again.
93 $this->flush_messages_queue();
94
95 while ( $this->has_items_to_be_processed() ) {
96 $this->process_sync_item();
97 }
98
99 while ( $this->has_network_alias_to_be_created() ) {
100 $this->create_network_alias();
101 }
102
103 $this->full_index_complete();
104 }
105
106 /**
107 * Method to stack everything that needs to be indexed.
108 *
109 * @since 4.0.0
110 */
111 protected function build_index_meta() {
112 Utils\update_option( 'ep_last_sync', time() );
113 Utils\delete_option( 'ep_need_upgrade_sync' );
114 Utils\delete_option( 'ep_feature_auto_activated_sync' );
115 delete_transient( 'ep_sync_interrupted' );
116
117 $start_date_time = date_create( 'now', wp_timezone() );
118
119 /**
120 * There are two ways to control pagination of things that need to be indexed:
121 * - offset: The number of items to skip on each iteration
122 * - id range: Given an ID range, process a batch and set the upper limit as the last processed ID -1
123 *
124 * Although in the first case offset is updated to really control the flow, in the
125 * second it is updated to simply output the number of items processed.
126 */
127 $pagination_method = ( ! empty( $this->args['offset'] ) || ! empty( $this->args['post-ids'] ) || ! empty( $this->args['include'] ) ) ?
128 'offset' :
129 'id_range';
130
131 $starting_indices = array_intersect(
132 Elasticsearch::factory()->get_index_names( 'all' ),
133 wp_list_pluck( Elasticsearch::factory()->get_cluster_indices(), 'index' )
134 );
135
136 $this->index_meta = [
137 'method' => ! empty( $this->args['method'] ) ? $this->args['method'] : 'web',
138 'put_mapping' => ! empty( $this->args['put_mapping'] ),
139 'offset' => ! empty( $this->args['offset'] ) ? absint( $this->args['offset'] ) : 0,
140 'pagination_method' => $pagination_method,
141 'start' => true,
142 'sync_stack' => [],
143 'network_alias' => [],
144 'start_time' => microtime( true ),
145 'start_date_time' => $start_date_time ? $start_date_time->format( DATE_ATOM ) : false,
146 'starting_indices' => $starting_indices,
147 'messages_queue' => [],
148 'trigger' => ! empty( $this->args['trigger'] ) ? sanitize_text_field( $this->args['trigger'] ) : null,
149 'totals' => [
150 'total' => 0,
151 'synced' => 0,
152 'skipped' => 0,
153 'failed' => 0,
154 'total_time' => 0,
155 'errors' => [],
156 ],
157 ];
158
159 $global_indexables = $this->filter_indexables( Indexables::factory()->get_all( true, true, 'all' ) );
160 $non_global_indexables = $this->filter_indexables( Indexables::factory()->get_all( false, true, 'all' ) );
161
162 $is_network_wide = isset( $this->args['network_wide'] ) && ! is_null( $this->args['network_wide'] );
163
164 if ( defined( 'EP_IS_NETWORK' ) && EP_IS_NETWORK && $is_network_wide ) {
165 if ( ! is_numeric( $this->args['network_wide'] ) ) {
166 $this->args['network_wide'] = 0;
167 }
168
169 $sites = Utils\get_sites( $this->args['network_wide'], true );
170
171 foreach ( $sites as $site ) {
172 switch_to_blog( $site['blog_id'] );
173
174 foreach ( $non_global_indexables as $indexable ) {
175 $this->add_sync_item_to_stack(
176 [
177 'url' => untrailingslashit( $site['domain'] . $site['path'] ),
178 'blog_id' => (int) $site['blog_id'],
179 'indexable' => $indexable,
180 ]
181 );
182
183 if ( Indexables::factory()->is_active( $indexable ) && ! in_array( $indexable, $this->index_meta['network_alias'], true ) ) {
184 $this->index_meta['network_alias'][] = $indexable;
185 }
186 }
187 }
188
189 restore_current_blog();
190 } else {
191 foreach ( $non_global_indexables as $indexable ) {
192 $this->add_sync_item_to_stack(
193 [
194 'url' => untrailingslashit( home_url() ),
195 'blog_id' => (int) get_current_blog_id(),
196 'indexable' => $indexable,
197 ]
198 );
199 }
200 }
201
202 foreach ( $global_indexables as $indexable ) {
203 $this->add_sync_item_to_stack(
204 [
205 'indexable' => $indexable,
206 ]
207 );
208 }
209
210 $this->index_meta['current_sync_item'] = false;
211 /**
212 * Fires at start of new index
213 *
214 * @since 4.0.0
215 *
216 * @hook ep_sync_start_index
217 * @param {array} $index_meta Index meta information
218 */
219 do_action( 'ep_sync_start_index', $this->index_meta );
220
221 /**
222 * Fires at start of new index
223 *
224 * @since 2.1 Previously called only as 'ep_dashboard_start_index'
225 * @since 4.0.0 Made available for all methods
226 *
227 * @hook ep_{$index_method}_start_index
228 * @param {array} $index_meta Index meta information
229 */
230 do_action( "ep_{$this->args['method']}_start_index", $this->index_meta );
231
232 /**
233 * Filter index meta during dashboard sync
234 *
235 * @since 3.0
236 * @hook ep_index_meta
237 * @param {array} $index_meta Current index meta
238 * @return {array} New index meta
239 */
240 $this->index_meta = apply_filters( 'ep_index_meta', $this->index_meta );
241 }
242
243 /**
244 * Given an array of indexables, check if they are part of the indexable args or not.
245 *
246 * @since 4.0.0
247 * @param array $indexables Indexable slugs.
248 * @return array
249 */
250 protected function filter_indexables( $indexables ) {
251 return array_filter(
252 $indexables,
253 function( $indexable ) {
254 return empty( $this->args['indexables'] ) || in_array( $indexable, $this->args['indexables'], true );
255 }
256 );
257 }
258
259 /**
260 * Check if there are still items to be processed in the stack.
261 *
262 * @since 4.0.0
263 * @return boolean
264 */
265 protected function has_items_to_be_processed() {
266 return ! empty( $this->index_meta['current_sync_item'] ) || count( $this->index_meta['sync_stack'] ) > 0;
267 }
268
269 /**
270 * Method to process the next item in the stack.
271 *
272 * @since 4.0.0
273 */
274 protected function process_sync_item() {
275 if ( empty( $this->index_meta['current_sync_item'] ) ) {
276 $this->index_meta['current_sync_item'] = array_merge(
277 array_shift( $this->index_meta['sync_stack'] ),
278 [
279 'total' => 0,
280 'synced' => 0,
281 'skipped' => 0,
282 'failed' => 0,
283 'errors' => [],
284 ]
285 );
286
287 $indexable_slug = $this->index_meta['current_sync_item']['indexable'];
288 $indexable = Indexables::factory()->get( $this->index_meta['current_sync_item']['indexable'] );
289
290 if ( ! Indexables::factory()->is_active( $indexable_slug ) ) {
291 return $this->process_not_active_indexable_sync_item();
292 } elseif ( ! empty( $this->index_meta['current_sync_item']['blog_id'] ) && defined( 'EP_IS_NETWORK' ) && EP_IS_NETWORK ) {
293 $this->output_success(
294 sprintf(
295 /* translators: 1: Indexable name, 2: Site ID */
296 esc_html__( 'Indexing %1$s on site %2$d…', 'elasticpress' ),
297 esc_html( strtolower( $indexable->labels['plural'] ) ),
298 $this->index_meta['current_sync_item']['blog_id']
299 )
300 );
301 } else {
302 $message_string = ( $indexable->global ) ?
303 /* translators: 1: Indexable name */
304 esc_html__( 'Indexing %1$s (globally)…', 'elasticpress' ) :
305 /* translators: 1: Indexable name */
306 esc_html__( 'Indexing %1$s…', 'elasticpress' );
307
308 $this->output_success(
309 sprintf(
310 /* translators: 1: Indexable name */
311 $message_string,
312 esc_html( strtolower( $indexable->labels['plural'] ) )
313 )
314 );
315 }
316 }
317
318 if ( defined( 'EP_IS_NETWORK' ) && EP_IS_NETWORK && ! empty( $this->index_meta['current_sync_item']['blog_id'] ) ) {
319 switch_to_blog( $this->index_meta['current_sync_item']['blog_id'] );
320 }
321
322 if ( $this->index_meta['current_sync_item']['put_mapping'] ) {
323 $this->put_mapping();
324 }
325
326 $this->index_objects();
327
328 if ( defined( 'EP_IS_NETWORK' ) && EP_IS_NETWORK && ! empty( $this->index_meta['current_sync_item']['blog_id'] ) ) {
329 restore_current_blog();
330 }
331 }
332
333 /**
334 * Delete an index and recreate it sending the mapping.
335 *
336 * @since 4.0.0
337 */
338 protected function put_mapping() {
339 $this->index_meta['current_sync_item']['put_mapping'] = false;
340
341 /**
342 * Filter whether we should delete index and send new mapping at the start of the sync
343 *
344 * @since 2.1
345 * @hook ep_skip_index_reset
346 * @param {bool} $skip True means skip
347 * @param {array} $index_meta Current index meta
348 * @return {bool} New skip value
349 */
350 if ( apply_filters( 'ep_skip_index_reset', false, $this->index_meta ) ) {
351 return;
352 }
353
354 $indexable = Indexables::factory()->get( $this->index_meta['current_sync_item']['indexable'] );
355
356 $indexable->delete_index();
357 $result = $indexable->put_mapping( 'raw' );
358
359 /**
360 * Fires after sync put mapping is completed
361 *
362 * @since 4.0.0
363 *
364 * @hook ep_sync_put_mapping
365 * @param {array} $index_meta Index meta information
366 * @param {Indexable} $indexable Indexable object
367 * @param {bool} $result Whether the request was successful or not
368 */
369 do_action( 'ep_sync_put_mapping', $this->index_meta, $indexable, $result );
370
371 /**
372 * Fires after dashboard put mapping is completed
373 *
374 * In this particular case, developer aiming a specific method should rely on
375 * `$index_meta['method']`, as historically `ep_dashboard_put_mapping` and
376 * `ep_cli_put_mapping` receive different parameters.
377 *
378 * @see Command::call_ep_cli_put_mapping()
379 *
380 * @since 2.1
381 * @hook ep_dashboard_put_mapping
382 * @param {array} $index_meta Index meta information
383 * @param {string} $status Current indexing status
384 */
385 do_action( 'ep_dashboard_put_mapping', $this->index_meta, 'start' );
386
387 if ( is_wp_error( $result ) ) {
388 $this->on_error_update_and_clean( array( 'message' => $result->get_error_message() ), 'mapping' );
389 return;
390 }
391
392 $index_exists = in_array( $indexable->get_index_name(), $this->index_meta['starting_indices'], true );
393 if ( $index_exists ) {
394 $message = esc_html__( 'Mapping sent', 'elasticpress' );
395 } else {
396 $message = esc_html__( 'Index not present. Mapping sent', 'elasticpress' );
397 }
398
399 $this->output_success( $message );
400 }
401
402 /**
403 * Index documents of an index.
404 *
405 * @since 4.0.0
406 */
407 protected function index_objects() {
408 global $wp_actions;
409 // Hold original wp_actions.
410 $this->temporary_wp_actions = $wp_actions;
411
412 $this->current_query = $this->get_objects_to_index();
413
414 $this->index_meta['from'] = $this->index_meta['offset'];
415 $this->index_meta['found_items'] = (int) $this->current_query['total_objects'];
416 $this->index_meta['current_sync_item']['total'] = (int) $this->index_meta['current_sync_item']['found_items'];
417
418 if ( 'offset' === $this->index_meta['pagination_method'] ) {
419 $indexable = Indexables::factory()->get( $this->index_meta['current_sync_item']['indexable'] );
420
421 if ( empty( $this->index_meta['current_sync_item']['shown_skip_message'] ) ) {
422 $this->index_meta['current_sync_item']['shown_skip_message'] = true;
423
424 $this->output(
425 sprintf(
426 /* translators: 1. Number of objects skipped 2. Indexable type */
427 esc_html__( 'Skipping %1$d %2$s…', 'elasticpress' ),
428 $this->index_meta['from'],
429 esc_html( strtolower( $indexable->labels['plural'] ) )
430 ),
431 'info',
432 'index_objects'
433 );
434 }
435 }
436
437 if ( $this->index_meta['found_items'] && $this->index_meta['offset'] < $this->index_meta['found_items'] ) {
438 $this->index_next_batch();
439 } else {
440 $this->index_cleanup();
441 }
442
443 usleep( 500 );
444
445 // Avoid running out of memory.
446 $this->stop_the_insanity();
447 }
448
449 /**
450 * Query the next objects to be indexed.
451 *
452 * @since 4.0.0
453 * @return array
454 */
455 protected function get_objects_to_index() {
456 $indexable = Indexables::factory()->get( $this->index_meta['current_sync_item']['indexable'] );
457
458 /**
459 * Fires right before entries are about to be indexed.
460 *
461 * @since 4.0.0
462 *
463 * @hook ep_pre_sync_index
464 * @param {array} $args Args to query content with
465 */
466 do_action( 'ep_pre_sync_index', $this->index_meta, ( $this->index_meta['start'] ? 'start' : false ), $indexable );
467
468 /**
469 * Fires right before entries are about to be indexed.
470 *
471 * @since 2.1 Previously called only as 'ep_pre_dashboard_index'
472 * @since 4.0.0 Made available for all methods
473 *
474 * @hook ep_pre_{$index_method}_index
475 * @param {array} $args Args to query content with
476 */
477 do_action( "ep_pre_{$this->args['method']}_index", $this->index_meta, ( $this->index_meta['start'] ? 'start' : false ), $indexable );
478
479 $per_page = $this->get_index_default_per_page();
480
481 if ( ! empty( $this->args['per_page'] ) ) {
482 $per_page = $this->args['per_page'];
483 }
484
485 if ( ! empty( $this->args['nobulk'] ) ) {
486 $per_page = 1;
487 }
488
489 $args = [
490 'per_page' => absint( $per_page ),
491 'ep_sync_id' => uniqid(),
492 ];
493
494 if ( ! $indexable->support_indexing_advanced_pagination || 'offset' === $this->index_meta['pagination_method'] ) {
495 $args['offset'] = $this->index_meta['offset'];
496 }
497
498 if ( ! empty( $this->args['post-ids'] ) ) {
499 $args['include'] = $this->args['post-ids'];
500 }
501
502 if ( ! empty( $this->args['include'] ) ) {
503 $include = ( is_array( $this->args['include'] ) ) ? $this->args['include'] : explode( ',', str_replace( ' ', '', $this->args['include'] ) );
504 $args['include'] = array_map( 'absint', $include );
505 $args['per_page'] = count( $args['include'] );
506 }
507
508 if ( ! empty( $this->args['post_type'] ) ) {
509 $args['post_type'] = ( is_array( $this->args['post_type'] ) ) ? $this->args['post_type'] : explode( ',', $this->args['post_type'] );
510 $args['post_type'] = array_map( 'trim', $args['post_type'] );
511 }
512
513 // Start of advanced pagination arguments.
514 if ( ! empty( $this->args['upper_limit_object_id'] ) && is_numeric( $this->args['upper_limit_object_id'] ) ) {
515 $args['ep_indexing_upper_limit_object_id'] = $this->args['upper_limit_object_id'];
516 }
517
518 if ( ! empty( $this->args['lower_limit_object_id'] ) && is_numeric( $this->args['lower_limit_object_id'] ) ) {
519 $args['ep_indexing_lower_limit_object_id'] = $this->args['lower_limit_object_id'];
520 }
521
522 if ( ! empty( $this->index_meta['current_sync_item']['last_processed_object_id'] ) &&
523 is_numeric( $this->index_meta['current_sync_item']['last_processed_object_id'] )
524 ) {
525 $args['ep_indexing_last_processed_object_id'] = $this->index_meta['current_sync_item']['last_processed_object_id'];
526 }
527 // End of advanced pagination arguments.
528
529 /**
530 * Filters arguments used to query for content for each indexable
531 *
532 * @since 4.0.0
533 *
534 * @hook ep_sync_index_args
535 * @param {array} $args Args to query content with
536 * @return {array} New query args
537 */
538 $args = apply_filters( 'ep_sync_index_args', $args );
539
540 /**
541 * Filters arguments used to query for content for each indexable
542 *
543 * @since 3.0 Previously called only as 'ep_dashboard_index_args'
544 *
545 * @hook ep_{$index_method}_index_args
546 * @param {array} $args Args to query content with
547 * @return {array} New query args
548 */
549 $args = apply_filters( "ep_{$this->args['method']}_index_args", $args );
550
551 return $indexable->query_db( $args );
552 }
553
554 /**
555 * Index the next batch of documents.
556 *
557 * @since 4.0.0
558 */
559 protected function index_next_batch() {
560 $indexable = Indexables::factory()->get( $this->index_meta['current_sync_item']['indexable'] );
561
562 /**
563 * Fires right before entries are about to be indexed in a dashboard sync
564 *
565 * @since 4.0.0
566 * @hook ep_pre_index_batch
567 * @param {array} $index_meta Index meta
568 */
569 do_action( 'ep_pre_index_batch', $this->index_meta );
570
571 $queued_items = [];
572
573 foreach ( $this->current_query['objects'] as $object ) {
574 if ( $this->should_skip_object_index( $object, $indexable ) ) {
575 $this->index_meta['current_sync_item']['skipped']++;
576 } else {
577 $queued_items[ $object->ID ] = true;
578 }
579 }
580
581 $this->index_meta['offset'] = absint( $this->index_meta['offset'] + count( $this->current_query['objects'] ) );
582
583 if ( ! empty( $queued_items ) ) {
584 $total_attempts = ( ! empty( $this->args['total_attempts'] ) ) ? absint( $this->args['total_attempts'] ) : 1;
585 $queued_items_ids = array_keys( $queued_items );
586
587 /**
588 * Filters the number of times the index will try before failing.
589 *
590 * @since 3.0
591 * @hook ep_index_batch_attempts_number
592 * @param {int} $total_attempts Number of attempts
593 * @return {int} New number of attempts
594 */
595 $total_attempts = apply_filters( 'ep_index_batch_attempts_number', $total_attempts );
596
597 for ( $attempts = 1; $attempts <= $total_attempts; $attempts++ ) {
598 $nobulk = ! empty( $this->args['nobulk'] );
599 $failed_objects = [];
600
601 /**
602 * Fires before each attempt of indexing objects
603 *
604 * @hook ep_index_batch_new_attempt
605 * @param {int} $attempts Current attempt
606 * @param {int} $total_attempts Total number of attempts
607 */
608 do_action( 'ep_index_batch_new_attempt', $attempts, $total_attempts );
609
610 $should_retry = false;
611
612 if ( $nobulk ) {
613 $object_id = reset( $queued_items_ids );
614 $return = $indexable->index( $object_id, true );
615
616 /**
617 * Fires after one by one indexing an object
618 *
619 * @since 4.0.0
620 *
621 * @hook ep_sync_object_index
622 * @param {int} $object_id Object to index
623 * @param {Indexable} $indexable Current indexable
624 * @param {mixed} $return Return of the index() call
625 */
626 do_action( 'ep_sync_object_index', $object_id, $indexable, $return );
627
628 /**
629 * Fires after one by one indexing an object
630 *
631 * @since 3.0 Previously called only as 'ep_cli_object_index'
632 * @since 4.0.0 Made available for all methods
633 *
634 * @hook ep_{$index_method}_object_index
635 * @param {int} $object_id Object to index
636 * @param {Indexable} $indexable Current indexable
637 * @param {mixed} $return Return of the index() call
638 */
639 do_action( "ep_{$this->args['method']}_object_index", $object_id, $indexable, $return );
640
641 if ( is_object( $return ) && ! empty( $return->error ) ) {
642 if ( ! empty( $return->error->reason ) ) {
643 $failed_objects[ $object->ID ] = (array) $return->error;
644 } else {
645 $failed_objects[ $object->ID ] = null;
646 }
647 }
648
649 if ( is_wp_error( $return ) ) {
650 $should_retry = true;
651 }
652 } else {
653 if ( ! empty( $this->args['static_bulk'] ) ) {
654 $bulk_requests = [ $indexable->bulk_index( $queued_items_ids ) ];
655 } else {
656 $bulk_requests = $indexable->bulk_index_dynamically( $queued_items_ids );
657 }
658
659 $failed_objects = [];
660 foreach ( $bulk_requests as $return ) {
661 /**
662 * Fires after bulk indexing
663 *
664 * @hook ep_cli_{indexable_slug}_bulk_index
665 * @param {array} $objects Objects being indexed
666 * @param {array} response Elasticsearch bulk index response
667 */
668 do_action( "ep_cli_{$indexable->slug}_bulk_index", $queued_items, $return );
669
670 if ( is_wp_error( $return ) ) {
671 $should_retry = true;
672 }
673 if ( is_array( $return ) && isset( $return['errors'] ) && true === $return['errors'] ) {
674 $failed_objects = array_merge(
675 $failed_objects,
676 array_filter(
677 $return['items'],
678 function( $item ) {
679 return ! empty( $item['index']['error'] );
680 }
681 )
682 );
683 }
684 }
685 }
686
687 // Things worked, we don't need to try again.
688 if ( ! $should_retry && ! count( $failed_objects ) ) {
689 break;
690 }
691 }
692
693 if ( is_wp_error( $return ) ) {
694 $this->index_meta['current_sync_item']['failed'] += count( $queued_items );
695
696 $wp_error_messages = $return->get_error_messages();
697
698 $this->maybe_process_error_limit(
699 count( $this->index_meta['current_sync_item']['errors'] ) + count( $wp_error_messages ),
700 count( $this->index_meta['current_sync_item']['errors'] ),
701 $wp_error_messages
702 );
703
704 $this->queue_message( $wp_error_messages, 'warning' );
705 } elseif ( count( $failed_objects ) ) {
706 $errors_output = $this->output_index_errors( $failed_objects );
707
708 $this->index_meta['current_sync_item']['synced'] += count( $queued_items ) - count( $failed_objects );
709
710 $this->maybe_process_error_limit(
711 $this->index_meta['current_sync_item']['failed'] + count( $failed_objects ),
712 $this->index_meta['current_sync_item']['failed'],
713 $errors_output
714 );
715
716 $this->index_meta['current_sync_item']['failed'] += count( $failed_objects );
717 $error_type = ! empty( $this->args['stop_on_error'] ) ? 'error' : 'warning';
718
719 $this->queue_message( $errors_output, $error_type );
720 } else {
721 $this->index_meta['current_sync_item']['synced'] += count( $queued_items );
722 }
723 }
724
725 $this->index_meta['current_sync_item']['last_processed_object_id'] = end( $this->current_query['objects'] )->ID;
726
727 $summary = sprintf(
728 /* translators: 1. Indexable type 2. Offset start, 3. Offset end, 4. Found items 5. Last object ID */
729 esc_html__( 'Processed %1$s %2$d - %3$d of %4$d. Last Object ID: %5$d', 'elasticpress' ),
730 esc_html( strtolower( $indexable->labels['plural'] ) ),
731 $this->index_meta['from'],
732 $this->index_meta['offset'],
733 $this->index_meta['found_items'],
734 $this->index_meta['current_sync_item']['last_processed_object_id']
735 );
736
737 $this->queue_message( $summary, 'info', 'index_next_batch' );
738 $this->flush_messages_queue();
739 }
740
741 /**
742 * If the number of errors is greater than the limit, slice the array to the limit.
743 * If the number of errors is less than or equal the limit, add the error message to the array (if it's not there).
744 * Merges the new errors with the existing errors.
745 *
746 * @since 4.5.1
747 * @param int $count Number of errors.
748 * @param int $num Number of errors to subtract from $limit.
749 * @param array $errors Array of errors.
750 */
751 protected function maybe_process_error_limit( $count, $num, $errors ) {
752 $error_store_msg = __( 'Reached maximum number of errors to store', 'elasticpress' );
753
754 /**
755 * Filter the number of errors of a current sync that should be stored.
756 *
757 * @since 4.5.1
758 * @hook ep_current_sync_number_of_errors_stored
759 * @param {int} $number Number of errors to be logged.
760 * @return {int} New value
761 */
762 $limit = (int) apply_filters( 'ep_current_sync_number_of_errors_stored', 50 );
763
764 if ( $limit > 0 && $count > $limit ) {
765 $diff = $limit - $num;
766 if ( $diff > 0 ) {
767 $errors = array_slice( $errors, 0, $diff );
768 } else {
769 $errors = [];
770 if ( end( $this->index_meta['current_sync_item']['errors'] ) !== $error_store_msg ) {
771 $this->index_meta['current_sync_item']['errors'][] = $error_store_msg;
772 }
773 }
774 }
775
776 $this->index_meta['current_sync_item']['errors'] = array_merge( $this->index_meta['current_sync_item']['errors'], $errors );
777 }
778
779 /**
780 * Update the sync info with the totals from the last sync item.
781 *
782 * @since 4.2.0
783 */
784 protected function update_totals_from_current_sync_item() {
785 $current_sync_item = $this->index_meta['current_sync_item'];
786
787 $errors = array_merge(
788 $this->index_meta['totals']['errors'],
789 $current_sync_item['errors']
790 );
791
792 /**
793 * Filter the number of errors of a sync that should be stored.
794 *
795 * @since 4.2.0
796 * @hook ep_sync_number_of_errors_stored
797 * @param {int} $number Number of errors to be logged.
798 * @return {int} New value
799 */
800 $logged_errors = (int) apply_filters( 'ep_sync_number_of_errors_stored', 50 );
801
802 $this->index_meta['totals']['total'] += $current_sync_item['total'];
803 $this->index_meta['totals']['synced'] += $current_sync_item['synced'];
804 $this->index_meta['totals']['skipped'] += $current_sync_item['skipped'];
805 $this->index_meta['totals']['failed'] += $current_sync_item['failed'];
806 $this->index_meta['totals']['errors'] = array_slice( $errors, $logged_errors * -1 );
807 }
808
809 /**
810 * Make the necessary clean up after a sync item of the stack was completely done.
811 *
812 * @since 4.0.0
813 * @return void
814 */
815 protected function index_cleanup() {
816 wp_reset_postdata();
817
818 $this->update_totals_from_current_sync_item();
819
820 $indexable = Indexables::factory()->get( $this->index_meta['current_sync_item']['indexable'] );
821
822 $current_sync_item = $this->index_meta['current_sync_item'];
823
824 $this->index_meta['current_sync_item'] = null;
825 $this->index_meta['offset'] = 0;
826
827 if ( $current_sync_item['failed'] ) {
828 if ( ! empty( $current_sync_item['blog_id'] ) && defined( 'EP_IS_NETWORK' ) && EP_IS_NETWORK ) {
829 $message = sprintf(
830 /* translators: 1: indexable (plural), 2: Blog ID, 3: number of failed objects */
831 esc_html__( 'Number of %1$s index errors on site %2$d: %3$d', 'elasticpress' ),
832 esc_html( strtolower( $indexable->labels['plural'] ) ),
833 $current_sync_item['blog_id'],
834 $current_sync_item['failed']
835 );
836 } else {
837 $message = sprintf(
838 /* translators: 1: indexable (plural), 2: number of failed objects */
839 esc_html__( 'Number of %1$s index errors: %2$d', 'elasticpress' ),
840 esc_html( strtolower( $indexable->labels['plural'] ) ),
841 $current_sync_item['failed']
842 );
843 }
844
845 $this->output( $message, 'warning' );
846 }
847
848 if ( ! empty( $current_sync_item['blog_id'] ) && defined( 'EP_IS_NETWORK' ) && EP_IS_NETWORK ) {
849 $message = sprintf(
850 /* translators: 1: indexable (plural), 2: Blog ID, 3: number of synced objects */
851 esc_html__( 'Number of %1$s indexed on site %2$d: %3$d', 'elasticpress' ),
852 esc_html( strtolower( $indexable->labels['plural'] ) ),
853 $current_sync_item['blog_id'],
854 $current_sync_item['synced']
855 );
856 } else {
857 $message = sprintf(
858 /* translators: 1: indexable (plural), 2: number of synced objects */
859 esc_html__( 'Number of %1$s indexed: %2$d', 'elasticpress' ),
860 esc_html( strtolower( $indexable->labels['plural'] ) ),
861 $current_sync_item['synced']
862 );
863 }
864
865 $this->output_success( $message );
866 }
867
868 /**
869 * Update last sync info.
870 *
871 * @since 4.2.0
872 * @param string $final_status Optional final status
873 */
874 protected function update_last_index( string $final_status = '' ) {
875 $is_full_sync = $this->index_meta['put_mapping'];
876 $method = $this->index_meta['method'];
877 $start_time = $this->index_meta['start_time'];
878 $totals = $this->index_meta['totals'];
879 $trigger = $this->index_meta['trigger'];
880
881 $this->index_meta = null;
882
883 $end_date_time = date_create( 'now', wp_timezone() );
884 $start_time_sec = (int) $start_time;
885
886 // Time related info
887 $totals['end_date_time'] = $end_date_time ? $end_date_time->format( DATE_ATOM ) : false;
888 $totals['start_date_time'] = $start_time ? wp_date( DATE_ATOM, $start_time_sec ) : false;
889 $totals['end_time_gmt'] = time();
890 $totals['total_time'] = microtime( true ) - $start_time;
891
892 // Additional info
893 $totals['is_full_sync'] = $is_full_sync;
894 $totals['method'] = $method;
895 $totals['trigger'] = $trigger;
896
897 // Final status
898 if ( '' !== $final_status ) {
899 $totals['final_status'] = $final_status;
900 } elseif ( ! empty( $totals['failed'] ) ) {
901 $totals['final_status'] = 'with_errors';
902 } else {
903 $totals['final_status'] = 'success';
904 }
905
906 Utils\update_option( 'ep_last_cli_index', $totals, false );
907
908 $this->add_last_sync( $totals );
909 }
910
911 /**
912 * Add a sync to the list of all past syncs
913 *
914 * @since 5.0.0
915 * @param array $last_sync_info The latest sync info to be added to the log
916 * @return void
917 */
918 protected function add_last_sync( array $last_sync_info ) {
919 // Remove error messages from previous syncs - we only store msgs for the newest one.
920 $last_syncs = array_map(
921 function( $sync ) {
922 unset( $sync['errors'] );
923 return $sync;
924 },
925 $this->get_sync_history()
926 );
927
928 /**
929 * Filter the number of past syncs to keep info
930 *
931 * @since 5.0.0
932 * @hook ep_syncs_to_keep_info
933 * @param {int} $number Number of past syncs to keep info
934 * @return {int} New number
935 */
936 $syncs_to_keep = (int) apply_filters( 'ep_syncs_to_keep_info', 5 );
937
938 $last_syncs = array_slice( $last_syncs, 0, $syncs_to_keep - 1 );
939 array_unshift( $last_syncs, $last_sync_info );
940
941 Utils\update_option( 'ep_sync_history', $last_syncs, false );
942 }
943
944 /**
945 * Make the necessary clean up after everything was sync'd.
946 *
947 * @since 4.0.0
948 */
949 protected function full_index_complete() {
950 $this->update_last_index();
951
952 /**
953 * Fires after executing a reindex
954 *
955 * @since 4.0.0
956 * @hook ep_after_sync_index
957 */
958 do_action( 'ep_after_sync_index' );
959
960 /**
961 * Fires after executing a reindex
962 *
963 * @since 3.5.5 Previously called only as 'ep_after_dashboard_index'
964 * @since 4.0.0 Made available for all methods
965 * @hook ep_after_{$index_method}_index
966 */
967 do_action( "ep_after_{$this->args['method']}_index" );
968
969 $this->output_success( esc_html__( 'Sync complete', 'elasticpress' ) );
970 }
971
972 /**
973 * Check if network aliases need to be created.
974 *
975 * @since 4.0.0
976 * @return boolean
977 */
978 protected function has_network_alias_to_be_created() {
979 return count( $this->index_meta['network_alias'] ) > 0;
980 }
981
982 /**
983 * Create the next network alias.
984 *
985 * @since 4.0.0
986 */
987 protected function create_network_alias() {
988 $indexes = [];
989 $indexable = Indexables::factory()->get( array_shift( $this->index_meta['network_alias'] ) );
990
991 $sites = Utils\get_sites( 0, true );
992
993 foreach ( $sites as $site ) {
994 switch_to_blog( $site['blog_id'] );
995 $indexes[] = $indexable->get_index_name();
996 restore_current_blog();
997 }
998
999 $result = $indexable->create_network_alias( $indexes );
1000
1001 if ( $result ) {
1002 $this->output_success(
1003 sprintf(
1004 /* translators: 1: Indexable name */
1005 esc_html__( 'Network alias created for %1$s', 'elasticpress' ),
1006 esc_html( strtolower( $indexable->labels['plural'] ) )
1007 )
1008 );
1009 } else {
1010 $this->output_error(
1011 sprintf(
1012 /* translators: 1: Indexable name */
1013 esc_html__( 'Network alias creation failed for %1$s', 'elasticpress' ),
1014 esc_html( strtolower( $indexable->labels['plural'] ) )
1015 )
1016 );
1017 }
1018 }
1019
1020 /**
1021 * Output a message.
1022 *
1023 * @since 4.0.0
1024 * @param string|array $message_text Message to be outputted
1025 * @param string $type Type of message
1026 * @param string $context Context of the output
1027 * @return void
1028 */
1029 protected function output( $message_text, $type = 'info', $context = '' ) {
1030 if ( $this->index_meta ) {
1031 Utils\update_option( 'ep_index_meta', $this->index_meta );
1032 } else {
1033 Utils\delete_option( 'ep_index_meta' );
1034 $totals = $this->get_last_sync();
1035 }
1036
1037 $message = [
1038 'message' => ( is_array( $message_text ) ) ? implode( "\n", $message_text ) : $message_text,
1039 'index_meta' => $this->index_meta,
1040 'totals' => $totals ?? [],
1041 'status' => $type,
1042 ];
1043
1044 if ( in_array( $type, [ 'warning', 'error' ], true ) ) {
1045 $message['errors'] = $this->build_message_errors_data( $message_text );
1046 }
1047
1048 if ( is_callable( $this->args['output_method'] ) ) {
1049 call_user_func( $this->args['output_method'], $message, $this->args, $this->index_meta, $context );
1050 }
1051 }
1052
1053 /**
1054 * Wrapper to the `output` method with a success message.
1055 *
1056 * @since 4.0.0
1057 * @param string $message Message string.
1058 * @param string $context Context of the output.
1059 */
1060 protected function output_success( $message, $context = '' ) {
1061 $this->output( $message, 'success', $context );
1062 }
1063
1064 /**
1065 * Wrapper to the `output` method with an error message.
1066 *
1067 * @since 4.0.0
1068 * @param string $message Message string.
1069 * @param string $context Context of the output.
1070 */
1071 protected function output_error( $message, $context = '' ) {
1072 $this->output( $message, 'error', $context );
1073 }
1074
1075 /**
1076 * Output index errors of failed objects.
1077 *
1078 * @since 4.0.0
1079 * @param array $failed_objects Failed objects
1080 */
1081 protected function output_index_errors( $failed_objects ) {
1082 $indexable = Indexables::factory()->get( $this->index_meta['current_sync_item']['indexable'] );
1083
1084 $error_text = [];
1085
1086 foreach ( $failed_objects as $object ) {
1087 $error_text[] = ! empty( $object['index'] ) ? $object['index']['_id'] . ' (' . $indexable->labels['singular'] . '): [' . $object['index']['error']['type'] . '] ' . $object['index']['error']['reason'] : (string) $object;
1088 }
1089
1090 return $error_text;
1091 }
1092
1093 /**
1094 * Utilitary function to check if the indexable is being fully reindexed, i.e.,
1095 * the index was deleted, a new mapping was sent and content is being reindexed.
1096 *
1097 * @param string $indexable_slug Indexable slug.
1098 * @param int|null $blog_id Blog ID
1099 * @return boolean
1100 */
1101 public function is_full_reindexing( $indexable_slug, $blog_id = null ) {
1102 if ( empty( $this->index_meta ) || empty( $this->index_meta['put_mapping'] ) ) {
1103 /**
1104 * Filter if a fully reindex is being done to an indexable
1105 *
1106 * @since 4.0.0
1107 * @hook ep_is_full_reindexing_{$indexable_slug}
1108 * @param {bool} $is_full_reindexing If is fully reindexing
1109 * @return {bool} New value
1110 */
1111 return apply_filters( "ep_is_full_reindexing_{$indexable_slug}", false );
1112 }
1113
1114 $sync_stack = ( ! empty( $this->index_meta['sync_stack'] ) ) ? $this->index_meta['sync_stack'] : [];
1115 $current_sync_item = ( ! empty( $this->index_meta['current_sync_item'] ) ) ? $this->index_meta['current_sync_item'] : [];
1116
1117 $is_full_reindexing = false;
1118
1119 $all_items = $sync_stack;
1120 if ( ! empty( $current_sync_item ) ) {
1121 $all_items += [ $current_sync_item ];
1122 }
1123
1124 foreach ( $all_items as $sync_item ) {
1125 if ( $sync_item['indexable'] !== $indexable_slug ) {
1126 continue;
1127 }
1128
1129 if (
1130 ( empty( $sync_item['blog_id'] ) && ! $blog_id ) ||
1131 (int) $sync_item['blog_id'] === $blog_id
1132 ) {
1133 $is_full_reindexing = true;
1134 }
1135 }
1136
1137 /* this filter is documented above */
1138 return apply_filters( "ep_is_full_reindexing_{$indexable_slug}", $is_full_reindexing );
1139 }
1140
1141 /**
1142 * Get the previous syncs meta information.
1143 *
1144 * @since 5.0.0
1145 * @return array
1146 */
1147 public function get_sync_history() : array {
1148 return Utils\get_option( 'ep_sync_history', [] );
1149 }
1150
1151 /**
1152 * Get the last sync meta information.
1153 *
1154 * @since 5.0.0
1155 * @return array
1156 */
1157 public function get_last_sync() : array {
1158 $syncs = $this->get_sync_history();
1159 if ( empty( $syncs ) ) {
1160 return [];
1161 }
1162 return array_shift( $syncs );
1163 }
1164
1165 /**
1166 * Check if an object should be indexed or skipped.
1167 *
1168 * We used to have two different filters for this (one for the dashboard, another for CLI),
1169 * this method combines both.
1170 *
1171 * @param {stdClass} $object Object to be checked
1172 * @param {Indexable} $indexable Indexable
1173 * @return boolean
1174 */
1175 protected function should_skip_object_index( $object, $indexable ) {
1176 /**
1177 * Filter whether to not sync specific item in dashboard or not
1178 *
1179 * @since 2.1
1180 * @hook ep_item_sync_kill
1181 * @param {boolean} $kill False means dont sync
1182 * @param {array} $object Object to sync
1183 * @return {Indexable} Indexable that object belongs to
1184 */
1185 $ep_item_sync_kill = apply_filters( 'ep_item_sync_kill', false, $object, $indexable );
1186
1187 /**
1188 * Conditionally kill indexing for a post
1189 *
1190 * @hook ep_{indexable_slug}_index_kill
1191 * @param {bool} $index True means dont index
1192 * @param {int} $object_id Object ID
1193 * @return {bool} New value
1194 */
1195 $ep_indexable_sync_kill = apply_filters( 'ep_' . $indexable->slug . '_index_kill', false, $object->ID );
1196
1197 return $ep_item_sync_kill || $ep_indexable_sync_kill;
1198 }
1199
1200 /**
1201 * Given an array, create a new sync item and add it to the stack.
1202 *
1203 * @since 4.5.0
1204 * @param array $sync_stack_item The new sync item
1205 */
1206 protected function add_sync_item_to_stack( array $sync_stack_item ) {
1207 $indexable_slug = $sync_stack_item['indexable'];
1208 $indexable_object = Indexables::factory()->get( $indexable_slug );
1209
1210 if ( ! $indexable_object ) {
1211 return;
1212 }
1213
1214 $index_exists = in_array( $indexable_object->get_index_name(), $this->index_meta['starting_indices'], true );
1215
1216 $sync_stack_item['put_mapping'] = ! empty( $this->args['put_mapping'] ) || ! $index_exists;
1217
1218 if ( ! Indexables::factory()->is_active( $indexable_slug ) ) {
1219 array_unshift( $this->index_meta['sync_stack'], $sync_stack_item );
1220 return;
1221 }
1222
1223 // This is needed, because get_objects_to_index() calculates its total based on the current sync item.
1224 $this->index_meta['current_sync_item'] = $sync_stack_item;
1225
1226 $objects_to_index = $this->get_objects_to_index();
1227
1228 $sync_stack_item['found_items'] = $objects_to_index['total_objects'] ?? 0;
1229
1230 $this->index_meta['sync_stack'][] = $sync_stack_item;
1231 }
1232
1233 /**
1234 * Processes an indexable that is not active.
1235 *
1236 * If running a full sync, delete the index of an unused indexable.
1237 *
1238 * @since 4.5.0
1239 */
1240 protected function process_not_active_indexable_sync_item() {
1241 $current_sync_item = $this->index_meta['current_sync_item'];
1242
1243 $this->index_meta['current_sync_item'] = null;
1244
1245 if ( empty( $current_sync_item['put_mapping'] ) ) {
1246 return;
1247 }
1248
1249 $indexable = Indexables::factory()->get( $current_sync_item['indexable'] );
1250
1251 if ( ! in_array( $indexable->get_index_name(), $this->index_meta['starting_indices'], true ) ) {
1252 return;
1253 }
1254
1255 $indexable->delete_index();
1256
1257 $this->output_success(
1258 sprintf(
1259 /* translators: Index name */
1260 esc_html__( 'Index %s deleted', 'elasticpress' ),
1261 $indexable->get_index_name()
1262 )
1263 );
1264 }
1265
1266 /**
1267 * Resets some values to reduce memory footprint.
1268 */
1269 protected function stop_the_insanity() {
1270 global $wpdb, $wp_object_cache, $wp_actions;
1271
1272 $wpdb->queries = [];
1273
1274 /*
1275 * Runtime flushing was introduced in WordPress 6.0 and will flush only the
1276 * in-memory cache for persistent object caches
1277 */
1278 if ( function_exists( 'wp_cache_flush_runtime' ) ) {
1279 wp_cache_flush_runtime();
1280 } else {
1281 /*
1282 * In the case where we're not using an external object cache, we need to call flush on the default
1283 * WordPress object cache class to clear the values from the cache property
1284 */
1285 if ( ! wp_using_ext_object_cache() ) {
1286 wp_cache_flush();
1287 }
1288 }
1289
1290 if ( is_object( $wp_object_cache ) ) {
1291 $wp_object_cache->group_ops = [];
1292 $wp_object_cache->stats = [];
1293 $wp_object_cache->memcache_debug = [];
1294
1295 // Make sure this is a public property, before trying to clear it.
1296 try {
1297 $cache_property = new \ReflectionProperty( $wp_object_cache, 'cache' );
1298 if ( $cache_property->isPublic() ) {
1299 $wp_object_cache->cache = [];
1300 }
1301 unset( $cache_property );
1302 } catch ( \ReflectionException $e ) {
1303 // No need to catch.
1304 }
1305
1306 if ( is_callable( $wp_object_cache, '__remoteset' ) ) {
1307 call_user_func( [ $wp_object_cache, '__remoteset' ] );
1308 }
1309 }
1310
1311 // Prevent wp_actions from growing out of control.
1312 // phpcs:disable
1313 $wp_actions = $this->temporary_wp_actions;
1314 // phpcs:enable
1315
1316 // It's high memory consuming as WP_Query instance holds all query results inside itself
1317 // and in theory $wp_filter will not stop growing until Out Of Memory exception occurs.
1318 remove_filter( 'get_term_metadata', [ wp_metadata_lazyloader(), 'lazyload_term_meta' ] );
1319
1320 /**
1321 * Fires after reducing the memory footprint
1322 *
1323 * @since 4.3.0
1324 * @hook ep_stop_the_insanity
1325 */
1326 do_action( 'ep_stop_the_insanity' );
1327 }
1328
1329 /**
1330 * Utilitary function to delete the index meta option.
1331 *
1332 * @since 4.0.0
1333 */
1334 public function clear_index_meta() {
1335 if ( ! empty( $this->index_meta ) ) {
1336 $this->update_last_index( 'aborted' );
1337 }
1338 $this->index_meta = false;
1339 Utils\delete_option( 'ep_index_meta', false );
1340 }
1341
1342 /**
1343 * Utilitary function to get the index meta option.
1344 *
1345 * @return array
1346 * @since 4.0.0
1347 */
1348 public function get_index_meta() {
1349 return Utils\get_option( 'ep_index_meta', [] );
1350 }
1351
1352 /**
1353 * Handle fatal errors during syncs.
1354 *
1355 * Added by register_shutdown_function. It will not be called if `WP_DISABLE_FATAL_ERROR_HANDLER` is false (default.)
1356 *
1357 * @since 4.2.0
1358 */
1359 public function handle_index_error() {
1360 $error = error_get_last();
1361 if ( empty( $error['type'] ) || E_ERROR !== $error['type'] ) {
1362 return;
1363 }
1364
1365 $this->on_error_update_and_clean( $error );
1366 }
1367
1368 /**
1369 * Handle fatal errors during syncs.
1370 *
1371 * Added via the `wp_php_error_message` filter. It will be called only if `WP_DISABLE_FATAL_ERROR_HANDLER` is false (default.)
1372 *
1373 * @since 4.2.0
1374 * @param bool $message HTML error message to display.
1375 * @param array $error Error information retrieved from error_get_last().
1376 * @return bool
1377 */
1378 public function wp_handle_index_error( $message, $error ) {
1379 $this->on_error_update_and_clean( $error );
1380 return $message;
1381 }
1382
1383 /**
1384 * Logs the error and clears the sync status, preventing the sync status from being stuck.
1385 *
1386 * @since 4.2.0
1387 * @param array $error Error information retrieved from error_get_last().
1388 * @param string $context Context of the error.
1389 */
1390 protected function on_error_update_and_clean( $error, $context = 'sync' ) {
1391 $this->update_totals_from_current_sync_item();
1392
1393 $totals = $this->index_meta['totals'];
1394
1395 $this->index_meta['totals']['errors'][] = $error['message'];
1396 $this->index_meta['totals']['failed'] = $totals['total'] - ( $totals['synced'] + $totals['skipped'] );
1397 $this->update_last_index( 'failed' );
1398
1399 /**
1400 * Fires after a sync failed due to a PHP fatal error.
1401 *
1402 * @since 4.2.0
1403 * @hook ep_after_sync_error
1404 * @param {array} $error The error
1405 */
1406 do_action( 'ep_after_sync_error', $error );
1407
1408 switch ( $context ) {
1409 case 'mapping':
1410 $message = sprintf(
1411 /* translators: Error message */
1412 esc_html__( 'Mapping failed: %s', 'elasticpress' ),
1413 Utils\get_elasticsearch_error_reason( $error['message'] )
1414 );
1415 $message .= "\n";
1416 $message .= esc_html__( 'Mapping has failed, which will cause ElasticPress search results to be incorrect. Please click `Delete all Data and Start a Fresh Sync` to retry mapping.', 'elasticpress' );
1417 break;
1418 default:
1419 /* translators: Error message */
1420 $message = sprintf( esc_html__( 'Index failed: %s', 'elasticpress' ), $error['message'] );
1421 break;
1422 }
1423
1424 $this->output_error( $message );
1425 }
1426
1427 /**
1428 * Return the default number of documents to be sent to Elasticsearch on each batch.
1429 *
1430 * @since 4.4.0
1431 * @return integer
1432 */
1433 public function get_index_default_per_page() : int {
1434 /**
1435 * Filter number of items to index per cycle in the dashboard
1436 *
1437 * @since 2.1
1438 * @hook ep_index_default_per_page
1439 * @param {int} Entries per cycle
1440 * @return {int} New number of entries
1441 */
1442 return (int) apply_filters( 'ep_index_default_per_page', Utils\get_option( 'ep_bulk_setting', 350 ) );
1443 }
1444
1445 /**
1446 * Add a message to the queue
1447 *
1448 * @since 4.7.0
1449 * @param string|array $message_text Message to be outputted
1450 * @param string $type Type of message
1451 * @param string $context Context of the output
1452 */
1453 protected function queue_message( $message_text, string $type, string $context = '' ) {
1454 $this->index_meta['messages_queue'][] = [
1455 'text' => $message_text,
1456 'type' => $type,
1457 'context' => $context,
1458 ];
1459 }
1460
1461 /**
1462 * Display messages in the queue.
1463 *
1464 * NOTE: As the dashboard sync exits after every output call (to respond the AJAX request),
1465 * this will just output one message. As the method is called every time the script is called,
1466 * all messages will be displayed but one at a time.
1467 *
1468 * @since 4.7.0
1469 */
1470 protected function flush_messages_queue() {
1471 if ( ! is_array( $this->index_meta['messages_queue'] ) ) {
1472 return;
1473 }
1474
1475 $messages_count = count( $this->index_meta['messages_queue'] );
1476 if ( 0 === $messages_count ) {
1477 return;
1478 }
1479
1480 for ( $i = 0; $i < $messages_count; $i++ ) {
1481 $next_message = array_shift( $this->index_meta['messages_queue'] );
1482 $this->output( $next_message['text'], $next_message['type'], $next_message['context'] );
1483 }
1484 }
1485
1486 /**
1487 * Get data for a given error message(s)
1488 *
1489 * @since 5.0.0
1490 * @param string|array $messages Messages
1491 * @return array
1492 */
1493 protected function build_message_errors_data( $messages ) : array {
1494 $messages = (array) $messages;
1495 $error_interpreter = new \ElasticPress\ElasticsearchErrorInterpreter();
1496
1497 $errors_list = [];
1498 foreach ( $messages as $message ) {
1499 $error = $error_interpreter->maybe_suggest_solution_for_es( $message );
1500
1501 if ( ! isset( $errors_list[ $error['error'] ] ) ) {
1502 $errors_list[ $error['error'] ] = [
1503 'solution' => $error['solution'],
1504 'count' => 1,
1505 ];
1506 } else {
1507 $errors_list[ $error['error'] ]['count']++;
1508 }
1509 }
1510 return $errors_list;
1511 }
1512
1513 /**
1514 * If this is a full sync, apply the draft feature settings
1515 *
1516 * @since 5.0.0
1517 */
1518 protected function maybe_apply_feature_settings() {
1519 if ( empty( $this->args['put_mapping'] ) ) {
1520 return;
1521 }
1522
1523 Features::factory()->apply_draft_feature_settings();
1524 }
1525
1526 /**
1527 * Return singleton instance of class.
1528 *
1529 * @return self
1530 * @since 4.0.0
1531 */
1532 public static function factory() {
1533 static $instance = false;
1534
1535 if ( ! $instance ) {
1536 $instance = new self();
1537 $instance->setup();
1538 }
1539
1540 return $instance;
1541 }
1542
1543 /**
1544 * DEPRECATED. Get the last index/sync meta information.
1545 *
1546 * @since 4.2.0
1547 * @deprecated 5.0.0
1548 * @return array
1549 */
1550 public function get_last_index() {
1551 _deprecated_function( __METHOD__, '5.0.0', '\ElasticPress\IndexHelper::get_last_sync' );
1552 return $this->get_last_sync();
1553 }
1554 }
1555