PluginProbe
ElasticPress / 4.5.1
ElasticPress v4.5.1
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.5.1, at includes/classes/Indexable/Post/QueryIntegration.php

553 lines 14.8 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 _deprecated_argument( __FUNCTION__, '4.4.0', esc_html__( 'sites is deprecated. Use site__in instead.', 'elasticpress' ) );
284 }
285
286 if ( ! empty( $query_vars['site__in'] ) || ! empty( $query_vars['sites'] ) ) {
287 $site__in = ! empty( $query_vars['site__in'] ) ? (array) $query_vars['site__in'] : (array) $query_vars['sites'];
288
289 if ( in_array( 'all', $site__in, true ) ) {
290 $scope = 'all';
291 } elseif ( in_array( 'current', $site__in, true ) ) {
292 $site__in = (array) get_current_blog_id();
293 }
294 }
295
296 if ( ! empty( $query_vars['site__not_in'] ) ) {
297 $site__not_in = (array) $query_vars['site__not_in'];
298 }
299
300 $formatted_args = Indexables::factory()->get( 'post' )->format_args( $query_vars, $query );
301
302 /**
303 * Filter post query scope
304 *
305 * @hook ep_search_scope
306 * @param {string} $scope Current scope
307 * @return {string} New scope
308 * @since 2.1
309 */
310 $scope = apply_filters( 'ep_search_scope', $scope );
311
312 if ( ! defined( 'EP_IS_NETWORK' ) || ! EP_IS_NETWORK ) {
313 // @codeCoverageIgnoreStart
314 $scope = 'current';
315 // @codeCoverageIgnoreEnd
316 }
317
318 $index = null;
319
320 if ( 'all' === $scope ) {
321 $index = Indexables::factory()->get( 'post' )->get_network_alias();
322 } elseif ( ! empty( $site__in ) ) {
323 $index = [];
324
325 foreach ( $site__in as $site_id ) {
326 $index[] = Indexables::factory()->get( 'post' )->get_index_name( $site_id );
327 }
328
329 $index = implode( ',', $index );
330 } elseif ( ! empty( $site__not_in ) ) {
331
332 $sites = get_sites(
333 array(
334 'fields' => 'ids',
335 'site__not_in' => $site__not_in,
336 )
337 );
338 foreach ( $sites as $site_id ) {
339 if ( ! Utils\is_site_indexable( $site_id ) ) {
340 continue;
341 }
342 $index[] = Indexables::factory()->get( 'post' )->get_index_name( $site_id );
343 }
344
345 $index = implode( ',', $index );
346 }
347
348 $ep_query = Indexables::factory()->get( 'post' )->query_es( $formatted_args, $query->query_vars, $index, $query );
349
350 /**
351 * ES failed. Go back to MySQL.
352 */
353 if ( false === $ep_query ) {
354 $query->elasticsearch_success = false;
355 return null;
356 }
357
358 $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
359 $query->found_posts = $found_documents;
360 $query->num_posts = $query->found_posts;
361 $query->max_num_pages = ceil( $found_documents / $query->get( 'posts_per_page' ) );
362 $query->elasticsearch_success = true;
363
364 // Determine how we should format the results from ES based on the fields parameter.
365 $fields = $query->get( 'fields', '' );
366
367 switch ( $fields ) {
368 case 'ids':
369 $new_posts = $this->format_hits_as_ids( $ep_query['documents'], $new_posts );
370 break;
371
372 case 'id=>parent':
373 $new_posts = $this->format_hits_as_id_parents( $ep_query['documents'], $new_posts );
374 break;
375
376 default:
377 $new_posts = $this->format_hits_as_posts( $ep_query['documents'], $new_posts );
378 break;
379 }
380
381 /**
382 * Fires after non cached post query
383 *
384 * @hook ep_wp_query_non_cached_search
385 * @param {array} $new_posts Array of posts from query
386 * @param {array} $ep_query Raw Elasticsearch query
387 * @param {WP_Query} $query WordPress query
388 */
389 do_action( 'ep_wp_query_non_cached_search', $new_posts, $ep_query, $query );
390 }
391
392 /**
393 * Fires before returning posts from query
394 *
395 * @hook ep_wp_query
396 * @param {array} $new_posts Array of posts from query
397 * @param {array} $ep_query Raw Elasticsearch query
398 * @param {WP_Query} $query WordPress query
399 */
400 do_action( 'ep_wp_query', $new_posts, $ep_query, $query );
401
402 /**
403 * Fires before returning posts from query
404 *
405 * Pre-3.0 backwards compat
406 *
407 * @hook ep_wp_query_search
408 * @param {array} $new_posts Array of posts from query
409 * @param {array} $ep_query Raw Elasticsearch query
410 * @param {WP_Query} $query WordPress query
411 */
412 do_action( 'ep_wp_query_search', $new_posts, $ep_query, $query );
413
414 return $new_posts;
415 }
416
417 /**
418 * Format the ES hits/results as post objects.
419 *
420 * @since 2.4.0
421 *
422 * @param array $posts The posts that should be formatted.
423 * @param array $new_posts Array of posts from cache.
424 *
425 * @return array
426 */
427 protected function format_hits_as_posts( $posts, $new_posts ) {
428 foreach ( $posts as $post_array ) {
429 $post = new \stdClass();
430
431 $post->ID = $post_array['post_id'];
432 $post->site_id = get_current_blog_id();
433
434 if ( ! empty( $post_array['site_id'] ) ) {
435 $post->site_id = $post_array['site_id'];
436 }
437 /**
438 * Filter post object properties set after query
439 *
440 * @hook ep_search_post_return_args
441 * @param {array} $properties Post properties
442 * @return {array} New properties
443 */
444 $post_return_args = apply_filters(
445 'ep_search_post_return_args',
446 array(
447 'post_type',
448 'post_author',
449 'post_name',
450 'post_status',
451 'post_title',
452 'post_parent',
453 'post_content',
454 'post_excerpt',
455 'post_date',
456 'post_date_gmt',
457 'post_modified',
458 'post_modified_gmt',
459 'post_mime_type',
460 'comment_count',
461 'comment_status',
462 'ping_status',
463 'menu_order',
464 'permalink',
465 'terms',
466 'post_meta',
467 'meta',
468 )
469 );
470
471 foreach ( $post_return_args as $key ) {
472 if ( 'post_author' === $key ) {
473 $post->$key = $post_array[ $key ]['id'];
474 } elseif ( isset( $post_array[ $key ] ) ) {
475 $post->$key = $post_array[ $key ];
476 }
477 }
478
479 /**
480 * Replace post attributes with highlighted versions if available.
481 *
482 * $post_array['highlight'] is set from $hit['highlight'] in Elasticsearch.php
483 * when going through the returned results, and that is defined by
484 * the Highlighting Feature on setup, calling ep_formatted_args to
485 * define the highlight array of fields.
486 */
487 if ( isset( $post_array['highlight'] ) ) {
488 foreach ( $post_array['highlight'] as $key => $val ) {
489 // e.g. $post->post_content
490 if ( isset( $post->$key ) ) {
491 /**
492 * e.g. replaces post content value with the highlighted value
493 * $post->post_content = implode( ' ', $post_array['highlight']['post_content'] );
494 *
495 * Depending on how highlight.fields.<field>.number_of_fragments is set in the query,
496 * Elasticsearch can return an array with N entries, with N being the number
497 * of matches found.
498 */
499 $post->$key = implode( ' ', $val );
500 }
501 }
502 }
503
504 $post->elasticsearch = true; // Super useful for debugging
505
506 if ( $post ) {
507 $new_posts[] = $post;
508 }
509 }
510
511 return $new_posts;
512 }
513
514 /**
515 * Format the ES hits/results as an array of ids.
516 *
517 * @since 2.4.0
518 *
519 * @param array $posts The posts that should be formatted.
520 * @param array $new_posts Array of posts from cache.
521 *
522 * @return array
523 */
524 protected function format_hits_as_ids( $posts, $new_posts ) {
525 foreach ( $posts as $post_array ) {
526 $new_posts[] = $post_array['post_id'];
527 }
528
529 return $new_posts;
530 }
531
532 /**
533 * Format the ES hits/results as objects containing id and parent id.
534 *
535 * @since 2.4.0
536 *
537 * @param array $posts The posts that should be formatted.
538 * @param array $new_posts Array of posts from cache.
539 *
540 * @return array
541 */
542 protected function format_hits_as_id_parents( $posts, $new_posts ) {
543 foreach ( $posts as $post_array ) {
544 $post = new \stdClass();
545 $post->ID = $post_array['post_id'];
546 $post->post_parent = $post_array['post_parent'];
547 $post->elasticsearch = true; // Super useful for debugging
548 $new_posts[] = $post;
549 }
550 return $new_posts;
551 }
552 }
553