Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
44.72% covered (danger)
44.72%
89 / 199
31.75% covered (danger)
31.75%
20 / 63
CRAP
0.00% covered (danger)
0.00%
0 / 1
SearchContext
44.72% covered (danger)
44.72%
89 / 199
31.75% covered (danger)
31.75%
20 / 63
1489.63
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%
1 / 1
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%
38 / 38
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
 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\MediaWikiServices;
21use 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 */
46class 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 !empty( 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 ( empty( $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 ( empty( $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 ( empty( $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}