Completed
Push — add/changelog70 ( 0b1482...18b486 )
by Jeremy
12:28
created

Jetpack_Search::do_search()   D

Complexity

Conditions 15
Paths 81

Size

Total Lines 107

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 15
nc 81
nop 1
dl 0
loc 107
rs 4.7333
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
/**
11
 * The main class for the Jetpack Search module.
12
 *
13
 * @since 5.0.0
14
 */
15
class Jetpack_Search {
16
17
	/**
18
	 * The number of found posts.
19
	 *
20
	 * @since 5.0.0
21
	 *
22
	 * @var int
23
	 */
24
	protected $found_posts = 0;
25
26
	/**
27
	 * The search result, as returned by the WordPress.com REST API.
28
	 *
29
	 * @since 5.0.0
30
	 *
31
	 * @var array
32
	 */
33
	protected $search_result;
34
35
	/**
36
	 * This site's blog ID on WordPress.com.
37
	 *
38
	 * @since 5.0.0
39
	 *
40
	 * @var int
41
	 */
42
	protected $jetpack_blog_id;
43
44
	/**
45
	 * The Elasticsearch aggregations (filters).
46
	 *
47
	 * @since 5.0.0
48
	 *
49
	 * @var array
50
	 */
51
	protected $aggregations = array();
52
53
	/**
54
	 * The maximum number of aggregations allowed.
55
	 *
56
	 * @since 5.0.0
57
	 *
58
	 * @var int
59
	 */
60
	protected $max_aggregations_count = 100;
61
62
	/**
63
	 * Statistics about the last Elasticsearch query.
64
	 *
65
	 * @since 5.6.0
66
	 *
67
	 * @var array
68
	 */
69
	protected $last_query_info = array();
70
71
	/**
72
	 * Statistics about the last Elasticsearch query failure.
73
	 *
74
	 * @since 5.6.0
75
	 *
76
	 * @var array
77
	 */
78
	protected $last_query_failure_info = array();
79
80
	/**
81
	 * The singleton instance of this class.
82
	 *
83
	 * @since 5.0.0
84
	 *
85
	 * @var Jetpack_Search
86
	 */
87
	protected static $instance;
88
89
	/**
90
	 * Languages with custom analyzers. Other languages are supported, but are analyzed with the default analyzer.
91
	 *
92
	 * @since 5.0.0
93
	 *
94
	 * @var array
95
	 */
96
	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' );
97
98
	/**
99
	 * Jetpack_Search constructor.
100
	 *
101
	 * @since 5.0.0
102
	 *
103
	 * Doesn't do anything. This class needs to be initialized via the instance() method instead.
104
	 */
105
	protected function __construct() {
106
	}
107
108
	/**
109
	 * Prevent __clone()'ing of this class.
110
	 *
111
	 * @since 5.0.0
112
	 */
113
	public function __clone() {
114
		wp_die( "Please don't __clone Jetpack_Search" );
115
	}
116
117
	/**
118
	 * Prevent __wakeup()'ing of this class.
119
	 *
120
	 * @since 5.0.0
121
	 */
122
	public function __wakeup() {
123
		wp_die( "Please don't __wakeup Jetpack_Search" );
124
	}
125
126
	/**
127
	 * Get singleton instance of Jetpack_Search.
128
	 *
129
	 * Instantiates and sets up a new instance if needed, or returns the singleton.
130
	 *
131
	 * @since 5.0.0
132
	 *
133
	 * @return Jetpack_Search The Jetpack_Search singleton.
134
	 */
135
	public static function instance() {
136
		if ( ! isset( self::$instance ) ) {
137
			self::$instance = new Jetpack_Search();
138
139
			self::$instance->setup();
140
		}
141
142
		return self::$instance;
143
	}
144
145
	/**
146
	 * Perform various setup tasks for the class.
147
	 *
148
	 * Checks various pre-requisites and adds hooks.
149
	 *
150
	 * @since 5.0.0
151
	 */
152
	public function setup() {
153
		if ( ! Jetpack::is_active() || ! Jetpack::active_plan_supports( 'search' ) ) {
154
			return;
155
		}
156
157
		$this->jetpack_blog_id = Jetpack::get_option( 'id' );
158
159
		if ( ! $this->jetpack_blog_id ) {
160
			return;
161
		}
162
163
		require_once dirname( __FILE__ ) . '/class.jetpack-search-helpers.php';
164
		require_once dirname( __FILE__ ) . '/class.jetpack-search-template-tags.php';
165
		require_once JETPACK__PLUGIN_DIR . 'modules/widgets/search.php';
166
167
		$this->init_hooks();
168
	}
169
170
	/**
171
	 * Setup the various hooks needed for the plugin to take over search duties.
172
	 *
173
	 * @since 5.0.0
174
	 */
175
	public function init_hooks() {
176
		if ( ! is_admin() ) {
177
			add_filter( 'posts_pre_query', array( $this, 'filter__posts_pre_query' ), 10, 2 );
178
179
			add_filter( 'jetpack_search_es_wp_query_args', array( $this, 'filter__add_date_filter_to_query' ), 10, 2 );
180
181
			add_action( 'did_jetpack_search_query', array( $this, 'store_last_query_info' ) );
182
			add_action( 'failed_jetpack_search_query', array( $this, 'store_query_failure' ) );
183
184
			add_action( 'init', array( $this, 'set_filters_from_widgets' ) );
185
186
			add_action( 'pre_get_posts', array( $this, 'maybe_add_post_type_as_var' ) );
187
		} else {
188
			add_action( 'update_option', array( $this, 'track_widget_updates' ), 10, 3 );
189
		}
190
191
		add_action( 'jetpack_deactivate_module_search', array( $this, 'move_search_widgets_to_inactive' ) );
192
	}
193
194
	/**
195
	 * When an Elasticsearch query fails, this stores it and enqueues some debug information in the footer.
196
	 *
197
	 * @since 5.6.0
198
	 *
199
	 * @param array $meta Information about the failure.
200
	 */
201
	public function store_query_failure( $meta ) {
202
		$this->last_query_failure_info = $meta;
203
		add_action( 'wp_footer', array( $this, 'print_query_failure' ) );
204
	}
205
206
	/**
207
	 * Outputs information about the last Elasticsearch failure.
208
	 *
209
	 * @since 5.6.0
210
	 */
211
	public function print_query_failure() {
212
		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...
213
			printf(
214
				'<!-- Jetpack Search failed with code %s: %s - %s -->',
215
				esc_html( $this->last_query_failure_info['response_code'] ),
216
				esc_html( $this->last_query_failure_info['json']['error'] ),
217
				esc_html( $this->last_query_failure_info['json']['message'] )
218
			);
219
		}
220
	}
221
222
	/**
223
	 * Stores information about the last Elasticsearch query and enqueues some debug information in the footer.
224
	 *
225
	 * @since 5.6.0
226
	 *
227
	 * @param array $meta Information about the query.
228
	 */
229
	public function store_last_query_info( $meta ) {
230
		$this->last_query_info = $meta;
231
		add_action( 'wp_footer', array( $this, 'print_query_success' ) );
232
	}
233
234
	/**
235
	 * Outputs information about the last Elasticsearch search.
236
	 *
237
	 * @since 5.6.0
238
	 */
239
	public function print_query_success() {
240
		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...
241
			printf(
242
				'<!-- Jetpack Search took %s ms, ES time %s ms -->',
243
				intval( $this->last_query_info['elapsed_time'] ),
244
				esc_html( $this->last_query_info['es_time'] )
245
			);
246
		}
247
	}
248
249
	/**
250
	 * Returns the last query information, or false if no information was stored.
251
	 *
252
	 * @since 5.8.0
253
	 *
254
	 * @return bool|array
255
	 */
256
	public function get_last_query_info() {
257
		return empty( $this->last_query_info ) ? false : $this->last_query_info;
258
	}
259
260
	/**
261
	 * Returns the last query failure information, or false if no failure information was stored.
262
	 *
263
	 * @since 5.8.0
264
	 *
265
	 * @return bool|array
266
	 */
267
	public function get_last_query_failure_info() {
268
		return empty( $this->last_query_failure_info ) ? false : $this->last_query_failure_info;
269
	}
