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

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