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