270
271
	/**
272
	 * Wraps a WordPress filter called "jetpack_search_disable_widget_filters" that allows
273
	 * developers to disable filters supplied by the search widget. Useful if filters are
274
	 * being defined at the code level.
275
	 *
276
	 * @since      5.7.0
277
	 * @deprecated 5.8.0 Use Jetpack_Search_Helpers::are_filters_by_widget_disabled() directly.
278
	 *
279
	 * @return bool
280
	 */
281
	public function are_filters_by_widget_disabled() {
282
		return Jetpack_Search_Helpers::are_filters_by_widget_disabled();
283
	}
284
285
	/**
286
	 * Retrieves a list of known Jetpack search filters widget IDs, gets the filters for each widget,
287
	 * and applies those filters to this Jetpack_Search object.
288
	 *
289
	 * @since 5.7.0
290
	 */
291
	public function set_filters_from_widgets() {
292
		if ( Jetpack_Search_Helpers::are_filters_by_widget_disabled() ) {
293
			return;
294
		}
295
296
		$filters = Jetpack_Search_Helpers::get_filters_from_widgets();
297
298
		if ( ! empty( $filters ) ) {
299
			$this->set_filters( $filters );
300
		}
301
	}
302
303
	/**
304
	 * Restricts search results to certain post types via a GET argument.
305
	 *
306
	 * @since 5.8.0
307
	 *
308
	 * @param WP_Query $query A WP_Query instance.
309
	 */
310
	public function maybe_add_post_type_as_var( WP_Query $query ) {
311
		if ( $query->is_main_query() && $query->is_search && ! empty( $_GET['post_type'] ) ) {
312
			$post_types = ( is_string( $_GET['post_type'] ) && false !== strpos( $_GET['post_type'], ',' ) )
313
				? $post_type = explode( ',', $_GET['post_type'] )
314
				: (array) $_GET['post_type'];
315
			$post_types = array_map( 'sanitize_key', $post_types );
316
			$query->set( 'post_type', $post_types );
317
		}
318
	}
319
320
	/*
321
	 * Run a search on the WordPress.com public API.
322
	 *
323
	 * @since 5.0.0
324
	 *
325
	 * @param array $es_args Args conforming to the WP.com /sites/<blog_id>/search endpoint.
326
	 *
327
	 * @return object|WP_Error The response from the public API, or a WP_Error.
328
	 */
329
	public function search( array $es_args ) {
330
		$endpoint    = sprintf( '/sites/%s/search', $this->jetpack_blog_id );
331
		$service_url = 'https://public-api.wordpress.com/rest/v1' . $endpoint;
332
333
		$do_authenticated_request = false;
334
335
		if ( class_exists( 'Jetpack_Client' ) &&
336
			isset( $es_args['authenticated_request'] ) &&
337
			true === $es_args['authenticated_request'] ) {
338
			$do_authenticated_request = true;
339
		}
340
341
		unset( $es_args['authenticated_request'] );
342
343
		$request_args = array(
344
			'headers' => array(
345
				'Content-Type' => 'application/json',
346
			),
347
			'timeout'    => 10,
348
			'user-agent' => 'jetpack_search',
349
		);
350
351
		$request_body = wp_json_encode( $es_args );
352
353
		$start_time = microtime( true );
354
355
		if ( $do_authenticated_request ) {
356
			$request_args['method'] = 'POST';
357
358
			$request = Jetpack_Client::wpcom_json_api_request_as_blog( $endpoint, Jetpack_Client::WPCOM_JSON_API_VERSION, $request_args, $request_body );
359
		} else {
360
			$request_args = array_merge( $request_args, array(
361
				'body' => $request_body,
362
			) );
363
364
			$request = wp_remote_post( $service_url, $request_args );
365
		}
366
367
		$end_time = microtime( true );
368
369
		if ( is_wp_error( $request ) ) {
370
			return $request;
371
		}
372
373
		$response_code = wp_remote_retrieve_response_code( $request );
374
375
		$response = json_decode( wp_remote_retrieve_body( $request ), true );
376
377
		$took = is_array( $response ) && ! empty( $response['took'] )
378
			? $response['took']
379
			: null;
380
381
		$query = array(
382
			'args'          => $es_args,
383
			'response'      => $response,
384
			'response_code' => $response_code,
385
			'elapsed_time'  => ( $end_time - $start_time ) * 1000, // Convert from float seconds to ms.
386
			'es_time'       => $took,
387
			'url'           => $service_url,
388
		);
389
390
		/**
391
		 * Fires after a search request has been performed.
392
		 *
393
		 * Includes the following info in the $query parameter:
394
		 *
395
		 * array args Array of Elasticsearch arguments for the search
396
		 * array response Raw API response, JSON decoded
397
		 * int response_code HTTP response code of the request
398
		 * float elapsed_time Roundtrip time of the search request, in milliseconds
399
		 * float es_time Amount of time Elasticsearch spent running the request, in milliseconds
400
		 * string url API url that was queried
401
		 *
402
		 * @module search
403
		 *
404
		 * @since  5.0.0
405
		 * @since  5.8.0 This action now fires on all queries instead of just successful queries.
406
		 *
407
		 * @param array $query Array of information about the query performed
408
		 */
409
		do_action( 'did_jetpack_search_query', $query );
410
411
		if ( ! $response_code || $response_code < 200 || $response_code >= 300 ) {
412
			/**
413
			 * Fires after a search query request has failed
414
			 *
415
			 * @module search
416
			 *
417
			 * @since  5.6.0
418
			 *
419
			 * @param array Array containing the response code and response from the failed search query
420
			 */
421
			do_action( 'failed_jetpack_search_query', array(
422
				'response_code' => $response_code,
423
				'json'          => $response,
424
			) );
425
426
			return new WP_Error( 'invalid_search_api_response', 'Invalid response from API - ' . $response_code );
427
		}
428
429
		return $response;
430
	}
431
432
	/**
433
	 * Bypass the normal Search query and offload it to Jetpack servers.
434
	 *
435
	 * This is the main hook of the plugin and is responsible for returning the posts that match the search query.
436
	 *
437
	 * @since 5.0.0
438
	 *
439
	 * @param array    $posts Current array of posts (still pre-query).
440
	 * @param WP_Query $query The WP_Query being filtered.
441
	 *
442
	 * @return array Array of matching posts.
443
	 */
444
	public function filter__posts_pre_query( $posts, $query ) {
445
		/**
446
		 * Determine whether a given WP_Query should be handled by ElasticSearch.
447
		 *
448
		 * @module search
449
		 *
450
		 * @since  5.6.0
451
		 *
452
		 * @param bool     $should_handle Should be handled by Jetpack Search.
453
		 * @param WP_Query $query         The WP_Query object.
454
		 */
455
		if ( ! apply_filters( 'jetpack_search_should_handle_query', ( $query->is_main_query() && $query->is_search() ), $query ) ) {
456
			return $posts;
457
		}
458
459
		$this->do_search( $query );
460
461
		if ( ! is_array( $this->search_result ) ) {
462
			return $posts;
463
		}
464
465
		// If no results, nothing to do
466
		if ( ! count( $this->search_result['results']['hits'] ) ) {
467
			return array();
468
		}
469
470
		$post_ids = array();
471
472
		foreach ( $this->search_result['results']['hits'] as $result ) {
473
			$post_ids[] = (int) $result['fields']['post_id'];
474
		}
475
476
		// Query all posts now
477
		$args = array(
478
			'post__in'            => $post_ids,
479
			'orderby'             => 'post__in',
480
			'perm'                => 'readable',
481
			'post_type'           => 'any',
482
			'ignore_sticky_posts' => true,
483
			'suppress_filters'    => true,
484
		);
485
486
		$posts_query = new WP_Query( $args );
487
488
		// 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.
489
		$query->found_posts   = $this->found_posts;
490
		$query->max_num_pages = ceil( $this->found_posts / $query->get( 'posts_per_page' ) );
491
492
		return $posts_query->posts;
493
	}
494
495
	/**
496
	 * Build up the search, then run it against the Jetpack servers.
497
	 *
498
	 * @since 5.0.0
499
	 *
500
	 * @param WP_Query $query The original WP_Query to use for the parameters of our search.
501
	 */
