Completed
Push — instant-search-master ( 8be3b4...336413 )
by
unknown
06:37 queued 10s
created

Jetpack_WPES_Query_Builder   C

Complexity

Total Complexity 54

Size/Duplication

Total Lines 376
Duplicated Lines 0 %

Coupling/Cohesion

Components 3
Dependencies 0

Importance

Changes 0
Metric Value
dl 0
loc 376
rs 6.4799
c 0
b 0
f 0
wmc 54
lcom 3
cbo 0

20 Methods

Rating   Name   Duplication   Size   Complexity  
A build_filter() 0 13 3
B build_aggregation() 0 31 8
A get_langs() 0 6 2
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 set_all_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 4 1
F build_query() 0 96 20

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
 * Provides an interface for easily building a complex search query that
5
 * combines multiple ranking signals.
6
 *
7
 *
8
 * $bldr = new Jetpack_WPES_Query_Builder();
9
 * $bldr->add_filter( ... );
10
 * $bldr->add_filter( ... );
11
 * $bldr->add_query( ... );
12
 * $es_query = $bldr->build_query();
13
 *
14
 *
15
 * All ES queries take a standard form with main query (with some filters),
16
 *  wrapped in a function_score
17
 *
18
 * Most functions are chainable, e.g. $bldr->add_filter( ... )->add_query( ... )->build_query();
19
 *
20
 * Bucketed queries use an aggregation to diversify results. eg a bunch
21
 *  of separate filters where to get different sets of results.
22
 *
23
 */
