Completed
Push — add/sync-import-event ( 8770af...5ff453 )
by
unknown
130:35 queued 120:40
created

Jetpack_WPES_Query_Builder::add_query()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 16
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 12
nc 4
nop 2
dl 0
loc 16
rs 9.2
c 0
b 0
f 0
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
 * Bucketed queries use an aggregation to diversify results. eg a bunch
20
 *  of separate filters where to get different sets of results.
21
 *
22
 */
23
24
class Jetpack_WPES_Query_Builder {
25
26
	public $es_filters = array();
27
28
	// Custom boosting with function_score
29
	public $functions = array();
30
	public $decays    = array();
31
	public $scripts   = array();
32
	public $functions_max_boost  = 2.0;
33
	public $functions_score_mode = 'multiply';
34
	public $query_bool_boost     = null;
35
36
	// General aggregations for buckets and metrics
37
	public $aggs_query = false;
38
	public $aggs       = array();
39
40
	// The set of top level text queries to combine
41
	public $must_queries    = array();
42
	public $should_queries  = array();
43
	public $dis_max_queries = array();
44
45
	public $diverse_buckets_query = false;
46
	public $bucket_filters        = array();
47
	public $bucket_sub_aggs       = array();
48
49
	////////////////////////////////////
50
	// Methods for building a query
51
52
	public function add_filter( $filter ) {
53
		$this->es_filters[] = $filter;
54
	}
55
56
	public function add_query( $query, $type = 'must' ) {
57
		switch ( $type ) {
58
			case 'dis_max':
59
				$this->dis_max_queries[] = $query;
60
				break;
61
62
			case 'should':
63
				$this->should_queries[] = $query;
64
				break;
65
66
			case 'must':
67
			default:
68
				$this->must_queries[] = $query;
69
				break;
70
		}
71
	}
72
73
	/**
74
	 * Add a scoring function to the query
75
	 *
76
	 * NOTE: For decays (linear, exp, or gauss), use Jetpack_WPES_Query_Builder::add_decay() instead
77
	 *
78
	 * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-function-score-query.html
79
	 *
80
	 * @param $function string name of the function
81
	 * @param $params array functions parameters
82
	 *
83
	 * @return void
84
	 */
85
	public function add_function( $function, $params ) {
86
		$this->functions[ $function ][] = $params;
87
	}
88
89
	/**
90
	 * Add a decay function to score results
91
	 *
92
	 * This method should be used instead of Jetpack_WPES_Query_Builder::add_function() for decays, as the internal  ES structure
93
	 * is slightly different for them.
94
	 *
95
	 * @see https://www.elastic.co/guide/en/elasticsearch/guide/current/decay-functions.html
96
	 *
97
	 * @param $function string name of the decay function - linear, exp, or gauss
98
	 * @param $params array The decay functions parameters, passed to ES directly
99
	 *
100
	 * @return void
101
	 */
102
	public function add_decay( $function, $params ) {
103
		$this->decays[ $function ][] = $params;
104
	}
105
106
	/**
107
	 * Add a scoring mode to the query
108
	 *
109
	 * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-function-score-query.html
110
	 *
111
	 * @param $mode string name of how to score
112
	 *
113
	 * @return void
114
	 */
115
	public function add_score_mode_to_functions( $mode='multiply' ) {
116
		$this->functions_score_mode = $mode;
117
	}
118
119
	public function add_max_boost_to_functions( $boost ) {
120
		$this->functions_max_boost = $boost;
121
	}
122
123
	public function add_boost_to_query_bool( $boost ) {
124
		$this->query_bool_boost = $boost;
125
	}
126
127
	public function add_aggs( $aggs_name, $aggs ) {
128
		$this->aggs_query = true;
129
		$this->aggs[$aggs_name] = $aggs;
130
	}
131
132
	public function add_aggs_sub_aggs( $aggs_name, $sub_aggs ) {
133
		if ( ! array_key_exists( 'aggs', $this->aggs[$aggs_name] ) ) {
134
			$this->aggs[$aggs_name]['aggs'] = array();
135
		}
136
		$this->aggs[$aggs_name]['aggs'] = $sub_aggs;
137
	}
138
139
	public function add_bucketed_query( $name, $query ) {
140
		$this->_add_bucket_filter( $name, $query );
141
142
		$this->add_query( $query, 'dis_max' );
143
	}
144
145
	public function add_bucketed_terms( $name, $field, $terms, $boost = 1 ) {
146
		if ( ! is_array( $terms ) ) {
147
			$terms = array( $terms );
148
		}
149
150
		$this->_add_bucket_filter( $name, array(
151
			'terms' => array(
152
				$field => $terms,
153
			),
154
		));
155
156
		$this->add_query( array(
157
			'constant_score' => array(
158
				'filter' => array(
159
					'terms' => array(
160
						$field => $terms,
161
					),
162
				),
163
				'boost' => $boost,
164
			),
165
		), 'dis_max' );
166
	}
167
168
	public function add_bucket_sub_aggs( $agg ) {
169
		$this->bucket_sub_aggs = array_merge( $this->bucket_sub_aggs, $agg );
170
	}
171
172
	public function _add_bucket_filter( $name, $filter ) {
173
		$this->diverse_buckets_query   = true;
174
		$this->bucket_filters[ $name ] = $filter;
175
	}
176
177
	////////////////////////////////////
178
	// Building Final Query
179
180
	/**
181
	 * Combine all the queries, functions, decays, scripts, and max_boost into an ES query
182
	 *
183
	 * @return array Array representation of the built ES query
184
	 */
185
	public function build_query() {
186
		$query = array();
0 ignored issues
show
Unused Code introduced by
$query is not used, you could remove the assignment.

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

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

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

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

Loading history...
187
188
		//dis_max queries just become a single must query
189
		if ( ! empty( $this->dis_max_queries ) ) {
190
			$this->must_queries[] = array(
191
				'dis_max' => array(
192
					'queries' => $this->dis_max_queries,
193
				),
194
			);
195
		}
196
197
		if ( empty( $this->must_queries ) ) {
198
			$this->must_queries = array(
199
				array(
200
					'match_all' => array(),
201
				),
202
			);
203
		}
204
205
		if ( empty( $this->should_queries ) ) {
206
			if ( 1 == count( $this->must_queries ) ) {
207
				$query = $this->must_queries[0];
208
			} else {
209
				$query = array(
210
					'bool' => array(
211
						'must' => $this->must_queries,
212
					),
213
				);
214
			}
215
		} else {
216
			$query = array(
217
				'bool' => array(
218
					'must'   => $this->must_queries,
219
					'should' => $this->should_queries,
220
				),
221
			);
222
		}
223
224
		if ( ! is_null( $this->query_bool_boost ) && isset( $query['bool'] ) ) {
225
			$query['bool']['boost'] = $this->query_bool_boost;
226
		}
227
228
		// If there are any function score adjustments, then combine those
229
		if ( $this->functions || $this->decays || $this->scripts ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->functions of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
Bug Best Practice introduced by
The expression $this->decays of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
Bug Best Practice introduced by
The expression $this->scripts of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
230
			$weighting_functions = array();
231
232
			if ( $this->functions ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->functions of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
233
				foreach ( $this->functions as $function_type => $configs ) {
234
					foreach ( $configs as $config ) {
235
						foreach ( $config as $field => $params ) {
236
							$func_arr = $params;
237
238
							$func_arr['field'] = $field;
239
240
							$weighting_functions[] = array(
241
								$function_type => $func_arr,
242
							);
243
						}
244
					}
245
				}
246
			}
247
248
			if ( $this->decays ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->decays of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
249
				foreach ( $this->decays as $decay_type => $configs ) {
250
					foreach ( $configs as $config ) {
251
						foreach ( $config as $field => $params ) {
252
							$weighting_functions[] = array(
253
								$decay_type => array(
254
									$field => $params,
255
								),
256
							);
257
						}
258
					}
259
				}
260
			}
261
262
			if ( $this->scripts ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->scripts of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
263
				foreach ( $this->scripts as $script ) {
264
					$weighting_functions[] = array(
265
						'script_score' => array(
266
							'script' => $script,
267
						),
268
					);
269
				}
270
			}
271
272
			$query = array(
273
				'function_score' => array(
274
					'query'     => $query,
275
					'functions' => $weighting_functions,
276
					'max_boost' => $this->functions_max_boost,
277
					'score_mode' => $this->functions_score_mode,
278
				),
279
			);
280
		} // End if().
281
282
		return $query;
283
	}
284
285
	/**
286
	 * Assemble the 'filter' portion of an ES query, from all registered filters
287
	 *
288
	 * @return array|null Combiled ES filters, or null if none have been defined
289
	 */
290
	public function build_filter() {
291
		if ( empty( $this->es_filters ) ) {
292
			$filter = null;
293
		} elseif ( 1 == count( $this->es_filters ) ) {
294
			$filter = $this->es_filters[0];
295
		} else {
296
			$filter = array(
297
				'and' => $this->es_filters,
298
			);
299
		}
300
301
		return $filter;
302
	}
303
304
	/**
305
	 * Assemble the 'aggregation' portion of an ES query, from all general aggregations.
306
	 *
307
	 * @return an aggregation query as an array of topics, filters, and bucket names
308
	 */
309
	function build_aggregation() {
310
		if ( empty( $this->bucket_sub_aggs ) && empty( $this->aggs_query ) ) {
311
			return array();
312
		}
313
314
		if ( ! $this->diverse_buckets_query && empty( $this->aggs_query ) ) {
315
			return $this->bucket_sub_aggs;
316
		}
317
318
		$aggregations = array(
319
			'topics' => array(
320
				'filters' => array(
321
					'filters' => array(),
322
				),
323
			),
324
		);
325
326
		if ( ! empty( $this->bucket_sub_aggs ) ) {
327
			$aggregations['topics']['aggs'] = $this->bucket_sub_aggs;
328
		}
329
330
		foreach ( $this->bucket_filters as $bucket_name => $filter ) {
331
			$aggregations['topics']['filters']['filters'][ $bucket_name ] = $filter;
332
		}
333
334
		if ( ! empty( $this->aggs_query ) ) {
335
			$aggregations = $this->aggs;
336
		}
337
338
		return $aggregations;
339
	}
340
341
}
342