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

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