24
25
class Jetpack_WPES_Query_Builder {
26
27
	protected $es_filters = array();
28
29
	// Custom boosting with function_score
30
	protected $functions = array();
31
	protected $weighting_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 $functions_boost_mode = 'multiply';
37
	protected $query_bool_boost     = null;
38
39
	// General aggregations for buckets and metrics
40
	protected $aggs_query = false;
41
	protected $aggs       = array();
42
43
	// The set of top level text queries to combine
44
	protected $must_queries    = array();
45
	protected $should_queries  = array();
46
	protected $dis_max_queries = array();
47
48
	protected $diverse_buckets_query = false;
49
	protected $bucket_filters        = array();
50
	protected $bucket_sub_aggs       = array();
51
52
	public function get_langs() {
53
		if ( isset( $this->langs ) ) {
54
			return $this->langs;
0 ignored issues
show
Bug introduced by
The property langs does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
55
		}
56
		return false;
57
	}
58
59
	////////////////////////////////////
60
	// Methods for building a query
61
62
	public function add_filter( $filter ) {
63
		$this->es_filters[] = $filter;
64
65
		return $this;
66
	}
67
68
	public function add_query( $query, $type = 'must' ) {
69
		switch ( $type ) {
70
			case 'dis_max':
71
				$this->dis_max_queries[] = $query;
72
				break;
73
74
			case 'should':
75
				$this->should_queries[] = $query;
76
				break;
77
78
			case 'must':
79
			default:
80
				$this->must_queries[] = $query;
81
				break;
82
		}
83
84
		return $this;
85
	}
86
87
	/**
88
	 * Add any weighting function to the query
89
	 *
90
	 * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-function-score-query.html
91
	 *
92
	 * @param $function array A function structure to apply to the query
93
	 *
94
	 * @return void
95
	 */
96
	public function add_weighting_function( $function ) {
97
		$this->weighting_functions[] = $function;
98
99
		return $this;
100
	}
101
102
	/**
103
	 * Add a scoring function to the query
104
	 *
105
	 * NOTE: For decays (linear, exp, or gauss), use Jetpack_WPES_Query_Builder::add_decay() instead
106
	 *
107
	 * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-function-score-query.html
108
	 *
109
	 * @param $function string name of the function
110
	 * @param $params array functions parameters
111
	 *
112
	 * @return void
113
	 */
114
	public function add_function( $function, $params ) {
115
		$this->functions[ $function ][] = $params;
116
117
		return $this;
118
	}
119
120
	/**
121
	 * Add a decay function to score results
122
	 *
123
	 * This method should be used instead of Jetpack_WPES_Query_Builder::add_function() for decays, as the internal  ES structure
124
	 * is slightly different for them.
125
	 *
126
	 * @see https://www.elastic.co/guide/en/elasticsearch/guide/current/decay-functions.html
127
	 *
128
	 * @param $function string name of the decay function - linear, exp, or gauss
129
	 * @param $params array The decay functions parameters, passed to ES directly
130
	 *
131
	 * @return void
132
	 */
133
	public function add_decay( $function, $params ) {
134
		$this->decays[ $function ][] = $params;
135
136
		return $this;
137
	}
138
139
	/**
140
	 * Add a scoring mode to the query
141
	 *
142
	 * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-function-score-query.html
143
	 *
144
	 * @param $mode string name of how to score
145
	 *
146
	 * @return void
147
	 */
148
	public function add_score_mode_to_functions( $mode='multiply' ) {
149
		$this->functions_score_mode = $mode;
150
151
		return $this;
152
	}
153
154
	public function add_boost_mode_to_functions( $mode='multiply' ) {
155
		$this->functions_boost_mode = $mode;
156
157
		return $this;
158
	}
159
160
	public function add_max_boost_to_functions( $boost ) {
161
		$this->functions_max_boost = $boost;
162
163
		return $this;
164
	}
165
166
	public function add_boost_to_query_bool( $boost ) {
167
		$this->query_bool_boost = $boost;
168
169
		return $this;
170
	}
171
172
	public function add_aggs( $aggs_name, $aggs ) {
173
		$this->aggs_query = true;
174
		$this->aggs[$aggs_name] = $aggs;
175
176
		return $this;
177
	}
178
179
	public function set_all_aggs( $aggs ) {
180
		$this->aggs_query = true;
181
		$this->aggs = $aggs;
182
183
		return $this;
184
	}
185
186
	public function add_aggs_sub_aggs( $aggs_name, $sub_aggs ) {
187
		if ( ! array_key_exists( 'aggs', $this->aggs[$aggs_name] ) ) {
188
			$this->aggs[$aggs_name]['aggs'] = array();
189
		}
190
		$this->aggs[$aggs_name]['aggs'] = $sub_aggs;
191
192
		return $this;
193
	}
194
195
	public function add_bucketed_query( $name, $query ) {
196
		$this->_add_bucket_filter( $name, $query );
197
198
		$this->add_query( $query, 'dis_max' );
199
200
		return $this;
201
	}
202
203
	public function add_bucketed_terms( $name, $field, $terms, $boost = 1 ) {
204
		if ( ! is_array( $terms ) ) {
205
			$terms = array( $terms );
206
		}
207
208
		$this->_add_bucket_filter( $name, array(
209
			'terms' => array(
210
				$field => $terms,
211
			),
212
		));
213
214
		$this->add_query( array(
215
			'constant_score' => array(
216
				'filter' => array(
217
					'terms' => array(
218
						$field => $terms,
219
					),
220
				),
221
				'boost' => $boost,
222
			),
223
		), 'dis_max' );
224
225
		return $this;
226
	}
227
228
	public function add_bucket_sub_aggs( $agg ) {
229
		$this->bucket_sub_aggs = array_merge( $this->bucket_sub_aggs, $agg );
230
231
		return $this;
232
	}
233
234
	protected function _add_bucket_filter( $name, $filter ) {
235
		$this->diverse_buckets_query   = true;
236
		$this->bucket_filters[ $name ] = $filter;
237
	}
238
239
	////////////////////////////////////
240
	// Building Final Query
241
242
	/**
243
	 * Combine all the queries, functions, decays, scripts, and max_boost into an ES query
244
	 *
245
	 * @return array Array representation of the built ES query
246
	 */
247
	public function build_query() {
248
		$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...
249
250
		//dis_max queries just become a single must query
251
		if ( ! empty( $this->dis_max_queries ) ) {
252
			$this->must_queries[] = array(
253
				'dis_max' => array(
254
					'queries' => $this->dis_max_queries,
255
				),
256
			);
257
		}
258
259
		if ( empty( $this->must_queries ) ) {
260
			$this->must_queries = array(
261
				array(
262
					'match_all' => array(),
263
				),
264
			);
265
		}
266
267
		if ( empty( $this->should_queries ) ) {
268
			$query = array(
269
				'bool' => array(
270
					'must' => $this->must_queries,
271
				),
272
			);
273
		} else {
274
			$query = array(
275
				'bool' => array(
276
					'must'   => $this->must_queries,
277
					'should' => $this->should_queries,
278
				),
279
			);
280
		}
281
282
		if ( ! is_null( $this->query_bool_boost ) && isset( $query['bool'] ) ) {
283
			$query['bool']['boost'] = $this->query_bool_boost;
284
		}
285
286
		// If there are any function score adjustments, then combine those
287
		if ( $this->functions || $this->decays || $this->scripts || $this->weighting_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...
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...
Bug Best Practice introduced by
The expression $this->weighting_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...
288
			$weighting_functions = array();
289
290
			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...
291
				foreach ( $this->functions as $function_type => $configs ) {
292
					foreach ( $configs as $config ) {
293
						foreach ( $config as $field => $params ) {
294
							$func_arr = $params;
295
296
							$func_arr['field'] = $field;
297
298
							$weighting_functions[] = array(
299
								$function_type => $func_arr,
300
							);
301
						}
302
					}
303
				}
304
			}
305
306
			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...
307
				foreach ( $this->decays as $decay_type => $configs ) {
308
					foreach ( $configs as $config ) {
309
						foreach ( $config as $field => $params ) {
310
							$weighting_functions[] = array(
311
								$decay_type => array(
312
									$field => $params,
313
								),
314
							);
315
						}
316
					}
317
				}
318
			}
319
320
			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...
321
				foreach ( $this->scripts as $script ) {
322
					$weighting_functions[] = array(
323
						'script_score' => array(
324
							'script' => $script,
325
						),
326
					);
327
				}
328
			}
