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 +449 -130 4.4.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 https://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.
@@ -475,9 +486,10 @@
475 486 $per_page = 1;
476 487 }
477 488
478 489 $args = [
479 - 'per_page' => absint( $per_page ),
490 + 'per_page' => absint( $per_page ),
491 + 'ep_sync_id' => uniqid(),
480 492 ];
481 493
482 494 if ( ! $indexable->support_indexing_advanced_pagination || 'offset' === $this->index_meta['pagination_method'] ) {
483 495 $args['offset'] = $this->index_meta['offset'];
@@ -559,9 +571,9 @@
559 571 $queued_items = [];
560 572
561 573 foreach ( $this->current_query['objects'] as $object ) {
562 574 if ( $this->should_skip_object_index( $object, $indexable ) ) {
563 - $this->index_meta['current_sync_item']['skipped']++;
575 + ++$this->index_meta['current_sync_item']['skipped'];
564 576 } else {
565 577 $queued_items[ $object->ID ] = true;
566 578 }
567 579 }
@@ -662,9 +674,9 @@
662 674 $failed_objects = array_merge(
663 675 $failed_objects,
664 676 array_filter(
665 677 $return['items'],
666 - function( $item ) {
678 + function ( $item ) {
667 679 return ! empty( $item['index']['error'] );
668 680 }
669 681 )
670 682 );
@@ -679,19 +691,33 @@
679 691 }
680 692
681 693 if ( is_wp_error( $return ) ) {
682 694 $this->index_meta['current_sync_item']['failed'] += count( $queued_items );
683 - $this->index_meta['current_sync_item']['errors'] = array_merge( $this->index_meta['current_sync_item']['errors'], $return->get_error_messages() );
684 695
685 - $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' );
686 705 } elseif ( count( $failed_objects ) ) {
687 706 $errors_output = $this->output_index_errors( $failed_objects );
688 707
689 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 +
690 716 $this->index_meta['current_sync_item']['failed'] += count( $failed_objects );
691 - $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';
692 718
693 - $this->output( $errors_output, 'warning' );
719 + $this->queue_message( $errors_output, $error_type );
694 720 } else {
695 721 $this->index_meta['current_sync_item']['synced'] += count( $queued_items );
696 722 }
697 723 }
@@ -697,24 +723,61 @@
697 723 }
698 724
699 725 $this->index_meta['current_sync_item']['last_processed_object_id'] = end( $this->current_query['objects'] )->ID;
700 726
701 - $this->output(
702 - sprintf(
703 - /* translators: 1. Indexable type 2. Offset start, 3. Offset end, 4. Found items 5. Last object ID */
704 - esc_html__( 'Processed %1$s %2$d - %3$d of %4$d. Last Object ID: %5$d', 'elasticpress' ),
705 - esc_html( strtolower( $indexable->labels['plural'] ) ),
706 - $this->index_meta['from'],
707 - $this->index_meta['offset'],
708 - $this->index_meta['found_items'],
709 - $this->index_meta['current_sync_item']['last_processed_object_id']
710 - ),
711 - 'info',
712 - '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']
713 735 );
736 +
737 + $this->queue_message( $summary, 'info', 'index_next_batch' );
738 + $this->flush_messages_queue();
714 739 }
715 740
716 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 + /**
717 780 * Update the sync info with the totals from the last sync item.
718 781 *
719 782 * @since 4.2.0
720 783 */
@@ -758,8 +821,9 @@
758 821
759 822 $current_sync_item = $this->index_meta['current_sync_item'];
760 823
761 824 $this->index_meta['current_sync_item'] = null;
825 + $this->index_meta['offset'] = 0;
762 826
763 827 if ( $current_sync_item['failed'] ) {
764 828 if ( ! empty( $current_sync_item['blog_id'] ) && defined( 'EP_IS_NETWORK' ) && EP_IS_NETWORK ) {
765 829 $message = sprintf(
@@ -780,10 +844,8 @@
780 844
781 845 $this->output( $message, 'warning' );
782 846 }
783 847
784 - $this->index_meta['offset'] = 0;
785 -
786 848 if ( ! empty( $current_sync_item['blog_id'] ) && defined( 'EP_IS_NETWORK' ) && EP_IS_NETWORK ) {
787 849 $message = sprintf(
788 850 /* translators: 1: indexable (plural), 2: Blog ID, 3: number of synced objects */
789 851 esc_html__( 'Number of %1$s indexed on site %2$d: %3$d', 'elasticpress' ),
@@ -806,13 +868,16 @@
806 868 /**
807 869 * Update last sync info.
808 870 *
809 871 * @since 4.2.0
872 + * @param string $final_status Optional final status
810 873 */
811 - protected function update_last_index() {
812 - $start_time = $this->index_meta['start_time'];
813 - $totals = $this->index_meta['totals'];
814 - $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'];
815 880
816 881 $this->index_meta = null;
817 882
818 883 $end_date_time = date_create( 'now', wp_timezone() );
@@ -817,18 +882,67 @@
817 882
818 883 $end_date_time = date_create( 'now', wp_timezone() );
819 884 $start_time_sec = (int) $start_time;
820 885
886 + // Time related info
821 887 $totals['end_date_time'] = $end_date_time ? $end_date_time->format( DATE_ATOM ) : false;
822 888 $totals['start_date_time'] = $start_time ? wp_date( DATE_ATOM, $start_time_sec ) : false;
823 889 $totals['end_time_gmt'] = time();
824 890 $totals['total_time'] = microtime( true ) - $start_time;
825 - $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 +
826 906 Utils\update_option( 'ep_last_cli_index', $totals, false );
827 - Utils\update_option( 'ep_last_index', $totals, false );
907 +
908 + $this->add_last_sync( $totals );
828 909 }
829 910
830 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 + /**
831 945 * Make the necessary clean up after everything was sync'd.
832 946 *
833 947 * @since 4.0.0
834 948 */
@@ -838,11 +952,12 @@
838 952 /**
839 953 * Fires after executing a reindex
840 954 *
841 955 * @since 4.0.0
956 + * @param array $args Sync arguments.
842 957 * @hook ep_after_sync_index
843 958 */
844 - do_action( 'ep_after_sync_index' );
959 + do_action( 'ep_after_sync_index', $this->args );
845 960
846 961 /**
847 962 * Fires after executing a reindex
848 963 *
@@ -873,16 +988,11 @@
873 988 protected function create_network_alias() {
874 989 $indexes = [];
875 990 $indexable = Indexables::factory()->get( array_shift( $this->index_meta['network_alias'] ) );
876 991
877 - $sites = Utils\get_sites();
992 + $sites = Utils\get_sites( 0, true );
878 993
879 994 foreach ( $sites as $site ) {
880 -
881 - if ( ! Utils\is_site_indexable( $site['blog_id'] ) ) {
882 - continue;
883 - }
884 -
885 995 switch_to_blog( $site['blog_id'] );
886 996 $indexes[] = $indexable->get_index_name();
887 997 restore_current_blog();
888 998 }
@@ -921,9 +1031,9 @@
921 1031 if ( $this->index_meta ) {
922 1032 Utils\update_option( 'ep_index_meta', $this->index_meta );
923 1033 } else {
924 1034 Utils\delete_option( 'ep_index_meta' );
925 - $totals = $this->get_last_index();
1035 + $totals = $this->get_last_sync();
926 1036 }
927 1037
928 1038 $message = [
929 1039 'message' => ( is_array( $message_text ) ) ? implode( "\n", $message_text ) : $message_text,
@@ -931,8 +1041,12 @@
931 1041 'totals' => $totals ?? [],
932 1042 'status' => $type,
933 1043 ];
934 1044
1045 + if ( in_array( $type, [ 'warning', 'error' ], true ) ) {
1046 + $message['errors'] = $this->build_message_errors_data( $message_text );
1047 + }
1048 +
935 1049 if ( is_callable( $this->args['output_method'] ) ) {
936 1050 call_user_func( $this->args['output_method'], $message, $this->args, $this->index_meta, $context );
937 1051 }
938 1052 }
@@ -970,9 +1084,9 @@
970 1084
971 1085 $error_text = [];
972 1086
973 1087 foreach ( $failed_objects as $object ) {
974 - $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;
975 1089 }
976 1090
977 1091 return $error_text;
978 1092 }
@@ -977,9 +1091,9 @@
977 1091 return $error_text;
978 1092 }
979 1093
980 1094 /**
981 - * 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.,
982 1096 * the index was deleted, a new mapping was sent and content is being reindexed.
983 1097 *
984 1098 * @param string $indexable_slug Indexable slug.
985 1099 * @param int|null $blog_id Blog ID
@@ -1025,53 +1139,140 @@
1025 1139 return apply_filters( "ep_is_full_reindexing_{$indexable_slug}", $is_full_reindexing );
1026 1140 }
1027 1141
1028 1142 /**
1029 - * Get the last index/sync meta information.
1143 + * Get the previous syncs meta information.
1030 1144 *
1031 - * @since 4.2.0
1145 + * @since 5.0.0
1032 1146 * @return array
1033 1147 */
1034 - public function get_last_index() {
1035 - return Utils\get_option( 'ep_last_index', [] );
1148 + public function get_sync_history(): array {
1149 + return Utils\get_option( 'ep_sync_history', [] );
1036 1150 }
1037 1151
1038 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 + /**
1039 1167 * Check if an object should be indexed or skipped.
1040 1168 *
1041 1169 * We used to have two different filters for this (one for the dashboard, another for CLI),
1042 1170 * this method combines both.
1043 1171 *
1044 - * @param {stdClass} $object Object to be checked
1172 + * @param {stdClass} $indexable_object Object to be checked
1045 1173 * @param {Indexable} $indexable Indexable
1046 1174 * @return boolean
1047 1175 */
1048 - protected function should_skip_object_index( $object, $indexable ) {
1176 + protected function should_skip_object_index( $indexable_object, $indexable ) {
1049 1177 /**
1050 1178 * Filter whether to not sync specific item in dashboard or not
1051 1179 *
1052 1180 * @since 2.1
1181 + * @deprecated 5.3.3 Use ep_{indexable_slug}_sync_kill instead
1053 1182 * @hook ep_item_sync_kill
1054 1183 * @param {boolean} $kill False means dont sync
1055 - * @param {array} $object Object to sync
1184 + * @param {array} $indexable_object Object to sync
1056 1185 * @return {Indexable} Indexable that object belongs to
1057 1186 */
1058 - $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 + );
1059 1193
1060 - /**
1061 - * Conditionally kill indexing for a post
1062 - *
1063 - * @hook ep_{indexable_slug}_index_kill
1064 - * @param {bool} $index True means dont index
1065 - * @param {int} $object_id Object ID
1066 - * @return {bool} New value
1067 - */
1068 - $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 + );
1069 1201
1070 - 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;
1071 1206 }
1072 1207
1073 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 + /**
1074 1275 * Resets some values to reduce memory footprint.
1075 1276 */
1076 1277 protected function stop_the_insanity() {
1077 1278 global $wpdb, $wp_object_cache, $wp_actions;
@@ -1083,16 +1284,14 @@
1083 1284 * in-memory cache for persistent object caches
1084 1285 */
1085 1286 if ( function_exists( 'wp_cache_flush_runtime' ) ) {
1086 1287 wp_cache_flush_runtime();
1087 - } else {
1288 + } elseif ( ! wp_using_ext_object_cache() ) {
1088 1289 /*
1089 1290 * In the case where we're not using an external object cache, we need to call flush on the default
1090 1291 * WordPress object cache class to clear the values from the cache property
1091 1292 */
1092 - if ( ! wp_using_ext_object_cache() ) {
1093 - wp_cache_flush();
1094 - }
1293 + wp_cache_flush();
1095 1294 }
1096 1295
1097 1296 if ( is_object( $wp_object_cache ) ) {
1098 1297 $wp_object_cache->group_ops = [];
@@ -1133,18 +1332,22 @@
1133 1332 do_action( 'ep_stop_the_insanity' );
1134 1333 }
1135 1334
1136 1335 /**
1137 - * Utilitary function to delete the index meta option.
1336 + * Utility function to delete the index meta option.
1138 1337 *
1139 1338 * @since 4.0.0
1140 1339 */
1141 1340 public function clear_index_meta() {
1341 + if ( ! empty( $this->index_meta ) ) {
1342 + $this->update_last_index( 'aborted' );
1343 + }
1344 + $this->index_meta = false;
1142 1345 Utils\delete_option( 'ep_index_meta', false );
1143 1346 }
1144 1347
1145 1348 /**
1146 - * Utilitary function to get the index meta option.
1349 + * Utility function to get the index meta option.
1147 1350 *
1148 1351 * @return array
1149 1352 * @since 4.0.0
1150 1353 */
@@ -1186,11 +1389,12 @@
1186 1389 /**
1187 1390 * Logs the error and clears the sync status, preventing the sync status from being stuck.
1188 1391 *
1189 1392 * @since 4.2.0
1190 - * @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.
1191 1395 */
1192 - protected function on_error_update_and_clean( $error ) {
1396 + protected function on_error_update_and_clean( $error, $context = 'sync' ) {
1193 1397 $this->update_totals_from_current_sync_item();
1194 1398
1195 1399 $totals = $this->index_meta['totals'];
1196 1400
@@ -1195,9 +1399,9 @@
1195 1399 $totals = $this->index_meta['totals'];
1196 1400
1197 1401 $this->index_meta['totals']['errors'][] = $error['message'];
1198 1402 $this->index_meta['totals']['failed'] = $totals['total'] - ( $totals['synced'] + $totals['skipped'] );
1199 - $this->update_last_index();
1403 + $this->update_last_index( 'failed' );
1200 1404
1201 1405 /**
1202 1406 * Fires after a sync failed due to a PHP fatal error.
1203 1407 *
@@ -1206,15 +1410,27 @@
1206 1410 * @param {array} $error The error
1207 1411 */
1208 1412 do_action( 'ep_after_sync_error', $error );
1209 1413
1210 - $this->output_error(
1211 - 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:
1212 1427 /* translators: Error message */
1213 - esc_html__( 'Index failed: %s', 'elasticpress' ),
1214 - $error['message']
1215 - )
1216 - );
1428 + $message = sprintf( esc_html__( 'Index failed: %s', 'elasticpress' ), $error['message'] );
1429 + break;
1430 + }
1431 +
1432 + $this->output_error( $message );
1217 1433 }
1218 1434
1219 1435 /**
1220 1436 * Return the default number of documents to be sent to Elasticsearch on each batch.
@@ -1221,9 +1437,9 @@
1221 1437 *
1222 1438 * @since 4.4.0
1223 1439 * @return integer
1224 1440 */
1225 - public function get_index_default_per_page() : int {
1441 + public function get_index_default_per_page(): int {
1226 1442 /**
1227 1443 * Filter number of items to index per cycle in the dashboard
1228 1444 *
1229 1445 * @since 2.1
@@ -1234,8 +1450,99 @@
1234 1450 return (int) apply_filters( 'ep_index_default_per_page', Utils\get_option( 'ep_bulk_setting', 350 ) );
1235 1451 }
1236 1452
1237 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 + /**
1238 1545 * Return singleton instance of class.
1239 1546 *
1240 1547 * @return self
1241 1548 * @since 4.0.0
@@ -1248,6 +1555,18 @@
1248 1555 $instance->setup();
1249 1556 }
1250 1557
1251 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();
1252 1571 }
1253 1572 }