502
	public function do_search( WP_Query $query ) {
503
		if ( ! $query->is_main_query() || ! $query->is_search() ) {
504
			return;
505
		}
506
507
		$page = ( $query->get( 'paged' ) ) ? absint( $query->get( 'paged' ) ) : 1;
508
509
		// Get maximum allowed offset and posts per page values for the API.
510
		$max_offset         = Jetpack_Search_Helpers::get_max_offset();
511
		$max_posts_per_page = Jetpack_Search_Helpers::get_max_posts_per_page();
512
513
		$posts_per_page = $query->get( 'posts_per_page' );
514
		if ( $posts_per_page > $max_posts_per_page ) {
515
			$posts_per_page = $max_posts_per_page;
516
		}
517
518
		// Start building the WP-style search query args.
519
		// They'll be translated to ES format args later.
520
		$es_wp_query_args = array(
521
			'query'          => $query->get( 's' ),
522
			'posts_per_page' => $posts_per_page,
523
			'paged'          => $page,
524
			'orderby'        => $query->get( 'orderby' ),
525
			'order'          => $query->get( 'order' ),
526
		);
527
528
		if ( ! empty( $this->aggregations ) ) {
529
			$es_wp_query_args['aggregations'] = $this->aggregations;
530
		}
531
532
		// Did we query for authors?
533
		if ( $query->get( 'author_name' ) ) {
534
			$es_wp_query_args['author_name'] = $query->get( 'author_name' );
535
		}
536
537
		$es_wp_query_args['post_type'] = $this->get_es_wp_query_post_type_for_query( $query );
538
		$es_wp_query_args['terms']     = $this->get_es_wp_query_terms_for_query( $query );
539
540
		/**
541
		 * Modify the search query parameters, such as controlling the post_type.
542
		 *
543
		 * These arguments are in the format of WP_Query arguments
544
		 *
545
		 * @module search
546
		 *
547
		 * @since  5.0.0
548
		 *
549
		 * @param array    $es_wp_query_args The current query args, in WP_Query format.
550
		 * @param WP_Query $query            The original WP_Query object.
551
		 */
552
		$es_wp_query_args = apply_filters( 'jetpack_search_es_wp_query_args', $es_wp_query_args, $query );
553
554
		// If page * posts_per_page is greater than our max offset, send a 404. This is necessary because the offset is
555
		// capped at Jetpack_Search_Helpers::get_max_offset(), so a high page would always return the last page of results otherwise.
556
		if ( ( $es_wp_query_args['paged'] * $es_wp_query_args['posts_per_page'] ) > $max_offset ) {
557
			$query->set_404();
558
559
			return;
560
		}
561
562
		// If there were no post types returned, then 404 to avoid querying against non-public post types, which could
563
		// happen if we don't add the post type restriction to the ES query.
564
		if ( empty( $es_wp_query_args['post_type'] ) ) {
565
			$query->set_404();
566
567
			return;
568
		}
569
570
		// Convert the WP-style args into ES args.
571
		$es_query_args = $this->convert_wp_es_to_es_args( $es_wp_query_args );
572
573
		//Only trust ES to give us IDs, not the content since it is a mirror
574
		$es_query_args['fields'] = array(
575
			'post_id',
576
		);
577
578
		/**
579
		 * Modify the underlying ES query that is passed to the search endpoint. The returned args must represent a valid ES query
580
		 *
581
		 * This filter is harder to use if you're unfamiliar with ES, but allows complete control over the query
582
		 *
583
		 * @module search
584
		 *
585
		 * @since  5.0.0
586
		 *
587
		 * @param array    $es_query_args The raw Elasticsearch query args.
588
		 * @param WP_Query $query         The original WP_Query object.
589
		 */
590
		$es_query_args = apply_filters( 'jetpack_search_es_query_args', $es_query_args, $query );
591
592
		// Do the actual search query!
593
		$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...
594
595
		if ( is_wp_error( $this->search_result ) || ! is_array( $this->search_result ) || empty( $this->search_result['results'] ) || empty( $this->search_result['results']['hits'] ) ) {
596
			$this->found_posts = 0;
597
598
			return;
599
		}
600
601
		// If we have aggregations, fix the ordering to match the input order (ES doesn't guarantee the return order).
602
		if ( isset( $this->search_result['results']['aggregations'] ) && ! empty( $this->search_result['results']['aggregations'] ) ) {
603
			$this->search_result['results']['aggregations'] = $this->fix_aggregation_ordering( $this->search_result['results']['aggregations'], $this->aggregations );
604
		}
605
606
		// Total number of results for paging purposes. Capped at $max_offset + $posts_per_page, as deep paging gets quite expensive.
607
		$this->found_posts = min( $this->search_result['results']['total'], $max_offset + $posts_per_page );
608
	}
609
610
	/**
611
	 * If the query has already been run before filters have been updated, then we need to re-run the query
612
	 * to get the latest aggregations.
613
	 *
614
	 * This is especially useful for supporting widget management in the customizer.
615
	 *
616
	 * @since 5.8.0
617
	 *
618
	 * @return bool Whether the query was successful or not.
619
	 */
620
	public function update_search_results_aggregations() {
621
		if ( empty( $this->last_query_info ) || empty( $this->last_query_info['args'] ) ) {
622
			return false;
623
		}
624
625
		$es_args = $this->last_query_info['args'];
626
		$builder = new Jetpack_WPES_Query_Builder();
627
		$this->add_aggregations_to_es_query_builder( $this->aggregations, $builder );
628
		$es_args['aggregations'] = $builder->build_aggregation();
629
630
		$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...
631
632
		return ! is_wp_error( $this->search_result );
633
	}
634
635
	/**
636
	 * Given a WP_Query, convert its WP_Tax_Query (if present) into the WP-style Elasticsearch term arguments for the search.
637
	 *
638
	 * @since 5.0.0
639
	 *
640
	 * @param WP_Query $query The original WP_Query object for which to parse the taxonomy query.
641
	 *
642
	 * @return array The new WP-style Elasticsearch arguments (that will be converted into 'real' Elasticsearch arguments).
643
	 */
644
	public function get_es_wp_query_terms_for_query( WP_Query $query ) {
645
		$args = array();
646
647
		$the_tax_query = $query->tax_query;
648
649
		if ( ! $the_tax_query ) {
650
			return $args;
651
		}
652
653
654
		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...
655
			return $args;
656
		}
657
658
		$args = array();
659
660
		foreach ( $the_tax_query->queries as $tax_query ) {
661
			// Right now we only support slugs...see note above
662
			if ( ! is_array( $tax_query ) || 'slug' !== $tax_query['field'] ) {
663
				continue;
664
			}
665
666
			$taxonomy = $tax_query['taxonomy'];
667
668 View Code Duplication
			if ( ! isset( $args[ $taxonomy ] ) || ! is_array( $args[ $taxonomy ] ) ) {
669
				$args[ $taxonomy ] = array();
670
			}
671
672
			$args[ $taxonomy ] = array_merge( $args[ $taxonomy ], $tax_query['terms'] );
673
		}
674
675
		return $args;
676
	}
677
678
	/**
679
	 * Parse out the post type from a WP_Query.
680
	 *
681
	 * Only allows post types that are not marked as 'exclude_from_search'.
682
	 *
683
	 * @since 5.0.0
684
	 *
685
	 * @param WP_Query $query Original WP_Query object.
686
	 *
687
	 * @return array Array of searchable post types corresponding to the original query.
688
	 */
689
	public function get_es_wp_query_post_type_for_query( WP_Query $query ) {
690
		$post_types = $query->get( 'post_type' );
691
692
		// If we're searching 'any', we want to only pass searchable post types to Elasticsearch.
693
		if ( 'any' === $post_types ) {
694
			$post_types = array_values( get_post_types( array(
695
				'exclude_from_search' => false,
696
			) ) );
697
		}
698
699
		if ( ! is_array( $post_types ) ) {
700
			$post_types = array( $post_types );
701
		}
702
703
		$post_types = array_unique( $post_types );
704
705
		$sanitized_post_types = array();
706
707
		// Make sure the post types are queryable.
708
		foreach ( $post_types as $post_type ) {
709
			if ( ! $post_type ) {
710
				continue;
711
			}
712
713
			$post_type_object = get_post_type_object( $post_type );
714
			if ( ! $post_type_object || $post_type_object->exclude_from_search ) {
715
				continue;
716
			}
717
718
			$sanitized_post_types[] = $post_type;
719
		}
720
721
		return $sanitized_post_types;
722
	}
