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/IndexHelper.php +468 -134 4.3.05.3.5 View file →
@@ -1,17 +1,20 @@
1 1 <?php
2 2 /**
3 3 * Index Helper
4 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 + *
5 9 * @since 4.0.0
6 - * @see docs/indexing-process.md
7 - * @see http://10up.github.io/ElasticPress/tutorial-indexing-process.html
10 + * @see https://www.elasticpress.io/resources/articles/sync-process/
8 11 * @package elasticpress
9 12 */
10 13
11 14 namespace ElasticPress;
12 15
13 -use ElasticPress\Utils as Utils;
16 +use ElasticPress\Utils;
14 17
15 18 /**
16 19 * Index Helper Class.
17 20 *
@@ -68,14 +71,28 @@
68 71 register_shutdown_function( [ $this, 'handle_index_error' ] );
69 72 add_filter( 'wp_php_error_message', [ $this, 'wp_handle_index_error' ], 10, 2 );
70 73
71 74 $this->index_meta = Utils\get_indexing_status();
72 - $this->args = $args;
73 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 +
74 87 if ( false === $this->index_meta ) {
88 + $this->maybe_apply_feature_settings();
75 89 $this->build_index_meta();
76 90 }
77 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 +
78 95 while ( $this->has_items_to_be_processed() ) {
79 96 $this->process_sync_item();
80 97 }
81 98
@@ -110,8 +127,13 @@
110 127 $pagination_method = ( ! empty( $this->args['offset'] ) || ! empty( $this->args['post-ids'] ) || ! empty( $this->args['include'] ) ) ?
111 128 'offset' :
112 129 'id_range';
113 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 +
114 136 $this->index_meta = [
115 137 'method' => ! empty( $this->args['method'] ) ? $this->args['method'] : 'web',
116 138 'put_mapping' => ! empty( $this->args['put_mapping'] ),
117 139 'offset' => ! empty( $this->args['offset'] ) ? absint( $this->args['offset'] ) : 0,
@@ -120,8 +142,11 @@
120 142 'sync_stack' => [],
121 143 'network_alias' => [],
122 144 'start_time' => microtime( true ),
123 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,
124 149 'totals' => [
125 150 'total' => 0,
126 151 'synced' => 0,
127 152 'skipped' => 0,
@@ -130,10 +155,10 @@
130 155 'errors' => [],
131 156 ],
132 157 ];
133 158
134 - $global_indexables = $this->filter_indexables( Indexables::factory()->get_all( true, true ) );
135 - $non_global_indexables = $this->filter_indexables( Indexables::factory()->get_all( false, true ) );
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' ) );
136 161
137 162 $is_network_wide = isset( $this->args['network_wide'] ) && ! is_null( $this->args['network_wide'] );
138 163
139 164 if ( defined( 'EP_IS_NETWORK' ) && EP_IS_NETWORK && $is_network_wide ) {
@@ -140,34 +165,23 @@
140 165 if ( ! is_numeric( $this->args['network_wide'] ) ) {
141 166 $this->args['network_wide'] = 0;
142 167 }
143 168
144 - $sites = Utils\get_sites( $this->args['network_wide'] );
169 + $sites = Utils\get_sites( $this->args['network_wide'], true );
145 170
146 171 foreach ( $sites as $site ) {
147 - if ( ! Utils\is_site_indexable( $site['blog_id'] ) ) {
148 - continue;
149 - }
150 -
151 172 switch_to_blog( $site['blog_id'] );
152 173
153 174 foreach ( $non_global_indexables as $indexable ) {
154 - $sync_stack_item = [
155 - 'url' => untrailingslashit( $site['domain'] . $site['path'] ),
156 - 'blog_id' => (int) $site['blog_id'],
157 - 'indexable' => $indexable,
158 - 'put_mapping' => ! empty( $this->args['put_mapping'] ),
159 - ];
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 + );
160 182
161 - $this->index_meta['current_sync_item'] = $sync_stack_item;
162 -
163 - $objects_to_index = $this->get_objects_to_index();
164 -
165 - $sync_stack_item['found_items'] = $objects_to_index['total_objects'] ?? 0;
166 -
167 - $this->index_meta['sync_stack'][] = $sync_stack_item;
168 -
169 - if ( ! in_array( $indexable, $this->index_meta['network_alias'], true ) ) {
183 + if ( Indexables::factory()->is_active( $indexable ) && ! in_array( $indexable, $this->index_meta['network_alias'], true ) ) {
170 184 $this->index_meta['network_alias'][] = $indexable;
171 185 }
172 186 }
173 187 }
@@ -174,38 +188,24 @@
174 188
175 189 restore_current_blog();
176 190 } else {
177 191 foreach ( $non_global_indexables as $indexable ) {
178 - $sync_stack_item = [
179 - 'url' => untrailingslashit( home_url() ),
180 - 'blog_id' => (int) get_current_blog_id(),
181 - 'indexable' => $indexable,
182 - 'put_mapping' => ! empty( $this->args['put_mapping'] ),
183 - ];
184 -
185 - $this->index_meta['current_sync_item'] = $sync_stack_item;
186 -
187 - $objects_to_index = $this->get_objects_to_index();
188 -
189 - $sync_stack_item['found_items'] = $objects_to_index['total_objects'] ?? 0;
190 -
191 - $this->index_meta['sync_stack'][] = $sync_stack_item;
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 + );
192 199 }
193 200 }
194 201
195 202 foreach ( $global_indexables as $indexable ) {
196 - $sync_stack_item = [
197 - 'indexable' => $indexable,
198 - 'put_mapping' => ! empty( $this->args['put_mapping'] ),
199 - ];
200 -
201 - $this->index_meta['current_sync_item'] = $sync_stack_item;
202 -
203 - $objects_to_index = $this->get_objects_to_index();
204 -
205 - $sync_stack_item['found_items'] = $objects_to_index['total_objects'] ?? 0;
206 -
207 - $this->index_meta['sync_stack'][] = $sync_stack_item;
203 + $this->add_sync_item_to_stack(
204 + [
205 + 'indexable' => $indexable,
206 + ]
207 + );
208 208 }
209 209
210 210 $this->index_meta['current_sync_item'] = false;
211 211 /**
@@ -249,9 +249,9 @@
249 249 */
250 250 protected function filter_indexables( $indexables ) {
251 251 return array_filter(
252 252 $indexables,
253 - function( $indexable ) {
253 + function ( $indexable ) {
254 254 return empty( $this->args['indexables'] ) || in_array( $indexable, $this->args['indexables'], true );
255 255 }
256 256 );
257 257 }
@@ -283,11 +283,14 @@
283 283 'errors' => [],
284 284 ]
285 285 );
286 286
287 - $indexable = Indexables::factory()->get( $this->index_meta['current_sync_item']['indexable'] );
287 + $indexable_slug = $this->index_meta['current_sync_item']['indexable'];
288 + $indexable = Indexables::factory()->get( $this->index_meta['current_sync_item']['indexable'] );
288 289
289 - if ( ! empty( $this->index_meta['current_sync_item']['blog_id'] ) && defined( 'EP_IS_NETWORK' ) && EP_IS_NETWORK ) {
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 ) {
290 293 $this->output_success(
291 294 sprintf(
292 295 /* translators: 1: Indexable name, 2: Site ID */
293 296 esc_html__( 'Indexing %1$s on site %2$d…', 'elasticpress' ),
@@ -350,9 +353,9 @@
350 353
351 354 $indexable = Indexables::factory()->get( $this->index_meta['current_sync_item']['indexable'] );
352 355
353 356 $indexable->delete_index();
354 - $result = $indexable->put_mapping();
357 + $result = $indexable->put_mapping( 'raw' );
355 358
356 359 /**
357 360 * Fires after sync put mapping is completed
358 361 *
@@ -380,13 +383,21 @@
380 383 * @param {string} $status Current indexing status
381 384 */
382 385 do_action( 'ep_dashboard_put_mapping', $this->index_meta, 'start' );
383 386
384 - if ( $result ) {
385 - $this->output_success( esc_html__( 'Mapping sent', 'elasticpress' ) );
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' );
386 395 } else {
387 - $this->output_error( esc_html__( 'Mapping failed', 'elasticpress' ) );
396 + $message = esc_html__( 'Index not present. Mapping sent', 'elasticpress' );
388 397 }
398 +
399 + $this->output_success( $message );
389 400 }
390 401
391 402 /**
392 403 * Index documents of an index.
@@ -401,9 +412,9 @@
401 412 $this->current_query = $this->get_objects_to_index();
402 413
403 414 $this->index_meta['from'] = $this->index_meta['offset'];
404 415 $this->index_meta['found_items'] = (int) $this->current_query['total_objects'];
405 - $this->index_meta['current_sync_item']['total'] = $this->index_meta['found_items'];
416 + $this->index_meta['current_sync_item']['total'] = (int) $this->index_meta['current_sync_item']['found_items'];
406 417
407 418 if ( 'offset' === $this->index_meta['pagination_method'] ) {
408 419 $indexable = Indexables::factory()->get( $this->index_meta['current_sync_item']['indexable'] );
409 420
@@ -464,17 +475,9 @@
464 475 * @param {array} $args Args to query content with
465 476 */
466 477 do_action( "ep_pre_{$this->args['method']}_index", $this->index_meta, ( $this->index_meta['start'] ? 'start' : false ), $indexable );
467 478
468 - /**
469 - * Filter number of items to index per cycle in the dashboard
470 - *
471 - * @since 2.1
472 - * @hook ep_index_default_per_page
473 - * @param {int} Entries per cycle
474 - * @return {int} New number of entries
475 - */
476 - $per_page = apply_filters( 'ep_index_default_per_page', Utils\get_option( 'ep_bulk_setting', 350 ) );
479 + $per_page = $this->get_index_default_per_page();
477 480
478 481 if ( ! empty( $this->args['per_page'] ) ) {
479 482 $per_page = $this->args['per_page'];
480 483 }
@@ -483,9 +486,10 @@
483 486 $per_page = 1;
484 487 }
485 488
486 489 $args = [
487 - 'per_page' => absint( $per_page ),
490 + 'per_page' => absint( $per_page ),
491 + 'ep_sync_id' => uniqid(),
488 492 ];
489 493
490 494 if ( ! $indexable->support_indexing_advanced_pagination || 'offset' === $this->index_meta['pagination_method'] ) {
491 495 $args['offset'] = $this->index_meta['offset'];
@@ -567,9 +571,9 @@
567 571 $queued_items = [];
568 572
569 573 foreach ( $this->current_query['objects'] as $object ) {
570 574 if ( $this->should_skip_object_index( $object, $indexable ) ) {
571 - $this->index_meta['current_sync_item']['skipped']++;
575 + ++$this->index_meta['current_sync_item']['skipped'];
572 576 } else {
573 577 $queued_items[ $object->ID ] = true;
574 578 }
575 579 }
@@ -670,9 +674,9 @@
670 674 $failed_objects = array_merge(
671 675 $failed_objects,
672 676 array_filter(
673 677 $return['items'],
674 - function( $item ) {
678 + function ( $item ) {
675 679 return ! empty( $item['index']['error'] );
676 680 }
677 681 )
678 682 );
@@ -687,19 +691,33 @@
687 691 }
688 692
689 693 if ( is_wp_error( $return ) ) {
690 694 $this->index_meta['current_sync_item']['failed'] += count( $queued_items );
691 - $this->index_meta['current_sync_item']['errors'] = array_merge( $this->index_meta['current_sync_item']['errors'], $return->get_error_messages() );
692 695
693 - $this->output( implode( "\n", $return->get_error_messages() ), 'warning' );
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' );
694 705 } elseif ( count( $failed_objects ) ) {
695 706 $errors_output = $this->output_index_errors( $failed_objects );
696 707
697 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 +
698 716 $this->index_meta['current_sync_item']['failed'] += count( $failed_objects );
699 - $this->index_meta['current_sync_item']['errors'] = array_merge( $this->index_meta['current_sync_item']['errors'], $errors_output );
717 + $error_type = ! empty( $this->args['stop_on_error'] ) ? 'error' : 'warning';
700 718
701 - $this->output( $errors_output, 'warning' );
719 + $this->queue_message( $errors_output, $error_type );
702 720 } else {
703 721 $this->index_meta['current_sync_item']['synced'] += count( $queued_items );
704 722 }
705 723 }
@@ -705,24 +723,61 @@
705 723 }
706 724
707 725 $this->index_meta['current_sync_item']['last_processed_object_id'] = end( $this->current_query['objects'] )->ID;
708 726
709 - $this->output(
710 - sprintf(
711 - /* translators: 1. Indexable type 2. Offset start, 3. Offset end, 4. Found items 5. Last object ID */
712 - esc_html__( 'Processed %1$s %2$d - %3$d of %4$d. Last Object ID: %5$d', 'elasticpress' ),
713 - esc_html( strtolower( $indexable->labels['plural'] ) ),
714 - $this->index_meta['from'],
715 - $this->index_meta['offset'],
716 - $this->index_meta['found_items'],
717 - $this->index_meta['current_sync_item']['last_processed_object_id']
718 - ),
719 - 'info',
720 - 'index_next_batch'
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']
721 735 );
736 +
737 + $this->queue_message( $summary, 'info', 'index_next_batch' );
738 + $this->flush_messages_queue();
722 739 }
723 740
724 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 + /**
725 780 * Update the sync info with the totals from the last sync item.
726 781 *
727 782 * @since 4.2.0
728 783 */
@@ -766,8 +821,9 @@
766 821
767 822 $current_sync_item = $this->index_meta['current_sync_item'];
768 823
769 824 $this->index_meta['current_sync_item'] = null;
825 + $this->index_meta['offset'] = 0;
770 826
771 827 if ( $current_sync_item['failed'] ) {
772 828 if ( ! empty( $current_sync_item['blog_id'] ) && defined( 'EP_IS_NETWORK' ) && EP_IS_NETWORK ) {
773 829 $message = sprintf(
@@ -788,10 +844,8 @@
788 844
789 845 $this->output( $message, 'warning' );
790 846 }
791 847
792 - $this->index_meta['offset'] = 0;
793 -
794 848 if ( ! empty( $current_sync_item['blog_id'] ) && defined( 'EP_IS_NETWORK' ) && EP_IS_NETWORK ) {
795 849 $message = sprintf(
796 850 /* translators: 1: indexable (plural), 2: Blog ID, 3: number of synced objects */
797 851 esc_html__( 'Number of %1$s indexed on site %2$d: %3$d', 'elasticpress' ),
@@ -814,13 +868,16 @@
814 868 /**
815 869 * Update last sync info.
816 870 *
817 871 * @since 4.2.0
872 + * @param string $final_status Optional final status
818 873 */
819 - protected function update_last_index() {
820 - $start_time = $this->index_meta['start_time'];
821 - $totals = $this->index_meta['totals'];
822 - $method = $this->index_meta['method'];
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'];
823 880
824 881 $this->index_meta = null;
825 882
826 883 $end_date_time = date_create( 'now', wp_timezone() );
@@ -825,18 +882,67 @@
825 882
826 883 $end_date_time = date_create( 'now', wp_timezone() );
827 884 $start_time_sec = (int) $start_time;
828 885
886 + // Time related info
829 887 $totals['end_date_time'] = $end_date_time ? $end_date_time->format( DATE_ATOM ) : false;
830 888 $totals['start_date_time'] = $start_time ? wp_date( DATE_ATOM, $start_time_sec ) : false;
831 889 $totals['end_time_gmt'] = time();
832 890 $totals['total_time'] = microtime( true ) - $start_time;
833 - $totals['method'] = $method;
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 +
834 906 Utils\update_option( 'ep_last_cli_index', $totals, false );
835 - Utils\update_option( 'ep_last_index', $totals, false );
907 +
908 + $this->add_last_sync( $totals );
836 909 }
837 910
838 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 + /**
839 945 * Make the necessary clean up after everything was sync'd.
840 946 *
841 947 * @since 4.0.0
842 948 */
@@ -846,11 +952,12 @@
846 952 /**
847 953 * Fires after executing a reindex
848 954 *
849 955 * @since 4.0.0
956 + * @param array $args Sync arguments.
850 957 * @hook ep_after_sync_index
851 958 */
852 - do_action( 'ep_after_sync_index' );
959 + do_action( 'ep_after_sync_index', $this->args );
853 960
854 961 /**
855 962 * Fires after executing a reindex
856 963 *
@@ -881,9 +988,9 @@
881 988 protected function create_network_alias() {
882 989 $indexes = [];
883 990 $indexable = Indexables::factory()->get( array_shift( $this->index_meta['network_alias'] ) );
884 991
885 - $sites = Utils\get_sites();
992 + $sites = Utils\get_sites( 0, true );
886 993
887 994 foreach ( $sites as $site ) {
888 995 switch_to_blog( $site['blog_id'] );
889 996 $indexes[] = $indexable->get_index_name();
@@ -924,9 +1031,9 @@
924 1031 if ( $this->index_meta ) {
925 1032 Utils\update_option( 'ep_index_meta', $this->index_meta );
926 1033 } else {
927 1034 Utils\delete_option( 'ep_index_meta' );
928 - $totals = $this->get_last_index();
1035 + $totals = $this->get_last_sync();
929 1036 }
930 1037
931 1038 $message = [
932 1039 'message' => ( is_array( $message_text ) ) ? implode( "\n", $message_text ) : $message_text,
@@ -934,8 +1041,12 @@
934 1041 'totals' => $totals ?? [],
935 1042 'status' => $type,
936 1043 ];
937 1044
1045 + if ( in_array( $type, [ 'warning', 'error' ], true ) ) {
1046 + $message['errors'] = $this->build_message_errors_data( $message_text );
1047 + }
1048 +
938 1049 if ( is_callable( $this->args['output_method'] ) ) {
939 1050 call_user_func( $this->args['output_method'], $message, $this->args, $this->index_meta, $context );
940 1051 }
941 1052 }
@@ -973,9 +1084,9 @@
973 1084
974 1085 $error_text = [];
975 1086
976 1087 foreach ( $failed_objects as $object ) {
977 - $error_text[] = $object['index']['_id'] . ' (' . $indexable->labels['singular'] . '): [' . $object['index']['error']['type'] . '] ' . $object['index']['error']['reason'];
1088 + $error_text[] = ! empty( $object['index'] ) ? $object['index']['_id'] . ' (' . $indexable->labels['singular'] . '): [' . $object['index']['error']['type'] . '] ' . $object['index']['error']['reason'] : (string) $object;
978 1089 }
979 1090
980 1091 return $error_text;
981 1092 }
@@ -980,9 +1091,9 @@
980 1091 return $error_text;
981 1092 }
982 1093
983 1094 /**
984 - * Utilitary function to check if the indexable is being fully reindexed, i.e.,
1095 + * Utility function to check if the indexable is being fully reindexed, i.e.,
985 1096 * the index was deleted, a new mapping was sent and content is being reindexed.
986 1097 *
987 1098 * @param string $indexable_slug Indexable slug.
988 1099 * @param int|null $blog_id Blog ID
@@ -1028,53 +1139,140 @@
1028 1139 return apply_filters( "ep_is_full_reindexing_{$indexable_slug}", $is_full_reindexing );
1029 1140 }
1030 1141
1031 1142 /**
1032 - * Get the last index/sync meta information.
1143 + * Get the previous syncs meta information.
1033 1144 *
1034 - * @since 4.2.0
1145 + * @since 5.0.0
1035 1146 * @return array
1036 1147 */
1037 - public function get_last_index() {
1038 - return Utils\get_option( 'ep_last_index', [] );
1148 + public function get_sync_history(): array {
1149 + return Utils\get_option( 'ep_sync_history', [] );
1039 1150 }
1040 1151
1041 1152 /**
1153 + * Get the last sync meta information.
1154 + *
1155 + * @since 5.0.0
1156 + * @return array
1157 + */
1158 + public function get_last_sync(): array {
1159 + $syncs = $this->get_sync_history();
1160 + if ( empty( $syncs ) ) {
1161 + return [];
1162 + }
1163 + return array_shift( $syncs );
1164 + }
1165 +
1166 + /**
1042 1167 * Check if an object should be indexed or skipped.
1043 1168 *
1044 1169 * We used to have two different filters for this (one for the dashboard, another for CLI),
1045 1170 * this method combines both.
1046 1171 *
1047 - * @param {stdClass} $object Object to be checked
1172 + * @param {stdClass} $indexable_object Object to be checked
1048 1173 * @param {Indexable} $indexable Indexable
1049 1174 * @return boolean
1050 1175 */
1051 - protected function should_skip_object_index( $object, $indexable ) {
1176 + protected function should_skip_object_index( $indexable_object, $indexable ) {
1052 1177 /**
1053 1178 * Filter whether to not sync specific item in dashboard or not
1054 1179 *
1055 1180 * @since 2.1
1181 + * @deprecated 5.3.3 Use ep_{indexable_slug}_sync_kill instead
1056 1182 * @hook ep_item_sync_kill
1057 1183 * @param {boolean} $kill False means dont sync
1058 - * @param {array} $object Object to sync
1184 + * @param {array} $indexable_object Object to sync
1059 1185 * @return {Indexable} Indexable that object belongs to
1060 1186 */
1061 - $ep_item_sync_kill = apply_filters( 'ep_item_sync_kill', false, $object, $indexable );
1187 + $ep_item_sync_kill = apply_filters_deprecated(
1188 + 'ep_item_sync_kill',
1189 + [ false, $indexable_object, $indexable ],
1190 + 'ElasticPress 5.3.3',
1191 + 'ep_' . $indexable->slug . '_sync_kill'
1192 + );
1062 1193
1063 - /**
1064 - * Conditionally kill indexing for a post
1065 - *
1066 - * @hook ep_{indexable_slug}_index_kill
1067 - * @param {bool} $index True means dont index
1068 - * @param {int} $object_id Object ID
1069 - * @return {bool} New value
1070 - */
1071 - $ep_indexable_sync_kill = apply_filters( 'ep_' . $indexable->slug . '_index_kill', false, $object->ID );
1194 + /** This filter is documented in includes/classes/Indexable.php */
1195 + $ep_indexable_index_kill = apply_filters_deprecated(
1196 + 'ep_' . $indexable->slug . '_index_kill',
1197 + [ false, $indexable_object->ID ],
1198 + 'ElasticPress 5.3.3',
1199 + 'ep_' . $indexable->slug . '_sync_kill'
1200 + );
1072 1201
1073 - return $ep_item_sync_kill || $ep_indexable_sync_kill;
1202 + /** This filter is documented in includes/classes/Indexable.php */
1203 + $ep_indexable_sync_kill = apply_filters( 'ep_' . $indexable->slug . '_sync_kill', false, $indexable_object->ID );
1204 +
1205 + return $ep_item_sync_kill || $ep_indexable_sync_kill || $ep_indexable_index_kill;
1074 1206 }
1075 1207
1076 1208 /**
1209 + * Given an array, create a new sync item and add it to the stack.
1210 + *
1211 + * @since 4.5.0
1212 + * @param array $sync_stack_item The new sync item
1213 + */
1214 + protected function add_sync_item_to_stack( array $sync_stack_item ) {
1215 + $indexable_slug = $sync_stack_item['indexable'];
1216 + $indexable_object = Indexables::factory()->get( $indexable_slug );
1217 +
1218 + if ( ! $indexable_object ) {
1219 + return;
1220 + }
1221 +
1222 + $index_exists = in_array( $indexable_object->get_index_name(), $this->index_meta['starting_indices'], true );
1223 +
1224 + $sync_stack_item['put_mapping'] = ! empty( $this->args['put_mapping'] ) || ! $index_exists;
1225 +
1226 + if ( ! Indexables::factory()->is_active( $indexable_slug ) ) {
1227 + array_unshift( $this->index_meta['sync_stack'], $sync_stack_item );
1228 + return;
1229 + }
1230 +
1231 + // This is needed, because get_objects_to_index() calculates its total based on the current sync item.
1232 + $this->index_meta['current_sync_item'] = $sync_stack_item;
1233 +
1234 + $objects_to_index = $this->get_objects_to_index();
1235 +
1236 + $sync_stack_item['found_items'] = $objects_to_index['total_objects'] ?? 0;
1237 +
1238 + $this->index_meta['sync_stack'][] = $sync_stack_item;
1239 + }
1240 +
1241 + /**
1242 + * Processes an indexable that is not active.
1243 + *
1244 + * If running a full sync, delete the index of an unused indexable.
1245 + *
1246 + * @since 4.5.0
1247 + */
1248 + protected function process_not_active_indexable_sync_item() {
1249 + $current_sync_item = $this->index_meta['current_sync_item'];
1250 +
1251 + $this->index_meta['current_sync_item'] = null;
1252 +
1253 + if ( empty( $current_sync_item['put_mapping'] ) ) {
1254 + return;
1255 + }
1256 +
1257 + $indexable = Indexables::factory()->get( $current_sync_item['indexable'] );
1258 +
1259 + if ( ! in_array( $indexable->get_index_name(), $this->index_meta['starting_indices'], true ) ) {
1260 + return;
1261 + }
1262 +
1263 + $indexable->delete_index();
1264 +
1265 + $this->output_success(
1266 + sprintf(
1267 + /* translators: Index name */
1268 + esc_html__( 'Index %s deleted', 'elasticpress' ),
1269 + $indexable->get_index_name()
1270 + )
1271 + );
1272 + }
1273 +
1274 + /**
1077 1275 * Resets some values to reduce memory footprint.
1078 1276 */
1079 1277 protected function stop_the_insanity() {
1080 1278 global $wpdb, $wp_object_cache, $wp_actions;
@@ -1086,16 +1284,14 @@
1086 1284 * in-memory cache for persistent object caches
1087 1285 */
1088 1286 if ( function_exists( 'wp_cache_flush_runtime' ) ) {
1089 1287 wp_cache_flush_runtime();
1090 - } else {
1288 + } elseif ( ! wp_using_ext_object_cache() ) {
1091 1289 /*
1092 1290 * In the case where we're not using an external object cache, we need to call flush on the default
1093 1291 * WordPress object cache class to clear the values from the cache property
1094 1292 */
1095 - if ( ! wp_using_ext_object_cache() ) {
1096 - wp_cache_flush();
1097 - }
1293 + wp_cache_flush();
1098 1294 }
1099 1295
1100 1296 if ( is_object( $wp_object_cache ) ) {
1101 1297 $wp_object_cache->group_ops = [];
@@ -1136,18 +1332,22 @@
1136 1332 do_action( 'ep_stop_the_insanity' );
1137 1333 }
1138 1334
1139 1335 /**
1140 - * Utilitary function to delete the index meta option.
1336 + * Utility function to delete the index meta option.
1141 1337 *
1142 1338 * @since 4.0.0
1143 1339 */
1144 1340 public function clear_index_meta() {
1341 + if ( ! empty( $this->index_meta ) ) {
1342 + $this->update_last_index( 'aborted' );
1343 + }
1344 + $this->index_meta = false;
1145 1345 Utils\delete_option( 'ep_index_meta', false );
1146 1346 }
1147 1347
1148 1348 /**
1149 - * Utilitary function to get the index meta option.
1349 + * Utility function to get the index meta option.
1150 1350 *
1151 1351 * @return array
1152 1352 * @since 4.0.0
1153 1353 */
@@ -1189,11 +1389,12 @@
1189 1389 /**
1190 1390 * Logs the error and clears the sync status, preventing the sync status from being stuck.
1191 1391 *
1192 1392 * @since 4.2.0
1193 - * @param array $error Error information retrieved from error_get_last().
1393 + * @param array $error Error information retrieved from error_get_last().
1394 + * @param string $context Context of the error.
1194 1395 */
1195 - protected function on_error_update_and_clean( $error ) {
1396 + protected function on_error_update_and_clean( $error, $context = 'sync' ) {
1196 1397 $this->update_totals_from_current_sync_item();
1197 1398
1198 1399 $totals = $this->index_meta['totals'];
1199 1400
@@ -1198,9 +1399,9 @@
1198 1399 $totals = $this->index_meta['totals'];
1199 1400
1200 1401 $this->index_meta['totals']['errors'][] = $error['message'];
1201 1402 $this->index_meta['totals']['failed'] = $totals['total'] - ( $totals['synced'] + $totals['skipped'] );
1202 - $this->update_last_index();
1403 + $this->update_last_index( 'failed' );
1203 1404
1204 1405 /**
1205 1406 * Fires after a sync failed due to a PHP fatal error.
1206 1407 *
@@ -1209,18 +1410,139 @@
1209 1410 * @param {array} $error The error
1210 1411 */
1211 1412 do_action( 'ep_after_sync_error', $error );
1212 1413
1213 - $this->output_error(
1214 - sprintf(
1414 + switch ( $context ) {
1415 + case 'mapping':
1416 + $message = sprintf(
1417 + /* translators: Error message */
1418 + esc_html__( 'Mapping failed: %s', 'elasticpress' ),
1419 + Utils\get_elasticsearch_error_reason( $error['message'] )
1420 + );
1421 + if ( $this->should_suggest_retry( $message ) ) {
1422 + $message .= "\n";
1423 + $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' );
1424 + }
1425 + break;
1426 + default:
1215 1427 /* translators: Error message */
1216 - esc_html__( 'Index failed: %s', 'elasticpress' ),
1217 - $error['message']
1218 - )
1219 - );
1428 + $message = sprintf( esc_html__( 'Index failed: %s', 'elasticpress' ), $error['message'] );
1429 + break;
1430 + }
1431 +
1432 + $this->output_error( $message );
1220 1433 }
1221 1434
1222 1435 /**
1436 + * Return the default number of documents to be sent to Elasticsearch on each batch.
1437 + *
1438 + * @since 4.4.0
1439 + * @return integer
1440 + */
1441 + public function get_index_default_per_page(): int {
1442 + /**
1443 + * Filter number of items to index per cycle in the dashboard
1444 + *
1445 + * @since 2.1
1446 + * @hook ep_index_default_per_page
1447 + * @param {int} Entries per cycle
1448 + * @return {int} New number of entries
1449 + */
1450 + return (int) apply_filters( 'ep_index_default_per_page', Utils\get_option( 'ep_bulk_setting', 350 ) );
1451 + }
1452 +
1453 + /**
1454 + * Add a message to the queue
1455 + *
1456 + * @since 4.7.0
1457 + * @param string|array $message_text Message to be outputted
1458 + * @param string $type Type of message
1459 + * @param string $context Context of the output
1460 + */
1461 + protected function queue_message( $message_text, string $type, string $context = '' ) {
1462 + $this->index_meta['messages_queue'][] = [
1463 + 'text' => $message_text,
1464 + 'type' => $type,
1465 + 'context' => $context,
1466 + ];
1467 + }
1468 +
1469 + /**
1470 + * Display messages in the queue.
1471 + *
1472 + * NOTE: As the dashboard sync exits after every output call (to respond the AJAX request),
1473 + * this will just output one message. As the method is called every time the script is called,
1474 + * all messages will be displayed but one at a time.
1475 + *
1476 + * @since 4.7.0
1477 + */
1478 + protected function flush_messages_queue() {
1479 + if ( ! is_array( $this->index_meta['messages_queue'] ) ) {
1480 + return;
1481 + }
1482 +
1483 + $messages_count = count( $this->index_meta['messages_queue'] );
1484 + if ( 0 === $messages_count ) {
1485 + return;
1486 + }
1487 +
1488 + for ( $i = 0; $i < $messages_count; $i++ ) {
1489 + $next_message = array_shift( $this->index_meta['messages_queue'] );
1490 + $this->output( $next_message['text'], $next_message['type'], $next_message['context'] );
1491 + }
1492 + }
1493 +
1494 + /**
1495 + * Get data for a given error message(s)
1496 + *
1497 + * @since 5.0.0
1498 + * @param string|array $messages Messages
1499 + * @return array
1500 + */
1501 + protected function build_message_errors_data( $messages ): array {
1502 + $messages = (array) $messages;
1503 + $error_interpreter = new \ElasticPress\ElasticsearchErrorInterpreter();
1504 +
1505 + $errors_list = [];
1506 + foreach ( $messages as $message ) {
1507 + $error = $error_interpreter->maybe_suggest_solution_for_es( $message );
1508 +
1509 + if ( ! isset( $errors_list[ $error['error'] ] ) ) {
1510 + $errors_list[ $error['error'] ] = [
1511 + 'solution' => $error['solution'],
1512 + 'count' => 1,
1513 + ];
1514 + } else {
1515 + ++$errors_list[ $error['error'] ]['count'];
1516 + }
1517 + }
1518 + return $errors_list;
1519 + }
1520 +
1521 + /**
1522 + * If this is a full sync, apply the draft feature settings
1523 + *
1524 + * @since 5.0.0
1525 + */
1526 + protected function maybe_apply_feature_settings() {
1527 + if ( empty( $this->args['put_mapping'] ) ) {
1528 + return;
1529 + }
1530 +
1531 + Features::factory()->apply_draft_feature_settings();
1532 + }
1533 +
1534 + /**
1535 + * Whether to suggest retrying the sync or not.
1536 + *
1537 + * @param string $message The message returned by the hosting server
1538 + * @return boolean
1539 + */
1540 + protected function should_suggest_retry( $message ) {
1541 + return ! preg_match( '/you have reached the limit of indices your plan supports/', $message );
1542 + }
1543 +
1544 + /**
1223 1545 * Return singleton instance of class.
1224 1546 *
1225 1547 * @return self
1226 1548 * @since 4.0.0
@@ -1233,6 +1555,18 @@
1233 1555 $instance->setup();
1234 1556 }
1235 1557
1236 1558 return $instance;
1559 + }
1560 +
1561 + /**
1562 + * DEPRECATED. Get the last index/sync meta information.
1563 + *
1564 + * @since 4.2.0
1565 + * @deprecated 5.0.0
1566 + * @return array
1567 + */
1568 + public function get_last_index() {
1569 + _deprecated_function( __METHOD__, '5.0.0', '\ElasticPress\IndexHelper::get_last_sync' );
1570 + return $this->get_last_sync();
1237 1571 }
1238 1572 }