Completed
Push — sync/search-libraries ( 8c5075 )
by
unknown
09:16
created

Jetpack_WPES_Query_Builder   C

Complexity

Total Complexity 54

Size/Duplication

Total Lines 374
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 0

Importance

Changes 0
Metric Value
dl 0
loc 374
rs 6.4799
c 0
b 0
f 0
wmc 54
lcom 1
cbo 0

18 Methods

Rating   Name   Duplication   Size   Complexity  
A add_filter() 0 5 1
A add_query() 0 18 4
A add_weighting_function() 0 5 1
A add_function() 0 5 1
A add_decay() 0 5 1
A add_score_mode_to_functions() 0 5 1
A add_boost_mode_to_functions() 0 5 1
A add_max_boost_to_functions() 0 5 1
A add_boost_to_query_bool() 0 5 1
A add_aggs() 0 6 1
A add_aggs_sub_aggs() 0 8 2
A add_bucketed_query() 0 7 1
A add_bucketed_terms() 0 24 2
A add_bucket_sub_aggs() 0 5 1
A _add_bucket_filter() 0 6 1
F build_query() 0 108 23
A build_filter() 0 13 3
B build_aggregation() 0 31 8

How to fix   Complexity   

Complex Class

Complex classes like Jetpack_WPES_Query_Builder often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Jetpack_WPES_Query_Builder, and based on these observations, apply Extract Interface, too.

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;
0 ignored issues
show
Bug introduced by
The property weighting_functions does not seem to exist. Did you mean functions?

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.

Loading history...
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;
0 ignored issues
show
Bug introduced by
The property functions_boost_mode does not seem to exist. Did you mean functions?

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.

Loading history...
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();
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...
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 ) {
0 ignored issues
show
Bug introduced by
The property weighting_functions does not seem to exist. Did you mean functions?

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.

Loading history...
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...
285
286
			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...
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(
0 ignored issues
show
Bug introduced by
The property weighting_functions does not seem to exist. Did you mean functions?

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.

Loading history...
295
								$function_type => $func_arr,
296
							);
297
						}
298
					}
299
				}
300
			}
301
302
			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...
303
				foreach ( $this->decays as $decay_type => $configs ) {
304
					foreach ( $configs as $config ) {
305
						foreach ( $config as $field => $params ) {
306
							$this->weighting_functions[] = array(
0 ignored issues
show
Bug introduced by
The property weighting_functions does not seem to exist. Did you mean functions?

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.

Loading history...
307
								$decay_type => array(
308
									$field => $params,
309
								),
310
							);
311
						}
312
					}
313
				}
314
			}
315
316
			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...
317
				foreach ( $this->scripts as $script ) {
318
					$this->weighting_functions[] = array(
0 ignored issues
show
Bug introduced by
The property weighting_functions does not seem to exist. Did you mean functions?

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.

Loading history...
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,
0 ignored issues
show
Bug introduced by
The property weighting_functions does not seem to exist. Did you mean functions?

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.

Loading history...
330
					'max_boost' => $this->functions_max_boost,
331
					'score_mode' => $this->functions_score_mode,
332
					'boost_mode' => $this->functions_boost_mode,
0 ignored issues
show
Bug introduced by
The property functions_boost_mode does not seem to exist. Did you mean functions?

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.

Loading history...
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