723
724
	/**
725
	 * Get the Elasticsearch result.
726
	 *
727
	 * @since 5.0.0
728
	 *
729
	 * @param bool $raw If true, does not check for WP_Error or return the 'results' array - the JSON decoded HTTP response.
730
	 *
731
	 * @return array|bool The search results, or false if there was a failure.
732
	 */
733
	public function get_search_result( $raw = false ) {
734
		if ( $raw ) {
735
			return $this->search_result;
736
		}
737
738
		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;
739
	}
740
741
	/**
742
	 * Add the date portion of a WP_Query onto the query args.
743
	 *
744
	 * @since 5.0.0
745
	 *
746
	 * @param array    $es_wp_query_args The Elasticsearch query arguments in WordPress form.
747
	 * @param WP_Query $query            The original WP_Query.
748
	 *
749
	 * @return array The es wp query args, with date filters added (as needed).
750
	 */
751
	public function filter__add_date_filter_to_query( array $es_wp_query_args, WP_Query $query ) {
752
		if ( $query->get( 'year' ) ) {
753
			if ( $query->get( 'monthnum' ) ) {
754
				// Padding
755
				$date_monthnum = sprintf( '%02d', $query->get( 'monthnum' ) );
756
757
				if ( $query->get( 'day' ) ) {
758
					// Padding
759
					$date_day = sprintf( '%02d', $query->get( 'day' ) );
760
761
					$date_start = $query->get( 'year' ) . '-' . $date_monthnum . '-' . $date_day . ' 00:00:00';
762
					$date_end   = $query->get( 'year' ) . '-' . $date_monthnum . '-' . $date_day . ' 23:59:59';
763
				} else {
764
					$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
765
766
					$date_start = $query->get( 'year' ) . '-' . $date_monthnum . '-01 00:00:00';
767
					$date_end   = $query->get( 'year' ) . '-' . $date_monthnum . '-' . $days_in_month . ' 23:59:59';
768
				}
769
			} else {
770
				$date_start = $query->get( 'year' ) . '-01-01 00:00:00';
771
				$date_end   = $query->get( 'year' ) . '-12-31 23:59:59';
772
			}
773
774
			$es_wp_query_args['date_range'] = array(
775
				'field' => 'date',
776
				'gte'   => $date_start,
777
				'lte'   => $date_end,
778
			);
779
		}
780
781
		return $es_wp_query_args;
782
	}
783
784
	/**
785
	 * Converts WP_Query style args to Elasticsearch args.
786
	 *
787
	 * @since 5.0.0
788
	 *
789
	 * @param array $args Array of WP_Query style arguments.
790
	 *
791
	 * @return array Array of ES style query arguments.
792
	 */
