Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
20.93% covered (danger)
20.93%
45 / 215
16.67% covered (danger)
16.67%
4 / 24
CRAP
0.00% covered (danger)
0.00%
0 / 1
CirrusSearch
20.93% covered (danger)
20.93%
45 / 215
16.67% covered (danger)
16.67%
4 / 24
3324.41
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
30
 setConnection
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getConnection
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getConfig
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 supports
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
6.10
 doSearchText
12.50% covered (danger)
12.50%
4 / 32
0.00% covered (danger)
0.00%
0 / 1
30.12
 isFeatureEnabled
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 searchTextReal
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
182
 getSuggestions
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
20
 getValidSorts
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
6
 getLastSearchMetrics
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 completionSearchBackend
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
72
 completionSearchWithVariants
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 prefixSearch
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
30
 getProfiles
100.00% covered (success)
100.00%
23 / 23
100.00% covered (success)
100.00%
1 / 1
7
 extractProfileFromFeatureData
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 makeSearchFieldMapping
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 searchArchiveTitle
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
30
 updateWeightedTags
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 resetWeightedTags
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getUpdater
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 countContentWords
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 makeSearcher
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 getCirrusSearchHookRunner
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2
3namespace CirrusSearch;
4
5use CirrusSearch\Extra\MultiList\MultiListBuilder;
6use CirrusSearch\Parser\NamespacePrefixParser;
7use CirrusSearch\Parser\QueryStringRegex\SearchQueryParseException;
8use CirrusSearch\Profile\ContextualProfileOverride;
9use CirrusSearch\Profile\SearchProfileService;
10use CirrusSearch\Search\ArrayCirrusSearchResult;
11use CirrusSearch\Search\CirrusSearchIndexFieldFactory;
12use CirrusSearch\Search\CirrusSearchResultSet;
13use CirrusSearch\Search\FancyTitleResultsType;
14use CirrusSearch\Search\SearchMetricsProvider;
15use CirrusSearch\Search\SearchQuery;
16use CirrusSearch\Search\SearchQueryBuilder;
17use CirrusSearch\Search\TitleHelper;
18use CirrusSearch\Search\TitleResultsType;
19use ISearchResultSet;
20use MediaWiki\Context\RequestContext;
21use MediaWiki\MediaWikiServices;
22use MediaWiki\Page\ProperPageIdentity;
23use MediaWiki\Parser\Sanitizer;
24use MediaWiki\Request\WebRequest;
25use MediaWiki\Status\Status;
26use MediaWiki\Title\Title;
27use MediaWiki\User\User;
28use MediaWiki\WikiMap\WikiMap;
29use SearchEngine;
30use SearchIndexField;
31use SearchSuggestionSet;
32use 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 */
55class 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}