Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
18.36% covered (danger)
18.36%
38 / 207
13.64% covered (danger)
13.64%
3 / 22
CRAP
0.00% covered (danger)
0.00%
0 / 1
CirrusSearch
18.36% covered (danger)
18.36%
38 / 207
13.64% covered (danger)
13.64%
3 / 22
3475.28
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
 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\Parser\NamespacePrefixParser;
6use CirrusSearch\Parser\QueryStringRegex\SearchQueryParseException;
7use CirrusSearch\Profile\ContextualProfileOverride;
8use CirrusSearch\Profile\SearchProfileService;
9use CirrusSearch\Search\ArrayCirrusSearchResult;
10use CirrusSearch\Search\CirrusSearchIndexFieldFactory;
11use CirrusSearch\Search\CirrusSearchResultSet;
12use CirrusSearch\Search\FancyTitleResultsType;
13use CirrusSearch\Search\SearchMetricsProvider;
14use CirrusSearch\Search\SearchQuery;
15use CirrusSearch\Search\SearchQueryBuilder;
16use CirrusSearch\Search\TitleHelper;
17use CirrusSearch\Search\TitleResultsType;
18use ISearchResultSet;
19use MediaWiki\Context\RequestContext;
20use MediaWiki\MediaWikiServices;
21use MediaWiki\Parser\Sanitizer;
22use MediaWiki\Request\WebRequest;
23use MediaWiki\Status\Status;
24use MediaWiki\Title\Title;
25use MediaWiki\User\User;
26use MediaWiki\WikiMap\WikiMap;
27use SearchEngine;
28use SearchIndexField;
29use 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 */
52class 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}