Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
44.72% |
89 / 199 |
|
31.75% |
20 / 63 |
CRAP | |
0.00% |
0 / 1 |
SearchContext | |
44.72% |
89 / 199 |
|
31.75% |
20 / 63 |
1489.63 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
1 | |||
withConfig | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
6 | |||
loadConfig | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
__clone | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
isDirty | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getConfig | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getNamespaces | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setNamespaces | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getProfileContext | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getProfileContextParams | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setProfileContext | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 | |||
getRescoreProfile | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
setRescoreProfile | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
areResultsPossible | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setResultsPossible | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
isSyntaxUsed | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
isSpecialKeywordUsed | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
1 | |||
getSyntaxUsed | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getSyntaxDescription | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
addSyntaxUsed | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getSearchType | |
75.00% |
3 / 4 |
|
0.00% |
0 / 1 |
2.06 | |||
addFilter | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
addNotFilter | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
setHighlightQuery | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
addNonTextHighlightQuery | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
getFetchPhaseBuilder | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getHighlight | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
12 | |||
getHighlightQuery | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
20 | |||
getRescore | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
6 | |||
getQuery | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
72 | |||
setMainQuery | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
addNonTextQuery | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
getSearchQuery | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getLimitSearchToLocalWiki | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setLimitSearchToLocalWiki | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
getCacheTtl | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setCacheTtl | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
getOriginalSearchTerm | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setOriginalSearchTerm | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getCleanedSearchTerm | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setCleanedSearchTerm | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
escaper | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getExtraScoreBuilders | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
addCustomRescoreComponent | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
addWarning | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
getWarnings | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getFulltextQueryBuilderProfile | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
setFulltextQueryBuilderProfile | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
setResultsType | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getResultsType | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
getExtraIndices | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
getPhraseRescoreQuery | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setPhraseRescoreQuery | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
addAggregation | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
getAggregations | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getDebugOptions | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getFilters | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
must | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
mustNot | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
fromSearchQuery | |
100.00% |
38 / 38 |
|
100.00% |
1 / 1 |
4 | |||
getFallbackRunner | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setTrackTotalHits | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
getTrackTotalHits | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | namespace CirrusSearch\Search; |
4 | |
5 | use CirrusSearch\CirrusDebugOptions; |
6 | use CirrusSearch\CirrusSearchHookRunner; |
7 | use CirrusSearch\ExternalIndex; |
8 | use CirrusSearch\Fallbacks\FallbackRunner; |
9 | use CirrusSearch\OtherIndexesUpdater; |
10 | use CirrusSearch\Parser\BasicQueryClassifier; |
11 | use CirrusSearch\Profile\SearchProfileService; |
12 | use CirrusSearch\Query\Builder\FilterBuilder; |
13 | use CirrusSearch\Search\Fetch\FetchPhaseConfigBuilder; |
14 | use CirrusSearch\Search\Rescore\BoostFunctionBuilder; |
15 | use CirrusSearch\Search\Rescore\RescoreBuilder; |
16 | use CirrusSearch\SearchConfig; |
17 | use CirrusSearch\WarningCollector; |
18 | use Elastica\Aggregation\AbstractAggregation; |
19 | use Elastica\Query\AbstractQuery; |
20 | use MediaWiki\MediaWikiServices; |
21 | use Wikimedia\Assert\Assert; |
22 | |
23 | /** |
24 | * The search context, maintains the state of the current search query. |
25 | * |
26 | * This program is free software; you can redistribute it and/or modify |
27 | * it under the terms of the GNU General Public License as published by |
28 | * the Free Software Foundation; either version 2 of the License, or |
29 | * (at your option) any later version. |
30 | * |
31 | * This program is distributed in the hope that it will be useful, |
32 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
33 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
34 | * GNU General Public License for more details. |
35 | * |
36 | * You should have received a copy of the GNU General Public License along |
37 | * with this program; if not, write to the Free Software Foundation, Inc., |
38 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
39 | * http://www.gnu.org/copyleft/gpl.html |
40 | */ |
41 | |
42 | /** |
43 | * The SearchContext stores the various states maintained |
44 | * during the query building process. |
45 | */ |
46 | class SearchContext implements WarningCollector, FilterBuilder { |
47 | /** |
48 | * @var SearchConfig |
49 | */ |
50 | private $config; |
51 | |
52 | /** |
53 | * @var int[]|null list of namespaces |
54 | */ |
55 | private $namespaces; |
56 | |
57 | /** |
58 | * @var string |
59 | */ |
60 | private $profileContext = SearchProfileService::CONTEXT_DEFAULT; |
61 | |
62 | /** |
63 | * @var string[] |
64 | */ |
65 | private $profileContextParams = []; |
66 | |
67 | /** |
68 | * @var string rescore profile to use |
69 | */ |
70 | private $rescoreProfile; |
71 | |
72 | /** |
73 | * @var BoostFunctionBuilder[] Extra scoring builders to use. |
74 | */ |
75 | private $extraScoreBuilders = []; |
76 | |
77 | /** |
78 | * @var bool Could this query possibly return results? |
79 | */ |
80 | private $resultsPossible = true; |
81 | |
82 | /** |
83 | * @var int[] List of features in the user suplied query string. Features are |
84 | * held in the array key, value is how "complex" the feature is. |
85 | */ |
86 | private $syntaxUsed = []; |
87 | |
88 | /** |
89 | * @var AbstractQuery[] List of filters that query results must match |
90 | */ |
91 | private $filters = []; |
92 | |
93 | /** |
94 | * @var AbstractQuery[] List of filters that query results must not match |
95 | */ |
96 | private $notFilters = []; |
97 | |
98 | /** |
99 | * @var AbstractQuery|null Query that should be used for highlighting if different |
100 | * from the query used for selecting. |
101 | */ |
102 | private $highlightQuery; |
103 | |
104 | /** |
105 | * @var AbstractQuery[] queries that don't use Elastic's "query string" query, |
106 | * for more advanced highlighting (e.g. match_phrase_prefix for regular |
107 | * quoted strings). |
108 | */ |
109 | private $nonTextHighlightQueries = []; |
110 | |
111 | /** |
112 | * @var AbstractQuery|null phrase rescore query |
113 | */ |
114 | private $phraseRescoreQuery; |
115 | |
116 | /** |
117 | * @var AbstractQuery|null main query. null defaults to MatchAll |
118 | */ |
119 | private $mainQuery; |
120 | |
121 | /** |
122 | * @var \Elastica\Query\MatchQuery[] Queries that don't use Elastic's "query string" query, for |
123 | * more advanced searching (e.g. match_phrase_prefix for regular quoted strings). |
124 | */ |
125 | private $nonTextQueries = []; |
126 | |
127 | /** |
128 | * @var SearchQuery |
129 | */ |
130 | private $searchQuery; |
131 | |
132 | /** |
133 | * @var bool Should this search limit results to the local wiki? |
134 | */ |
135 | private $limitSearchToLocalWiki = false; |
136 | |
137 | /** |
138 | * @var int The number of seconds to cache results for |
139 | */ |
140 | private $cacheTtl = 0; |
141 | |
142 | /** |
143 | * @var string The original search |
144 | */ |
145 | private $originalSearchTerm; |
146 | |
147 | /** |
148 | * @var string The users search term with keywords removed |
149 | */ |
150 | private $cleanedSearchTerm; |
151 | |
152 | /** |
153 | * @var Escaper |
154 | */ |
155 | private $escaper; |
156 | |
157 | /** |
158 | * @var FetchPhaseConfigBuilder |
159 | */ |
160 | private $fetchPhaseBuilder; |
161 | |
162 | /** |
163 | * @var int[] weights of different syntaxes |
164 | */ |
165 | private static $syntaxWeights = [ |
166 | // regex is really tough |
167 | 'full_text' => 10, |
168 | 'regex' => PHP_INT_MAX, |
169 | 'more_like' => 100, |
170 | 'near_match' => 10, |
171 | 'prefix' => 2, |
172 | // Deep category searches |
173 | 'deepcategory' => 20, |
174 | ]; |
175 | |
176 | /** |
177 | * @var array[] Warnings to be passed into StatusValue::warning() |
178 | */ |
179 | private $warnings = []; |
180 | |
181 | /** |
182 | * @var string name of the fulltext query builder profile |
183 | */ |
184 | private $fulltextQueryBuilderProfile; |
185 | |
186 | /** |
187 | * @var bool Have custom options that effect the search results been set |
188 | * outside the defaults from config? |
189 | */ |
190 | private $isDirty = false; |
191 | |
192 | /** |
193 | * @var ResultsType|FullTextResultsType Type of the result for the context. |
194 | */ |
195 | private $resultsType; |
196 | |
197 | /** |
198 | * @var AbstractAggregation[] Aggregations to perform |
199 | */ |
200 | private $aggs = []; |
201 | |
202 | /** |
203 | * @var CirrusDebugOptions |
204 | */ |
205 | private $debugOptions; |
206 | |
207 | /** |
208 | * @var FallbackRunner|null |
209 | */ |
210 | private $fallbackRunner; |
211 | /** |
212 | * @var CirrusSearchHookRunner|null |
213 | */ |
214 | private $cirrusSearchHookRunner; |
215 | |
216 | /** |
217 | * @var bool |
218 | */ |
219 | private $trackTotalHits = true; |
220 | |
221 | /** |
222 | * @param SearchConfig $config |
223 | * @param int[]|null $namespaces |
224 | * @param CirrusDebugOptions|null $options |
225 | * @param FallbackRunner|null $fallbackRunner |
226 | * @param FetchPhaseConfigBuilder|null $fetchPhaseConfigBuilder |
227 | * @param CirrusSearchHookRunner|null $cirrusSearchHookRunner |
228 | */ |
229 | public function __construct( |
230 | SearchConfig $config, |
231 | ?array $namespaces = null, |
232 | ?CirrusDebugOptions $options = null, |
233 | ?FallbackRunner $fallbackRunner = null, |
234 | ?FetchPhaseConfigBuilder $fetchPhaseConfigBuilder = null, |
235 | ?CirrusSearchHookRunner $cirrusSearchHookRunner = null |
236 | ) { |
237 | $this->config = $config; |
238 | $this->namespaces = $namespaces; |
239 | $this->debugOptions = $options ?? CirrusDebugOptions::defaultOptions(); |
240 | $this->fallbackRunner = $fallbackRunner ?? FallbackRunner::noopRunner(); |
241 | $this->fetchPhaseBuilder = $fetchPhaseConfigBuilder ?? new FetchPhaseConfigBuilder( $config ); |
242 | $this->loadConfig(); |
243 | $this->cirrusSearchHookRunner = $cirrusSearchHookRunner ?? new CirrusSearchHookRunner( |
244 | MediaWikiServices::getInstance()->getHookContainer() ); |
245 | } |
246 | |
247 | /** |
248 | * Return a copy of this context with a new configuration. |
249 | * |
250 | * @param SearchConfig $config The new configuration |
251 | * @return SearchContext |
252 | */ |
253 | public function withConfig( SearchConfig $config ) { |
254 | $other = clone $this; |
255 | $other->config = $config; |
256 | $other->fetchPhaseBuilder = $this->fetchPhaseBuilder->withConfig( $config ); |
257 | if ( $other->resultsType instanceof FullTextResultsType ) { |
258 | $other->resultsType = $this->resultsType->withFetchPhaseBuilder( $other->fetchPhaseBuilder ); |
259 | } |
260 | $other->loadConfig(); |
261 | |
262 | return $other; |
263 | } |
264 | |
265 | private function loadConfig() { |
266 | $this->escaper = new Escaper( $this->config->get( 'LanguageCode' ), $this->config->get( 'CirrusSearchAllowLeadingWildcard' ) ); |
267 | } |
268 | |
269 | public function __clone() { |
270 | if ( $this->mainQuery ) { |
271 | $this->mainQuery = clone $this->mainQuery; |
272 | } |
273 | } |
274 | |
275 | /** |
276 | * Have custom options that effect the search results been set outside the |
277 | * defaults from config? |
278 | * |
279 | * @return bool |
280 | */ |
281 | public function isDirty() { |
282 | return $this->isDirty; |
283 | } |
284 | |
285 | /** |
286 | * @return SearchConfig the Cirrus config object |
287 | */ |
288 | public function getConfig() { |
289 | return $this->config; |
290 | } |
291 | |
292 | /** |
293 | * mediawiki namespace id's being requested. |
294 | * NOTE: this value may change during the Searcher process. |
295 | * |
296 | * @return int[]|null |
297 | */ |
298 | public function getNamespaces() { |
299 | return $this->namespaces; |
300 | } |
301 | |
302 | /** |
303 | * set the mediawiki namespace id's |
304 | * |
305 | * @param int[]|null $namespaces array of integer |
306 | */ |
307 | public function setNamespaces( $namespaces ) { |
308 | $this->isDirty = true; |
309 | $this->namespaces = $namespaces; |
310 | } |
311 | |
312 | /** |
313 | * @return string |
314 | */ |
315 | public function getProfileContext() { |
316 | return $this->profileContext; |
317 | } |
318 | |
319 | /** |
320 | * @return string[] |
321 | */ |
322 | public function getProfileContextParams(): array { |
323 | return $this->profileContextParams; |
324 | } |
325 | |
326 | /** |
327 | * @param string $profileContext |
328 | * @param string[] $contextParams |
329 | */ |
330 | public function setProfileContext( $profileContext, array $contextParams = [] ) { |
331 | $this->isDirty = $this->isDirty || |
332 | $this->profileContext !== $profileContext || |
333 | $this->profileContextParams !== $contextParams; |
334 | $this->profileContext = $profileContext; |
335 | $this->profileContextParams = $contextParams; |
336 | } |
337 | |
338 | /** |
339 | * @return string the rescore profile to use |
340 | */ |
341 | public function getRescoreProfile() { |
342 | if ( $this->rescoreProfile === null ) { |
343 | $this->rescoreProfile = $this->config->getProfileService() |
344 | ->getProfileName( SearchProfileService::RESCORE, $this->profileContext, $this->profileContextParams ); |
345 | } |
346 | return $this->rescoreProfile; |
347 | } |
348 | |
349 | /** |
350 | * @param string $rescoreProfile the rescore profile to use |
351 | */ |
352 | public function setRescoreProfile( $rescoreProfile ) { |
353 | $this->isDirty = true; |
354 | $this->rescoreProfile = $rescoreProfile; |
355 | } |
356 | |
357 | /** |
358 | * @return bool Could this query possibly return results? |
359 | */ |
360 | public function areResultsPossible() { |
361 | return $this->resultsPossible; |
362 | } |
363 | |
364 | /** |
365 | * @param bool $possible Could this query possible return results? Defaults to true |
366 | * if not called. |
367 | */ |
368 | public function setResultsPossible( $possible ) { |
369 | $this->isDirty = true; |
370 | $this->resultsPossible = $possible; |
371 | } |
372 | |
373 | /** |
374 | * @param string|null $type type of syntax to check, null for any type |
375 | * @return bool True when the query uses $type kind of syntax |
376 | */ |
377 | public function isSyntaxUsed( $type = null ) { |
378 | if ( $type === null ) { |
379 | return $this->syntaxUsed !== []; |
380 | } |
381 | return isset( $this->syntaxUsed[$type] ); |
382 | } |
383 | |
384 | /** |
385 | * @return bool true if a special keyword or syntax was used in the query |
386 | */ |
387 | public function isSpecialKeywordUsed() { |
388 | // full_text is not considered a special keyword |
389 | // TODO: investigate using BasicQueryClassifier::SIMPLE_BAG_OF_WORDS instead |
390 | return array_diff_key( $this->syntaxUsed, [ |
391 | 'full_text' => true, |
392 | 'full_text_simple_match' => true, |
393 | 'full_text_querystring' => true, |
394 | BasicQueryClassifier::SIMPLE_BAG_OF_WORDS => true, |
395 | BasicQueryClassifier::SIMPLE_PHRASE => true, |
396 | BasicQueryClassifier::BAG_OF_WORDS_WITH_PHRASE => true, |
397 | ] ) !== []; |
398 | } |
399 | |
400 | /** |
401 | * @return string[] List of syntax used in the query |
402 | */ |
403 | public function getSyntaxUsed() { |
404 | return array_keys( $this->syntaxUsed ); |
405 | } |
406 | |
407 | /** |
408 | * @return string Text description of syntax used by query. |
409 | */ |
410 | public function getSyntaxDescription() { |
411 | return implode( ',', $this->getSyntaxUsed() ); |
412 | } |
413 | |
414 | /** |
415 | * @param string $feature Name of a syntax feature used in the query string |
416 | * @param int|null $weight How "complex" is this feature. |
417 | */ |
418 | public function addSyntaxUsed( $feature, $weight = null ) { |
419 | $this->isDirty = true; |
420 | $this->syntaxUsed[$feature] = $weight ?? self::$syntaxWeights[$feature] ?? 1; |
421 | } |
422 | |
423 | /** |
424 | * @return string The type of search being performed, ex: full_text, near_match, prefix, etc. |
425 | * Using getSyntaxUsed() is better in most cases. |
426 | */ |
427 | public function getSearchType() { |
428 | if ( !$this->syntaxUsed ) { |
429 | return 'full_text'; |
430 | } |
431 | arsort( $this->syntaxUsed ); |
432 | // Return the first heaviest syntax |
433 | return key( $this->syntaxUsed ); |
434 | } |
435 | |
436 | /** |
437 | * @param AbstractQuery $filter Query results must match this filter |
438 | */ |
439 | public function addFilter( AbstractQuery $filter ) { |
440 | $this->isDirty = true; |
441 | $this->filters[] = $filter; |
442 | } |
443 | |
444 | /** |
445 | * @param AbstractQuery $filter Query results must not match this filter |
446 | */ |
447 | public function addNotFilter( AbstractQuery $filter ) { |
448 | $this->isDirty = true; |
449 | $this->notFilters[] = $filter; |
450 | } |
451 | |
452 | /** |
453 | * @param AbstractQuery|null $query Query that should be used for highlighting if different |
454 | * from the query used for selecting. |
455 | */ |
456 | public function setHighlightQuery( ?AbstractQuery $query = null ) { |
457 | $this->isDirty = true; |
458 | $this->highlightQuery = $query; |
459 | } |
460 | |
461 | /** |
462 | * @param AbstractQuery $query queries that don't use Elastic's "query |
463 | * string" query, for more advanced highlighting (e.g. match_phrase_prefix |
464 | * for regular quoted strings). |
465 | */ |
466 | public function addNonTextHighlightQuery( AbstractQuery $query ) { |
467 | $this->isDirty = true; |
468 | $this->nonTextHighlightQueries[] = $query; |
469 | } |
470 | |
471 | /** |
472 | * @return FetchPhaseConfigBuilder |
473 | */ |
474 | public function getFetchPhaseBuilder(): FetchPhaseConfigBuilder { |
475 | return $this->fetchPhaseBuilder; |
476 | } |
477 | |
478 | /** |
479 | * @param ResultsType $resultsType |
480 | * @param AbstractQuery $mainQuery Will be combined with highlighting query |
481 | * to provide highlightable terms. |
482 | * @return array|null Fetch portion of query to be sent to elasticsearch |
483 | */ |
484 | public function getHighlight( ResultsType $resultsType, AbstractQuery $mainQuery ) { |
485 | $highlight = $resultsType->getHighlightingConfiguration( [] ); |
486 | if ( !$highlight ) { |
487 | return null; |
488 | } |
489 | |
490 | $query = $this->getHighlightQuery( $mainQuery ); |
491 | if ( $query ) { |
492 | $highlight['highlight_query'] = $query->toArray(); |
493 | } |
494 | |
495 | return $highlight; |
496 | } |
497 | |
498 | /** |
499 | * @param AbstractQuery $mainQuery |
500 | * @return AbstractQuery|null Query that should be used for highlighting if different |
501 | * from the query used for selecting. |
502 | */ |
503 | private function getHighlightQuery( AbstractQuery $mainQuery ) { |
504 | if ( !$this->nonTextHighlightQueries ) { |
505 | // If no explicit highlight query is provided elastic |
506 | // will fallback to $mainQuery without specifying it. |
507 | return $this->highlightQuery; |
508 | } |
509 | |
510 | $bool = new \Elastica\Query\BoolQuery(); |
511 | // If no explicit highlight query is provided we |
512 | // need to include the main query along with |
513 | // the non-text queries to highlight those fields. |
514 | $bool->addShould( $this->highlightQuery ?: $mainQuery ); |
515 | foreach ( $this->nonTextHighlightQueries as $nonTextHighlightQuery ) { |
516 | $bool->addShould( $nonTextHighlightQuery ); |
517 | } |
518 | |
519 | return $bool; |
520 | } |
521 | |
522 | /** |
523 | * rescore_query has to be in array form before we send it to Elasticsearch but it is way |
524 | * easier to work with if we leave it in query form until now |
525 | * |
526 | * @return array[] Rescore configurations as used by elasticsearch. |
527 | */ |
528 | public function getRescore() { |
529 | $rescores = ( new RescoreBuilder( $this, $this->cirrusSearchHookRunner ) )->build(); |
530 | $result = []; |
531 | foreach ( $rescores as $rescore ) { |
532 | $rescore['query']['rescore_query'] = $rescore['query']['rescore_query']->toArray(); |
533 | $result[] = $rescore; |
534 | } |
535 | |
536 | return $result; |
537 | } |
538 | |
539 | /** |
540 | * @return AbstractQuery The primary query to be sent to elasticsearch. Includes |
541 | * the main quedry, non text queries, and any additional filters. |
542 | */ |
543 | public function getQuery() { |
544 | if ( !$this->nonTextQueries ) { |
545 | $mainQuery = $this->mainQuery ?: new \Elastica\Query\MatchAll(); |
546 | } else { |
547 | $mainQuery = new \Elastica\Query\BoolQuery(); |
548 | if ( $this->mainQuery ) { |
549 | $mainQuery->addMust( $this->mainQuery ); |
550 | } |
551 | foreach ( $this->nonTextQueries as $nonTextQuery ) { |
552 | $mainQuery->addMust( $nonTextQuery ); |
553 | } |
554 | } |
555 | $filters = $this->filters; |
556 | if ( $this->getNamespaces() ) { |
557 | // We must take an array_values here, or it can be json-encoded into an object instead |
558 | // of a list which elasticsearch will interpret as terms lookup. |
559 | $filters[] = new \Elastica\Query\Terms( 'namespace', array_values( $this->getNamespaces() ) ); |
560 | } |
561 | |
562 | // Wrap $mainQuery in a filtered query if there are any filters |
563 | $unifiedFilter = Filters::unify( $filters, $this->notFilters ); |
564 | if ( $unifiedFilter !== null ) { |
565 | if ( !( $mainQuery instanceof \Elastica\Query\BoolQuery ) ) { |
566 | $bool = new \Elastica\Query\BoolQuery(); |
567 | $bool->addMust( $mainQuery ); |
568 | $mainQuery = $bool; |
569 | } |
570 | $mainQuery->addFilter( $unifiedFilter ); |
571 | } |
572 | |
573 | return $mainQuery; |
574 | } |
575 | |
576 | /** |
577 | * @param AbstractQuery $query The primary query to be passed to |
578 | * elasticsearch. |
579 | */ |
580 | public function setMainQuery( AbstractQuery $query ) { |
581 | $this->isDirty = true; |
582 | $this->mainQuery = $query; |
583 | } |
584 | |
585 | /** |
586 | * @param \Elastica\Query\AbstractQuery $match Queries that don't use Elastic's |
587 | * "query string" query, for more advanced searching (e.g. |
588 | * match_phrase_prefix for regular quoted strings). |
589 | */ |
590 | public function addNonTextQuery( \Elastica\Query\AbstractQuery $match ) { |
591 | $this->isDirty = true; |
592 | $this->nonTextQueries[] = $match; |
593 | } |
594 | |
595 | /** |
596 | * @return SearchQuery |
597 | */ |
598 | public function getSearchQuery() { |
599 | return $this->searchQuery; |
600 | } |
601 | |
602 | /** |
603 | * @return bool Should this search limit results to the local wiki? If |
604 | * not called the default is false. |
605 | */ |
606 | public function getLimitSearchToLocalWiki() { |
607 | return $this->limitSearchToLocalWiki; |
608 | } |
609 | |
610 | /** |
611 | * @param bool $localWikiOnly Should this search limit results to the local wiki? If |
612 | * not called the default is false. |
613 | */ |
614 | public function setLimitSearchToLocalWiki( $localWikiOnly ) { |
615 | if ( $localWikiOnly !== $this->limitSearchToLocalWiki ) { |
616 | $this->isDirty = true; |
617 | $this->limitSearchToLocalWiki = $localWikiOnly; |
618 | } |
619 | } |
620 | |
621 | /** |
622 | * @return int The number of seconds to cache results for |
623 | */ |
624 | public function getCacheTtl() { |
625 | return $this->cacheTtl; |
626 | } |
627 | |
628 | /** |
629 | * @param int $ttl The number of seconds to cache results for |
630 | */ |
631 | public function setCacheTtl( $ttl ) { |
632 | $this->isDirty = true; |
633 | $this->cacheTtl = $ttl; |
634 | } |
635 | |
636 | /** |
637 | * @return string the original search term |
638 | */ |
639 | public function getOriginalSearchTerm() { |
640 | return $this->originalSearchTerm; |
641 | } |
642 | |
643 | /** |
644 | * @param string $term |
645 | */ |
646 | public function setOriginalSearchTerm( $term ) { |
647 | // Intentionally does not set dirty to true. This is used only |
648 | // for logging, as of july 2017. |
649 | $this->originalSearchTerm = $term; |
650 | } |
651 | |
652 | /** |
653 | * @return string The search term with keywords removed |
654 | */ |
655 | public function getCleanedSearchTerm() { |
656 | return $this->cleanedSearchTerm; |
657 | } |
658 | |
659 | /** |
660 | * @param string $term The search term with keywords removed |
661 | */ |
662 | public function setCleanedSearchTerm( $term ) { |
663 | $this->isDirty = true; |
664 | $this->cleanedSearchTerm = $term; |
665 | } |
666 | |
667 | /** |
668 | * @return Escaper |
669 | */ |
670 | public function escaper() { |
671 | return $this->escaper; |
672 | } |
673 | |
674 | /** |
675 | * @return BoostFunctionBuilder[] |
676 | */ |
677 | public function getExtraScoreBuilders() { |
678 | return $this->extraScoreBuilders; |
679 | } |
680 | |
681 | /** |
682 | * Add custom scoring function to the context. |
683 | * The rescore builder will pick it up. |
684 | * @param BoostFunctionBuilder $rescore |
685 | */ |
686 | public function addCustomRescoreComponent( BoostFunctionBuilder $rescore ) { |
687 | $this->isDirty = true; |
688 | $this->extraScoreBuilders[] = $rescore; |
689 | } |
690 | |
691 | /** |
692 | * @param string $message i18n message key |
693 | * @param mixed ...$params |
694 | */ |
695 | public function addWarning( $message, ...$params ) { |
696 | $this->isDirty = true; |
697 | $this->warnings[] = array_filter( func_get_args(), static function ( $v ) { |
698 | return $v !== null; |
699 | } ); |
700 | } |
701 | |
702 | /** |
703 | * @return array[] Array of arrays. Each sub array is a set of values |
704 | * suitable for creating an i18n message. |
705 | * @phan-return non-empty-array[] |
706 | */ |
707 | public function getWarnings() { |
708 | return $this->warnings; |
709 | } |
710 | |
711 | /** |
712 | * @return string the name of the fulltext query builder profile |
713 | */ |
714 | public function getFulltextQueryBuilderProfile() { |
715 | if ( $this->fulltextQueryBuilderProfile === null ) { |
716 | $this->fulltextQueryBuilderProfile = $this->config->getProfileService() |
717 | ->getProfileName( SearchProfileService::FT_QUERY_BUILDER, $this->profileContext ); |
718 | } |
719 | return $this->fulltextQueryBuilderProfile; |
720 | } |
721 | |
722 | /** |
723 | * @param string $profile set the name of the fulltext query builder profile |
724 | */ |
725 | public function setFulltextQueryBuilderProfile( $profile ) { |
726 | $this->isDirty = true; |
727 | $this->fulltextQueryBuilderProfile = $profile; |
728 | } |
729 | |
730 | /** |
731 | * @param ResultsType $resultsType results type to return |
732 | */ |
733 | public function setResultsType( $resultsType ) { |
734 | $this->resultsType = $resultsType; |
735 | } |
736 | |
737 | /** |
738 | * @return ResultsType |
739 | */ |
740 | public function getResultsType() { |
741 | Assert::precondition( $this->resultsType !== null, "resultsType unset" ); |
742 | return $this->resultsType; |
743 | } |
744 | |
745 | /** |
746 | * Get the list of extra indices to query. |
747 | * Generally needed to query externilized file index. |
748 | * Must be called only once the list of namespaces has been set. |
749 | * |
750 | * @return ExternalIndex[] |
751 | * @see OtherIndexesUpdater::getExtraIndexesForNamespaces() |
752 | */ |
753 | public function getExtraIndices() { |
754 | if ( $this->getLimitSearchToLocalWiki() || !$this->getNamespaces() ) { |
755 | return []; |
756 | } |
757 | return OtherIndexesUpdater::getExtraIndexesForNamespaces( |
758 | $this->config, |
759 | $this->getNamespaces() |
760 | ); |
761 | } |
762 | |
763 | /** |
764 | * Get the phrase rescore query if available |
765 | * @return AbstractQuery|null |
766 | */ |
767 | public function getPhraseRescoreQuery() { |
768 | return $this->phraseRescoreQuery; |
769 | } |
770 | |
771 | /** |
772 | * @param AbstractQuery|null $phraseRescoreQuery |
773 | */ |
774 | public function setPhraseRescoreQuery( $phraseRescoreQuery ) { |
775 | $this->phraseRescoreQuery = $phraseRescoreQuery; |
776 | $this->isDirty = true; |
777 | } |
778 | |
779 | /** |
780 | * Add aggregation to perform on search. |
781 | * @param AbstractAggregation $agg |
782 | */ |
783 | public function addAggregation( AbstractAggregation $agg ) { |
784 | $this->aggs[] = $agg; |
785 | $this->isDirty = true; |
786 | } |
787 | |
788 | /** |
789 | * Get the list of aggregations. |
790 | * @return AbstractAggregation[] |
791 | */ |
792 | public function getAggregations() { |
793 | return $this->aggs; |
794 | } |
795 | |
796 | /** |
797 | * @return CirrusDebugOptions |
798 | */ |
799 | public function getDebugOptions() { |
800 | return $this->debugOptions; |
801 | } |
802 | |
803 | /** |
804 | * NOTE: public for testing purposes. |
805 | * @return AbstractQuery[] |
806 | */ |
807 | public function getFilters(): array { |
808 | return $this->filters; |
809 | } |
810 | |
811 | /** |
812 | * @param AbstractQuery $query |
813 | */ |
814 | public function must( AbstractQuery $query ) { |
815 | $this->addFilter( $query ); |
816 | } |
817 | |
818 | /** |
819 | * @param AbstractQuery $query |
820 | */ |
821 | public function mustNot( AbstractQuery $query ) { |
822 | $this->addNotFilter( $query ); |
823 | } |
824 | |
825 | /** |
826 | * Builds a SearchContext based on a SearchQuery. |
827 | * |
828 | * Helper function used for building blocks that still work on top |
829 | * of the SearchContext+queryString instead of SearchQuery. |
830 | * |
831 | * States initialized: |
832 | * - limitSearchToLocalWiki |
833 | * - suggestion |
834 | * - custom rescoreProfile/fulltextQueryBuilderProfile |
835 | * - contextual filters: (eg. SearchEngine::$prefix) |
836 | * - SuggestPrefix (DYM prefix: ~ and/or namespace header) |
837 | * |
838 | * @param SearchQuery $query |
839 | * @param FallbackRunner|null $fallbackRunner |
840 | * @param CirrusSearchHookRunner|null $cirrusSearchHookRunner |
841 | * @return SearchContext |
842 | * @throws \CirrusSearch\Parser\ParsedQueryClassifierException |
843 | */ |
844 | public static function fromSearchQuery( |
845 | SearchQuery $query, |
846 | ?FallbackRunner $fallbackRunner = null, |
847 | ?CirrusSearchHookRunner $cirrusSearchHookRunner = null |
848 | ): SearchContext { |
849 | $searchContext = new SearchContext( |
850 | $query->getSearchConfig(), |
851 | $query->getNamespaces(), |
852 | $query->getDebugOptions(), |
853 | $fallbackRunner, |
854 | new FetchPhaseConfigBuilder( |
855 | $query->getSearchConfig(), |
856 | $query->getSearchEngineEntryPoint(), |
857 | $query->shouldProvideAllSnippets() |
858 | ), |
859 | $cirrusSearchHookRunner |
860 | ); |
861 | |
862 | $searchContext->searchQuery = $query; |
863 | |
864 | $searchContext->limitSearchToLocalWiki = !$query->getCrossSearchStrategy()->isExtraIndicesSearchSupported(); |
865 | |
866 | $searchContext->rescoreProfile = $query->getForcedProfile( SearchProfileService::RESCORE ); |
867 | |
868 | $profileContext = $query->getSearchConfig() |
869 | ->getProfileService() |
870 | ->getDispatchService() |
871 | ->bestRoute( $query ) |
872 | ->getProfileContext(); |
873 | $searchContext->setProfileContext( $profileContext ); |
874 | $parsedQuery = $query->getParsedQuery(); |
875 | $basicQueryClasses = [ |
876 | BasicQueryClassifier::SIMPLE_BAG_OF_WORDS, |
877 | BasicQueryClassifier::SIMPLE_PHRASE, |
878 | BasicQueryClassifier::BAG_OF_WORDS_WITH_PHRASE, |
879 | BasicQueryClassifier::COMPLEX_QUERY, |
880 | ]; |
881 | |
882 | foreach ( $basicQueryClasses as $klass ) { |
883 | if ( $parsedQuery->isQueryOfClass( $klass ) ) { |
884 | $searchContext->syntaxUsed[$klass] = 1; |
885 | } |
886 | } |
887 | // TODO: Clarify what happens when user forces a profile, should we disable the dispatch service? |
888 | $searchContext->fulltextQueryBuilderProfile = $query->getForcedProfile( SearchProfileService::FT_QUERY_BUILDER ); |
889 | $searchContext->profileContextParams = $query->getProfileContextParameters(); |
890 | |
891 | foreach ( $query->getContextualFilters() as $filter ) { |
892 | $filter->populate( $searchContext ); |
893 | } |
894 | $pQuery = $query->getParsedQuery(); |
895 | $searchContext->originalSearchTerm = $pQuery->getRawQuery(); |
896 | return $searchContext; |
897 | } |
898 | |
899 | /** |
900 | * @return FallbackRunner |
901 | */ |
902 | public function getFallbackRunner(): FallbackRunner { |
903 | return $this->fallbackRunner; |
904 | } |
905 | |
906 | public function setTrackTotalHits( bool $trackTotalHits ): void { |
907 | if ( $trackTotalHits !== $this->trackTotalHits ) { |
908 | $this->isDirty = true; |
909 | $this->trackTotalHits = $trackTotalHits; |
910 | } |
911 | } |
912 | |
913 | public function getTrackTotalHits(): bool { |
914 | return $this->trackTotalHits; |
915 | } |
916 | } |