Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
97.18% covered (success)
97.18%
69 / 71
92.00% covered (success)
92.00%
23 / 25
CRAP
0.00% covered (danger)
0.00%
0 / 1
SearchQuery
97.18% covered (success)
97.18%
69 / 71
92.00% covered (success)
92.00%
23 / 25
39
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
1
 getDebugOptions
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getParsedQuery
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getInitialNamespaces
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getInitialCrossSearchStrategy
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getCrossSearchStrategy
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 getContextualFilters
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getSearchEngineEntryPoint
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getSort
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getRandomSeed
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getForcedProfiles
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getOffset
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getLimit
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getMaximumResultPosition
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getNamespaces
93.33% covered (success)
93.33%
14 / 15
0.00% covered (danger)
0.00%
0 / 1
8.02
 getSearchConfig
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getForcedProfile
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasForcedProfile
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isWithDYMSuggestion
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isAllowRewrite
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getProfileContextParameters
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getExtraFieldsToExtract
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 shouldProvideAllSnippets
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 mustTrackTotalHits
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
5
 isUsingDefaultSearchedNamespaces
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3namespace CirrusSearch\Search;
4
5use CirrusSearch\CirrusDebugOptions;
6use CirrusSearch\CrossSearchStrategy;
7use CirrusSearch\Parser\AST\ParsedQuery;
8use CirrusSearch\SearchConfig;
9use MediaWiki\MainConfigNames;
10
11/**
12 * A search query, it contains all the necessary information to build and send a query to the backend.
13 * NOTE: Immutable value class.
14 */
15class SearchQuery {
16    /**
17     * Identifier for the fulltext SearchEngine entry point
18     * @see \SearchEngine::searchText()
19     */
20    public const SEARCH_TEXT = 'searchText';
21
22    /**
23     * @var ParsedQuery
24     */
25    private $parsedQuery;
26
27    /**
28     * @var int[]
29     */
30    private $initialNamespaces;
31
32    /**
33     * @var CrossSearchStrategy
34     */
35    private $initialCrossSearchStrategy;
36
37    /**
38     * @var CrossSearchStrategy|null (lazy loaded)
39     */
40    private $crossSearchStrategy;
41
42    /**
43     * @var \CirrusSearch\Query\Builder\ContextualFilter[]
44     */
45    private $contextualFilters;
46
47    /**
48     * Entry point from the SearchEngine
49     * TODO: clarify its usage and see whether or not another
50     * entry point var is needed to carry some provenance information
51     * from the UI.
52     * @var string
53     */
54    private $searchEngineEntryPoint;
55
56    /**
57     * @var string
58     */
59    private $sort;
60
61    /**
62     * @var int|null
63     */
64    private $randomSeed;
65
66    /**
67     * @var string[]
68     */
69    private $forcedProfiles;
70
71    /**
72     * @var int
73     */
74    private $offset;
75
76    /**
77     * @var int
78     */
79    private $limit;
80
81    /**
82     * @var CirrusDebugOptions
83     */
84    private $debugOptions;
85
86    /**
87     * @var SearchConfig
88     */
89    private $searchConfig;
90
91    /**
92     * @var bool
93     */
94    private $withDYMSuggestion;
95
96    /**
97     * @var bool
98     */
99    private $allowRewrite;
100
101    /**
102     * @var string[] parameters for SearchProfileService
103     * @see \CirrusSearch\Profile\ContextualProfileOverride
104     */
105    private $profileContextParameters;
106
107    /**
108     * @var string[] list of extra fields to extract
109     */
110    private $extraFieldsToExtract;
111
112    /**
113     * @var bool
114     */
115    private $provideAllSnippets;
116
117    /**
118     * @param ParsedQuery $parsedQuery
119     * @param int[] $initialNamespaces
120     * @param CrossSearchStrategy $initialCrosswikiStrategy
121     * @param \CirrusSearch\Query\Builder\ContextualFilter[] $contextualFilters
122     * @param string $searchEngineEntryPoint
123     * @param string $sort
124     * @param int|null $randomSeed
125     * @param string[] $forcedProfiles
126     * @param int $offset
127     * @param int $limit
128     * @param CirrusDebugOptions $debugOptions
129     * @param SearchConfig $searchConfig
130     * @param bool $withDYMSuggestion
131     * @param bool $allowRewrite
132     * @param string[] $profileContextParameters
133     * @param string[] $extraFieldsToExtract
134     * @param bool $provideAllSnippets
135     * @see SearchQueryBuilder
136     */
137    public function __construct(
138        ParsedQuery $parsedQuery,
139        array $initialNamespaces,
140        CrossSearchStrategy $initialCrosswikiStrategy,
141        array $contextualFilters,
142        $searchEngineEntryPoint,
143        $sort,
144        $randomSeed,
145        array $forcedProfiles,
146        $offset,
147        $limit,
148        CirrusDebugOptions $debugOptions,
149        SearchConfig $searchConfig,
150        $withDYMSuggestion,
151        $allowRewrite,
152        array $profileContextParameters,
153        array $extraFieldsToExtract,
154        bool $provideAllSnippets
155    ) {
156        $this->parsedQuery = $parsedQuery;
157        $this->initialNamespaces = $initialNamespaces;
158        $this->initialCrossSearchStrategy = $initialCrosswikiStrategy;
159        $this->contextualFilters = $contextualFilters;
160        $this->searchEngineEntryPoint = $searchEngineEntryPoint;
161        $this->sort = $sort;
162        $this->randomSeed = $randomSeed;
163        $this->forcedProfiles = $forcedProfiles;
164        $this->offset = $offset;
165        $this->limit = $limit;
166        $this->debugOptions = $debugOptions;
167        $this->searchConfig = $searchConfig;
168        $this->withDYMSuggestion = $withDYMSuggestion;
169        $this->allowRewrite = $allowRewrite;
170        $this->profileContextParameters = $profileContextParameters;
171        $this->extraFieldsToExtract = $extraFieldsToExtract;
172        $this->provideAllSnippets = $provideAllSnippets;
173    }
174
175    public function getDebugOptions(): CirrusDebugOptions {
176        return $this->debugOptions;
177    }
178
179    public function getParsedQuery(): ParsedQuery {
180        return $this->parsedQuery;
181    }
182
183    /**
184     * @return int[]
185     */
186    public function getInitialNamespaces() {
187        return $this->initialNamespaces;
188    }
189
190    public function getInitialCrossSearchStrategy(): CrossSearchStrategy {
191        return $this->initialCrossSearchStrategy;
192    }
193
194    public function getCrossSearchStrategy(): CrossSearchStrategy {
195        if ( $this->crossSearchStrategy === null ) {
196            if ( $this->contextualFilters !== [] ) {
197                $this->crossSearchStrategy = CrossSearchStrategy::hostWikiOnlyStrategy();
198            } else {
199                $this->crossSearchStrategy = $this->parsedQuery
200                    ->getCrossSearchStrategy()
201                    ->intersect( $this->initialCrossSearchStrategy );
202            }
203        }
204        return $this->crossSearchStrategy;
205    }
206
207    /**
208     * @return \CirrusSearch\Query\Builder\ContextualFilter[]
209     */
210    public function getContextualFilters(): array {
211        return $this->contextualFilters;
212    }
213
214    /**
215     * From which SearchEngine method this query entered CirrusSearch
216     * @return string
217     */
218    public function getSearchEngineEntryPoint() {
219        return $this->searchEngineEntryPoint;
220    }
221
222    /**
223     * @return string
224     */
225    public function getSort() {
226        return $this->sort;
227    }
228
229    public function getRandomSeed(): ?int {
230        return $this->randomSeed;
231    }
232
233    /**
234     * @return string[]
235     */
236    public function getForcedProfiles(): array {
237        return $this->forcedProfiles;
238    }
239
240    /**
241     * @return int
242     */
243    public function getOffset() {
244        return $this->offset;
245    }
246
247    /**
248     * @return int
249     */
250    public function getLimit() {
251        return $this->limit;
252    }
253
254    public function getMaximumResultPosition(): int {
255        return $this->offset + $this->limit;
256    }
257
258    /**
259     * List of namespaces required to run this query.
260     *
261     * @return int[] list of namespaces, empty array means that all namespaces
262     * are required.
263     */
264    public function getNamespaces(): array {
265        $additionalRequired = null;
266        if ( $this->initialNamespaces != [] && $this->contextualFilters != [] ) {
267            foreach ( $this->contextualFilters as $filter ) {
268                $additional = $filter->requiredNamespaces();
269                if ( $additional === null ) {
270                    continue;
271                }
272                if ( $additional === [] ) {
273                    $additionalRequired = [];
274                    break;
275                }
276                if ( $additionalRequired === null ) {
277                    $additionalRequired = $additional;
278                } else {
279                    $additionalRequired = array_merge( $additionalRequired, $additional );
280                }
281            }
282            if ( $additionalRequired !== null ) {
283                $additionalRequired = array_unique( $additionalRequired );
284            }
285        }
286        return $this->parsedQuery->getActualNamespaces( $this->initialNamespaces, $additionalRequired );
287    }
288
289    public function getSearchConfig(): SearchConfig {
290        return $this->searchConfig;
291    }
292
293    /**
294     * @param string $profileType
295     * @see \CirrusSearch\Profile\SearchProfileService
296     * @return string|null name of the profile or null if nothing forced for this type
297     */
298    public function getForcedProfile( $profileType ) {
299        return $this->forcedProfiles[$profileType] ?? null;
300    }
301
302    /**
303     * @return bool
304     */
305    public function hasForcedProfile() {
306        return $this->forcedProfiles !== [];
307    }
308
309    /**
310     * @return bool
311     */
312    public function isWithDYMSuggestion() {
313        return $this->withDYMSuggestion;
314    }
315
316    /**
317     * @return bool
318     */
319    public function isAllowRewrite() {
320        return $this->allowRewrite;
321    }
322
323    /**
324     * @return string[]
325     * @see \CirrusSearch\Profile\ContextualProfileOverride
326     */
327    public function getProfileContextParameters() {
328        return $this->profileContextParameters;
329    }
330
331    /**
332     * @return string[]
333     * @see \CirrusSearch\Search\FullTextResultsType
334     */
335    public function getExtraFieldsToExtract(): array {
336        return $this->extraFieldsToExtract;
337    }
338
339    public function shouldProvideAllSnippets(): bool {
340        return $this->provideAllSnippets;
341    }
342
343    public function mustTrackTotalHits(): bool {
344        $queryClasses = $this->getSearchConfig()->get( 'CirrusSearchMustTrackTotalHits' ) ?: [];
345        foreach ( $queryClasses as $queryClass => $track ) {
346            if ( $queryClass === "default" ) {
347                continue;
348            }
349            if ( $this->parsedQuery->isQueryOfClass( $queryClass ) ) {
350                return $track;
351            }
352        }
353        return $queryClasses['default'] ?? true;
354    }
355
356    /**
357     * Identify if this query initially targets the default set of namespaces
358     * @return bool true if the initial namespaces are equals to the default searched namespaces
359     */
360    public function isUsingDefaultSearchedNamespaces(): bool {
361        $defaultSearchedNamespaces = $this->getSearchConfig()->get( MainConfigNames::NamespacesToBeSearchedDefault );
362        if ( is_array( $defaultSearchedNamespaces ) ) {
363            $defaultSearchedNamespaces = array_map( static fn ( $n ) => intval( $n ), array_keys( $defaultSearchedNamespaces, true ) );
364            return $this->initialNamespaces == $defaultSearchedNamespaces;
365        }
366        return false;
367    }
368}