793
	public function convert_wp_es_to_es_args( array $args ) {
794
		jetpack_require_lib( 'jetpack-wpes-query-builder/jetpack-wpes-query-parser' );
795
796
		$defaults = array(
797
			'blog_id'        => get_current_blog_id(),
798
			'query'          => null,    // Search phrase
799
			'query_fields'   => array(), // list of fields to search
800
			'excess_boost'   => array(), // map of field to excess boost values (multiply)
801
			'post_type'      => null,    // string or an array
802
			'terms'          => array(), // ex: array( 'taxonomy-1' => array( 'slug' ), 'taxonomy-2' => array( 'slug-a', 'slug-b' ) )
803
			'author'         => null,    // id or an array of ids
804
			'author_name'    => array(), // string or an array
805
			'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'
806
			'orderby'        => null,    // Defaults to 'relevance' if query is set, otherwise 'date'. Pass an array for multiple orders.
807
			'order'          => 'DESC',
808
			'posts_per_page' => 10,
809
			'offset'         => null,
810
			'paged'          => null,
811
			/**
812
			 * Aggregations. Examples:
813
			 * array(
814
			 *     'Tag'       => array( 'type' => 'taxonomy', 'taxonomy' => 'post_tag', 'count' => 10 ) ),
815
			 *     'Post Type' => array( 'type' => 'post_type', 'count' => 10 ) ),
816
			 * );
817
			 */
818
			'aggregations'   => null,
819
		);
820
821
		$args = wp_parse_args( $args, $defaults );
822
823
		$parser = new Jetpack_WPES_Search_Query_Parser( $args['query'], array( get_locale() ) );
824
825
		if ( empty( $args['query_fields'] ) ) {
826
			if ( defined( 'JETPACK_SEARCH_VIP_INDEX' ) && JETPACK_SEARCH_VIP_INDEX ) {
827
				// VIP indices do not have per language fields
828
				$match_fields = $this->_get_caret_boosted_fields(
829
					array(
830
						'title'         => 0.1,
831
						'content'       => 0.1,
832
						'excerpt'       => 0.1,
833
						'tag.name'      => 0.1,
834
						'category.name' => 0.1,
835
						'author_login'  => 0.1,
836
						'author'        => 0.1,
837
					)
838
				);
839
840
				$boost_fields = $this->_get_caret_boosted_fields(
841
					$this->_apply_boosts_multiplier( array(
842
						'title'         => 2,
843
						'tag.name'      => 1,
844
						'category.name' => 1,
845
						'author_login'  => 1,
846
						'author'        => 1,
847
					), $args['excess_boost'] )
848
				);
849
850
				$boost_phrase_fields = $this->_get_caret_boosted_fields(
851
					array(
852
						'title'         => 1,
853
						'content'       => 1,
854
						'excerpt'       => 1,
855
						'tag.name'      => 1,
856
						'category.name' => 1,
857
						'author'        => 1,
858
					)
859
				);
860
			} else {
861
				$match_fields = $parser->merge_ml_fields(
862
					array(
863
						'title'         => 0.1,
864
						'content'       => 0.1,
865
						'excerpt'       => 0.1,
866
						'tag.name'      => 0.1,
867
						'category.name' => 0.1,
868
					),
869
					$this->_get_caret_boosted_fields( array(
870
						'author_login'  => 0.1,
871
						'author'        => 0.1,
872
					) )
873
				);
874
875
				$boost_fields = $parser->merge_ml_fields(
876
					$this->_apply_boosts_multiplier( array(
877
						'title'         => 2,
878
						'tag.name'      => 1,
879
						'category.name' => 1,
880
					), $args['excess_boost'] ),
881
					$this->_get_caret_boosted_fields( $this->_apply_boosts_multiplier( array(
882
						'author_login'  => 1,
883
						'author'        => 1,
884
					), $args['excess_boost'] ) )
885
				);
886
887
				$boost_phrase_fields = $parser->merge_ml_fields(
888
					array(
889
						'title'         => 1,
890
						'content'       => 1,
891
						'excerpt'       => 1,
892
						'tag.name'      => 1,
893
						'category.name' => 1,
894
					),
895
					$this->_get_caret_boosted_fields( array(
896
						'author'        => 1,
897
					) )
898
				);
899
			}
900
		} else {
901
			// If code is overriding the fields, then use that. Important for backwards compatibility.
902
			$match_fields        = $args['query_fields'];
903
			$boost_phrase_fields = $match_fields;
904
			$boost_fields        = null;
905
		}
906
907
		$parser->phrase_filter( array(
908
			'must_query_fields'  => $match_fields,
909
			'boost_query_fields' => null,
910
		) );
911
		$parser->remaining_query( array(
912
			'must_query_fields'  => $match_fields,
913
			'boost_query_fields' => $boost_fields,
914
		) );
915
916
		// Boost on phrase matches
917
		$parser->remaining_query( array(
918
			'boost_query_fields' => $boost_phrase_fields,
919
			'boost_query_type'   => 'phrase',
920
		) );
921
922
		/**
923
		 * Modify the recency decay parameters for the search query.
924
		 *
925
		 * The recency decay lowers the search scores based on the age of a post relative to an origin date. Basic adjustments:
926
		 *  - origin: A date. Posts with this date will have the highest score and no decay applied. Default is today.
927
		 *  - 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.
928
		 *  - scale: The number of days/months/years from the origin+offset at which the decay will equal the decay param. Default 360d
929
		 *  - decay: The amount of decay applied at offset+scale. Default 0.9.
930
		 *
931
		 * 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}
932
		 *
933
		 * @module search
934
		 *
935
		 * @since  5.8.0
936
		 *
937
		 * @param array $decay_params The decay parameters.
938
		 * @param array $args         The WP query parameters.
939
		 */
940
		$decay_params = apply_filters(
941
			'jetpack_search_recency_score_decay',
942
			array(
943
				'origin' => date( 'Y-m-d' ),
944
				'scale'  => '360d',
945
				'decay'  => 0.9,
946
			),
947
			$args
948
		);
949
950
		if ( ! empty( $decay_params ) ) {
951
			// Newer content gets weighted slightly higher
952
			$parser->add_decay( 'gauss', array(
953
				'date_gmt' => $decay_params
954
			) );
955
		}
956
957
		$es_query_args = array(
958
			'blog_id' => absint( $args['blog_id'] ),
959
			'size'    => absint( $args['posts_per_page'] ),
960
		);
961
962
		// ES "from" arg (offset)
963
		if ( $args['offset'] ) {
964
			$es_query_args['from'] = absint( $args['offset'] );
965
		} elseif ( $args['paged'] ) {
966
			$es_query_args['from'] = max( 0, ( absint( $args['paged'] ) - 1 ) * $es_query_args['size'] );
967
		}
968
969
		$es_query_args['from'] = min( $es_query_args['from'], Jetpack_Search_Helpers::get_max_offset() );
970
971
		if ( ! is_array( $args['author_name'] ) ) {
972
			$args['author_name'] = array( $args['author_name'] );
973
		}
974
975
		// ES stores usernames, not IDs, so transform
976
		if ( ! empty( $args['author'] ) ) {
977
			if ( ! is_array( $args['author'] ) ) {
978
				$args['author'] = array( $args['author'] );
979
			}
980
981
			foreach ( $args['author'] as $author ) {
982
				$user = get_user_by( 'id', $author );
983
984
				if ( $user && ! empty( $user->user_login ) ) {
985
					$args['author_name'][] = $user->user_login;
986
				}
987
			}
988
		}
989
990
		//////////////////////////////////////////////////
991
		// Build the filters from the query elements.
992
		// Filters rock because they are cached from one query to the next
993
		// but they are cached as individual filters, rather than all combined together.
994
		// May get performance boost by also caching the top level boolean filter too.
995
996
		if ( $args['post_type'] ) {
997
			if ( ! is_array( $args['post_type'] ) ) {
998
				$args['post_type'] = array( $args['post_type'] );
999
			}
1000
1001
			$parser->add_filter( array(
1002
				'terms' => array(
1003
					'post_type' => $args['post_type'],
1004
				),
1005
			) );
1006
		}
1007
1008
		if ( $args['author_name'] ) {
1009
			$parser->add_filter( array(
1010
				'terms' => array(
1011
					'author_login' => $args['author_name'],
1012
				),
1013
			) );
1014
		}
1015
1016
		if ( ! empty( $args['date_range'] ) && isset( $args['date_range']['field'] ) ) {
1017
			$field = $args['date_range']['field'];
1018
1019
			unset( $args['date_range']['field'] );
1020
1021
			$parser->add_filter( array(
1022
				'range' => array(
1023
					$field => $args['date_range'],
1024
				),
1025
			) );
1026
		}
1027
1028
		if ( is_array( $args['terms'] ) ) {
1029
			foreach ( $args['terms'] as $tax => $terms ) {
1030
				$terms = (array) $terms;
1031
1032
				if ( count( $terms ) && mb_strlen( $tax ) ) {
1033 View Code Duplication
					switch ( $tax ) {
1034
						case 'post_tag':
1035
							$tax_fld = 'tag.slug';
1036
1037
							break;
1038
1039
						case 'category':
1040
							$tax_fld = 'category.slug';
1041
1042
							break;
1043
1044
						default:
1045
							$tax_fld = 'taxonomy.' . $tax . '.slug';
1046
1047
							break;
1048
					}
1049
1050
					foreach ( $terms as $term ) {
1051
						$parser->add_filter( array(
1052
							'term' => array(
1053
								$tax_fld => $term,
1054
							),
1055
						) );
1056
					}
1057
				}
1058
			}
1059
		}
1060
1061
		if ( ! $args['orderby'] ) {
1062
			if ( $args['query'] ) {
1063
				$args['orderby'] = array( 'relevance' );
1064
			} else {
1065
				$args['orderby'] = array( 'date' );
1066
			}
1067
		}
1068
1069
		// Validate the "order" field
1070
		switch ( strtolower( $args['order'] ) ) {
1071
			case 'asc':
1072
				$args['order'] = 'asc';
1073
				break;
1074
1075
			case 'desc':
1076
			default:
1077
				$args['order'] = 'desc';
1078
				break;
1079
		}
1080
1081
		$es_query_args['sort'] = array();
1082
1083
		foreach ( (array) $args['orderby'] as $orderby ) {
1084
			// Translate orderby from WP field to ES field
1085
			switch ( $orderby ) {
1086
				case 'relevance' :
1087
					//never order by score ascending
1088
					$es_query_args['sort'][] = array(
1089
						'_score' => array(
1090
							'order' => 'desc',
1091
						),
1092
					);
1093
1094
					break;
1095
1096 View Code Duplication
				case 'date' :
1097
					$es_query_args['sort'][] = array(
1098
						'date' => array(
1099
							'order' => $args['order'],
1100
						),
1101
					);
1102
1103
					break;
1104
1105 View Code Duplication
				case 'ID' :
1106
					$es_query_args['sort'][] = array(
1107
						'id' => array(
1108
							'order' => $args['order'],
1109
						),
1110
					);
1111
1112
					break;
1113
1114
				case 'author' :
1115
					$es_query_args['sort'][] = array(
1116
						'author.raw' => array(
1117
							'order' => $args['order'],
1118
						),
1119
					);
1120
1121
					break;
1122
			} // End switch().
1123
		} // End foreach().
1124
1125
		if ( empty( $es_query_args['sort'] ) ) {
1126
			unset( $es_query_args['sort'] );
1127
		}
1128
1129
		// Aggregations
1130
		if ( ! empty( $args['aggregations'] ) ) {
1131
			$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...
1132
		}
1133
1134
		$es_query_args['filter']       = $parser->build_filter();
1135
		$es_query_args['query']        = $parser->build_query();
1136
		$es_query_args['aggregations'] = $parser->build_aggregation();
1137
1138
		return $es_query_args;
1139
	}
1140
1141
	/**
1142
	 * Given an array of aggregations, parse and add them onto the Jetpack_WPES_Query_Builder object for use in Elasticsearch.
1143
	 *
1144
	 * @since 5.0.0
1145
	 *
1146
	 * @param array                      $aggregations Array of aggregations (filters) to add to the Jetpack_WPES_Query_Builder.
1147
	 * @param Jetpack_WPES_Query_Builder $builder      The builder instance that is creating the Elasticsearch query.
1148
	 */
1149
	public function add_aggregations_to_es_query_builder( array $aggregations, Jetpack_WPES_Query_Builder $builder ) {
1150
		foreach ( $aggregations as $label => $aggregation ) {
1151
			switch ( $aggregation['type'] ) {
1152
				case 'taxonomy':
1153
					$this->add_taxonomy_aggregation_to_es_query_builder( $aggregation, $label, $builder );
1154
1155
					break;
1156
1157
				case 'post_type':
1158
					$this->add_post_type_aggregation_to_es_query_builder( $aggregation, $label, $builder );
1159
1160
					break;
1161
1162
				case 'date_histogram':
1163
					$this->add_date_histogram_aggregation_to_es_query_builder( $aggregation, $label, $builder );
1164
1165
					break;
1166
			}
1167
		}
1168
	}
1169
1170
	/**
1171
	 * Given an individual taxonomy aggregation, add it to the Jetpack_WPES_Query_Builder object for use in Elasticsearch.
1172
	 *
1173
	 * @since 5.0.0
1174
	 *
1175
	 * @param array                      $aggregation The aggregation to add to the query builder.
1176
	 * @param string                     $label       The 'label' (unique id) for this aggregation.
1177
	 * @param Jetpack_WPES_Query_Builder $builder     The builder instance that is creating the Elasticsearch query.
1178
	 */