329
330
			$query = array(
331
				'function_score' => array(
332
					'query'     => $query,
333
					'functions' => $weighting_functions,
334
					'max_boost' => $this->functions_max_boost,
335
					'score_mode' => $this->functions_score_mode,
336
					'boost_mode' => $this->functions_boost_mode,
337
				),
338
			);
339
		} // End if().
340
341
		return $query;
342
	}
343
344
	/**
345
	 * Assemble the 'filter' portion of an ES query, from all registered filters
346
	 *
347
	 * @return array|null Combined ES filters, or null if none have been defined
348
	 */
349
	public function build_filter() {
350
		if ( empty( $this->es_filters ) ) {
351
			$filter = null;
352
		} elseif ( 1 == count( $this->es_filters ) ) {
353
			$filter = $this->es_filters[0];
354
		} else {
355
			$filter = array(
356
				'and' => $this->es_filters,
357
			);
358
		}
359
360
		return $filter;
361
	}
362
363
	/**
364
	 * Assemble the 'aggregation' portion of an ES query, from all general aggregations.
365
	 *
366
	 * @return array An aggregation query as an array of topics, filters, and bucket names
367
	 */
368
	public function build_aggregation() {
369
		if ( empty( $this->bucket_sub_aggs ) && empty( $this->aggs_query ) ) {
370
			return array();
371
		}
372
373
		if ( ! $this->diverse_buckets_query && empty( $this->aggs_query ) ) {
374
			return $this->bucket_sub_aggs;
375
		}
376
377
		$aggregations = array(
378
			'topics' => array(
379
				'filters' => array(
380
					'filters' => array(),
381
				),
382
			),
383
		);
384
385
		if ( ! empty( $this->bucket_sub_aggs ) ) {
386
			$aggregations['topics']['aggs'] = $this->bucket_sub_aggs;
387
		}
388
389
		foreach ( $this->bucket_filters as $bucket_name => $filter ) {
390
			$aggregations['topics']['filters']['filters'][ $bucket_name ] = $filter;
391
		}
392
393
		if ( ! empty( $this->aggs_query ) ) {
394
			$aggregations = $this->aggs;
395
		}
396
397
		return $aggregations;
398
	}
399
400
}
401