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