1179
	public function add_taxonomy_aggregation_to_es_query_builder( array $aggregation, $label, Jetpack_WPES_Query_Builder $builder ) {
1180
		$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...
1181
1182
		switch ( $aggregation['taxonomy'] ) {
1183
			case 'post_tag':
1184
				$field = 'tag';
1185
				break;
1186
1187
			case 'category':
1188
				$field = 'category';
1189
				break;
1190
1191
			default:
1192
				$field = 'taxonomy.' . $aggregation['taxonomy'];
1193
				break;
1194
		}
1195
1196
		$builder->add_aggs( $label, array(
1197
			'terms' => array(
1198
				'field' => $field . '.slug',
1199
				'size'  => min( (int) $aggregation['count'], $this->max_aggregations_count ),
1200
			),
1201
		) );
1202
	}
1203
1204
	/**
1205
	 * Given an individual post_type aggregation, add it to the Jetpack_WPES_Query_Builder object for use in Elasticsearch.
1206
	 *
1207
	 * @since 5.0.0
1208
	 *
1209
	 * @param array                      $aggregation The aggregation to add to the query builder.
1210
	 * @param string                     $label       The 'label' (unique id) for this aggregation.
1211
	 * @param Jetpack_WPES_Query_Builder $builder     The builder instance that is creating the Elasticsearch query.
1212
	 */
1213
	public function add_post_type_aggregation_to_es_query_builder( array $aggregation, $label, Jetpack_WPES_Query_Builder $builder ) {
1214
		$builder->add_aggs( $label, array(
1215
			'terms' => array(
1216
				'field' => 'post_type',
1217
				'size'  => min( (int) $aggregation['count'], $this->max_aggregations_count ),
1218
			),
1219
		) );
1220
	}
1221
1222
	/**
1223
	 * Given an individual date_histogram aggregation, add it to the Jetpack_WPES_Query_Builder object for use in Elasticsearch.
1224
	 *
1225
	 * @since 5.0.0
1226
	 *
1227
	 * @param array                      $aggregation The aggregation to add to the query builder.
1228
	 * @param string                     $label       The 'label' (unique id) for this aggregation.
1229
	 * @param Jetpack_WPES_Query_Builder $builder     The builder instance that is creating the Elasticsearch query.
1230
	 */
1231
	public function add_date_histogram_aggregation_to_es_query_builder( array $aggregation, $label, Jetpack_WPES_Query_Builder $builder ) {
1232
		$args = array(
1233
			'interval' => $aggregation['interval'],
1234
			'field'    => ( ! empty( $aggregation['field'] ) && 'post_date_gmt' == $aggregation['field'] ) ? 'date_gmt' : 'date',
1235
		);
1236
1237
		if ( isset( $aggregation['min_doc_count'] ) ) {
1238
			$args['min_doc_count'] = intval( $aggregation['min_doc_count'] );
1239
		} else {
1240
			$args['min_doc_count'] = 1;
1241
		}
1242
1243
		$builder->add_aggs( $label, array(
1244
			'date_histogram' => $args,
1245
		) );
1246
	}
1247
1248
	/**
1249
	 * And an existing filter object with a list of additional filters.
1250
	 *
1251
	 * Attempts to optimize the filters somewhat.
1252
	 *
1253
	 * @since 5.0.0
1254
	 *
1255
	 * @param array $curr_filter The existing filters to build upon.
1256
	 * @param array $filters     The new filters to add.
1257
	 *
1258
	 * @return array The resulting merged filters.
1259
	 */
1260
	public static function and_es_filters( array $curr_filter, array $filters ) {
1261
		if ( ! is_array( $curr_filter ) || isset( $curr_filter['match_all'] ) ) {
1262
			if ( 1 === count( $filters ) ) {
1263
				return $filters[0];
1264
			}
1265
1266
			return array(
1267
				'and' => $filters,
1268
			);
1269
		}
1270
1271
		return array(
1272
			'and' => array_merge( array( $curr_filter ), $filters ),
1273
		);
1274
	}
1275
1276
	/**
1277
	 * Set the available filters for the search.
1278
	 *
1279
	 * These get rendered via the Jetpack_Search_Widget() widget.
1280
	 *
1281
	 * Behind the scenes, these are implemented using Elasticsearch Aggregations.
1282
	 *
1283
	 * If you do not require counts of how many documents match each filter, please consider using regular WP Query
1284
	 * arguments instead, such as via the jetpack_search_es_wp_query_args filter
1285
	 *
1286
	 * @see    https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations.html
1287
	 *
1288
	 * @since  5.0.0
1289
	 *
1290
	 * @param array $aggregations Array of filters (aggregations) to apply to the search
1291
	 */
1292
	public function set_filters( array $aggregations ) {
1293
		foreach ( (array) $aggregations as $key => $agg ) {
1294
			if ( empty( $agg['name'] ) ) {
1295
				$aggregations[ $key ]['name'] = $key;
1296
			}
1297
		}
1298
		$this->aggregations = $aggregations;
1299
	}
1300
1301
	/**
1302
	 * Set the search's facets (deprecated).
1303
	 *
1304
	 * @deprecated 5.0 Please use Jetpack_Search::set_filters() instead.
1305
	 *
1306
	 * @see        Jetpack_Search::set_filters()
1307
	 *
1308
	 * @param array $facets Array of facets to apply to the search.
1309
	 */
1310
	public function set_facets( array $facets ) {
1311
		_deprecated_function( __METHOD__, 'jetpack-5.0', 'Jetpack_Search::set_filters()' );
1312
1313
		$this->set_filters( $facets );
1314
	}
1315
1316
	/**
1317
	 * Get the raw Aggregation results from the Elasticsearch response.
1318
	 *
1319
	 * @since  5.0.0
1320
	 *
1321
	 * @see    https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations.html
1322
	 *
1323
	 * @return array Array of Aggregations performed on the search.
1324
	 */
1325
	public function get_search_aggregations_results() {
1326
		$aggregations = array();
1327
1328
		$search_result = $this->get_search_result();
1329
1330
		if ( ! empty( $search_result ) && ! empty( $search_result['aggregations'] ) ) {
1331
			$aggregations = $search_result['aggregations'];
1332
		}
1333
1334
		return $aggregations;
1335
	}
1336
1337
	/**
1338
	 * Get the raw Facet results from the Elasticsearch response.
1339
	 *
1340
	 * @deprecated 5.0 Please use Jetpack_Search::get_search_aggregations_results() instead.
1341
	 *
1342
	 * @see        Jetpack_Search::get_search_aggregations_results()
1343
	 *
1344
	 * @return array Array of Facets performed on the search.
1345
	 */
1346
	public function get_search_facets() {
1347
		_deprecated_function( __METHOD__, 'jetpack-5.0', 'Jetpack_Search::get_search_aggregations_results()' );
1348
1349
		return $this->get_search_aggregations_results();
1350
	}
1351
1352
	/**
1353
	 * Get the results of the Filters performed, including the number of matching documents.
1354
	 *
1355
	 * Returns an array of Filters (keyed by $label, as passed to Jetpack_Search::set_filters()), containing the Filter and all resulting
1356
	 * matching buckets, the url for applying/removing each bucket, etc.
1357
	 *
1358
	 * NOTE - if this is called before the search is performed, an empty array will be returned. Use the $aggregations class
1359
	 * member if you need to access the raw filters set in Jetpack_Search::set_filters().
1360
	 *
1361
	 * @since 5.0.0
1362
	 *
1363
	 * @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...
1364
	 *
1365
	 * @return array Array of filters applied and info about them.
1366
	 */
1367
	public function get_filters( WP_Query $query = null ) {
1368
		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...
1369
			global $wp_query;
1370
1371
			$query = $wp_query;
1372
		}
1373
1374
		$aggregation_data = $this->aggregations;
1375
1376
		if ( empty( $aggregation_data ) ) {
1377
			return $aggregation_data;
1378
		}
1379
1380
		$aggregation_results = $this->get_search_aggregations_results();
