Completed
Push — try/dummy-deactivate-dialog ( bb9008...b7ea92 )
by
unknown
19:07 queued 12:31
created

Jetpack_Search::store_last_query_info()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 1
dl 0
loc 4
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * Jetpack Search: Main Jetpack_Search class
4
 *
5
 * @package    Jetpack
6
 * @subpackage Jetpack Search
7
 * @since      5.0.0
8
 */
9
10
use Automattic\Jetpack\Connection\Client;
11
12
/**
13
 * The main class for the Jetpack Search module.
14
 *
15
 * @since 5.0.0
16
 */
17
class Jetpack_Search {
18
19
	/**
20
	 * The number of found posts.
21
	 *
22
	 * @since 5.0.0
23
	 *
24
	 * @var int
25
	 */
26
	protected $found_posts = 0;
27
28
	/**
29
	 * The search result, as returned by the WordPress.com REST API.
30
	 *
31
	 * @since 5.0.0
32
	 *
33
	 * @var array
34
	 */
35
	protected $search_result;
36
37
	/**
38
	 * This site's blog ID on WordPress.com.
39
	 *
40
	 * @since 5.0.0
41
	 *
42
	 * @var int
43
	 */
44
	protected $jetpack_blog_id;
45
46
	/**
47
	 * The Elasticsearch aggregations (filters).
48
	 *
49
	 * @since 5.0.0
50
	 *
51
	 * @var array
52
	 */
53
	protected $aggregations = array();
54
55
	/**
56
	 * The maximum number of aggregations allowed.
57
	 *
58
	 * @since 5.0.0
59
	 *
60
	 * @var int
61
	 */
62
	protected $max_aggregations_count = 100;
63
64
	/**
65
	 * Statistics about the last Elasticsearch query.
66
	 *
67
	 * @since 5.6.0
68
	 *
69
	 * @var array
70
	 */
71
	protected $last_query_info = array();
72
73
	/**
74
	 * Statistics about the last Elasticsearch query failure.
75
	 *
76
	 * @since 5.6.0
77
	 *
78
	 * @var array
79
	 */
80
	protected $last_query_failure_info = array();
81
82
	/**
83
	 * The singleton instance of this class.
84
	 *
85
	 * @since 5.0.0
86
	 *
87
	 * @var Jetpack_Search
88
	 */
89
	protected static $instance;
90
91
	/**
92
	 * Languages with custom analyzers. Other languages are supported, but are analyzed with the default analyzer.
93
	 *
94
	 * @since 5.0.0
95
	 *
96
	 * @var array
97
	 */
98
	public static $analyzed_langs = array( 'ar', 'bg', 'ca', 'cs', 'da', 'de', 'el', 'en', 'es', 'eu', 'fa', 'fi', 'fr', 'he', 'hi', 'hu', 'hy', 'id', 'it', 'ja', 'ko', 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr', 'zh' );
99
100
	/**
101
	 * Jetpack_Search constructor.
102
	 *
103
	 * @since 5.0.0
104
	 *
105
	 * Doesn't do anything. This class needs to be initialized via the instance() method instead.
106
	 */
107
	protected function __construct() {
108
	}
109
110
	/**
111
	 * Prevent __clone()'ing of this class.
112
	 *
113
	 * @since 5.0.0
114
	 */
115
	public function __clone() {
116
		wp_die( "Please don't __clone Jetpack_Search" );
117
	}
118
119
	/**
120
	 * Prevent __wakeup()'ing of this class.
121
	 *
122
	 * @since 5.0.0
123
	 */
124
	public function __wakeup() {
125
		wp_die( "Please don't __wakeup Jetpack_Search" );
126
	}
127
128
	/**
129
	 * Get singleton instance of Jetpack_Search.
130
	 *
131
	 * Instantiates and sets up a new instance if needed, or returns the singleton.
132
	 *
133
	 * @since 5.0.0
134
	 *
135
	 * @return Jetpack_Search The Jetpack_Search singleton.
136
	 */
137
	public static function instance() {
138
		if ( ! isset( self::$instance ) ) {
139
			self::$instance = new Jetpack_Search();
140
141
			self::$instance->setup();
142
		}
143
144
		return self::$instance;
145
	}
146
147
	/**
148
	 * Perform various setup tasks for the class.
149
	 *
150
	 * Checks various pre-requisites and adds hooks.
151
	 *
152
	 * @since 5.0.0
153
	 */
154
	public function setup() {
155
		if ( ! Jetpack::is_active() || ! Jetpack_Plan::supports( 'search' ) ) {
156
			return;
157
		}
158
159
		$this->jetpack_blog_id = Jetpack::get_option( 'id' );
160
161
		if ( ! $this->jetpack_blog_id ) {
162
			return;
163
		}
164
165
		require_once dirname( __FILE__ ) . '/class.jetpack-search-helpers.php';
166
		require_once dirname( __FILE__ ) . '/class.jetpack-search-template-tags.php';
167
		require_once JETPACK__PLUGIN_DIR . 'modules/widgets/search.php';
168
169
		$this->init_hooks();
170
	}
171
172
	/**
173
	 * Setup the various hooks needed for the plugin to take over search duties.
174
	 *
175
	 * @since 5.0.0
176
	 */
177
	public function init_hooks() {
178
		if ( ! is_admin() ) {
179
			add_filter( 'posts_pre_query', array( $this, 'filter__posts_pre_query' ), 10, 2 );
180
181
			add_filter( 'jetpack_search_es_wp_query_args', array( $this, 'filter__add_date_filter_to_query' ), 10, 2 );
182
183
			add_action( 'did_jetpack_search_query', array( $this, 'store_last_query_info' ) );
184
			add_action( 'failed_jetpack_search_query', array( $this, 'store_query_failure' ) );
185
186
			add_action( 'init', array( $this, 'set_filters_from_widgets' ) );
187
188
			add_action( 'pre_get_posts', array( $this, 'maybe_add_post_type_as_var' ) );
189
		} else {
190
			add_action( 'update_option', array( $this, 'track_widget_updates' ), 10, 3 );
191
		}
192
193
		add_action( 'jetpack_deactivate_module_search', array( $this, 'move_search_widgets_to_inactive' ) );
194
	}
195
196
	/**
197
	 * When an Elasticsearch query fails, this stores it and enqueues some debug information in the footer.
198
	 *
199
	 * @since 5.6.0
200
	 *
201
	 * @param array $meta Information about the failure.
202
	 */
203
	public function store_query_failure( $meta ) {
204
		$this->last_query_failure_info = $meta;
205
		add_action( 'wp_footer', array( $this, 'print_query_failure' ) );
206
	}
207
208
	/**
209
	 * Outputs information about the last Elasticsearch failure.
210
	 *
211
	 * @since 5.6.0
212
	 */
213
	public function print_query_failure() {
214
		if ( $this->last_query_failure_info ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->last_query_failure_info of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
215
			printf(
216
				'<!-- Jetpack Search failed with code %s: %s - %s -->',
217
				esc_html( $this->last_query_failure_info['response_code'] ),
218
				esc_html( $this->last_query_failure_info['json']['error'] ),
219
				esc_html( $this->last_query_failure_info['json']['message'] )
220
			);
221
		}
222
	}
223
224
	/**
225
	 * Stores information about the last Elasticsearch query and enqueues some debug information in the footer.
226
	 *
227
	 * @since 5.6.0
228
	 *
229
	 * @param array $meta Information about the query.
230
	 */
231
	public function store_last_query_info( $meta ) {
232
		$this->last_query_info = $meta;
233
		add_action( 'wp_footer', array( $this, 'print_query_success' ) );
234
	}
235
236
	/**
237
	 * Outputs information about the last Elasticsearch search.
238
	 *
239
	 * @since 5.6.0
240
	 */
241
	public function print_query_success() {
242
		if ( $this->last_query_info ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->last_query_info of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
243
			printf(
244
				'<!-- Jetpack Search took %s ms, ES time %s ms -->',
245
				intval( $this->last_query_info['elapsed_time'] ),
246
				esc_html( $this->last_query_info['es_time'] )
247
			);
248
249
			if ( isset( $_GET['searchdebug'] ) ) {
250
				printf(
251
					'<!-- Query response data: %s -->',
252
					esc_html( print_r( $this->last_query_info, 1 ) )
253
				);
254
			}
255
		}
256
	}
257
258
	/**
259
	 * Returns the last query information, or false if no information was stored.
260
	 *
261
	 * @since 5.8.0
262
	 *
263
	 * @return bool|array
264
	 */
265
	public function get_last_query_info() {
266
		return empty( $this->last_query_info ) ? false : $this->last_query_info;
267
	}
268
269
	/**
270
	 * Returns the last query failure information, or false if no failure information was stored.
271
	 *
272
	 * @since 5.8.0
273
	 *
274
	 * @return bool|array
275
	 */
276
	public function get_last_query_failure_info() {
277
		return empty( $this->last_query_failure_info ) ? false : $this->last_query_failure_info;
278
	}
279
280
	/**
281
	 * Wraps a WordPress filter called "jetpack_search_disable_widget_filters" that allows
282
	 * developers to disable filters supplied by the search widget. Useful if filters are
283
	 * being defined at the code level.
284
	 *
285
	 * @since      5.7.0
286
	 * @deprecated 5.8.0 Use Jetpack_Search_Helpers::are_filters_by_widget_disabled() directly.
287
	 *
288
	 * @return bool
289
	 */
290
	public function are_filters_by_widget_disabled() {
291
		return Jetpack_Search_Helpers::are_filters_by_widget_disabled();
292
	}
293
294
	/**
295
	 * Retrieves a list of known Jetpack search filters widget IDs, gets the filters for each widget,
296
	 * and applies those filters to this Jetpack_Search object.
297
	 *
298
	 * @since 5.7.0
299
	 */
300
	public function set_filters_from_widgets() {
301
		if ( Jetpack_Search_Helpers::are_filters_by_widget_disabled() ) {
302
			return;
303
		}
304
305
		$filters = Jetpack_Search_Helpers::get_filters_from_widgets();
306
307
		if ( ! empty( $filters ) ) {
308
			$this->set_filters( $filters );
309
		}
310
	}
311
312
	/**
313
	 * Restricts search results to certain post types via a GET argument.
314
	 *
315
	 * @since 5.8.0
316
	 *
317
	 * @param WP_Query $query A WP_Query instance.
318
	 */
319
	public function maybe_add_post_type_as_var( WP_Query $query ) {
320
		if ( $this->should_handle_query( $query ) && ! empty( $_GET['post_type'] ) ) {
321
			$post_types = ( is_string( $_GET['post_type'] ) && false !== strpos( $_GET['post_type'], ',' ) )
322
				? $post_type = explode( ',', $_GET['post_type'] )
323
				: (array) $_GET['post_type'];
324
			$post_types = array_map( 'sanitize_key', $post_types );
325
			$query->set( 'post_type', $post_types );
326
		}
327
	}
328
329
	/*
330
	 * Run a search on the WordPress.com public API.
331
	 *
332
	 * @since 5.0.0
333
	 *
334
	 * @param array $es_args Args conforming to the WP.com /sites/<blog_id>/search endpoint.
335
	 *
336
	 * @return object|WP_Error The response from the public API, or a WP_Error.
337
	 */
338
	public function search( array $es_args ) {
339
		$endpoint    = sprintf( '/sites/%s/search', $this->jetpack_blog_id );
340
		$service_url = 'https://public-api.wordpress.com/rest/v1' . $endpoint;
341
342
		$do_authenticated_request = false;
343
344
		if ( class_exists( 'Client' ) &&
345
			isset( $es_args['authenticated_request'] ) &&
346
			true === $es_args['authenticated_request'] ) {
347
			$do_authenticated_request = true;
348
		}
349
350
		unset( $es_args['authenticated_request'] );
351
352
		$request_args = array(
353
			'headers' => array(
354
				'Content-Type' => 'application/json',
355
			),
356
			'timeout'    => 10,
357
			'user-agent' => 'jetpack_search',
358
		);
359
360
		$request_body = wp_json_encode( $es_args );
361
362
		$start_time = microtime( true );
363
364
		if ( $do_authenticated_request ) {
365
			$request_args['method'] = 'POST';
366
367
			$request = Client::wpcom_json_api_request_as_blog( $endpoint, Client::WPCOM_JSON_API_VERSION, $request_args, $request_body );
368
		} else {
369
			$request_args = array_merge( $request_args, array(
370
				'body' => $request_body,
371
			) );
372
373
			$request = wp_remote_post( $service_url, $request_args );
374
		}
375
376
		$end_time = microtime( true );
377
378
		if ( is_wp_error( $request ) ) {
379
			return $request;
380
		}
381
382
		$response_code = wp_remote_retrieve_response_code( $request );
383
384
		$response = json_decode( wp_remote_retrieve_body( $request ), true );
385
386
		$took = is_array( $response ) && ! empty( $response['took'] )
387
			? $response['took']
388
			: null;
389
390
		$query = array(
391
			'args'          => $es_args,
392
			'response'      => $response,
393
			'response_code' => $response_code,
394
			'elapsed_time'  => ( $end_time - $start_time ) * 1000, // Convert from float seconds to ms.
395
			'es_time'       => $took,
396
			'url'           => $service_url,
397
		);
398
399
		/**
400
		 * Fires after a search request has been performed.
401
		 *
402
		 * Includes the following info in the $query parameter:
403
		 *
404
		 * array args Array of Elasticsearch arguments for the search
405
		 * array response Raw API response, JSON decoded
406
		 * int response_code HTTP response code of the request
407
		 * float elapsed_time Roundtrip time of the search request, in milliseconds
408
		 * float es_time Amount of time Elasticsearch spent running the request, in milliseconds
409
		 * string url API url that was queried
410
		 *
411
		 * @module search
412
		 *
413
		 * @since  5.0.0
414
		 * @since  5.8.0 This action now fires on all queries instead of just successful queries.
415
		 *
416
		 * @param array $query Array of information about the query performed
417
		 */
418
		do_action( 'did_jetpack_search_query', $query );
419
420
		if ( ! $response_code || $response_code < 200 || $response_code >= 300 ) {
421
			/**
422
			 * Fires after a search query request has failed
423
			 *
424
			 * @module search
425
			 *
426
			 * @since  5.6.0
427
			 *
428
			 * @param array Array containing the response code and response from the failed search query
429
			 */
430
			do_action( 'failed_jetpack_search_query', array(
431
				'response_code' => $response_code,
432
				'json'          => $response,
433
			) );
434
435
			return new WP_Error( 'invalid_search_api_response', 'Invalid response from API - ' . $response_code );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'invalid_search_api_response'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
436
		}
437
438
		return $response;
439
	}
440
441
	/**
442
	 * Bypass the normal Search query and offload it to Jetpack servers.
443
	 *
444
	 * This is the main hook of the plugin and is responsible for returning the posts that match the search query.
445
	 *
446
	 * @since 5.0.0
447
	 *
448
	 * @param array    $posts Current array of posts (still pre-query).
449
	 * @param WP_Query $query The WP_Query being filtered.
450
	 *
451
	 * @return array Array of matching posts.
452
	 */
453
	public function filter__posts_pre_query( $posts, $query ) {
454
		if ( ! $this->should_handle_query( $query ) ) {
455
			return $posts;
456
		}
457
458
		$this->do_search( $query );
459
460
		if ( ! is_array( $this->search_result ) ) {
461
			return $posts;
462
		}
463
464
		// If no results, nothing to do
465
		if ( ! count( $this->search_result['results']['hits'] ) ) {
466
			return array();
467
		}
468
469
		$post_ids = array();
470
471
		foreach ( $this->search_result['results']['hits'] as $result ) {
472
			$post_ids[] = (int) $result['fields']['post_id'];
473
		}
474
475
		// Query all posts now
476
		$args = array(
477
			'post__in'            => $post_ids,
478
			'orderby'             => 'post__in',
479
			'perm'                => 'readable',
480
			'post_type'           => 'any',
481
			'ignore_sticky_posts' => true,
482
			'suppress_filters'    => true,
483
		);
484
485
		$posts_query = new WP_Query( $args );
486
487
		// WP Core doesn't call the set_found_posts and its filters when filtering posts_pre_query like we do, so need to do these manually.
488
		$query->found_posts   = $this->found_posts;
489
		$query->max_num_pages = ceil( $this->found_posts / $query->get( 'posts_per_page' ) );
490
491
		return $posts_query->posts;
492
	}
493
494
	/**
495
	 * Build up the search, then run it against the Jetpack servers.
496
	 *
497
	 * @since 5.0.0
498
	 *
499
	 * @param WP_Query $query The original WP_Query to use for the parameters of our search.
500
	 */
501
	public function do_search( WP_Query $query ) {
502
		if ( ! $this->should_handle_query( $query ) ) {
503
			return;
504
		}
505
506
		$page = ( $query->get( 'paged' ) ) ? absint( $query->get( 'paged' ) ) : 1;
507
508
		// Get maximum allowed offset and posts per page values for the API.
509
		$max_offset         = Jetpack_Search_Helpers::get_max_offset();
510
		$max_posts_per_page = Jetpack_Search_Helpers::get_max_posts_per_page();
511
512
		$posts_per_page = $query->get( 'posts_per_page' );
513
		if ( $posts_per_page > $max_posts_per_page ) {
514
			$posts_per_page = $max_posts_per_page;
515
		}
516
517
		// Start building the WP-style search query args.
518
		// They'll be translated to ES format args later.
519
		$es_wp_query_args = array(
520
			'query'          => $query->get( 's' ),
521
			'posts_per_page' => $posts_per_page,
522
			'paged'          => $page,
523
			'orderby'        => $query->get( 'orderby' ),
524
			'order'          => $query->get( 'order' ),
525
		);
526
527
		if ( ! empty( $this->aggregations ) ) {
528
			$es_wp_query_args['aggregations'] = $this->aggregations;
529
		}
530
531
		// Did we query for authors?
532
		if ( $query->get( 'author_name' ) ) {
533
			$es_wp_query_args['author_name'] = $query->get( 'author_name' );
534
		}
535
536
		$es_wp_query_args['post_type'] = $this->get_es_wp_query_post_type_for_query( $query );
537
		$es_wp_query_args['terms']     = $this->get_es_wp_query_terms_for_query( $query );
538
539
		/**
540
		 * Modify the search query parameters, such as controlling the post_type.
541
		 *
542
		 * These arguments are in the format of WP_Query arguments
543
		 *
544
		 * @module search
545
		 *
546
		 * @since  5.0.0
547
		 *
548
		 * @param array    $es_wp_query_args The current query args, in WP_Query format.
549
		 * @param WP_Query $query            The original WP_Query object.
550
		 */
551
		$es_wp_query_args = apply_filters( 'jetpack_search_es_wp_query_args', $es_wp_query_args, $query );
552
553
		// If page * posts_per_page is greater than our max offset, send a 404. This is necessary because the offset is
554
		// capped at Jetpack_Search_Helpers::get_max_offset(), so a high page would always return the last page of results otherwise.
555
		if ( ( $es_wp_query_args['paged'] * $es_wp_query_args['posts_per_page'] ) > $max_offset ) {
556
			$query->set_404();
557
558
			return;
559
		}
560
561
		// If there were no post types returned, then 404 to avoid querying against non-public post types, which could
562
		// happen if we don't add the post type restriction to the ES query.
563
		if ( empty( $es_wp_query_args['post_type'] ) ) {
564
			$query->set_404();
565
566
			return;
567
		}
568
569
		// Convert the WP-style args into ES args.
570
		$es_query_args = $this->convert_wp_es_to_es_args( $es_wp_query_args );
571
572
		//Only trust ES to give us IDs, not the content since it is a mirror
573
		$es_query_args['fields'] = array(
574
			'post_id',
575
		);
576
577
		/**
578
		 * Modify the underlying ES query that is passed to the search endpoint. The returned args must represent a valid ES query
579
		 *
580
		 * This filter is harder to use if you're unfamiliar with ES, but allows complete control over the query
581
		 *
582
		 * @module search
583
		 *
584
		 * @since  5.0.0
585
		 *
586
		 * @param array    $es_query_args The raw Elasticsearch query args.
587
		 * @param WP_Query $query         The original WP_Query object.
588
		 */
589
		$es_query_args = apply_filters( 'jetpack_search_es_query_args', $es_query_args, $query );
590
591
		// Do the actual search query!
592
		$this->search_result = $this->search( $es_query_args );
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->search($es_query_args) of type object is incompatible with the declared type array of property $search_result.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
593
594
		if ( is_wp_error( $this->search_result ) || ! is_array( $this->search_result ) || empty( $this->search_result['results'] ) || empty( $this->search_result['results']['hits'] ) ) {
595
			$this->found_posts = 0;
596
597
			return;
598
		}
599
600
		// If we have aggregations, fix the ordering to match the input order (ES doesn't guarantee the return order).
601
		if ( isset( $this->search_result['results']['aggregations'] ) && ! empty( $this->search_result['results']['aggregations'] ) ) {
602
			$this->search_result['results']['aggregations'] = $this->fix_aggregation_ordering( $this->search_result['results']['aggregations'], $this->aggregations );
603
		}
604
605
		// Total number of results for paging purposes. Capped at $max_offset + $posts_per_page, as deep paging gets quite expensive.
606
		$this->found_posts = min( $this->search_result['results']['total'], $max_offset + $posts_per_page );
607
	}
608
609
	/**
610
	 * If the query has already been run before filters have been updated, then we need to re-run the query
611
	 * to get the latest aggregations.
612
	 *
613
	 * This is especially useful for supporting widget management in the customizer.
614
	 *
615
	 * @since 5.8.0
616
	 *
617
	 * @return bool Whether the query was successful or not.
618
	 */
619
	public function update_search_results_aggregations() {
620
		if ( empty( $this->last_query_info ) || empty( $this->last_query_info['args'] ) ) {
621
			return false;
622
		}
623
624
		$es_args = $this->last_query_info['args'];
625
		$builder = new Jetpack_WPES_Query_Builder();
626
		$this->add_aggregations_to_es_query_builder( $this->aggregations, $builder );
627
		$es_args['aggregations'] = $builder->build_aggregation();
628
629
		$this->search_result = $this->search( $es_args );
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->search($es_args) of type object is incompatible with the declared type array of property $search_result.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
630
631
		return ! is_wp_error( $this->search_result );
632
	}
633
634
	/**
635
	 * Given a WP_Query, convert its WP_Tax_Query (if present) into the WP-style Elasticsearch term arguments for the search.
636
	 *
637
	 * @since 5.0.0
638
	 *
639
	 * @param WP_Query $query The original WP_Query object for which to parse the taxonomy query.
640
	 *
641
	 * @return array The new WP-style Elasticsearch arguments (that will be converted into 'real' Elasticsearch arguments).
642
	 */
643
	public function get_es_wp_query_terms_for_query( WP_Query $query ) {
644
		$args = array();
645
646
		$the_tax_query = $query->tax_query;
647
648
		if ( ! $the_tax_query ) {
649
			return $args;
650
		}
651
652
653
		if ( ! $the_tax_query instanceof WP_Tax_Query || empty( $the_tax_query->queried_terms ) || ! is_array( $the_tax_query->queried_terms ) ) {
0 ignored issues
show
Bug introduced by
The class WP_Tax_Query does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
654
			return $args;
655
		}
656
657
		$args = array();
658
659
		foreach ( $the_tax_query->queries as $tax_query ) {
660
			// Right now we only support slugs...see note above
661
			if ( ! is_array( $tax_query ) || 'slug' !== $tax_query['field'] ) {
662
				continue;
663
			}
664
665
			$taxonomy = $tax_query['taxonomy'];
666
667 View Code Duplication
			if ( ! isset( $args[ $taxonomy ] ) || ! is_array( $args[ $taxonomy ] ) ) {
668
				$args[ $taxonomy ] = array();
669
			}
670
671
			$args[ $taxonomy ] = array_merge( $args[ $taxonomy ], $tax_query['terms'] );
672
		}
673
674
		return $args;
675
	}
676
677
	/**
678
	 * Parse out the post type from a WP_Query.
679
	 *
680
	 * Only allows post types that are not marked as 'exclude_from_search'.
681
	 *
682
	 * @since 5.0.0
683
	 *
684
	 * @param WP_Query $query Original WP_Query object.
685
	 *
686
	 * @return array Array of searchable post types corresponding to the original query.
687
	 */
688
	public function get_es_wp_query_post_type_for_query( WP_Query $query ) {
689
		$post_types = $query->get( 'post_type' );
690
691
		// If we're searching 'any', we want to only pass searchable post types to Elasticsearch.
692
		if ( 'any' === $post_types ) {
693
			$post_types = array_values( get_post_types( array(
694
				'exclude_from_search' => false,
695
			) ) );
696
		}
697
698
		if ( ! is_array( $post_types ) ) {
699
			$post_types = array( $post_types );
700
		}
701
702
		$post_types = array_unique( $post_types );
703
704
		$sanitized_post_types = array();
705
706
		// Make sure the post types are queryable.
707
		foreach ( $post_types as $post_type ) {
708
			if ( ! $post_type ) {
709
				continue;
710
			}
711
712
			$post_type_object = get_post_type_object( $post_type );
713
			if ( ! $post_type_object || $post_type_object->exclude_from_search ) {
714
				continue;
715
			}
716
717
			$sanitized_post_types[] = $post_type;
718
		}
719
720
		return $sanitized_post_types;
721
	}
722
723
	/**
724
	 * Get the Elasticsearch result.
725
	 *
726
	 * @since 5.0.0
727
	 *
728
	 * @param bool $raw If true, does not check for WP_Error or return the 'results' array - the JSON decoded HTTP response.
729
	 *
730
	 * @return array|bool The search results, or false if there was a failure.
731
	 */
732
	public function get_search_result( $raw = false ) {
733
		if ( $raw ) {
734
			return $this->search_result;
735
		}
736
737
		return ( ! empty( $this->search_result ) && ! is_wp_error( $this->search_result ) && is_array( $this->search_result ) && ! empty( $this->search_result['results'] ) ) ? $this->search_result['results'] : false;
738
	}
739
740
	/**
741
	 * Add the date portion of a WP_Query onto the query args.
742
	 *
743
	 * @since 5.0.0
744
	 *
745
	 * @param array    $es_wp_query_args The Elasticsearch query arguments in WordPress form.
746
	 * @param WP_Query $query            The original WP_Query.
747
	 *
748
	 * @return array The es wp query args, with date filters added (as needed).
749
	 */
750
	public function filter__add_date_filter_to_query( array $es_wp_query_args, WP_Query $query ) {
751
		if ( $query->get( 'year' ) ) {
752
			if ( $query->get( 'monthnum' ) ) {
753
				// Padding
754
				$date_monthnum = sprintf( '%02d', $query->get( 'monthnum' ) );
755
756
				if ( $query->get( 'day' ) ) {
757
					// Padding
758
					$date_day = sprintf( '%02d', $query->get( 'day' ) );
759
760
					$date_start = $query->get( 'year' ) . '-' . $date_monthnum . '-' . $date_day . ' 00:00:00';
761
					$date_end   = $query->get( 'year' ) . '-' . $date_monthnum . '-' . $date_day . ' 23:59:59';
762
				} else {
763
					$days_in_month = date( 't', mktime( 0, 0, 0, $query->get( 'monthnum' ), 14, $query->get( 'year' ) ) ); // 14 = middle of the month so no chance of DST issues
764
765
					$date_start = $query->get( 'year' ) . '-' . $date_monthnum . '-01 00:00:00';
766
					$date_end   = $query->get( 'year' ) . '-' . $date_monthnum . '-' . $days_in_month . ' 23:59:59';
767
				}
768
			} else {
769
				$date_start = $query->get( 'year' ) . '-01-01 00:00:00';
770
				$date_end   = $query->get( 'year' ) . '-12-31 23:59:59';
771
			}
772
773
			$es_wp_query_args['date_range'] = array(
774
				'field' => 'date',
775
				'gte'   => $date_start,
776
				'lte'   => $date_end,
777
			);
778
		}
779
780
		return $es_wp_query_args;
781
	}
782
783
	/**
784
	 * Converts WP_Query style args to Elasticsearch args.
785
	 *
786
	 * @since 5.0.0
787
	 *
788
	 * @param array $args Array of WP_Query style arguments.
789
	 *
790
	 * @return array Array of ES style query arguments.
791
	 */
792
	public function convert_wp_es_to_es_args( array $args ) {
793
		jetpack_require_lib( 'jetpack-wpes-query-builder/jetpack-wpes-query-parser' );
794
795
		$defaults = array(
796
			'blog_id'        => get_current_blog_id(),
797
			'query'          => null,    // Search phrase
798
			'query_fields'   => array(), // list of fields to search
799
			'excess_boost'   => array(), // map of field to excess boost values (multiply)
800
			'post_type'      => null,    // string or an array
801
			'terms'          => array(), // ex: array( 'taxonomy-1' => array( 'slug' ), 'taxonomy-2' => array( 'slug-a', 'slug-b' ) )
802
			'author'         => null,    // id or an array of ids
803
			'author_name'    => array(), // string or an array
804
			'date_range'     => null,    // array( 'field' => 'date', 'gt' => 'YYYY-MM-dd', 'lte' => 'YYYY-MM-dd' ); date formats: 'YYYY-MM-dd' or 'YYYY-MM-dd HH:MM:SS'
805
			'orderby'        => null,    // Defaults to 'relevance' if query is set, otherwise 'date'. Pass an array for multiple orders.
806
			'order'          => 'DESC',
807
			'posts_per_page' => 10,
808
			'offset'         => null,
809
			'paged'          => null,
810
			/**
811
			 * Aggregations. Examples:
812
			 * array(
813
			 *     'Tag'       => array( 'type' => 'taxonomy', 'taxonomy' => 'post_tag', 'count' => 10 ) ),
814
			 *     'Post Type' => array( 'type' => 'post_type', 'count' => 10 ) ),
815
			 * );
816
			 */
817
			'aggregations'   => null,
818
		);
819
820
		$args = wp_parse_args( $args, $defaults );
821
822
		$parser = new Jetpack_WPES_Search_Query_Parser( $args['query'], array( get_locale() ) );
823
824
		if ( empty( $args['query_fields'] ) ) {
825
			if ( defined( 'JETPACK_SEARCH_VIP_INDEX' ) && JETPACK_SEARCH_VIP_INDEX ) {
826
				// VIP indices do not have per language fields
827
				$match_fields = $this->_get_caret_boosted_fields(
828
					array(
829
						'title'         => 0.1,
830
						'content'       => 0.1,
831
						'excerpt'       => 0.1,
832
						'tag.name'      => 0.1,
833
						'category.name' => 0.1,
834
						'author_login'  => 0.1,
835
						'author'        => 0.1,
836
					)
837
				);
838
839
				$boost_fields = $this->_get_caret_boosted_fields(
840
					$this->_apply_boosts_multiplier( array(
841
						'title'         => 2,
842
						'tag.name'      => 1,
843
						'category.name' => 1,
844
						'author_login'  => 1,
845
						'author'        => 1,
846
					), $args['excess_boost'] )
847
				);
848
849
				$boost_phrase_fields = $this->_get_caret_boosted_fields(
850
					array(
851
						'title'         => 1,
852
						'content'       => 1,
853
						'excerpt'       => 1,
854
						'tag.name'      => 1,
855
						'category.name' => 1,
856
						'author'        => 1,
857
					)
858
				);
859
			} else {
860
				$match_fields = $parser->merge_ml_fields(
861
					array(
862
						'title'         => 0.1,
863
						'content'       => 0.1,
864
						'excerpt'       => 0.1,
865
						'tag.name'      => 0.1,
866
						'category.name' => 0.1,
867
					),
868
					$this->_get_caret_boosted_fields( array(
869
						'author_login'  => 0.1,
870
						'author'        => 0.1,
871
					) )
872
				);
873
874
				$boost_fields = $parser->merge_ml_fields(
875
					$this->_apply_boosts_multiplier( array(
876
						'title'         => 2,
877
						'tag.name'      => 1,
878
						'category.name' => 1,
879
					), $args['excess_boost'] ),
880
					$this->_get_caret_boosted_fields( $this->_apply_boosts_multiplier( array(
881
						'author_login'  => 1,
882
						'author'        => 1,
883
					), $args['excess_boost'] ) )
884
				);
885
886
				$boost_phrase_fields = $parser->merge_ml_fields(
887
					array(
888
						'title'         => 1,
889
						'content'       => 1,
890
						'excerpt'       => 1,
891
						'tag.name'      => 1,
892
						'category.name' => 1,
893
					),
894
					$this->_get_caret_boosted_fields( array(
895
						'author'        => 1,
896
					) )
897
				);
898
			}
899
		} else {
900
			// If code is overriding the fields, then use that. Important for backwards compatibility.
901
			$match_fields        = $args['query_fields'];
902
			$boost_phrase_fields = $match_fields;
903
			$boost_fields        = null;
904
		}
905
906
		$parser->phrase_filter( array(
907
			'must_query_fields'  => $match_fields,
908
			'boost_query_fields' => null,
909
		) );
910
		$parser->remaining_query( array(
911
			'must_query_fields'  => $match_fields,
912
			'boost_query_fields' => $boost_fields,
913
		) );
914
915
		// Boost on phrase matches
916
		$parser->remaining_query( array(
917
			'boost_query_fields' => $boost_phrase_fields,
918
			'boost_query_type'   => 'phrase',
919
		) );
920
921
		/**
922
		 * Modify the recency decay parameters for the search query.
923
		 *
924
		 * The recency decay lowers the search scores based on the age of a post relative to an origin date. Basic adjustments:
925
		 *  - origin: A date. Posts with this date will have the highest score and no decay applied. Default is today.
926
		 *  - offset: Number of days/months/years (eg 30d). All posts within this time range of the origin (before and after) will have no decay applied. Default is no offset.
927
		 *  - scale: The number of days/months/years from the origin+offset at which the decay will equal the decay param. Default 360d
928
		 *  - decay: The amount of decay applied at offset+scale. Default 0.9.
929
		 *
930
		 * The curve applied is a Gaussian. More details available at {@see https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-function-score-query.html#function-decay}
931
		 *
932
		 * @module search
933
		 *
934
		 * @since  5.8.0
935
		 *
936
		 * @param array $decay_params The decay parameters.
937
		 * @param array $args         The WP query parameters.
938
		 */
939
		$decay_params = apply_filters(
940
			'jetpack_search_recency_score_decay',
941
			array(
942
				'origin' => date( 'Y-m-d' ),
943
				'scale'  => '360d',
944
				'decay'  => 0.9,
945
			),
946
			$args
947
		);
948
949
		if ( ! empty( $decay_params ) ) {
950
			// Newer content gets weighted slightly higher
951
			$parser->add_decay( 'gauss', array(
952
				'date_gmt' => $decay_params
953
			) );
954
		}
955
956
		$es_query_args = array(
957
			'blog_id' => absint( $args['blog_id'] ),
958
			'size'    => absint( $args['posts_per_page'] ),
959
		);
960
961
		// ES "from" arg (offset)
962
		if ( $args['offset'] ) {
963
			$es_query_args['from'] = absint( $args['offset'] );
964
		} elseif ( $args['paged'] ) {
965
			$es_query_args['from'] = max( 0, ( absint( $args['paged'] ) - 1 ) * $es_query_args['size'] );
966
		}
967
968
		$es_query_args['from'] = min( $es_query_args['from'], Jetpack_Search_Helpers::get_max_offset() );
969
970
		if ( ! is_array( $args['author_name'] ) ) {
971
			$args['author_name'] = array( $args['author_name'] );
972
		}
973
974
		// ES stores usernames, not IDs, so transform
975
		if ( ! empty( $args['author'] ) ) {
976
			if ( ! is_array( $args['author'] ) ) {
977
				$args['author'] = array( $args['author'] );
978
			}
979
980
			foreach ( $args['author'] as $author ) {
981
				$user = get_user_by( 'id', $author );
982
983
				if ( $user && ! empty( $user->user_login ) ) {
984
					$args['author_name'][] = $user->user_login;
985
				}
986
			}
987
		}
988
989
		//////////////////////////////////////////////////
990
		// Build the filters from the query elements.
991
		// Filters rock because they are cached from one query to the next
992
		// but they are cached as individual filters, rather than all combined together.
993
		// May get performance boost by also caching the top level boolean filter too.
994
995
		if ( $args['post_type'] ) {
996
			if ( ! is_array( $args['post_type'] ) ) {
997
				$args['post_type'] = array( $args['post_type'] );
998
			}
999
1000
			$parser->add_filter( array(
1001
				'terms' => array(
1002
					'post_type' => $args['post_type'],
1003
				),
1004
			) );
1005
		}
1006
1007
		if ( $args['author_name'] ) {
1008
			$parser->add_filter( array(
1009
				'terms' => array(
1010
					'author_login' => $args['author_name'],
1011
				),
1012
			) );
1013
		}
1014
1015
		if ( ! empty( $args['date_range'] ) && isset( $args['date_range']['field'] ) ) {
1016
			$field = $args['date_range']['field'];
1017
1018
			unset( $args['date_range']['field'] );
1019
1020
			$parser->add_filter( array(
1021
				'range' => array(
1022
					$field => $args['date_range'],
1023
				),
1024
			) );
1025
		}
1026
1027
		if ( is_array( $args['terms'] ) ) {
1028
			foreach ( $args['terms'] as $tax => $terms ) {
1029
				$terms = (array) $terms;
1030
1031
				if ( count( $terms ) && mb_strlen( $tax ) ) {
1032 View Code Duplication
					switch ( $tax ) {
1033
						case 'post_tag':
1034
							$tax_fld = 'tag.slug';
1035
1036
							break;
1037
1038
						case 'category':
1039
							$tax_fld = 'category.slug';
1040
1041
							break;
1042
1043
						default:
1044
							$tax_fld = 'taxonomy.' . $tax . '.slug';
1045
1046
							break;
1047
					}
1048
1049
					foreach ( $terms as $term ) {
1050
						$parser->add_filter( array(
1051
							'term' => array(
1052
								$tax_fld => $term,
1053
							),
1054
						) );
1055
					}
1056
				}
1057
			}
1058
		}
1059
1060
		if ( ! $args['orderby'] ) {
1061
			if ( $args['query'] ) {
1062
				$args['orderby'] = array( 'relevance' );
1063
			} else {
1064
				$args['orderby'] = array( 'date' );
1065
			}
1066
		}
1067
1068
		// Validate the "order" field
1069
		switch ( strtolower( $args['order'] ) ) {
1070
			case 'asc':
1071
				$args['order'] = 'asc';
1072
				break;
1073
1074
			case 'desc':
1075
			default:
1076
				$args['order'] = 'desc';
1077
				break;
1078
		}
1079
1080
		$es_query_args['sort'] = array();
1081
1082
		foreach ( (array) $args['orderby'] as $orderby ) {
1083
			// Translate orderby from WP field to ES field
1084
			switch ( $orderby ) {
1085
				case 'relevance' :
1086
					//never order by score ascending
1087
					$es_query_args['sort'][] = array(
1088
						'_score' => array(
1089
							'order' => 'desc',
1090
						),
1091
					);
1092
1093
					break;
1094
1095 View Code Duplication
				case 'date' :
1096
					$es_query_args['sort'][] = array(
1097
						'date' => array(
1098
							'order' => $args['order'],
1099
						),
1100
					);
1101
1102
					break;
1103
1104 View Code Duplication
				case 'ID' :
1105
					$es_query_args['sort'][] = array(
1106
						'id' => array(
1107
							'order' => $args['order'],
1108
						),
1109
					);
1110
1111
					break;
1112
1113
				case 'author' :
1114
					$es_query_args['sort'][] = array(
1115
						'author.raw' => array(
1116
							'order' => $args['order'],
1117
						),
1118
					);
1119
1120
					break;
1121
			} // End switch().
1122
		} // End foreach().
1123
1124
		if ( empty( $es_query_args['sort'] ) ) {
1125
			unset( $es_query_args['sort'] );
1126
		}
1127
1128
		// Aggregations
1129
		if ( ! empty( $args['aggregations'] ) ) {
1130
			$this->add_aggregations_to_es_query_builder( $args['aggregations'], $parser );
0 ignored issues
show
Documentation introduced by
$args['aggregations'] is of type string, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1131
		}
1132
1133
		$es_query_args['filter']       = $parser->build_filter();
1134
		$es_query_args['query']        = $parser->build_query();
1135
		$es_query_args['aggregations'] = $parser->build_aggregation();
1136
1137
		return $es_query_args;
1138
	}
1139
1140
	/**
1141
	 * Given an array of aggregations, parse and add them onto the Jetpack_WPES_Query_Builder object for use in Elasticsearch.
1142
	 *
1143
	 * @since 5.0.0
1144
	 *
1145
	 * @param array                      $aggregations Array of aggregations (filters) to add to the Jetpack_WPES_Query_Builder.
1146
	 * @param Jetpack_WPES_Query_Builder $builder      The builder instance that is creating the Elasticsearch query.
1147
	 */
1148
	public function add_aggregations_to_es_query_builder( array $aggregations, Jetpack_WPES_Query_Builder $builder ) {
1149
		foreach ( $aggregations as $label => $aggregation ) {
1150
			switch ( $aggregation['type'] ) {
1151
				case 'taxonomy':
1152
					$this->add_taxonomy_aggregation_to_es_query_builder( $aggregation, $label, $builder );
1153
1154
					break;
1155
1156
				case 'post_type':
1157
					$this->add_post_type_aggregation_to_es_query_builder( $aggregation, $label, $builder );
1158
1159
					break;
1160
1161
				case 'date_histogram':
1162
					$this->add_date_histogram_aggregation_to_es_query_builder( $aggregation, $label, $builder );
1163
1164
					break;
1165
			}
1166
		}
1167
	}
1168
1169
	/**
1170
	 * Given an individual taxonomy aggregation, add it to the Jetpack_WPES_Query_Builder object for use in Elasticsearch.
1171
	 *
1172
	 * @since 5.0.0
1173
	 *
1174
	 * @param array                      $aggregation The aggregation to add to the query builder.
1175
	 * @param string                     $label       The 'label' (unique id) for this aggregation.
1176
	 * @param Jetpack_WPES_Query_Builder $builder     The builder instance that is creating the Elasticsearch query.
1177
	 */
1178
	public function add_taxonomy_aggregation_to_es_query_builder( array $aggregation, $label, Jetpack_WPES_Query_Builder $builder ) {
1179
		$field = null;
0 ignored issues
show
Unused Code introduced by
$field is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
1180
1181
		switch ( $aggregation['taxonomy'] ) {
1182
			case 'post_tag':
1183
				$field = 'tag';
1184
				break;
1185
1186
			case 'category':
1187
				$field = 'category';
1188
				break;
1189
1190
			default:
1191
				$field = 'taxonomy.' . $aggregation['taxonomy'];
1192
				break;
1193
		}
1194
1195
		$builder->add_aggs( $label, array(
1196
			'terms' => array(
1197
				'field' => $field . '.slug',
1198
				'size'  => min( (int) $aggregation['count'], $this->max_aggregations_count ),
1199
			),
1200
		) );
1201
	}
1202
1203
	/**
1204
	 * Given an individual post_type aggregation, add it to the Jetpack_WPES_Query_Builder object for use in Elasticsearch.
1205
	 *
1206
	 * @since 5.0.0
1207
	 *
1208
	 * @param array                      $aggregation The aggregation to add to the query builder.
1209
	 * @param string                     $label       The 'label' (unique id) for this aggregation.
1210
	 * @param Jetpack_WPES_Query_Builder $builder     The builder instance that is creating the Elasticsearch query.
1211
	 */
1212
	public function add_post_type_aggregation_to_es_query_builder( array $aggregation, $label, Jetpack_WPES_Query_Builder $builder ) {
1213
		$builder->add_aggs( $label, array(
1214
			'terms' => array(
1215
				'field' => 'post_type',
1216
				'size'  => min( (int) $aggregation['count'], $this->max_aggregations_count ),
1217
			),
1218
		) );
1219
	}
1220
1221
	/**
1222
	 * Given an individual date_histogram aggregation, add it to the Jetpack_WPES_Query_Builder object for use in Elasticsearch.
1223
	 *
1224
	 * @since 5.0.0
1225
	 *
1226
	 * @param array                      $aggregation The aggregation to add to the query builder.
1227
	 * @param string                     $label       The 'label' (unique id) for this aggregation.
1228
	 * @param Jetpack_WPES_Query_Builder $builder     The builder instance that is creating the Elasticsearch query.
1229
	 */
1230
	public function add_date_histogram_aggregation_to_es_query_builder( array $aggregation, $label, Jetpack_WPES_Query_Builder $builder ) {
1231
		$args = array(
1232
			'interval' => $aggregation['interval'],
1233
			'field'    => ( ! empty( $aggregation['field'] ) && 'post_date_gmt' == $aggregation['field'] ) ? 'date_gmt' : 'date',
1234
		);
1235
1236
		if ( isset( $aggregation['min_doc_count'] ) ) {
1237
			$args['min_doc_count'] = intval( $aggregation['min_doc_count'] );
1238
		} else {
1239
			$args['min_doc_count'] = 1;
1240
		}
1241
1242
		$builder->add_aggs( $label, array(
1243
			'date_histogram' => $args,
1244
		) );
1245
	}
1246
1247
	/**
1248
	 * And an existing filter object with a list of additional filters.
1249
	 *
1250
	 * Attempts to optimize the filters somewhat.
1251
	 *
1252
	 * @since 5.0.0
1253
	 *
1254
	 * @param array $curr_filter The existing filters to build upon.
1255
	 * @param array $filters     The new filters to add.
1256
	 *
1257
	 * @return array The resulting merged filters.
1258
	 */
1259
	public static function and_es_filters( array $curr_filter, array $filters ) {
1260
		if ( ! is_array( $curr_filter ) || isset( $curr_filter['match_all'] ) ) {
1261
			if ( 1 === count( $filters ) ) {
1262
				return $filters[0];
1263
			}
1264
1265
			return array(
1266
				'and' => $filters,
1267
			);
1268
		}
1269
1270
		return array(
1271
			'and' => array_merge( array( $curr_filter ), $filters ),
1272
		);
1273
	}
1274
1275
	/**
1276
	 * Set the available filters for the search.
1277
	 *
1278
	 * These get rendered via the Jetpack_Search_Widget() widget.
1279
	 *
1280
	 * Behind the scenes, these are implemented using Elasticsearch Aggregations.
1281
	 *
1282
	 * If you do not require counts of how many documents match each filter, please consider using regular WP Query
1283
	 * arguments instead, such as via the jetpack_search_es_wp_query_args filter
1284
	 *
1285
	 * @see    https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations.html
1286
	 *
1287
	 * @since  5.0.0
1288
	 *
1289
	 * @param array $aggregations Array of filters (aggregations) to apply to the search
1290
	 */
1291
	public function set_filters( array $aggregations ) {
1292
		foreach ( (array) $aggregations as $key => $agg ) {
1293
			if ( empty( $agg['name'] ) ) {
1294
				$aggregations[ $key ]['name'] = $key;
1295
			}
1296
		}
1297
		$this->aggregations = $aggregations;
1298
	}
1299
1300
	/**
1301
	 * Set the search's facets (deprecated).
1302
	 *
1303
	 * @deprecated 5.0 Please use Jetpack_Search::set_filters() instead.
1304
	 *
1305
	 * @see        Jetpack_Search::set_filters()
1306
	 *
1307
	 * @param array $facets Array of facets to apply to the search.
1308
	 */
1309
	public function set_facets( array $facets ) {
1310
		_deprecated_function( __METHOD__, 'jetpack-5.0', 'Jetpack_Search::set_filters()' );
1311
1312
		$this->set_filters( $facets );
1313
	}
1314
1315
	/**
1316
	 * Get the raw Aggregation results from the Elasticsearch response.
1317
	 *
1318
	 * @since  5.0.0
1319
	 *
1320
	 * @see    https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations.html
1321
	 *
1322
	 * @return array Array of Aggregations performed on the search.
1323
	 */
1324
	public function get_search_aggregations_results() {
1325
		$aggregations = array();
1326
1327
		$search_result = $this->get_search_result();
1328
1329
		if ( ! empty( $search_result ) && ! empty( $search_result['aggregations'] ) ) {
1330
			$aggregations = $search_result['aggregations'];
1331
		}
1332
1333
		return $aggregations;
1334
	}
1335
1336
	/**
1337
	 * Get the raw Facet results from the Elasticsearch response.
1338
	 *
1339
	 * @deprecated 5.0 Please use Jetpack_Search::get_search_aggregations_results() instead.
1340
	 *
1341
	 * @see        Jetpack_Search::get_search_aggregations_results()
1342
	 *
1343
	 * @return array Array of Facets performed on the search.
1344
	 */
1345
	public function get_search_facets() {
1346
		_deprecated_function( __METHOD__, 'jetpack-5.0', 'Jetpack_Search::get_search_aggregations_results()' );
1347
1348
		return $this->get_search_aggregations_results();
1349
	}
1350
1351
	/**
1352
	 * Get the results of the Filters performed, including the number of matching documents.
1353
	 *
1354
	 * Returns an array of Filters (keyed by $label, as passed to Jetpack_Search::set_filters()), containing the Filter and all resulting
1355
	 * matching buckets, the url for applying/removing each bucket, etc.
1356
	 *
1357
	 * NOTE - if this is called before the search is performed, an empty array will be returned. Use the $aggregations class
1358
	 * member if you need to access the raw filters set in Jetpack_Search::set_filters().
1359
	 *
1360
	 * @since 5.0.0
1361
	 *
1362
	 * @param WP_Query $query The optional original WP_Query to use for determining which filters are active. Defaults to the main query.
0 ignored issues
show
Documentation introduced by
Should the type for parameter $query not be null|WP_Query?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
1363
	 *
1364
	 * @return array Array of filters applied and info about them.
1365
	 */
1366
	public function get_filters( WP_Query $query = null ) {
1367
		if ( ! $query instanceof WP_Query ) {
0 ignored issues
show
Bug introduced by
The class WP_Query does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
1368
			global $wp_query;
1369
1370
			$query = $wp_query;
1371
		}
1372
1373
		$aggregation_data = $this->aggregations;
1374
1375
		if ( empty( $aggregation_data ) ) {
1376
			return $aggregation_data;
1377
		}
1378
1379
		$aggregation_results = $this->get_search_aggregations_results();
1380
1381
		if ( ! $aggregation_results ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $aggregation_results of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
1382
			return $aggregation_data;
1383
		}
1384
1385
		// NOTE - Looping over the _results_, not the original configured aggregations, so we get the 'real' data from ES
1386
		foreach ( $aggregation_results as $label => $aggregation ) {
1387
			if ( empty( $aggregation ) ) {
1388
				continue;
1389
			}
1390
1391
			$type = $this->aggregations[ $label ]['type'];
1392
1393
			$aggregation_data[ $label ]['buckets'] = array();
1394
1395
			$existing_term_slugs = array();
1396
1397
			$tax_query_var = null;
1398
1399
			// Figure out which terms are active in the query, for this taxonomy
1400
			if ( 'taxonomy' === $this->aggregations[ $label ]['type'] ) {
1401
				$tax_query_var = $this->get_taxonomy_query_var( $this->aggregations[ $label ]['taxonomy'] );
1402
1403
				if ( ! empty( $query->tax_query ) && ! empty( $query->tax_query->queries ) && is_array( $query->tax_query->queries ) ) {
1404
					foreach ( $query->tax_query->queries as $tax_query ) {
1405
						if ( is_array( $tax_query ) && $this->aggregations[ $label ]['taxonomy'] === $tax_query['taxonomy'] &&
1406
						     'slug' === $tax_query['field'] &&
1407
						     is_array( $tax_query['terms'] ) ) {
1408
							$existing_term_slugs = array_merge( $existing_term_slugs, $tax_query['terms'] );
1409
						}
1410
					}
1411
				}
1412
			}
1413
1414
			// Now take the resulting found aggregation items and generate the additional info about them, such as activation/deactivation url, name, count, etc.
1415
			$buckets = array();
1416
1417
			if ( ! empty( $aggregation['buckets'] ) ) {
1418
				$buckets = (array) $aggregation['buckets'];
1419
			}
1420
1421
			if ( 'date_histogram' == $type ) {
1422
				//re-order newest to oldest
1423
				$buckets = array_reverse( $buckets );
1424
			}
1425
1426
			// Some aggregation types like date_histogram don't support the max results parameter
1427
			if ( is_int( $this->aggregations[ $label ]['count'] ) && count( $buckets ) > $this->aggregations[ $label ]['count'] ) {
1428
				$buckets = array_slice( $buckets, 0, $this->aggregations[ $label ]['count'] );
1429
			}
1430
1431
			foreach ( $buckets as $item ) {
1432
				$query_vars = array();
1433
				$active     = false;
1434
				$remove_url = null;
1435
				$name       = '';
1436
1437
				// What type was the original aggregation?
1438
				switch ( $type ) {
1439
					case 'taxonomy':
1440
						$taxonomy = $this->aggregations[ $label ]['taxonomy'];
1441
1442
						$term = get_term_by( 'slug', $item['key'], $taxonomy );
1443
1444
						if ( ! $term || ! $tax_query_var ) {
1445
							continue 2; // switch() is considered a looping structure
1446
						}
1447
1448
						$query_vars = array(
1449
							$tax_query_var => implode( '+', array_merge( $existing_term_slugs, array( $term->slug ) ) ),
1450
						);
1451
1452
						$name = $term->name;
1453
1454
						// Let's determine if this term is active or not
1455
1456
						if ( in_array( $item['key'], $existing_term_slugs, true ) ) {
1457
							$active = true;
1458
1459
							$slug_count = count( $existing_term_slugs );
1460
1461 View Code Duplication
							if ( $slug_count > 1 ) {
1462
								$remove_url = Jetpack_Search_Helpers::add_query_arg(
1463
									$tax_query_var,
0 ignored issues
show
Bug introduced by
It seems like $tax_query_var can also be of type boolean; however, Jetpack_Search_Helpers::add_query_arg() does only seem to accept string|array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1464
									rawurlencode( implode( '+', array_diff( $existing_term_slugs, array( $item['key'] ) ) ) )
1465
								);
1466
							} else {
1467
								$remove_url = Jetpack_Search_Helpers::remove_query_arg( $tax_query_var );
0 ignored issues
show
Bug introduced by
It seems like $tax_query_var can also be of type boolean; however, Jetpack_Search_Helpers::remove_query_arg() does only seem to accept string|array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1468
							}
1469
						}
1470
1471
						break;
1472
1473
					case 'post_type':
1474
						$post_type = get_post_type_object( $item['key'] );
1475
1476
						if ( ! $post_type || $post_type->exclude_from_search ) {
1477
							continue 2;  // switch() is considered a looping structure
1478
						}
1479
1480
						$query_vars = array(
1481
							'post_type' => $item['key'],
1482
						);
1483
1484
						$name = $post_type->labels->singular_name;
1485
1486
						// Is this post type active on this search?
1487
						$post_types = $query->get( 'post_type' );
1488
1489
						if ( ! is_array( $post_types ) ) {
1490
							$post_types = array( $post_types );
1491
						}
1492
1493
						if ( in_array( $item['key'], $post_types ) ) {
1494
							$active = true;
1495
1496
							$post_type_count = count( $post_types );
1497
1498
							// For the right 'remove filter' url, we need to remove the post type from the array, or remove the param entirely if it's the only one
1499 View Code Duplication
							if ( $post_type_count > 1 ) {
1500
								$remove_url = Jetpack_Search_Helpers::add_query_arg(
1501
									'post_type',
1502
									rawurlencode( implode( ',', array_diff( $post_types, array( $item['key'] ) ) ) )
1503
								);
1504
							} else {
1505
								$remove_url = Jetpack_Search_Helpers::remove_query_arg( 'post_type' );
1506
							}
1507
						}
1508
1509
						break;
1510
1511
					case 'date_histogram':
1512
						$timestamp = $item['key'] / 1000;
1513
1514
						$current_year  = $query->get( 'year' );
1515
						$current_month = $query->get( 'monthnum' );
1516
						$current_day   = $query->get( 'day' );
1517
1518
						switch ( $this->aggregations[ $label ]['interval'] ) {
1519
							case 'year':
1520
								$year = (int) date( 'Y', $timestamp );
1521
1522
								$query_vars = array(
1523
									'year'     => $year,
1524
									'monthnum' => false,
1525
									'day'      => false,
1526
								);
1527
1528
								$name = $year;
1529
1530
								// Is this year currently selected?
1531
								if ( ! empty( $current_year ) && (int) $current_year === $year ) {
1532
									$active = true;
1533
1534
									$remove_url = Jetpack_Search_Helpers::remove_query_arg( array( 'year', 'monthnum', 'day' ) );
1535
								}
1536
1537
								break;
1538
1539
							case 'month':
1540
								$year  = (int) date( 'Y', $timestamp );
1541
								$month = (int) date( 'n', $timestamp );
1542
1543
								$query_vars = array(
1544
									'year'     => $year,
1545
									'monthnum' => $month,
1546
									'day'      => false,
1547
								);
1548
1549
								$name = date( 'F Y', $timestamp );
1550
1551
								// Is this month currently selected?
1552
								if ( ! empty( $current_year ) && (int) $current_year === $year &&
1553
								     ! empty( $current_month ) && (int) $current_month === $month ) {
1554
									$active = true;
1555
1556
									$remove_url = Jetpack_Search_Helpers::remove_query_arg( array( 'year', 'monthnum' ) );
1557
								}
1558
1559
								break;
1560
1561
							case 'day':
1562
								$year  = (int) date( 'Y', $timestamp );
1563
								$month = (int) date( 'n', $timestamp );
1564
								$day   = (int) date( 'j', $timestamp );
1565
1566
								$query_vars = array(
1567
									'year'     => $year,
1568
									'monthnum' => $month,
1569
									'day'      => $day,
1570
								);
1571
1572
								$name = date( 'F jS, Y', $timestamp );
1573
1574
								// Is this day currently selected?
1575
								if ( ! empty( $current_year ) && (int) $current_year === $year &&
1576
								     ! empty( $current_month ) && (int) $current_month === $month &&
1577
								     ! empty( $current_day ) && (int) $current_day === $day ) {
1578
									$active = true;
1579
1580
									$remove_url = Jetpack_Search_Helpers::remove_query_arg( array( 'day' ) );
1581
								}
1582
1583
								break;
1584
1585
							default:
1586
								continue 3; // switch() is considered a looping structure
1587
						} // End switch().
1588
1589
						break;
1590
1591
					default:
1592
						//continue 2; // switch() is considered a looping structure
1593
				} // End switch().
1594
1595
				// Need to urlencode param values since add_query_arg doesn't
1596
				$url_params = urlencode_deep( $query_vars );
1597
1598
				$aggregation_data[ $label ]['buckets'][] = array(
1599
					'url'        => Jetpack_Search_Helpers::add_query_arg( $url_params ),
1600
					'query_vars' => $query_vars,
1601
					'name'       => $name,
1602
					'count'      => $item['doc_count'],
1603
					'active'     => $active,
1604
					'remove_url' => $remove_url,
1605
					'type'       => $type,
1606
					'type_label' => $aggregation_data[ $label ]['name'],
1607
					'widget_id'  => ! empty( $aggregation_data[ $label ]['widget_id'] ) ? $aggregation_data[ $label ]['widget_id'] : 0
1608
				);
1609
			} // End foreach().
1610
		} // End foreach().
1611
1612
		/**
1613
		 * Modify the aggregation filters returned by get_filters().
1614
		 *
1615
		 * Useful if you are setting custom filters outside of the supported filters (taxonomy, post_type etc.) and
1616
		 * want to hook them up so they're returned when you call `get_filters()`.
1617
		 *
1618
		 * @module search
1619
		 *
1620
		 * @since  6.9.0
1621
		 *
1622
		 * @param array    $aggregation_data The array of filters keyed on label.
1623
		 * @param WP_Query $query            The WP_Query object.
1624
		 */
1625
		return apply_filters( 'jetpack_search_get_filters', $aggregation_data, $query );
1626
	}
1627
1628
	/**
1629
	 * Get the results of the facets performed.
1630
	 *
1631
	 * @deprecated 5.0 Please use Jetpack_Search::get_filters() instead.
1632
	 *
1633
	 * @see        Jetpack_Search::get_filters()
1634
	 *
1635
	 * @return array $facets Array of facets applied and info about them.
1636
	 */
1637
	public function get_search_facet_data() {
1638
		_deprecated_function( __METHOD__, 'jetpack-5.0', 'Jetpack_Search::get_filters()' );
1639
1640
		return $this->get_filters();
1641
	}
1642
1643
	/**
1644
	 * Get the filters that are currently applied to this search.
1645
	 *
1646
	 * @since 5.0.0
1647
	 *
1648
	 * @return array Array of filters that were applied.
1649
	 */
1650
	public function get_active_filter_buckets() {
1651
		$active_buckets = array();
1652
1653
		$filters = $this->get_filters();
1654
1655
		if ( ! is_array( $filters ) ) {
1656
			return $active_buckets;
1657
		}
1658
1659
		foreach ( $filters as $filter ) {
1660
			if ( isset( $filter['buckets'] ) && is_array( $filter['buckets'] ) ) {
1661
				foreach ( $filter['buckets'] as $item ) {
1662
					if ( isset( $item['active'] ) && $item['active'] ) {
1663
						$active_buckets[] = $item;
1664
					}
1665
				}
1666
			}
1667
		}
1668
1669
		return $active_buckets;
1670
	}
1671
1672
	/**
1673
	 * Get the filters that are currently applied to this search.
1674
	 *
1675
	 * @deprecated 5.0 Please use Jetpack_Search::get_active_filter_buckets() instead.
1676
	 *
1677
	 * @see        Jetpack_Search::get_active_filter_buckets()
1678
	 *
1679
	 * @return array Array of filters that were applied.
1680
	 */
1681
	public function get_current_filters() {
1682
		_deprecated_function( __METHOD__, 'jetpack-5.0', 'Jetpack_Search::get_active_filter_buckets()' );
1683
1684
		return $this->get_active_filter_buckets();
1685
	}
1686
1687
	/**
1688
	 * Calculate the right query var to use for a given taxonomy.
1689
	 *
1690
	 * Allows custom code to modify the GET var that is used to represent a given taxonomy, via the jetpack_search_taxonomy_query_var filter.
1691
	 *
1692
	 * @since 5.0.0
1693
	 *
1694
	 * @param string $taxonomy_name The name of the taxonomy for which to get the query var.
1695
	 *
1696
	 * @return bool|string The query var to use for this taxonomy, or false if none found.
1697
	 */
1698
	public function get_taxonomy_query_var( $taxonomy_name ) {
1699
		$taxonomy = get_taxonomy( $taxonomy_name );
1700
1701
		if ( ! $taxonomy || is_wp_error( $taxonomy ) ) {
1702
			return false;
1703
		}
1704
1705
		/**
1706
		 * Modify the query var to use for a given taxonomy
1707
		 *
1708
		 * @module search
1709
		 *
1710
		 * @since  5.0.0
1711
		 *
1712
		 * @param string $query_var     The current query_var for the taxonomy
1713
		 * @param string $taxonomy_name The taxonomy name
1714
		 */
1715
		return apply_filters( 'jetpack_search_taxonomy_query_var', $taxonomy->query_var, $taxonomy_name );
1716
	}
1717
1718
	/**
1719
	 * Takes an array of aggregation results, and ensures the array key ordering matches the key order in $desired
1720
	 * which is the input order.
1721
	 *
1722
	 * Necessary because ES does not always return aggregations in the same order that you pass them in,
1723
	 * and it should be possible to control the display order easily.
1724
	 *
1725
	 * @since 5.0.0
1726
	 *
1727
	 * @param array $aggregations Aggregation results to be reordered.
1728
	 * @param array $desired      Array with keys representing the desired ordering.
1729
	 *
1730
	 * @return array A new array with reordered keys, matching those in $desired.
1731
	 */
1732
	public function fix_aggregation_ordering( array $aggregations, array $desired ) {
1733
		if ( empty( $aggregations ) || empty( $desired ) ) {
1734
			return $aggregations;
1735
		}
1736
1737
		$reordered = array();
1738
1739
		foreach ( array_keys( $desired ) as $agg_name ) {
1740
			if ( isset( $aggregations[ $agg_name ] ) ) {
1741
				$reordered[ $agg_name ] = $aggregations[ $agg_name ];
1742
			}
1743
		}
1744
1745
		return $reordered;
1746
	}
1747
1748
	/**
1749
	 * Sends events to Tracks when a search filters widget is updated.
1750
	 *
1751
	 * @since 5.8.0
1752
	 *
1753
	 * @param string $option    The option name. Only "widget_jetpack-search-filters" is cared about.
1754
	 * @param array  $old_value The old option value.
1755
	 * @param array  $new_value The new option value.
1756
	 */
1757
	public function track_widget_updates( $option, $old_value, $new_value ) {
1758
		if ( 'widget_jetpack-search-filters' !== $option ) {
1759
			return;
1760
		}
1761
1762
		$event = Jetpack_Search_Helpers::get_widget_tracks_value( $old_value, $new_value );
1763
		if ( ! $event ) {
1764
			return;
1765
		}
1766
1767
		$tracking = new Automattic\Jetpack\Tracking();
1768
		$tracking->tracks_record_event(
1769
			wp_get_current_user(),
1770
			sprintf( 'jetpack_search_widget_%s', $event['action'] ),
1771
			$event['widget']
1772
		);
1773
	}
1774
1775
	/**
1776
	 * Moves any active search widgets to the inactive category.
1777
	 *
1778
	 * @since 5.9.0
1779
	 *
1780
	 * @param string $module Unused. The Jetpack module being disabled.
1781
	 */
1782
	public function move_search_widgets_to_inactive( $module ) {
1783
		if ( ! is_active_widget( false, false, Jetpack_Search_Helpers::FILTER_WIDGET_BASE, true ) ) {
1784
			return;
1785
		}
1786
1787
		$sidebars_widgets = wp_get_sidebars_widgets();
1788
1789
		if ( ! is_array( $sidebars_widgets ) ) {
1790
			return;
1791
		}
1792
1793
		$changed = false;
1794
1795
		foreach ( $sidebars_widgets as $sidebar => $widgets ) {
1796
			if ( 'wp_inactive_widgets' === $sidebar || 'orphaned_widgets' === substr( $sidebar, 0, 16 ) ) {
1797
				continue;
1798
			}
1799
1800
			if ( is_array( $widgets ) ) {
1801
				foreach ( $widgets as $key => $widget ) {
1802
					if ( _get_widget_id_base( $widget ) == Jetpack_Search_Helpers::FILTER_WIDGET_BASE ) {
1803
						$changed = true;
1804
1805
						array_unshift( $sidebars_widgets['wp_inactive_widgets'], $widget );
1806
						unset( $sidebars_widgets[ $sidebar ][ $key ] );
1807
					}
1808
				}
1809
			}
1810
		}
1811
1812
		if ( $changed ) {
1813
			wp_set_sidebars_widgets( $sidebars_widgets );
1814
		}
1815
	}
1816
1817
	/**
1818
	 * Determine whether a given WP_Query should be handled by ElasticSearch.
1819
	 *
1820
	 * @param WP_Query $query The WP_Query object.
1821
	 *
1822
	 * @return bool
1823
	 */
1824
	public function should_handle_query( $query ) {
1825
		/**
1826
		 * Determine whether a given WP_Query should be handled by ElasticSearch.
1827
		 *
1828
		 * @module search
1829
		 *
1830
		 * @since  5.6.0
1831
		 *
1832
		 * @param bool     $should_handle Should be handled by Jetpack Search.
1833
		 * @param WP_Query $query         The WP_Query object.
1834
		 */
1835
		return apply_filters( 'jetpack_search_should_handle_query', $query->is_main_query() && $query->is_search(), $query );
1836
	}
1837
1838
	/**
1839
	 * Transforms an array with fields name as keys and boosts as value into
1840
	 * shorthand "caret" format.
1841
	 *
1842
	 * @param array $fields_boost [ "title" => "2", "content" => "1" ]
1843
	 *
1844
	 * @return array [ "title^2", "content^1" ]
1845
	 */
1846
	private function _get_caret_boosted_fields( array $fields_boost ) {
1847
		$caret_boosted_fields = array();
1848
		foreach ( $fields_boost as $field => $boost ) {
1849
			$caret_boosted_fields[] = "$field^$boost";
1850
		}
1851
		return $caret_boosted_fields;
1852
	}
1853
1854
	/**
1855
	 * Apply a multiplier to boost values.
1856
	 *
1857
	 * @param array $fields_boost [ "title" => 2, "content" => 1 ]
1858
	 * @param array $fields_boost_multiplier [ "title" => 0.1234 ]
1859
	 *
1860
	 * @return array [ "title" => "0.247", "content" => "1.000" ]
1861
	 */
1862
	private function _apply_boosts_multiplier( array $fields_boost, array $fields_boost_multiplier ) {
1863
		foreach( $fields_boost as $field_name => $field_boost ) {
1864
			if ( isset( $fields_boost_multiplier[ $field_name ] ) ) {
1865
				$fields_boost[ $field_name ] *= $fields_boost_multiplier[ $field_name ];
1866
			}
1867
1868
			// Set a floor and format the number as string
1869
			$fields_boost[ $field_name ] = number_format(
1870
				max( 0.001, $fields_boost[ $field_name ] ),
1871
				3, '.', ''
1872
			);
1873
		}
1874
1875
		return $fields_boost;
1876
	}
1877
}
1878