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