1381
1382
		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...
1383
			return $aggregation_data;
1384
		}
1385
1386
		// NOTE - Looping over the _results_, not the original configured aggregations, so we get the 'real' data from ES
1387
		foreach ( $aggregation_results as $label => $aggregation ) {
1388
			if ( empty( $aggregation ) ) {
1389
				continue;
1390
			}
1391
1392
			$type = $this->aggregations[ $label ]['type'];
1393
1394
			$aggregation_data[ $label ]['buckets'] = array();
1395
1396
			$existing_term_slugs = array();
1397
1398
			$tax_query_var = null;
1399
1400
			// Figure out which terms are active in the query, for this taxonomy
1401
			if ( 'taxonomy' === $this->aggregations[ $label ]['type'] ) {
1402
				$tax_query_var = $this->get_taxonomy_query_var( $this->aggregations[ $label ]['taxonomy'] );
1403
1404
				if ( ! empty( $query->tax_query ) && ! empty( $query->tax_query->queries ) && is_array( $query->tax_query->queries ) ) {
1405
					foreach ( $query->tax_query->queries as $tax_query ) {
1406
						if ( is_array( $tax_query ) && $this->aggregations[ $label ]['taxonomy'] === $tax_query['taxonomy'] &&
1407
						     'slug' === $tax_query['field'] &&
1408
						     is_array( $tax_query['terms'] ) ) {
1409
							$existing_term_slugs = array_merge( $existing_term_slugs, $tax_query['terms'] );
1410
						}
1411
					}
1412
				}
1413
			}
1414
1415
			// Now take the resulting found aggregation items and generate the additional info about them, such as activation/deactivation url, name, count, etc.
1416
			$buckets = array();
1417
1418
			if ( ! empty( $aggregation['buckets'] ) ) {
1419
				$buckets = (array) $aggregation['buckets'];
1420
			}
1421
1422
			if ( 'date_histogram' == $type ) {
1423
				//re-order newest to oldest
1424
				$buckets = array_reverse( $buckets );
1425
			}
1426
1427
			// Some aggregation types like date_histogram don't support the max results parameter
1428
			if ( is_int( $this->aggregations[ $label ]['count'] ) && count( $buckets ) > $this->aggregations[ $label ]['count'] ) {
1429
				$buckets = array_slice( $buckets, 0, $this->aggregations[ $label ]['count'] );
1430
			}
1431
1432
			foreach ( $buckets as $item ) {
1433
				$query_vars = array();
1434
				$active     = false;
1435
				$remove_url = null;
1436
				$name       = '';
1437
1438
				// What type was the original aggregation?
1439
				switch ( $type ) {
1440
					case 'taxonomy':
1441
						$taxonomy = $this->aggregations[ $label ]['taxonomy'];
1442
1443
						$term = get_term_by( 'slug', $item['key'], $taxonomy );
1444
1445
						if ( ! $term || ! $tax_query_var ) {
1446
							continue 2; // switch() is considered a looping structure
1447
						}
1448
1449
						$query_vars = array(
1450
							$tax_query_var => implode( '+', array_merge( $existing_term_slugs, array( $term->slug ) ) ),
1451
						);
1452
1453
						$name = $term->name;
1454
1455
						// Let's determine if this term is active or not
1456
1457
						if ( in_array( $item['key'], $existing_term_slugs, true ) ) {
1458
							$active = true;
1459
1460
							$slug_count = count( $existing_term_slugs );
1461
1462 View Code Duplication
							if ( $slug_count > 1 ) {
1463
								$remove_url = Jetpack_Search_Helpers::add_query_arg(
1464
									$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...
1465
									rawurlencode( implode( '+', array_diff( $existing_term_slugs, array( $item['key'] ) ) ) )
1466
								);
1467
							} else {
1468
								$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...
1469
							}
1470
						}
1471
1472
						break;
1473
1474
					case 'post_type':
1475
						$post_type = get_post_type_object( $item['key'] );
1476
1477
						if ( ! $post_type || $post_type->exclude_from_search ) {
1478
							continue 2;  // switch() is considered a looping structure
1479
						}
1480
1481
						$query_vars = array(
1482
							'post_type' => $item['key'],
1483
						);
1484
1485
						$name = $post_type->labels->singular_name;
1486
1487
						// Is this post type active on this search?
1488
						$post_types = $query->get( 'post_type' );
1489
1490
						if ( ! is_array( $post_types ) ) {
1491
							$post_types = array( $post_types );
1492
						}
1493
1494
						if ( in_array( $item['key'], $post_types ) ) {
1495
							$active = true;
1496
1497
							$post_type_count = count( $post_types );
1498
1499
							// 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
1500 View Code Duplication
							if ( $post_type_count > 1 ) {
1501
								$remove_url = Jetpack_Search_Helpers::add_query_arg(
1502
									'post_type',
1503
									rawurlencode( implode( ',', array_diff( $post_types, array( $item['key'] ) ) ) )
1504
								);
1505
							} else {
1506
								$remove_url = Jetpack_Search_Helpers::remove_query_arg( 'post_type' );
1507
							}
1508
						}
1509
1510
						break;
1511
1512
					case 'date_histogram':
1513
						$timestamp = $item['key'] / 1000;
1514
1515
						$current_year  = $query->get( 'year' );
1516
						$current_month = $query->get( 'monthnum' );
1517
						$current_day   = $query->get( 'day' );
1518
1519
						switch ( $this->aggregations[ $label ]['interval'] ) {
1520
							case 'year':
1521
								$year = (int) date( 'Y', $timestamp );
1522
1523
								$query_vars = array(
1524
									'year'     => $year,
1525
									'monthnum' => false,
1526
									'day'      => false,
1527
								);
1528
1529
								$name = $year;
1530
1531
								// Is this year currently selected?
1532
								if ( ! empty( $current_year ) && (int) $current_year === $year ) {
1533
									$active = true;
1534
1535
									$remove_url = Jetpack_Search_Helpers::remove_query_arg( array( 'year', 'monthnum', 'day' ) );
1536
								}
1537
1538
								break;
1539
1540
							case 'month':
1541
								$year  = (int) date( 'Y', $timestamp );
1542
								$month = (int) date( 'n', $timestamp );
1543
1544
								$query_vars = array(
1545
									'year'     => $year,
1546
									'monthnum' => $month,
1547
									'day'      => false,
1548
								);
1549
1550
								$name = date( 'F Y', $timestamp );
1551
1552
								// Is this month currently selected?
1553
								if ( ! empty( $current_year ) && (int) $current_year === $year &&
1554
								     ! empty( $current_month ) && (int) $current_month === $month ) {
1555
									$active = true;
1556
1557
									$remove_url = Jetpack_Search_Helpers::remove_query_arg( array( 'year', 'monthnum' ) );
1558
								}
1559
1560
								break;
1561
1562
							case 'day':
1563
								$year  = (int) date( 'Y', $timestamp );
1564
								$month = (int) date( 'n', $timestamp );
1565
								$day   = (int) date( 'j', $timestamp );
1566
1567
								$query_vars = array(
1568
									'year'     => $year,
1569
									'monthnum' => $month,
1570
									'day'      => $day,
1571
								);
1572
1573
								$name = date( 'F jS, Y', $timestamp );
1574
1575
								// Is this day currently selected?
1576
								if ( ! empty( $current_year ) && (int) $current_year === $year &&
1577
								     ! empty( $current_month ) && (int) $current_month === $month &&
1578
								     ! empty( $current_day ) && (int) $current_day === $day ) {
1579
									$active = true;
1580
1581
									$remove_url = Jetpack_Search_Helpers::remove_query_arg( array( 'day' ) );
1582
								}
1583
1584
								break;
1585
1586
							default:
1587
								continue 3; // switch() is considered a looping structure
1588
						} // End switch().
1589
1590
						break;
1591
1592
					default:
1593
						//continue 2; // switch() is considered a looping structure
1594
				} // End switch().
1595
1596
				// Need to urlencode param values since add_query_arg doesn't
1597
				$url_params = urlencode_deep( $query_vars );
1598
1599
				$aggregation_data[ $label ]['buckets'][] = array(
1600
					'url'        => Jetpack_Search_Helpers::add_query_arg( $url_params ),
1601
					'query_vars' => $query_vars,
1602
					'name'       => $name,
1603
					'count'      => $item['doc_count'],
1604
					'active'     => $active,
1605
					'remove_url' => $remove_url,
1606
					'type'       => $type,
1607
					'type_label' => $aggregation_data[ $label ]['name'],
1608
					'widget_id'  => ! empty( $aggregation_data[ $label ]['widget_id'] ) ? $aggregation_data[ $label ]['widget_id'] : 0
1609
				);
1610
			} // End foreach().
1611
		} // End foreach().
