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