1
|
|
|
<?php namespace Nord\Lumen\Elasticsearch\Search\Query\FullText; |
2
|
|
|
|
3
|
|
|
use Nord\Lumen\Elasticsearch\Exceptions\InvalidArgument; |
4
|
|
|
use Nord\Lumen\Elasticsearch\Search\Query\Traits\HasField; |
5
|
|
|
use Nord\Lumen\Elasticsearch\Search\Query\Traits\HasType; |
6
|
|
|
use Nord\Lumen\Elasticsearch\Search\Query\Traits\HasValue; |
7
|
|
|
|
8
|
|
|
/** |
9
|
|
|
* A family of match queries that accepts text/numerics/dates, analyzes them, and constructs a query. |
10
|
|
|
* |
11
|
|
|
* @see https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-match-query.html |
12
|
|
|
*/ |
13
|
|
|
class MatchQuery extends AbstractQuery |
14
|
|
|
{ |
15
|
|
|
use HasField; |
16
|
|
|
use HasType; |
17
|
|
|
use HasValue; |
18
|
|
|
|
19
|
|
|
const OPERATOR_OR = 'or'; |
20
|
|
|
const OPERATOR_AND = 'and'; |
21
|
|
|
|
22
|
|
|
const ZERO_TERM_QUERY_NONE = 'none'; |
23
|
|
|
const ZERO_TERM_QUERY_ALL = 'all'; |
24
|
|
|
|
25
|
|
|
const TYPE_PHRASE = 'phrase'; |
26
|
|
|
const TYPE_PHRASE_PREFIX = 'phrase_prefix'; |
27
|
|
|
|
28
|
|
|
/** |
29
|
|
|
* @var string The operator flag can be set to "or" or "and" to control the boolean clauses (defaults to "or"). |
30
|
|
|
*/ |
31
|
|
|
private $operator; |
32
|
|
|
|
33
|
|
|
/** |
34
|
|
|
* @var string If the analyzer used removes all tokens in a query like a "stop" filter does, the default behavior |
35
|
|
|
* is to match no documents at all. In order to change that the zero_terms_query option can be used, which accepts |
36
|
|
|
* "none" (default) and "all" which corresponds to a "match_all" query. |
37
|
|
|
*/ |
38
|
|
|
private $zeroTermsQuery; |
39
|
|
|
|
40
|
|
|
/** |
41
|
|
|
* @var float The match query supports a cutoff_frequency that allows specifying an absolute or relative document |
42
|
|
|
* frequency where high frequency terms are moved into an optional subquery and are only scored if one of the low |
43
|
|
|
* frequency (below the cutoff) terms in the case of an or operator or all of the low frequency terms in the case |
44
|
|
|
* of an and operator match. |
45
|
|
|
*/ |
46
|
|
|
private $cutOffFrequency; |
47
|
|
|
|
48
|
|
|
/** |
49
|
|
|
* @var int A phrase query matches terms up to a configurable slop (which defaults to 0) in any order. |
50
|
|
|
* Transposed terms have a slop of 2. |
51
|
|
|
*/ |
52
|
|
|
private $slop; |
53
|
|
|
|
54
|
|
|
/** |
55
|
|
|
* @var int A "phrase_prefix" query option that controls to how many prefixes the last term will be expanded. |
56
|
|
|
* It is highly recommended to set it to an acceptable value to control the execution time of the query. |
57
|
|
|
*/ |
58
|
|
|
private $maxExpansions; |
59
|
|
|
|
60
|
|
|
/** |
61
|
|
|
* @var string The analyzer can be set to control which analyzer will perform the analysis process on the text. It |
62
|
|
|
* defaults to the field explicit mapping definition, or the default search analyzer, |
63
|
|
|
*/ |
64
|
|
|
private $analyzer; |
65
|
|
|
|
66
|
|
|
/** |
67
|
|
|
* @inheritdoc |
68
|
|
|
*/ |
69
|
|
|
public function toArray() |
70
|
|
|
{ |
71
|
|
|
$match = ['query' => $this->getValue()]; |
72
|
|
|
|
73
|
|
|
$match = $this->applyOptions($match); |
74
|
|
|
|
75
|
|
|
if (count($match) === 1 && isset($match['query'])) { |
76
|
|
|
$match = $match['query']; |
77
|
|
|
} |
78
|
|
|
|
79
|
|
|
return ['match' => [$this->getField() => $match]]; |
80
|
|
|
} |
81
|
|
|
|
82
|
|
|
|
83
|
|
|
/** |
84
|
|
|
* @param string $operator |
85
|
|
|
* @return MatchQuery |
86
|
|
|
* @throws InvalidArgument |
87
|
|
|
*/ |
88
|
|
|
public function setOperator($operator) |
89
|
|
|
{ |
90
|
|
|
$this->assertOperator($operator); |
91
|
|
|
$this->operator = $operator; |
92
|
|
|
return $this; |
93
|
|
|
} |
94
|
|
|
|
95
|
|
|
|
96
|
|
|
/** |
97
|
|
|
* @return string |
98
|
|
|
*/ |
99
|
|
|
public function getOperator() |
100
|
|
|
{ |
101
|
|
|
return $this->operator; |
102
|
|
|
} |
103
|
|
|
|
104
|
|
|
|
105
|
|
|
/** |
106
|
|
|
* @param string $zeroTermsQuery |
107
|
|
|
* @return MatchQuery |
108
|
|
|
* @throws InvalidArgument |
109
|
|
|
*/ |
110
|
|
|
public function setZeroTermsQuery($zeroTermsQuery) |
111
|
|
|
{ |
112
|
|
|
$this->assertZeroTermsQuery($zeroTermsQuery); |
113
|
|
|
$this->zeroTermsQuery = $zeroTermsQuery; |
114
|
|
|
return $this; |
115
|
|
|
} |
116
|
|
|
|
117
|
|
|
|
118
|
|
|
/** |
119
|
|
|
* @return string |
120
|
|
|
*/ |
121
|
|
|
public function getZeroTermsQuery() |
122
|
|
|
{ |
123
|
|
|
return $this->zeroTermsQuery; |
124
|
|
|
} |
125
|
|
|
|
126
|
|
|
|
127
|
|
|
/** |
128
|
|
|
* @param float $cutOffFrequency |
129
|
|
|
* @return MatchQuery |
130
|
|
|
* @throws InvalidArgument |
131
|
|
|
*/ |
132
|
|
|
public function setCutOffFrequency($cutOffFrequency) |
133
|
|
|
{ |
134
|
|
|
$this->assertCutOffFrequency($cutOffFrequency); |
135
|
|
|
$this->cutOffFrequency = $cutOffFrequency; |
136
|
|
|
return $this; |
137
|
|
|
} |
138
|
|
|
|
139
|
|
|
|
140
|
|
|
/** |
141
|
|
|
* @return float |
142
|
|
|
*/ |
143
|
|
|
public function getCutOffFrequency() |
144
|
|
|
{ |
145
|
|
|
return $this->cutOffFrequency; |
146
|
|
|
} |
147
|
|
|
|
148
|
|
|
|
149
|
|
|
/** |
150
|
|
|
* @param string $type |
151
|
|
|
* @return MatchQuery |
152
|
|
|
* @throws InvalidArgument |
153
|
|
|
*/ |
154
|
|
|
public function setType($type) |
155
|
|
|
{ |
156
|
|
|
$this->assertType($type); |
157
|
|
|
$this->type = $type; |
158
|
|
|
return $this; |
159
|
|
|
} |
160
|
|
|
|
161
|
|
|
|
162
|
|
|
/** |
163
|
|
|
* @param int $slop |
164
|
|
|
* @return MatchQuery |
165
|
|
|
* @throws InvalidArgument |
166
|
|
|
*/ |
167
|
|
|
public function setSlop($slop) |
168
|
|
|
{ |
169
|
|
|
$this->assertSlop($slop); |
170
|
|
|
$this->slop = $slop; |
171
|
|
|
return $this; |
172
|
|
|
} |
173
|
|
|
|
174
|
|
|
|
175
|
|
|
/** |
176
|
|
|
* @return int |
177
|
|
|
*/ |
178
|
|
|
public function getSlop() |
179
|
|
|
{ |
180
|
|
|
return $this->slop; |
181
|
|
|
} |
182
|
|
|
|
183
|
|
|
|
184
|
|
|
/** |
185
|
|
|
* @param int $maxExpansions |
186
|
|
|
* @return MatchQuery |
187
|
|
|
* @throws InvalidArgument |
188
|
|
|
*/ |
189
|
|
|
public function setMaxExpansions($maxExpansions) |
190
|
|
|
{ |
191
|
|
|
$this->assertMaxExpansions($maxExpansions); |
192
|
|
|
$this->maxExpansions = $maxExpansions; |
193
|
|
|
return $this; |
194
|
|
|
} |
195
|
|
|
|
196
|
|
|
|
197
|
|
|
/** |
198
|
|
|
* @return int |
199
|
|
|
*/ |
200
|
|
|
public function getMaxExpansions() |
201
|
|
|
{ |
202
|
|
|
return $this->maxExpansions; |
203
|
|
|
} |
204
|
|
|
|
205
|
|
|
|
206
|
|
|
/** |
207
|
|
|
* @param string $analyzer |
208
|
|
|
* @return MatchQuery |
209
|
|
|
*/ |
210
|
|
|
public function setAnalyzer($analyzer) |
211
|
|
|
{ |
212
|
|
|
$this->analyzer = $analyzer; |
213
|
|
|
return $this; |
214
|
|
|
} |
215
|
|
|
|
216
|
|
|
|
217
|
|
|
/** |
218
|
|
|
* @return string |
219
|
|
|
*/ |
220
|
|
|
public function getAnalyzer() |
221
|
|
|
{ |
222
|
|
|
return $this->analyzer; |
223
|
|
|
} |
224
|
|
|
|
225
|
|
|
|
226
|
|
|
/** |
227
|
|
|
* @param array $match |
228
|
|
|
* @return array |
229
|
|
|
*/ |
230
|
|
|
protected function applyOptions(array $match) |
231
|
|
|
{ |
232
|
|
|
$operator = $this->getOperator(); |
233
|
|
|
if (!is_null($operator)) { |
234
|
|
|
$match['operator'] = $operator; |
235
|
|
|
} |
236
|
|
|
$zeroTermsQuery = $this->getZeroTermsQuery(); |
237
|
|
|
if (!is_null($zeroTermsQuery)) { |
238
|
|
|
$match['zero_terms_query'] = $zeroTermsQuery; |
239
|
|
|
} |
240
|
|
|
$cutOffFreq = $this->getCutOffFrequency(); |
241
|
|
|
if (!is_null($cutOffFreq)) { |
242
|
|
|
$match['cutoff_frequency'] = $cutOffFreq; |
243
|
|
|
} |
244
|
|
|
$type = $this->getType(); |
245
|
|
|
if (!is_null($type)) { |
246
|
|
|
$match['type'] = $type; |
247
|
|
|
$slop = $this->getSlop(); |
248
|
|
|
if (!is_null($slop)) { |
249
|
|
|
$match['slop'] = $slop; |
250
|
|
|
} |
251
|
|
|
if ($match['type'] === self::TYPE_PHRASE_PREFIX) { |
252
|
|
|
$maxExp = $this->getMaxExpansions(); |
253
|
|
|
if (!is_null($maxExp)) { |
254
|
|
|
$match['max_expansions'] = $maxExp; |
255
|
|
|
} |
256
|
|
|
} |
257
|
|
|
} |
258
|
|
|
$analyzer = $this->getAnalyzer(); |
259
|
|
|
if (!is_null($analyzer)) { |
260
|
|
|
$match['analyzer'] = $analyzer; |
261
|
|
|
} |
262
|
|
|
|
263
|
|
|
return $match; |
264
|
|
|
} |
265
|
|
|
|
266
|
|
|
|
267
|
|
|
/** |
268
|
|
|
* @param string $operator |
269
|
|
|
* @throws InvalidArgument |
270
|
|
|
*/ |
271
|
|
View Code Duplication |
protected function assertOperator($operator) |
|
|
|
|
272
|
|
|
{ |
273
|
|
|
$validOperators = [self::OPERATOR_AND, self::OPERATOR_OR]; |
274
|
|
|
if (!in_array($operator, $validOperators)) { |
275
|
|
|
throw new InvalidArgument(sprintf( |
276
|
|
|
'Match Query `operator` must be one of "%s", "%s" given.', |
277
|
|
|
implode(', ', $validOperators), |
278
|
|
|
$operator |
279
|
|
|
)); |
280
|
|
|
} |
281
|
|
|
} |
282
|
|
|
|
283
|
|
|
|
284
|
|
|
/** |
285
|
|
|
* @param string $zeroTermsQuery |
286
|
|
|
* @throws InvalidArgument |
287
|
|
|
*/ |
288
|
|
View Code Duplication |
protected function assertZeroTermsQuery($zeroTermsQuery) |
|
|
|
|
289
|
|
|
{ |
290
|
|
|
$validZeroTermQueries = [self::ZERO_TERM_QUERY_NONE, self::ZERO_TERM_QUERY_ALL]; |
291
|
|
|
if (!in_array($zeroTermsQuery, $validZeroTermQueries)) { |
292
|
|
|
throw new InvalidArgument(sprintf( |
293
|
|
|
'Match Query `zero_terms_query` must be one of "%s", "%s" given.', |
294
|
|
|
implode(', ', $validZeroTermQueries), |
295
|
|
|
$zeroTermsQuery |
296
|
|
|
)); |
297
|
|
|
} |
298
|
|
|
} |
299
|
|
|
|
300
|
|
|
|
301
|
|
|
/** |
302
|
|
|
* @param float $cutOffFrequency |
303
|
|
|
* @throws InvalidArgument |
304
|
|
|
*/ |
305
|
|
|
protected function assertCutOffFrequency($cutOffFrequency) |
306
|
|
|
{ |
307
|
|
|
if (!is_float($cutOffFrequency)) { |
308
|
|
|
throw new InvalidArgument(sprintf( |
309
|
|
|
'Match Query `cutoff_frequency` must be a float value, "%s" given.', |
310
|
|
|
gettype($cutOffFrequency) |
311
|
|
|
)); |
312
|
|
|
} |
313
|
|
|
} |
314
|
|
|
|
315
|
|
|
|
316
|
|
|
/** |
317
|
|
|
* @param string $type |
318
|
|
|
* @throws InvalidArgument |
319
|
|
|
*/ |
320
|
|
View Code Duplication |
protected function assertType($type) |
|
|
|
|
321
|
|
|
{ |
322
|
|
|
$validTypes = [self::TYPE_PHRASE, self::TYPE_PHRASE_PREFIX]; |
323
|
|
|
if (!in_array($type, $validTypes)) { |
324
|
|
|
throw new InvalidArgument(sprintf( |
325
|
|
|
'Match Query `type` must be one of "%s", "%s" given.', |
326
|
|
|
implode(', ', $validTypes), |
327
|
|
|
$type |
328
|
|
|
)); |
329
|
|
|
} |
330
|
|
|
} |
331
|
|
|
|
332
|
|
|
|
333
|
|
|
/** |
334
|
|
|
* @param int $slop |
335
|
|
|
* @throws InvalidArgument |
336
|
|
|
*/ |
337
|
|
|
protected function assertSlop($slop) |
338
|
|
|
{ |
339
|
|
|
if (!is_int($slop)) { |
340
|
|
|
throw new InvalidArgument(sprintf( |
341
|
|
|
'Match Query `slop` must be an integer, "%s" given.', |
342
|
|
|
gettype($slop) |
343
|
|
|
)); |
344
|
|
|
} |
345
|
|
|
} |
346
|
|
|
|
347
|
|
|
|
348
|
|
|
/** |
349
|
|
|
* @param int $maxExpansions |
350
|
|
|
* @throws InvalidArgument |
351
|
|
|
*/ |
352
|
|
|
protected function assertMaxExpansions($maxExpansions) |
353
|
|
|
{ |
354
|
|
|
if (!is_int($maxExpansions)) { |
355
|
|
|
throw new InvalidArgument(sprintf( |
356
|
|
|
'Match Query `max_expansions` must be an integer, "%s" given.', |
357
|
|
|
gettype($maxExpansions) |
358
|
|
|
)); |
359
|
|
|
} |
360
|
|
|
} |
361
|
|
|
} |
362
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.