1612
1613
		/**
1614
		 * Modify the aggregation filters returned by get_filters().
1615
		 *
1616
		 * Useful if you are setting custom filters outside of the supported filters (taxonomy, post_type etc.) and
1617
		 * want to hook them up so they're returned when you call `get_filters()`.
1618
		 *
1619
		 * @module search
1620
		 *
1621
		 * @since  6.9.0
1622
		 *
1623
		 * @param array    $aggregation_data The array of filters keyed on label.
1624
		 * @param WP_Query $query            The WP_Query object.
1625
		 */
1626
		return apply_filters( 'jetpack_search_get_filters', $aggregation_data, $query );
1627
	}
1628
1629
	/**
1630
	 * Get the results of the facets performed.
1631
	 *
1632
	 * @deprecated 5.0 Please use Jetpack_Search::get_filters() instead.
1633
	 *
1634
	 * @see        Jetpack_Search::get_filters()
1635
	 *
1636
	 * @return array $facets Array of facets applied and info about them.
1637
	 */
1638
	public function get_search_facet_data() {
1639
		_deprecated_function( __METHOD__, 'jetpack-5.0', 'Jetpack_Search::get_filters()' );
1640
1641
		return $this->get_filters();
1642
	}
1643
1644
	/**
1645
	 * Get the filters that are currently applied to this search.
1646
	 *
1647
	 * @since 5.0.0
1648
	 *
1649
	 * @return array Array of filters that were applied.
1650
	 */
1651
	public function get_active_filter_buckets() {
1652
		$active_buckets = array();
1653
1654
		$filters = $this->get_filters();
1655
1656
		if ( ! is_array( $filters ) ) {
1657
			return $active_buckets;
1658
		}
1659
1660
		foreach ( $filters as $filter ) {
1661
			if ( isset( $filter['buckets'] ) && is_array( $filter['buckets'] ) ) {
1662
				foreach ( $filter['buckets'] as $item ) {
1663
					if ( isset( $item['active'] ) && $item['active'] ) {
1664
						$active_buckets[] = $item;
1665
					}
1666
				}
1667
			}
1668
		}
1669
1670
		return $active_buckets;
1671
	}
1672
1673
	/**
1674
	 * Get the filters that are currently applied to this search.
1675
	 *
1676
	 * @deprecated 5.0 Please use Jetpack_Search::get_active_filter_buckets() instead.
1677
	 *
1678
	 * @see        Jetpack_Search::get_active_filter_buckets()
1679
	 *
1680
	 * @return array Array of filters that were applied.
1681
	 */
1682
	public function get_current_filters() {
1683
		_deprecated_function( __METHOD__, 'jetpack-5.0', 'Jetpack_Search::get_active_filter_buckets()' );
1684
1685
		return $this->get_active_filter_buckets();
1686
	}
1687
1688
	/**
1689
	 * Calculate the right query var to use for a given taxonomy.
1690
	 *
1691
	 * Allows custom code to modify the GET var that is used to represent a given taxonomy, via the jetpack_search_taxonomy_query_var filter.
1692
	 *
1693
	 * @since 5.0.0
1694
	 *
1695
	 * @param string $taxonomy_name The name of the taxonomy for which to get the query var.
1696
	 *
1697
	 * @return bool|string The query var to use for this taxonomy, or false if none found.
1698
	 */
1699
	public function get_taxonomy_query_var( $taxonomy_name ) {
1700
		$taxonomy = get_taxonomy( $taxonomy_name );
1701
1702
		if ( ! $taxonomy || is_wp_error( $taxonomy ) ) {
1703
			return false;
1704
		}
1705
1706
		/**
1707
		 * Modify the query var to use for a given taxonomy
1708
		 *
1709
		 * @module search
1710
		 *
1711
		 * @since  5.0.0
1712
		 *
1713
		 * @param string $query_var     The current query_var for the taxonomy
1714
		 * @param string $taxonomy_name The taxonomy name
1715
		 */
1716
		return apply_filters( 'jetpack_search_taxonomy_query_var', $taxonomy->query_var, $taxonomy_name );
1717
	}
1718
1719
	/**
1720
	 * Takes an array of aggregation results, and ensures the array key ordering matches the key order in $desired
1721
	 * which is the input order.
1722
	 *
1723
	 * Necessary because ES does not always return aggregations in the same order that you pass them in,
1724
	 * and it should be possible to control the display order easily.
1725
	 *
1726
	 * @since 5.0.0
1727
	 *
1728
	 * @param array $aggregations Aggregation results to be reordered.
1729
	 * @param array $desired      Array with keys representing the desired ordering.
1730
	 *
1731
	 * @return array A new array with reordered keys, matching those in $desired.
1732
	 */
1733
	public function fix_aggregation_ordering( array $aggregations, array $desired ) {
1734
		if ( empty( $aggregations ) || empty( $desired ) ) {
1735
			return $aggregations;
1736
		}
1737
1738
		$reordered = array();
1739
1740
		foreach ( array_keys( $desired ) as $agg_name ) {
1741
			if ( isset( $aggregations[ $agg_name ] ) ) {
1742
				$reordered[ $agg_name ] = $aggregations[ $agg_name ];
1743
			}
1744
		}
1745
1746
		return $reordered;
1747
	}
1748
1749
	/**
1750
	 * Sends events to Tracks when a search filters widget is updated.
1751
	 *
1752
	 * @since 5.8.0
1753
	 *
1754
	 * @param string $option    The option name. Only "widget_jetpack-search-filters" is cared about.
1755
	 * @param array  $old_value The old option value.
1756
	 * @param array  $new_value The new option value.
1757
	 */
1758
	public function track_widget_updates( $option, $old_value, $new_value ) {
1759
		if ( 'widget_jetpack-search-filters' !== $option ) {
1760
			return;
1761
		}
1762
1763
		$event = Jetpack_Search_Helpers::get_widget_tracks_value( $old_value, $new_value );
1764
		if ( ! $event ) {
1765
			return;
1766
		}
1767
1768
		jetpack_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
	 * Transforms an array with fields name as keys and boosts as value into
1819
	 * shorthand "caret" format.
1820
	 *
1821
	 * @param array $fields_boost [ "title" => "2", "content" => "1" ]
1822
	 *
1823
	 * @return array [ "title^2", "content^1" ]
1824
	 */
1825
	private function _get_caret_boosted_fields( array $fields_boost ) {
1826
		$caret_boosted_fields = array();
1827
		foreach ( $fields_boost as $field => $boost ) {
1828
			$caret_boosted_fields[] = "$field^$boost";
1829
		}
1830
		return $caret_boosted_fields;
1831
	}
1832
1833
	/**
1834
	 * Apply a multiplier to boost values.
1835
	 *
1836
	 * @param array $fields_boost [ "title" => 2, "content" => 1 ]
1837
	 * @param array $fields_boost_multiplier [ "title" => 0.1234 ]
1838
	 *
1839
	 * @return array [ "title" => "0.247", "content" => "1.000" ]
1840
	 */
1841
	private function _apply_boosts_multiplier( array $fields_boost, array $fields_boost_multiplier ) {
1842
		foreach( $fields_boost as $field_name => $field_boost ) {
1843
			if ( isset( $fields_boost_multiplier[ $field_name ] ) ) {
1844
				$fields_boost[ $field_name ] *= $fields_boost_multiplier[ $field_name ];
1845
			}
1846
1847
			// Set a floor and format the number as string
1848
			$fields_boost[ $field_name ] = number_format(
1849
				max( 0.001, $fields_boost[ $field_name ] ),
1850
				3, '.', ''
1851
			);
1852
		}
1853
1854
		return $fields_boost;
1855
	}
1856
}
1857