Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
18.36% |
38 / 207 |
|
13.64% |
3 / 22 |
CRAP | |
0.00% |
0 / 1 |
CirrusSearch | |
18.36% |
38 / 207 |
|
13.64% |
3 / 22 |
3475.28 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 23 |
|
0.00% |
0 / 1 |
30 | |||
setConnection | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getConnection | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getConfig | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
supports | |
85.71% |
6 / 7 |
|
0.00% |
0 / 1 |
6.10 | |||
doSearchText | |
12.50% |
4 / 32 |
|
0.00% |
0 / 1 |
30.12 | |||
isFeatureEnabled | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
6 | |||
searchTextReal | |
0.00% |
0 / 25 |
|
0.00% |
0 / 1 |
182 | |||
getSuggestions | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
20 | |||
getValidSorts | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
6 | |||
getLastSearchMetrics | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
completionSearchBackend | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
72 | |||
completionSearchWithVariants | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
prefixSearch | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
30 | |||
getProfiles | |
100.00% |
23 / 23 |
|
100.00% |
1 / 1 |
7 | |||
extractProfileFromFeatureData | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
3 | |||
makeSearchFieldMapping | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
searchArchiveTitle | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
30 | |||
getUpdater | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
countContentWords | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
12 | |||
makeSearcher | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
getCirrusSearchHookRunner | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 |
1 | <?php |
2 | |
3 | namespace CirrusSearch; |
4 | |
5 | use CirrusSearch\Parser\NamespacePrefixParser; |
6 | use CirrusSearch\Parser\QueryStringRegex\SearchQueryParseException; |
7 | use CirrusSearch\Profile\ContextualProfileOverride; |
8 | use CirrusSearch\Profile\SearchProfileService; |
9 | use CirrusSearch\Search\ArrayCirrusSearchResult; |
10 | use CirrusSearch\Search\CirrusSearchIndexFieldFactory; |
11 | use CirrusSearch\Search\CirrusSearchResultSet; |
12 | use CirrusSearch\Search\FancyTitleResultsType; |
13 | use CirrusSearch\Search\SearchMetricsProvider; |
14 | use CirrusSearch\Search\SearchQuery; |
15 | use CirrusSearch\Search\SearchQueryBuilder; |
16 | use CirrusSearch\Search\TitleHelper; |
17 | use CirrusSearch\Search\TitleResultsType; |
18 | use ISearchResultSet; |
19 | use MediaWiki\Context\RequestContext; |
20 | use MediaWiki\MediaWikiServices; |
21 | use MediaWiki\Parser\Sanitizer; |
22 | use MediaWiki\Request\WebRequest; |
23 | use MediaWiki\Status\Status; |
24 | use MediaWiki\Title\Title; |
25 | use MediaWiki\User\User; |
26 | use MediaWiki\WikiMap\WikiMap; |
27 | use SearchEngine; |
28 | use SearchIndexField; |
29 | use SearchSuggestionSet; |
30 | |
31 | /** |
32 | * SearchEngine implementation for CirrusSearch. Delegates to |
33 | * CirrusSearchSearcher for searches and CirrusSearchUpdater for updates. Note |
34 | * that lots of search behavior is hooked in CirrusSearchHooks rather than |
35 | * overridden here. |
36 | * |
37 | * This program is free software; you can redistribute it and/or modify |
38 | * it under the terms of the GNU General Public License as published by |
39 | * the Free Software Foundation; either version 2 of the License, or |
40 | * (at your option) any later version. |
41 | * |
42 | * This program is distributed in the hope that it will be useful, |
43 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
44 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
45 | * GNU General Public License for more details. |
46 | * |
47 | * You should have received a copy of the GNU General Public License along |
48 | * with this program; if not, write to the Free Software Foundation, Inc., |
49 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
50 | * http://www.gnu.org/copyleft/gpl.html |
51 | */ |
52 | class CirrusSearch extends SearchEngine { |
53 | |
54 | /** |
55 | * Special profile to instruct this class to use profile |
56 | * selection mechanism. |
57 | * This allows to defer profile selection to when we actually perform |
58 | * the search. The reason is that the list of possible profiles |
59 | * is returned by self::getProfiles so instead of assigning a default |
60 | * profile at this point we use this special profile. |
61 | */ |
62 | public const AUTOSELECT_PROFILE = 'engine_autoselect'; |
63 | |
64 | /** @const string name of the prefixsearch fallback profile */ |
65 | public const COMPLETION_PREFIX_FALLBACK_PROFILE = 'classic'; |
66 | |
67 | /** |
68 | * @const int Maximum title length that we'll check in prefix and keyword searches. |
69 | * Since titles can be 255 bytes in length we're setting this to 255 |
70 | * characters. |
71 | */ |
72 | public const MAX_TITLE_SEARCH = 255; |
73 | |
74 | /** |
75 | * Name of the feature to extract more fields during a fulltext search request. |
76 | * Expected value is a list of strings identifying the fields to extract out |
77 | * of the source document. |
78 | * @see SearchEngine::supports() and SearchEngine::setFeatureData() |
79 | */ |
80 | public const EXTRA_FIELDS_TO_EXTRACT = 'extra-fields-to-extract'; |
81 | |
82 | /** |
83 | * Name of the entry in the extension data array holding the extracted field |
84 | * requested using the EXTRA_FIELDS_TO_EXTRACT feature. |
85 | * @see \SearchResult::getExtensionData() |
86 | */ |
87 | private const EXTRA_FIELDS = ArrayCirrusSearchResult::EXTRA_FIELDS; |
88 | |
89 | /** |
90 | * @var array metrics about the last thing we searched sourced from the |
91 | * Searcher instance |
92 | */ |
93 | private $lastSearchMetrics = []; |
94 | |
95 | /** |
96 | * @var array additional metrics about the search sourced within this class |
97 | */ |
98 | private $extraSearchMetrics = []; |
99 | |
100 | /** |
101 | * @var Connection |
102 | */ |
103 | private $connection; |
104 | |
105 | /** |
106 | * Search configuration. |
107 | * @var SearchConfig immutable |
108 | */ |
109 | private $config; |
110 | |
111 | /** |
112 | * Current request. |
113 | * @var WebRequest |
114 | */ |
115 | private $request; |
116 | |
117 | /** |
118 | * @var RequestContext |
119 | */ |
120 | private $requestContext; |
121 | |
122 | /** |
123 | * @var CirrusSearchIndexFieldFactory |
124 | */ |
125 | private $searchIndexFieldFactory; |
126 | |
127 | /** |
128 | * @var CirrusDebugOptions |
129 | */ |
130 | private $debugOptions; |
131 | |
132 | /** |
133 | * @var NamespacePrefixParser |
134 | */ |
135 | private $namespacePrefixParser; |
136 | |
137 | /** |
138 | * @var InterwikiResolver |
139 | */ |
140 | private $interwikiResolver; |
141 | |
142 | /** |
143 | * @var TitleHelper |
144 | */ |
145 | private $titleHelper; |
146 | |
147 | /** |
148 | * @var CirrusSearchHookRunner|null |
149 | */ |
150 | private $cirrusSearchHookRunner; |
151 | |
152 | /** |
153 | * @param SearchConfig|null $config |
154 | * @param CirrusDebugOptions|null $debugOptions |
155 | * @param NamespacePrefixParser|null $namespacePrefixParser |
156 | * @param InterwikiResolver|null $interwikiResolver |
157 | * @param TitleHelper|null $titleHelper |
158 | */ |
159 | public function __construct( ?SearchConfig $config = null, |
160 | ?CirrusDebugOptions $debugOptions = null, |
161 | ?NamespacePrefixParser $namespacePrefixParser = null, |
162 | ?InterwikiResolver $interwikiResolver = null, ?TitleHelper $titleHelper = null |
163 | ) { |
164 | // Initialize UserTesting before we create a Connection |
165 | // This is useful to do tests across multiple clusters |
166 | UserTestingStatus::getInstance(); |
167 | $this->config = $config ?? MediaWikiServices::getInstance() |
168 | ->getConfigFactory() |
169 | ->makeConfig( 'CirrusSearch' ); |
170 | $this->connection = new Connection( $this->config ); |
171 | $this->requestContext = RequestContext::getMain(); |
172 | $this->request = $this->requestContext->getRequest(); |
173 | $this->searchIndexFieldFactory = new CirrusSearchIndexFieldFactory( $this->config ); |
174 | $this->namespacePrefixParser = $namespacePrefixParser ?: new class() implements NamespacePrefixParser { |
175 | /** @inheritDoc */ |
176 | public function parse( $query ) { |
177 | return CirrusSearch::parseNamespacePrefixes( $query, true, true ); |
178 | } |
179 | }; |
180 | $this->interwikiResolver = $interwikiResolver ?: MediaWikiServices::getInstance()->getService( InterwikiResolver::SERVICE ); |
181 | |
182 | // enable interwiki by default |
183 | $this->features['interwiki'] = true; |
184 | $this->features['show-multimedia-search-results'] = $this->config->get( 'CirrusSearchCrossProjectShowMultimedia' ) == true; |
185 | $this->debugOptions = $debugOptions ?? CirrusDebugOptions::fromRequest( $this->request ); |
186 | $this->titleHelper = $titleHelper ?: new TitleHelper( WikiMap::getCurrentWikiId(), $interwikiResolver, |
187 | static function ( $v ) { |
188 | return Sanitizer::escapeIdForLink( $v ); |
189 | } |
190 | ); |
191 | $extraFieldsInSearchResults = $this->config->get( 'CirrusSearchExtraFieldsInSearchResults' ); |
192 | if ( $extraFieldsInSearchResults ) { |
193 | $this->features[ self::EXTRA_FIELDS_TO_EXTRACT ] = $extraFieldsInSearchResults; |
194 | } |
195 | } |
196 | |
197 | public function setConnection( Connection $connection ) { |
198 | $this->connection = $connection; |
199 | } |
200 | |
201 | /** |
202 | * @return Connection |
203 | */ |
204 | public function getConnection() { |
205 | return $this->connection; |
206 | } |
207 | |
208 | /** |
209 | * Get search config |
210 | * @return SearchConfig |
211 | */ |
212 | public function getConfig() { |
213 | return $this->config; |
214 | } |
215 | |
216 | /** |
217 | * Override supports to shut off updates to Cirrus via the SearchEngine infrastructure. Page |
218 | * updates and additions are chained on the end of the links update job. Deletes are noticed |
219 | * via the ArticleDeleteComplete hook. |
220 | * @param string $feature feature name |
221 | * @return bool is this feature supported? |
222 | */ |
223 | public function supports( $feature ) { |
224 | switch ( $feature ) { |
225 | case 'search-update': |
226 | case 'list-redirects': |
227 | return false; |
228 | case self::FT_QUERY_INDEP_PROFILE_TYPE: |
229 | case self::EXTRA_FIELDS_TO_EXTRACT: |
230 | return true; |
231 | default: |
232 | return parent::supports( $feature ); |
233 | } |
234 | } |
235 | |
236 | /** |
237 | * Overridden to delegate prefix searching to Searcher. |
238 | * @param string $term text to search |
239 | * @return Status Value is either SearchResultSet, or null on error. |
240 | */ |
241 | protected function doSearchText( $term ) { |
242 | try { |
243 | $builder = SearchQueryBuilder::newFTSearchQueryBuilder( $this->config, |
244 | $term, $this->namespacePrefixParser, $this->getCirrusSearchHookRunner() ); |
245 | } catch ( SearchQueryParseException $e ) { |
246 | return $e->asStatus(); |
247 | } |
248 | |
249 | $builder->setDebugOptions( $this->debugOptions ) |
250 | ->setInitialNamespaces( $this->namespaces ) |
251 | ->setLimit( $this->limit ) |
252 | ->setOffset( $this->offset ) |
253 | ->setSort( $this->getSort() ) |
254 | ->setRandomSeed( $this->getFeatureData( 'random_seed' ) ) |
255 | ->setExtraIndicesSearch( true ) |
256 | ->setCrossProjectSearch( $this->isFeatureEnabled( 'interwiki' ) ) |
257 | ->setWithDYMSuggestion( $this->showSuggestion ) |
258 | ->setAllowRewrite( $this->isFeatureEnabled( 'rewrite' ) ) |
259 | ->addProfileContextParameter( ContextualProfileOverride::LANGUAGE, |
260 | $this->requestContext->getLanguage()->getCode() ) |
261 | ->setExtraFieldsToExtract( $this->features[self::EXTRA_FIELDS_TO_EXTRACT] ?? [] ) |
262 | ->setProvideAllSnippets( !empty( $this->features['snippets'] ) ); |
263 | |
264 | if ( $this->prefix !== '' ) { |
265 | $builder->addContextualFilter( 'prefix', |
266 | \CirrusSearch\Query\PrefixFeature::asContextualFilter( $this->prefix ) ); |
267 | } |
268 | |
269 | $profile = $this->extractProfileFromFeatureData( SearchEngine::FT_QUERY_INDEP_PROFILE_TYPE ); |
270 | if ( $profile !== null ) { |
271 | $builder->addForcedProfile( SearchProfileService::RESCORE, $profile ); |
272 | } |
273 | |
274 | $query = $builder->build(); |
275 | |
276 | $status = $this->searchTextReal( $query ); |
277 | $matches = $status->getValue(); |
278 | if ( $matches instanceof CirrusSearchResultSet ) { |
279 | ElasticsearchIntermediary::setResultPages( [ $matches ] ); |
280 | } |
281 | if ( $matches instanceof SearchMetricsProvider ) { |
282 | $this->extraSearchMetrics += $status->getValue()->getMetrics(); |
283 | } |
284 | |
285 | return $status; |
286 | } |
287 | |
288 | /** |
289 | * @param string $feature |
290 | * @return bool |
291 | */ |
292 | private function isFeatureEnabled( $feature ) { |
293 | return isset( $this->features[$feature] ) && $this->features[$feature]; |
294 | } |
295 | |
296 | /** |
297 | * Do the hard part of the searching - actual Searcher invocation |
298 | * @param SearchQuery $query |
299 | * @return Status |
300 | */ |
301 | protected function searchTextReal( SearchQuery $query ) { |
302 | $searcher = $this->makeSearcher( $query->getSearchConfig() ); |
303 | $status = $searcher->search( $query ); |
304 | $this->lastSearchMetrics = $searcher->getSearchMetrics(); |
305 | if ( !$status->isOK() ) { |
306 | return $status; |
307 | } |
308 | |
309 | $result = $status->getValue(); |
310 | |
311 | // Add interwiki results, if we have a sane result |
312 | // Note that we have no way of sending warning back to the user. In this case all warnings |
313 | // are logged when they are added to the status object so we just ignore them here.... |
314 | // TODO: move this to the Searcher class and get rid of InterwikiSearcher |
315 | // there are no reasons we can't do this in a single msearch request. |
316 | if ( $query->getCrossSearchStrategy()->isCrossProjectSearchSupported() && |
317 | $searcher->getSearchContext()->areResultsPossible() && |
318 | ( $this->debugOptions->isReturnRaw() || method_exists( $result, 'addInterwikiResults' ) ) |
319 | ) { |
320 | $iwSearch = new InterwikiSearcher( $this->connection, $query->getSearchConfig(), $this->namespaces, null, |
321 | $this->debugOptions, $this->namespacePrefixParser, $this->interwikiResolver, $this->titleHelper, |
322 | $this->getCirrusSearchHookRunner() ); |
323 | $interwikiResults = $iwSearch->getInterwikiResults( $query ); |
324 | if ( $interwikiResults->isOK() && $interwikiResults->getValue() !== [] ) { |
325 | foreach ( $interwikiResults->getValue() as $interwiki => $interwikiResult ) { |
326 | if ( $this->debugOptions->isReturnRaw() ) { |
327 | $result[$interwiki] = $interwikiResult; |
328 | } elseif ( $interwikiResult && $interwikiResult->numRows() > 0 ) { |
329 | $result->addInterwikiResults( |
330 | $interwikiResult, ISearchResultSet::SECONDARY_RESULTS, $interwiki |
331 | ); |
332 | } |
333 | } |
334 | } |
335 | } |
336 | |
337 | if ( $this->debugOptions->isReturnRaw() ) { |
338 | $status->setResult( true, |
339 | $searcher->processRawReturn( $result, $this->request ) ); |
340 | } |
341 | |
342 | return $status; |
343 | } |
344 | |
345 | /** |
346 | * Look for suggestions using ES completion suggester. |
347 | * @param string $search Search string |
348 | * @param string[]|null $variants Search term variants |
349 | * @param SearchConfig $config search configuration |
350 | * @return SearchSuggestionSet Set of suggested names |
351 | */ |
352 | protected function getSuggestions( $search, $variants, SearchConfig $config ) { |
353 | // Inspect features to check if the user selected a specific profile |
354 | $profile = $this->extractProfileFromFeatureData( SearchEngine::COMPLETION_PROFILE_TYPE ); |
355 | |
356 | $clusterOverride = $config->getElement( 'CirrusSearchClusterOverrides', 'completion' ); |
357 | if ( $clusterOverride !== null ) { |
358 | $connection = Connection::getPool( $config, $clusterOverride ); |
359 | } else { |
360 | $connection = $this->connection; |
361 | } |
362 | $suggester = new CompletionSuggester( $connection, $this->limit, |
363 | $this->offset, $config, $this->namespaces, null, |
364 | false, $profile, $this->debugOptions ); |
365 | |
366 | $response = $suggester->suggest( $search, $variants ); |
367 | |
368 | if ( !$response->isOK() ) { |
369 | return SearchSuggestionSet::emptySuggestionSet(); |
370 | } |
371 | |
372 | $result = $response->getValue(); |
373 | |
374 | if ( $this->debugOptions->isReturnRaw() ) { |
375 | Util::processSearchRawReturn( $result, $this->request, $this->debugOptions ); |
376 | } |
377 | |
378 | // Errors will be logged, let's try the exact db match |
379 | return $result; |
380 | } |
381 | |
382 | /** |
383 | * Get the sort of sorts we allow |
384 | * @return string[] |
385 | */ |
386 | public function getValidSorts() { |
387 | $sorts = [ |
388 | 'relevance', 'just_match', 'none', |
389 | 'incoming_links_asc', 'incoming_links_desc', |
390 | 'last_edit_asc', 'last_edit_desc', |
391 | 'create_timestamp_asc', 'create_timestamp_desc', |
392 | 'random', 'user_random', |
393 | ]; |
394 | |
395 | if ( $this->config->getElement( 'CirrusSearchNaturalTitleSort', 'use' ) ) { |
396 | $sorts[] = 'title_natural_asc'; |
397 | $sorts[] = 'title_natural_desc'; |
398 | } |
399 | |
400 | return $sorts; |
401 | } |
402 | |
403 | /** |
404 | * Get the metrics for the last search we performed. Null if we haven't done any. |
405 | * @return array |
406 | */ |
407 | public function getLastSearchMetrics() { |
408 | return $this->lastSearchMetrics + $this->extraSearchMetrics; |
409 | } |
410 | |
411 | /** |
412 | * Perform a completion search. |
413 | * Does not resolve namespaces and does not check variants. |
414 | * We use parent search for: |
415 | * - Special: namespace |
416 | * We use old prefix search for: |
417 | * - Suggester not enabled |
418 | * - |
419 | * @param string $search |
420 | * @return SearchSuggestionSet |
421 | */ |
422 | protected function completionSearchBackend( $search ) { |
423 | if ( in_array( NS_SPECIAL, $this->namespaces ) ) { |
424 | // delegate special search to parent |
425 | return parent::completionSearchBackend( $search ); |
426 | } |
427 | |
428 | // Not really useful, mostly for testing purpose |
429 | $variants = $this->debugOptions->getCirrusCompletionVariant(); |
430 | if ( !$variants ) { |
431 | $converter = MediaWikiServices::getInstance()->getLanguageConverterFactory()->getLanguageConverter(); |
432 | $variants = $converter->autoConvertToAllVariants( $search ); |
433 | } elseif ( count( $variants ) > 3 ) { |
434 | // We should not allow too many variants |
435 | $variants = array_slice( $variants, 0, 3 ); |
436 | } |
437 | |
438 | if ( !$this->config->isCompletionSuggesterEnabled() ) { |
439 | // Completion suggester is not enabled, fallback to |
440 | // default implementation |
441 | return $this->prefixSearch( $search, $variants ); |
442 | } |
443 | |
444 | // the completion suggester is only worth a try if NS_MAIN is requested |
445 | if ( !in_array( NS_MAIN, $this->namespaces ) ) { |
446 | return $this->prefixSearch( $search, $variants ); |
447 | } |
448 | |
449 | $profile = $this->extractProfileFromFeatureData( SearchEngine::COMPLETION_PROFILE_TYPE ); |
450 | if ( $profile === null ) { |
451 | // Need to fetch the name to fallback to prefix (not ideal) |
452 | // We should probably refactor this to have a single code path for prefix and completion suggester. |
453 | $profile = $this->config->getProfileService() |
454 | ->getProfileName( SearchProfileService::COMPLETION, SearchProfileService::CONTEXT_DEFAULT ); |
455 | } |
456 | if ( $profile === self::COMPLETION_PREFIX_FALLBACK_PROFILE ) { |
457 | // Fallback to prefixsearch if the classic profile was selected. |
458 | return $this->prefixSearch( $search, $variants ); |
459 | } |
460 | |
461 | return $this->getSuggestions( $search, $variants, $this->config ); |
462 | } |
463 | |
464 | /** |
465 | * Override variants function because we always do variants |
466 | * in the backend. |
467 | * @see SearchEngine::completionSearchWithVariants() |
468 | * @param string $search |
469 | * @return SearchSuggestionSet |
470 | */ |
471 | public function completionSearchWithVariants( $search ) { |
472 | return $this->completionSearch( $search ); |
473 | } |
474 | |
475 | /** |
476 | * Older prefix search. |
477 | * @param string $search search text |
478 | * @param string[] $variants |
479 | * @return SearchSuggestionSet |
480 | */ |
481 | protected function prefixSearch( $search, $variants ) { |
482 | $searcher = $this->makeSearcher(); |
483 | |
484 | if ( $search ) { |
485 | $searcher->setResultsType( new FancyTitleResultsType( 'prefix' ) ); |
486 | } else { |
487 | // Empty searches always find the title. |
488 | $searcher->setResultsType( new TitleResultsType() ); |
489 | } |
490 | |
491 | $status = $searcher->prefixSearch( $search, $variants ); |
492 | |
493 | // There is no way to send errors or warnings back to the caller here so we have to make do with |
494 | // only sending results back if there are results and relying on the logging done at the status |
495 | // construction site to log errors. |
496 | if ( $status->isOK() ) { |
497 | if ( $this->debugOptions->isReturnRaw() ) { |
498 | Util::processSearchRawReturn( $status->getValue(), $this->request, |
499 | $this->debugOptions ); |
500 | } |
501 | if ( !$search ) { |
502 | // No need to unpack the simple title matches from non-fancy TitleResultsType |
503 | return SearchSuggestionSet::fromTitles( $status->getValue() ); |
504 | } |
505 | $results = array_filter( array_map( |
506 | [ FancyTitleResultsType::class, 'chooseBestTitleOrRedirect' ], |
507 | $status->getValue() ) ); |
508 | return SearchSuggestionSet::fromTitles( $results ); |
509 | } |
510 | |
511 | return SearchSuggestionSet::emptySuggestionSet(); |
512 | } |
513 | |
514 | /** |
515 | * @param string $profileType |
516 | * @param User|null $user |
517 | * @return array|null |
518 | * @see SearchEngine::getProfiles() |
519 | */ |
520 | public function getProfiles( $profileType, ?User $user = null ) { |
521 | $profileService = $this->config->getProfileService(); |
522 | $serviceProfileType = null; |
523 | switch ( $profileType ) { |
524 | case SearchEngine::COMPLETION_PROFILE_TYPE: |
525 | if ( $this->config->isCompletionSuggesterEnabled() ) { |
526 | $serviceProfileType = SearchProfileService::COMPLETION; |
527 | } |
528 | break; |
529 | case SearchEngine::FT_QUERY_INDEP_PROFILE_TYPE: |
530 | $serviceProfileType = SearchProfileService::RESCORE; |
531 | break; |
532 | } |
533 | |
534 | if ( $serviceProfileType === null ) { |
535 | return null; |
536 | } |
537 | |
538 | $allowedProfiles = $profileService->listExposedProfiles( $serviceProfileType ); |
539 | |
540 | $profiles = []; |
541 | foreach ( $allowedProfiles as $name => $profile ) { |
542 | // @todo: decide what to with profiles we declare |
543 | // in wmf-config with no i18n messages. |
544 | // Do we want to expose them anyway, or simply |
545 | // hide them but still allow Api to pass them to us. |
546 | // It may require a change in core since ApiBase is |
547 | // strict and won't allow unknown values to be set |
548 | // here. |
549 | $profiles[] = [ |
550 | 'name' => $name, |
551 | 'desc-message' => $profile['i18n_msg'] ?? null, |
552 | ]; |
553 | } |
554 | if ( $profiles !== [] ) { |
555 | $profiles[] = [ |
556 | 'name' => self::AUTOSELECT_PROFILE, |
557 | 'desc-message' => 'cirrussearch-autoselect-profile', |
558 | 'default' => true, |
559 | ]; |
560 | } |
561 | return $profiles; |
562 | } |
563 | |
564 | /** |
565 | * (public for testing purposes) |
566 | * @param string $profileType |
567 | * @return string|null the profile name set in SearchEngine::features |
568 | * null if none present or equal to self::AUTOSELECT_PROFILE |
569 | */ |
570 | public function extractProfileFromFeatureData( $profileType ) { |
571 | if ( isset( $this->features[$profileType] ) |
572 | && $this->features[$profileType] !== self::AUTOSELECT_PROFILE |
573 | ) { |
574 | return $this->features[$profileType]; |
575 | } |
576 | return null; |
577 | } |
578 | |
579 | /** |
580 | * Create a search field definition |
581 | * @param string $name |
582 | * @param string $type |
583 | * @return SearchIndexField |
584 | */ |
585 | public function makeSearchFieldMapping( $name, $type ): SearchIndexField { |
586 | return $this->searchIndexFieldFactory->makeSearchFieldMapping( $name, $type ); |
587 | } |
588 | |
589 | /** |
590 | * Perform a title search in the article archive. |
591 | * |
592 | * @param string $term Raw search term |
593 | * @return Status<Title[]> |
594 | */ |
595 | public function searchArchiveTitle( $term ) { |
596 | if ( !$this->config->get( 'CirrusSearchEnableArchive' ) ) { |
597 | return Status::newGood( [] ); |
598 | } |
599 | |
600 | $term = trim( $term ); |
601 | |
602 | if ( $term === '' ) { |
603 | return Status::newGood( [] ); |
604 | } |
605 | |
606 | $searcher = $this->makeSearcher(); |
607 | $status = $searcher->searchArchive( $term ); |
608 | if ( $status->isOK() && $searcher->isReturnRaw() ) { |
609 | $status->setResult( true, |
610 | $searcher->processRawReturn( $status->getValue(), $this->request ) ); |
611 | } |
612 | return $status; |
613 | } |
614 | |
615 | /** |
616 | * Helper method to facilitate mocking during tests. |
617 | * @return Updater |
618 | */ |
619 | protected function getUpdater(): Updater { |
620 | return new Updater( $this->connection ); |
621 | } |
622 | |
623 | /** |
624 | * @return Status Contains a single integer indicating the number |
625 | * of content words in the wiki |
626 | */ |
627 | public function countContentWords() { |
628 | $this->limit = 1; |
629 | $searcher = $this->makeSearcher(); |
630 | $status = $searcher->countContentWords(); |
631 | |
632 | if ( $status->isOK() && $searcher->isReturnRaw() ) { |
633 | $status->setResult( true, |
634 | $searcher->processRawReturn( $status->getValue(), $this->request ) ); |
635 | } |
636 | return $status; |
637 | } |
638 | |
639 | /** |
640 | * @param SearchConfig|null $config |
641 | * @return Searcher |
642 | */ |
643 | private function makeSearcher( ?SearchConfig $config = null ) { |
644 | return new Searcher( $this->connection, $this->offset, $this->limit, $config ?? $this->config, $this->namespaces, |
645 | null, false, $this->debugOptions, $this->namespacePrefixParser, $this->interwikiResolver, $this->titleHelper, |
646 | $this->getCirrusSearchHookRunner() ); |
647 | } |
648 | |
649 | private function getCirrusSearchHookRunner(): CirrusSearchHookRunner { |
650 | if ( $this->cirrusSearchHookRunner == null ) { |
651 | $this->cirrusSearchHookRunner = new CirrusSearchHookRunner( $this->getHookContainer() ); |
652 | } |
653 | return $this->cirrusSearchHookRunner; |
654 | } |
655 | } |