PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.2
Jetpack – WP Security, Backup, Speed, & Growth v16.2
16.3 16.3-beta 16.3-a.5 16.3-a.7 16.3-a.3 16.3-a.1 16.2 16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 All 508 releases
jetpack / jetpack_vendor / automattic / jetpack-search / src / wpes / class-query-builder.php

class-query-builder.php in Jetpack – WP Security, Backup, Speed, & Growth 16.2, at jetpack_vendor/automattic/jetpack-search/src/wpes/class-query-builder.php

438 lines 9.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Provides an interface for easily building a complex search query that
4 * combines multiple ranking signals.
5 *
6 * $bldr = new Query_Builder();
7 * $bldr->add_filter( ... );
8 * $bldr->add_filter( ... );
9 * $bldr->add_query( ... );
10 * $es_query = $bldr->build_query();
11 *
12 *
13 * All ES queries take a standard form with main query (with some filters),
14 * wrapped in a function_score
15 *
16 * Most functions are chainable, e.g. $bldr->add_filter( ... )->add_query( ... )->build_query();
17 *
18 * Bucketed queries use an aggregation to diversify results. eg a bunch
19 * of separate filters where to get different sets of results.
20 *
21 * @package automattic/jetpack-search
22 */
23
24 // Disables comment checks.
25 // phpcs:disable Squiz.Commenting
26
27 namespace Automattic\Jetpack\Search\WPES;
28
29 /**
30 * Query builder class.
31 */
32 class Query_Builder {
33 /**
34 * ElasticSerach filters.
35 *
36 * @var array
37 */
38 protected $es_filters = array();
39
40 //
41 // Variables for handling custom boosting with function_score.
42 //
43 protected $functions = array();
44 protected $weighting_functions = array();
45 protected $decays = array();
46 protected $scripts = array();
47 protected $functions_max_boost = 2.0;
48 protected $functions_score_mode = 'multiply';
49 protected $functions_boost_mode = 'multiply';
50 protected $query_bool_boost = null;
51
52 //
53 // General aggregations for buckets and metrics.
54 //
55 protected $aggs_query = false;
56 protected $aggs = array();
57
58 //
59 // The set of top level text queries to combine.
60 //
61 protected $must_queries = array();
62 protected $should_queries = array();
63 protected $dis_max_queries = array();
64
65 protected $diverse_buckets_query = false;
66 protected $bucket_filters = array();
67 protected $bucket_sub_aggs = array();
68
69 /**
70 * Language codes.
71 *
72 * @var array
73 */
74 private $langs;
75
76 public function get_langs() {
77 if ( isset( $this->langs ) ) {
78 return $this->langs;
79 }
80 return false;
81 }
82
83 //
84 // Methods for building a query.
85 //
86 public function add_filter( $filter ) {
87 $this->es_filters[] = $filter;
88
89 return $this;
90 }
91
92 public function add_query( $query, $type = 'must' ) {
93 switch ( $type ) {
94 case 'dis_max':
95 $this->dis_max_queries[] = $query;
96 break;
97
98 case 'should':
99 $this->should_queries[] = $query;
100 break;
101
102 case 'must':
103 default:
104 $this->must_queries[] = $query;
105 break;
106 }
107
108 return $this;
109 }
110
111 /**
112 * Add any weighting function to the query
113 *
114 * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-function-score-query.html
115 *
116 * @param $function array A function structure to apply to the query
117 *
118 * @return void
119 */
120 public function add_weighting_function( $function ) {
121 // check for danger.
122 if ( isset( $function['random_score'] ) ) {
123 return $this;
124 }
125 if ( isset( $function['script_score'] ) ) {
126 return $this;
127 }
128
129 $this->weighting_functions[] = $function;
130
131 return $this;
132 }
133
134 /**
135 * Add a scoring function to the query
136 *
137 * NOTE: For decays (linear, exp, or gauss), use Query_Builder::add_decay() instead
138 *
139 * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-function-score-query.html
140 *
141 * @param $function string name of the function
142 * @param $params array functions parameters
143 *
144 * @return void
145 */
146 public function add_function( $function, $params ) {
147 $this->functions[ $function ][] = $params;
148
149 return $this;
150 }
151
152 /**
153 * Add a decay function to score results
154 *
155 * This method should be used instead of Query_Builder::add_function() for decays, as the internal ES structure
156 * is slightly different for them.
157 *
158 * @see https://www.elastic.co/guide/en/elasticsearch/guide/current/decay-functions.html
159 *
160 * @param $function string name of the decay function - linear, exp, or gauss
161 * @param $params array The decay functions parameters, passed to ES directly
162 *
163 * @return void
164 */
165 public function add_decay( $function, $params ) {
166 $this->decays[ $function ][] = $params;
167
168 return $this;
169 }
170
171 /**
172 * Add a scoring mode to the query
173 *
174 * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-function-score-query.html
175 *
176 * @param $mode string name of how to score
177 *
178 * @return void
179 */
180 public function add_score_mode_to_functions( $mode = 'multiply' ) {
181 $this->functions_score_mode = $mode;
182
183 return $this;
184 }
185
186 public function add_boost_mode_to_functions( $mode = 'multiply' ) {
187 $this->functions_boost_mode = $mode;
188
189 return $this;
190 }
191
192 public function add_max_boost_to_functions( $boost ) {
193 $this->functions_max_boost = $boost;
194
195 return $this;
196 }
197
198 public function add_boost_to_query_bool( $boost ) {
199 $this->query_bool_boost = $boost;
200
201 return $this;
202 }
203
204 public function add_aggs( $aggs_name, $aggs ) {
205 $this->aggs_query = true;
206 $this->aggs[ $aggs_name ] = $aggs;
207
208 return $this;
209 }
210
211 public function set_all_aggs( $aggs ) {
212 $this->aggs_query = true;
213 $this->aggs = $aggs;
214
215 return $this;
216 }
217
218 public function add_aggs_sub_aggs( $aggs_name, $sub_aggs ) {
219 if ( ! array_key_exists( 'aggs', $this->aggs[ $aggs_name ] ) ) {
220 $this->aggs[ $aggs_name ]['aggs'] = array();
221 }
222 $this->aggs[ $aggs_name ]['aggs'] = $sub_aggs;
223
224 return $this;
225 }
226
227 public function add_bucketed_query( $name, $query ) {
228 $this->add_bucket_filter( $name, $query );
229
230 $this->add_query( $query, 'dis_max' );
231
232 return $this;
233 }
234
235 public function add_bucketed_terms( $name, $field, $terms, $boost = 1 ) {
236 if ( ! is_array( $terms ) ) {
237 $terms = array( $terms );
238 }
239
240 $this->add_bucket_filter(
241 $name,
242 array(
243 'terms' => array(
244 $field => $terms,
245 ),
246 )
247 );
248
249 $this->add_query(
250 array(
251 'constant_score' => array(
252 'filter' => array(
253 'terms' => array(
254 $field => $terms,
255 ),
256 ),
257 'boost' => $boost,
258 ),
259 ),
260 'dis_max'
261 );
262
263 return $this;
264 }
265
266 public function add_bucket_sub_aggs( $agg ) {
267 $this->bucket_sub_aggs = array_merge( $this->bucket_sub_aggs, $agg );
268
269 return $this;
270 }
271
272 protected function add_bucket_filter( $name, $filter ) {
273 $this->diverse_buckets_query = true;
274 $this->bucket_filters[ $name ] = $filter;
275 }
276
277 ////////////////////////////////////
278 // Building Final Query
279
280 /**
281 * Combine all the queries, functions, decays, scripts, and max_boost into an ES query
282 *
283 * @return array Array representation of the built ES query
284 */
285 public function build_query() {
286 $query = array();
287
288 //dis_max queries just become a single must query
289 if ( ! empty( $this->dis_max_queries ) ) {
290 $this->must_queries[] = array(
291 'dis_max' => array(
292 'queries' => $this->dis_max_queries,
293 ),
294 );
295 }
296
297 if ( empty( $this->must_queries ) ) {
298 $this->must_queries = array(
299 array(
300 'match_all' => array(),
301 ),
302 );
303 }
304
305 if ( empty( $this->should_queries ) ) {
306 $query = array(
307 'bool' => array(
308 'must' => $this->must_queries,
309 ),
310 );
311 } else {
312 $query = array(
313 'bool' => array(
314 'must' => $this->must_queries,
315 'should' => $this->should_queries,
316 ),
317 );
318 }
319
320 if ( $this->query_bool_boost !== null ) {
321 $query['bool']['boost'] = $this->query_bool_boost;
322 }
323
324 // If there are any function score adjustments, then combine those
325 if ( $this->functions || $this->decays || $this->scripts || $this->weighting_functions ) {
326 $weighting_functions = $this->weighting_functions;
327
328 if ( $this->functions ) {
329 foreach ( $this->functions as $function_type => $configs ) {
330 foreach ( $configs as $config ) {
331 foreach ( $config as $field => $params ) {
332 $func_arr = $params;
333
334 $func_arr['field'] = $field;
335
336 $weighting_functions[] = array(
337 $function_type => $func_arr,
338 );
339 }
340 }
341 }
342 }
343
344 if ( $this->decays ) {
345 foreach ( $this->decays as $decay_type => $configs ) {
346 foreach ( $configs as $config ) {
347 foreach ( $config as $field => $params ) {
348 $weighting_functions[] = array(
349 $decay_type => array(
350 $field => $params,
351 ),
352 );
353 }
354 }
355 }
356 }
357
358 if ( $this->scripts ) {
359 foreach ( $this->scripts as $script ) {
360 $weighting_functions[] = array(
361 'script_score' => array(
362 'script' => $script,
363 ),
364 );
365 }
366 }
367
368 $query = array(
369 'function_score' => array(
370 'query' => $query,
371 'functions' => $weighting_functions,
372 'max_boost' => $this->functions_max_boost,
373 'score_mode' => $this->functions_score_mode,
374 'boost_mode' => $this->functions_boost_mode,
375 ),
376 );
377 }
378
379 return $query;
380 }
381
382 /**
383 * Assemble the 'filter' portion of an ES query, from all registered filters
384 *
385 * @return array|null Combined ES filters, or null if none have been defined
386 */
387 public function build_filter() {
388 if ( empty( $this->es_filters ) ) {
389 $filter = null;
390 } elseif ( 1 === count( $this->es_filters ) ) {
391 $filter = $this->es_filters[0];
392 } else {
393 $filter = array(
394 'and' => $this->es_filters,
395 );
396 }
397
398 return $filter;
399 }
400
401 /**
402 * Assemble the 'aggregation' portion of an ES query, from all general aggregations.
403 *
404 * @return array An aggregation query as an array of topics, filters, and bucket names
405 */
406 public function build_aggregation() {
407 if ( empty( $this->bucket_sub_aggs ) && empty( $this->aggs_query ) ) {
408 return array();
409 }
410
411 if ( ! $this->diverse_buckets_query && empty( $this->aggs_query ) ) {
412 return $this->bucket_sub_aggs;
413 }
414
415 $aggregations = array(
416 'topics' => array(
417 'filters' => array(
418 'filters' => array(),
419 ),
420 ),
421 );
422
423 if ( ! empty( $this->bucket_sub_aggs ) ) {
424 $aggregations['topics']['aggs'] = $this->bucket_sub_aggs;
425 }
426
427 foreach ( $this->bucket_filters as $bucket_name => $filter ) {
428 $aggregations['topics']['filters']['filters'][ $bucket_name ] = $filter;
429 }
430
431 if ( ! empty( $this->aggs_query ) ) {
432 $aggregations = $this->aggs;
433 }
434
435 return $aggregations;
436 }
437 }
438