PluginProbe
ElasticPress / 4.4.0
ElasticPress v4.4.0
5.3.5 5.3.4 3.6.5 3.6.6 4.0.0 4.0.1 4.1.0 4.2.0 4.2.1 4.2.2 4.3.0 4.3.1 4.4.0 4.4.1 4.5.0 4.5.1 4.5.2 4.6.0 4.6.1 4.7.0 4.7.1 4.7.2 5.0.0 5.0.1 5.0.2 All 108 releases
elasticpress / includes / classes / Indexable / Post / QueryIntegration.php

QueryIntegration.php in ElasticPress 4.4.0, at includes/classes/Indexable/Post/QueryIntegration.php

552 lines 14.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Integrate with WP_Query
4 *
5 * @since 1.0
6 * @package elasticpress
7 */
8
9 namespace ElasticPress\Indexable\Post;
10
11 use ElasticPress\Indexables as Indexables;
12 use \WP_Query as WP_Query;
13 use ElasticPress\Utils as Utils;
14
15 if ( ! defined( 'ABSPATH' ) ) {
16 // @codeCoverageIgnoreStart
17 exit; // Exit if accessed directly.
18 // @codeCoverageIgnoreEnd
19 }
20
21 /**
22 * Query integration class
23 */
24 class QueryIntegration {
25
26 /**
27 * Is set only when we switch_to_blog in MS context
28 *
29 * @var boolean
30 */
31 private $switched = false;
32
33 /**
34 * Checks to see if we should be integrating and if so, sets up the appropriate actions and filters.
35 *
36 * @param string $indexable_slug Indexable slug. Optional.
37 *
38 * @since 0.9
39 * @since 3.6.0 Added $indexable_slug
40 */
41 public function __construct( $indexable_slug = 'post' ) {
42 // Ensure that we are currently allowing ElasticPress to override the normal WP_Query
43 // Indexable->is_full_reindexing() is not available at this point yet, so using the IndexHelper version of it.
44 if ( \ElasticPress\IndexHelper::factory()->is_full_reindexing( $indexable_slug, get_current_blog_id() ) ) {
45 return;
46 }
47
48 // Add header
49 add_action( 'pre_get_posts', array( $this, 'add_es_header' ), 5 );
50
51 // Query ES for posts
52 add_filter( 'posts_pre_query', array( $this, 'get_es_posts' ), 10, 2 );
53
54 // Properly restore blog if necessary
55 add_action( 'loop_end', array( $this, 'maybe_restore_blog' ), 10, 1 );
56
57 // Properly switch to blog if necessary
58 add_action( 'the_post', array( $this, 'maybe_switch_to_blog' ), 10, 2 );
59
60 // Sets the correct value for found_posts
61 add_filter( 'found_posts', array( $this, 'found_posts' ), 10, 2 );
62 }
63
64 /**
65 * Set the found_posts variable on WP_Query.
66 *
67 * @param int $found_posts Number of found posts
68 * @param WP_Query $query Query object
69 * @since 2.8.2
70 * @return int
71 */
72 public function found_posts( $found_posts, $query ) {
73 /**
74 * Filter to skip WP Query integration
75 *
76 * @hook ep_skip_query_integration
77 * @param {bool} $skip True to skip
78 * @param {WP_Query} $query WP Query to evaluate
79 * @return {bool} New skip value
80 */
81 if ( ( isset( $query->elasticsearch_success ) && false === $query->elasticsearch_success ) || ( ! Indexables::factory()->get( 'post' )->elasticpress_enabled( $query ) || apply_filters( 'ep_skip_query_integration', false, $query ) ) ) {
82 return $found_posts;
83 }
84
85 return $query->num_posts;
86 }
87
88 /**
89 * Disables cache_results, adds header.
90 *
91 * @param WP_Query $query WP_Query instance
92 * @since 0.9
93 */
94 public function add_es_header( $query ) {
95 /**
96 * Filter to skip WP Query integration
97 *
98 * @hook ep_skip_query_integration
99 * @param {bool} $skip True to skip
100 * @param {WP_Query} $query WP Query to evaluate
101 * @return {bool} New skip value
102 */
103 if ( ! Indexables::factory()->get( 'post' )->elasticpress_enabled( $query ) || apply_filters( 'ep_skip_query_integration', false, $query ) ) {
104 return;
105 }
106
107 /**
108 * `cache_results` defaults to false but can be enabled.
109 *
110 * @since 1.5
111 */
112 $query->set( 'cache_results', false );
113 if ( ! empty( $query->query['cache_results'] ) ) {
114 $query->set( 'cache_results', true );
115 }
116
117 if ( ! headers_sent() ) {
118 /**
119 * Manually setting a header as $wp_query isn't yet initialized when we
120 * call: add_filter('wp_headers', 'filter_wp_headers');
121 */
122 // @codeCoverageIgnoreStart
123 header( 'X-ElasticPress-Query: true' );
124 // @codeCoverageIgnoreEnd
125 }
126 }
127
128 /**
129 * Gets the blog ID that the class is currently switched to.
130 *
131 * @return int
132 */
133 public function get_switched() {
134 return $this->switched;
135 }
136
137 /**
138 * Switch to the correct site if the post site id is different than the actual one.
139 *
140 * Note: This function can bring a performance penalty in multisites with a high number of sites.
141 *
142 * @param WP_Post $post Post object
143 * @param WP_Query $query WP_Query instance. If null, the global query will be used.
144 * @since 0.9
145 * @since 3.6.2 `$query` parameter added.
146 */
147 public function maybe_switch_to_blog( $post, $query = null ) {
148 global $wp_query;
149 if ( ! $query ) {
150 $query = $wp_query;
151 }
152
153 if ( ! is_multisite() ) {
154 // @codeCoverageIgnoreStart
155 return;
156 // @codeCoverageIgnoreEnd
157 }
158
159 if ( ! empty( $post->site_id ) && get_current_blog_id() !== $post->site_id ) {
160 if ( $this->switched ) {
161 restore_current_blog();
162
163 $this->switched = false;
164 }
165
166 switch_to_blog( $post->site_id );
167
168 $this->switched = $post->site_id;
169
170 remove_action( 'the_post', array( $this, 'maybe_switch_to_blog' ), 10, 2 );
171 setup_postdata( $post );
172 add_action( 'the_post', array( $this, 'maybe_switch_to_blog' ), 10, 2 );
173
174 if ( $this->switched && ! $query->in_the_loop ) {
175 restore_current_blog();
176
177 $this->switched = false;
178 }
179 }
180
181 }
182
183 /**
184 * Make sure the correct blog is restored
185 *
186 * @param WP_Query $query WP_Query instance
187 * @since 0.9
188 */
189 public function maybe_restore_blog( $query ) {
190 if ( ! is_multisite() ) {
191 // @codeCoverageIgnoreStart
192 return;
193 // @codeCoverageIgnoreEnd
194 }
195
196 if ( $this->switched ) {
197 restore_current_blog();
198
199 $this->switched = false;
200 }
201 }
202
203 /**
204 * Get posts from Elasticsearch
205 *
206 * @param array $posts Array of posts
207 * @param WP_Query $query WP_Query instance
208 * @since 3.0
209 * @return string
210 */
211 public function get_es_posts( $posts, $query ) {
212 global $wpdb;
213
214 /**
215 * Filter to skip WP Query integration
216 *
217 * @hook ep_skip_query_integration
218 * @param {bool} $skip True to skip
219 * @param {WP_Query} $query WP Query to evaluate
220 * @return {bool} New skip value
221 */
222 if ( ! Indexables::factory()->get( 'post' )->elasticpress_enabled( $query ) || apply_filters( 'ep_skip_query_integration', false, $query ) ) {
223 return $posts;
224 }
225
226 $query_vars = $query->query_vars;
227
228 /**
229 * Filter post type query variables before WP Query
230 *
231 * @since 2.1
232 * @hook ep_query_post_type
233 * @param {string|array} $post_types Post types
234 * @param {WP_Query} $query WP Query object
235 * @return {string|array} New post types
236 */
237 $query_vars['post_type'] = apply_filters( 'ep_query_post_type', $query_vars['post_type'], $query );
238
239 if ( 'any' === $query_vars['post_type'] ) {
240 unset( $query_vars['post_type'] );
241 }
242
243 /**
244 * If not search and not set default to post. If not set and is search, use searchable post types
245 */
246 if ( empty( $query_vars['post_type'] ) ) {
247 if ( $query->is_tax() ) {
248 $query_vars['post_type'] = get_taxonomy( $query->get_queried_object()->taxonomy )->object_type;
249 } elseif ( empty( $query_vars['s'] ) ) {
250 $query_vars['post_type'] = 'post';
251 } else {
252 $query_vars['post_type'] = array_values( get_post_types( array( 'exclude_from_search' => false ) ) );
253 }
254 }
255
256 /**
257 * No post types so bail
258 */
259 if ( empty( $query_vars['post_type'] ) ) {
260 return [];
261 }
262
263 /**
264 * Filter cached posts pre-post query
265 *
266 * @hook ep_wp_query_cached_posts
267 * @param {array} $posts Array of posts
268 * @param {WP_Query} $query WP Query object
269 * @return {array} New cached posts
270 */
271 $new_posts = apply_filters( 'ep_wp_query_cached_posts', [], $query );
272
273 $ep_query = null;
274
275 if ( count( $new_posts ) < 1 ) {
276
277 $scope = 'current';
278
279 $site__in = '';
280 $site__not_in = '';
281
282 if ( ! empty( $query_vars['sites'] ) ) {
283
284 _deprecated_argument( __FUNCTION__, '4.4.0', esc_html__( 'sites is deprecated. Use site__in instead.', 'elasticpress' ) );
285 $site__in = (array) $query_vars['sites'];
286 $scope = 'all' === $query_vars['sites'] ? 'all' : $site__in;
287 }
288
289 if ( ! empty( $query_vars['site__in'] ) ) {
290
291 $site__in = (array) $query_vars['site__in'];
292 $scope = 'all' === $query_vars['site__in'] ? 'all' : $site__in;
293 }
294
295 if ( ! empty( $query_vars['site__not_in'] ) ) {
296 $site__not_in = (array) $query_vars['site__not_in'];
297 }
298
299 $formatted_args = Indexables::factory()->get( 'post' )->format_args( $query_vars, $query );
300
301 /**
302 * Filter post query scope
303 *
304 * @hook ep_search_scope
305 * @param {string} $scope Current scope
306 * @return {string} New scope
307 * @since 2.1
308 */
309 $scope = apply_filters( 'ep_search_scope', $scope );
310
311 if ( ! defined( 'EP_IS_NETWORK' ) || ! EP_IS_NETWORK ) {
312 // @codeCoverageIgnoreStart
313 $scope = 'current';
314 // @codeCoverageIgnoreEnd
315 }
316
317 $index = null;
318
319 if ( 'all' === $scope ) {
320 $index = Indexables::factory()->get( 'post' )->get_network_alias();
321 } elseif ( ! empty( $site__in ) ) {
322 $index = [];
323
324 foreach ( $site__in as $site_id ) {
325 $index[] = Indexables::factory()->get( 'post' )->get_index_name( $site_id );
326 }
327
328 $index = implode( ',', $index );
329 } elseif ( ! empty( $site__not_in ) ) {
330
331 $sites = get_sites(
332 array(
333 'fields' => 'ids',
334 'site__not_in' => $site__not_in,
335 )
336 );
337 foreach ( $sites as $site_id ) {
338 if ( ! Utils\is_site_indexable( $site_id ) ) {
339 continue;
340 }
341 $index[] = Indexables::factory()->get( 'post' )->get_index_name( $site_id );
342 }
343
344 $index = implode( ',', $index );
345 }
346
347 $ep_query = Indexables::factory()->get( 'post' )->query_es( $formatted_args, $query->query_vars, $index, $query );
348
349 /**
350 * ES failed. Go back to MySQL.
351 */
352 if ( false === $ep_query ) {
353 $query->elasticsearch_success = false;
354 return null;
355 }
356
357 $found_documents = is_array( $ep_query['found_documents'] ) ? $ep_query['found_documents']['value'] : $ep_query['found_documents']; // 7.0+ have this as an array rather than int
358 $query->found_posts = $found_documents;
359 $query->num_posts = $query->found_posts;
360 $query->max_num_pages = ceil( $found_documents / $query->get( 'posts_per_page' ) );
361 $query->elasticsearch_success = true;
362
363 // Determine how we should format the results from ES based on the fields parameter.
364 $fields = $query->get( 'fields', '' );
365
366 switch ( $fields ) {
367 case 'ids':
368 $new_posts = $this->format_hits_as_ids( $ep_query['documents'], $new_posts );
369 break;
370
371 case 'id=>parent':
372 $new_posts = $this->format_hits_as_id_parents( $ep_query['documents'], $new_posts );
373 break;
374
375 default:
376 $new_posts = $this->format_hits_as_posts( $ep_query['documents'], $new_posts );
377 break;
378 }
379
380 /**
381 * Fires after non cached post query
382 *
383 * @hook ep_wp_query_non_cached_search
384 * @param {array} $new_posts Array of posts from query
385 * @param {array} $ep_query Raw Elasticsearch query
386 * @param {WP_Query} $query WordPress query
387 */
388 do_action( 'ep_wp_query_non_cached_search', $new_posts, $ep_query, $query );
389 }
390
391 /**
392 * Fires before returning posts from query
393 *
394 * @hook ep_wp_query
395 * @param {array} $new_posts Array of posts from query
396 * @param {array} $ep_query Raw Elasticsearch query
397 * @param {WP_Query} $query WordPress query
398 */
399 do_action( 'ep_wp_query', $new_posts, $ep_query, $query );
400
401 /**
402 * Fires before returning posts from query
403 *
404 * Pre-3.0 backwards compat
405 *
406 * @hook ep_wp_query_search
407 * @param {array} $new_posts Array of posts from query
408 * @param {array} $ep_query Raw Elasticsearch query
409 * @param {WP_Query} $query WordPress query
410 */
411 do_action( 'ep_wp_query_search', $new_posts, $ep_query, $query );
412
413 return $new_posts;
414 }
415
416 /**
417 * Format the ES hits/results as post objects.
418 *
419 * @since 2.4.0
420 *
421 * @param array $posts The posts that should be formatted.
422 * @param array $new_posts Array of posts from cache.
423 *
424 * @return array
425 */
426 protected function format_hits_as_posts( $posts, $new_posts ) {
427 foreach ( $posts as $post_array ) {
428 $post = new \stdClass();
429
430 $post->ID = $post_array['post_id'];
431 $post->site_id = get_current_blog_id();
432
433 if ( ! empty( $post_array['site_id'] ) ) {
434 $post->site_id = $post_array['site_id'];
435 }
436 /**
437 * Filter post object properties set after query
438 *
439 * @hook ep_search_post_return_args
440 * @param {array} $properties Post properties
441 * @return {array} New properties
442 */
443 $post_return_args = apply_filters(
444 'ep_search_post_return_args',
445 array(
446 'post_type',
447 'post_author',
448 'post_name',
449 'post_status',
450 'post_title',
451 'post_parent',
452 'post_content',
453 'post_excerpt',
454 'post_date',
455 'post_date_gmt',
456 'post_modified',
457 'post_modified_gmt',
458 'post_mime_type',
459 'comment_count',
460 'comment_status',
461 'ping_status',
462 'menu_order',
463 'permalink',
464 'terms',
465 'post_meta',
466 'meta',
467 )
468 );
469
470 foreach ( $post_return_args as $key ) {
471 if ( 'post_author' === $key ) {
472 $post->$key = $post_array[ $key ]['id'];
473 } elseif ( isset( $post_array[ $key ] ) ) {
474 $post->$key = $post_array[ $key ];
475 }
476 }
477
478 /**
479 * Replace post attributes with highlighted versions if available.
480 *
481 * $post_array['highlight'] is set from $hit['highlight'] in Elasticsearch.php
482 * when going through the returned results, and that is defined by
483 * the Highlighting Feature on setup, calling ep_formatted_args to
484 * define the highlight array of fields.
485 */
486 if ( isset( $post_array['highlight'] ) ) {
487 foreach ( $post_array['highlight'] as $key => $val ) {
488 // e.g. $post->post_content
489 if ( isset( $post->$key ) ) {
490 /**
491 * e.g. replaces post content value with the highlighted value
492 * $post->post_content = implode( ' ', $post_array['highlight']['post_content'] );
493 *
494 * Depending on how highlight.fields.<field>.number_of_fragments is set in the query,
495 * Elasticsearch can return an array with N entries, with N being the number
496 * of matches found.
497 */
498 $post->$key = implode( ' ', $val );
499 }
500 }
501 }
502
503 $post->elasticsearch = true; // Super useful for debugging
504
505 if ( $post ) {
506 $new_posts[] = $post;
507 }
508 }
509
510 return $new_posts;
511 }
512
513 /**
514 * Format the ES hits/results as an array of ids.
515 *
516 * @since 2.4.0
517 *
518 * @param array $posts The posts that should be formatted.
519 * @param array $new_posts Array of posts from cache.
520 *
521 * @return array
522 */
523 protected function format_hits_as_ids( $posts, $new_posts ) {
524 foreach ( $posts as $post_array ) {
525 $new_posts[] = $post_array['post_id'];
526 }
527
528 return $new_posts;
529 }
530
531 /**
532 * Format the ES hits/results as objects containing id and parent id.
533 *
534 * @since 2.4.0
535 *
536 * @param array $posts The posts that should be formatted.
537 * @param array $new_posts Array of posts from cache.
538 *
539 * @return array
540 */
541 protected function format_hits_as_id_parents( $posts, $new_posts ) {
542 foreach ( $posts as $post_array ) {
543 $post = new \stdClass();
544 $post->ID = $post_array['post_id'];
545 $post->post_parent = $post_array['post_parent'];
546 $post->elasticsearch = true; // Super useful for debugging
547 $new_posts[] = $post;
548 }
549 return $new_posts;
550 }
551 }
552