private function buildWarmer($search)
 {
     // This has a couple of compromises:
     $searcher = new Searcher($this->maint->getConnection(), 0, 50, null, array(), null);
     $searcher->setReturnQuery(true);
     $searcher->setResultsType(new FullTextResultsType(FullTextResultsType::HIGHLIGHT_ALL));
     $searcher->limitSearchToLocalWiki(true);
     $query = $searcher->searchText($search, true)->getValue();
     return $query['query'];
 }
 public function execute()
 {
     $context = RequestContext::getMain();
     $user = $context->getUser();
     $conn = $this->getCirrusConnection();
     $searcher = new Searcher($conn, 0, $this->getParameter('limit'), null, null, $user);
     $queryText = $this->getParameter('text');
     if (!$queryText) {
         return;
     }
     $contextString = $this->getParameter('context');
     if ($contextString) {
         $context = @json_decode($contextString, true);
         /*
          * Validate the context, must be in the form of:
          * {
          *   name: { foo: bar, baz: qux }
          *   name2: { foo: bar, baz: qux }
          * }
          *
          */
         if (!is_array($context)) {
             $context = null;
         } else {
             foreach ($context as $name => $ctx) {
                 if (!is_array($ctx)) {
                     $this->dieUsage("Bad context element {$name}", 'cirrus-badcontext');
                 }
             }
         }
     } else {
         $context = null;
     }
     // TODO: add passing context here,
     // see https://www.elastic.co/guide/en/elasticsearch/reference/current/suggester-context.html
     $result = $searcher->suggest($queryText, $context);
     if ($result->isOK()) {
         $this->getResult()->addValue(null, 'suggest', $result->getValue());
     } else {
         $this->getResult()->addValue(null, "error", $result->getErrorsArray());
     }
 }
 /**
  * Constructor
  * @param int[] $namespaces Namespace numbers to search
  * @param User|null $user
  * @param string $index Base name for index to search from, defaults to wfWikiId()
  * @param string $interwiki Interwiki prefix we're searching
  */
 public function __construct(Connection $connection, array $namespaces, User $user = null, $index, $interwiki)
 {
     parent::__construct($connection, 0, self::MAX_RESULTS, null, $namespaces, $user, $index);
     $this->interwiki = $interwiki;
     // Only allow core namespaces. We can't be sure any others exist
     if ($this->namespaces !== null) {
         $this->namespaces = array_filter($namespaces, function ($namespace) {
             return $namespace <= 15;
         });
     }
 }
 public function onView()
 {
     // Disable regular results
     $this->getOutput()->disable();
     $response = $this->getRequest()->response();
     $response->header('Content-type: application/json; charset=UTF-8');
     $config = ConfigFactory::getDefaultInstance()->makeConfig('CirrusSearch');
     $conn = new Connection($config);
     $searcher = new Searcher($conn, 0, 0, null, array(), $this->getUser());
     $id = $this->getTitle()->getArticleID();
     $esSources = $searcher->get(array($id), true);
     if (!$esSources->isOk()) {
         // Exception has been logged
         echo '{}';
         return null;
     }
     $esSources = $esSources->getValue();
     $result = array();
     foreach ($esSources as $esSource) {
         $result[] = array('_index' => $esSource->getIndex(), '_type' => $esSource->getType(), '_id' => $esSource->getId(), '_version' => $esSource->getVersion(), '_source' => $esSource->getData());
     }
     echo json_encode($result);
     return null;
 }
 /**
  * Transform the search request into a Status object representing the
  * search result. Varies based on CLI input argument `type`.
  *
  * @param string $query
  * @return Status<ResultSet>
  */
 protected function searchFor($query)
 {
     $searchType = $this->getOption('type', 'full_text');
     switch ($searchType) {
         case 'full_text':
             // @todo pass through $this->getConnection() ?
             $engine = new CirrusSearch($this->indexBaseName);
             $engine->setConnection($this->getConnection());
             $result = $engine->searchText($query);
             if ($result instanceof Status) {
                 return $result;
             } else {
                 return Status::newGood($result);
             }
         case 'prefix':
             $searcher = new Searcher($this->getConnection(), 0, 10, null, null, null, $this->indexBaseName);
             return $searcher->prefixSearch($query);
         case 'suggest':
             $searcher = new Searcher($this->getConnection(), 0, 10, null, null, null, $this->indexBaseName);
             $result = $searcher->suggest($query);
             if ($result instanceof Status) {
                 return $result;
             } else {
                 return Status::newGood($result);
             }
         default:
             $this->error("\nERROR: Unknown search type {$searchType}\n");
             exit(1);
     }
 }
    /**
     * Search articles with provided term.
     * @param $term string term to search
     * @param boolean $showSuggestion should this search suggest alternative searches that might be better?
     * @return Status(mixed) status containing results defined by resultsType on success
     */
    public function searchText($term, $showSuggestion)
    {
        $checkLengthStatus = self::checkTextSearchRequestLength($term);
        if (!$checkLengthStatus->isOk()) {
            return $checkLengthStatus;
        }
        // Transform Mediawiki specific syntax to filters and extra (pre-escaped) query string
        $searcher = $this;
        $originalTerm = $term;
        $searchContainedSyntax = false;
        $this->term = $term;
        $this->boostLinks = $this->config->get('CirrusSearchBoostLinks');
        $searchType = 'full_text';
        // Handle title prefix notation
        $prefixPos = strpos($this->term, 'prefix:');
        if ($prefixPos !== false) {
            $value = substr($this->term, 7 + $prefixPos);
            $value = trim($value, '"');
            // Trim quotes in case the user wanted to quote the prefix
            if (strlen($value) > 0) {
                $searchContainedSyntax = true;
                $this->term = substr($this->term, 0, max(0, $prefixPos - 1));
                $this->suggestSuffixes[] = ' prefix:' . $value;
                // Suck namespaces out of $value
                $cirrusSearchEngine = new CirrusSearch();
                $cirrusSearchEngine->setConnection($this->connection);
                $value = trim($cirrusSearchEngine->replacePrefixes($value));
                $this->namespaces = $cirrusSearchEngine->namespaces;
                // If the namespace prefix wasn't the entire prefix filter then add a filter for the title
                if (strpos($value, ':') !== strlen($value) - 1) {
                    $value = str_replace('_', ' ', $value);
                    $prefixQuery = new \Elastica\Query\Match();
                    $prefixQuery->setFieldQuery('title.prefix', $value);
                    $this->filters[] = new \Elastica\Filter\Query($prefixQuery);
                }
            }
        }
        $preferRecentDecayPortion = $this->config->get('CirrusSearchPreferRecentDefaultDecayPortion');
        $preferRecentHalfLife = $this->config->get('CirrusSearchPreferRecentDefaultHalfLife');
        $unspecifiedDecayPortion = $this->config->get('CirrusSearchPreferRecentUnspecifiedDecayPortion');
        // Matches "prefer-recent:" and then an optional floating point number <= 1 but >= 0 (decay
        // portion) and then an optional comma followed by another floating point number >= 0 (half life)
        $this->extractSpecialSyntaxFromTerm('/prefer-recent:(1|0?(?:\\.\\d+)?)?(?:,(\\d*\\.?\\d+))? ?/', function ($matches) use($unspecifiedDecayPortion, &$preferRecentDecayPortion, &$preferRecentHalfLife, &$searchContainedSyntax) {
            if (isset($matches[1]) && strlen($matches[1])) {
                $preferRecentDecayPortion = floatval($matches[1]);
            } else {
                $preferRecentDecayPortion = $unspecifiedDecayPortion;
            }
            if (isset($matches[2])) {
                $preferRecentHalfLife = floatval($matches[2]);
            }
            $searchContainedSyntax = true;
            return '';
        });
        $this->preferRecentDecayPortion = $preferRecentDecayPortion;
        $this->preferRecentHalfLife = $preferRecentHalfLife;
        $this->extractSpecialSyntaxFromTerm('/^\\s*local:/', function ($matches) use($searcher) {
            $searcher->limitSearchToLocalWiki(true);
            return '';
        });
        // Handle other filters
        $filters = $this->filters;
        $notFilters = $this->notFilters;
        $boostTemplates = self::getDefaultBoostTemplates();
        $highlightSource = array();
        $this->extractSpecialSyntaxFromTerm('/(?<not>-)?insource:\\/(?<pattern>(?:[^\\\\\\/]|\\\\.)+)\\/(?<insensitive>i)? ?/', function ($matches) use($searcher, &$filters, &$notFilters, &$searchContainedSyntax, &$searchType, &$highlightSource) {
            if (!$searcher->config->get('CirrusSearchEnableRegex')) {
                return;
            }
            $searchContainedSyntax = true;
            $searchType = 'regex';
            $insensitive = !empty($matches['insensitive']);
            $filterDestination =& $filters;
            if (!empty($matches['not'])) {
                $filterDestination =& $notFilters;
            } else {
                $highlightSource[] = array('pattern' => $matches['pattern'], 'locale' => $searcher->config->get('LanguageCode'), 'insensitive' => $insensitive);
            }
            $regex = $searcher->config->getElement('CirrusSearchWikimediaExtraPlugin', 'regex');
            if ($regex && in_array('use', $regex)) {
                $filter = new SourceRegex($matches['pattern'], 'source_text', 'source_text.trigram');
                if (isset($regex['max_inspect'])) {
                    $filter->setMaxInspect($regex['max_inspect']);
                } else {
                    $filter->setMaxInspect(10000);
                }
                $filter->setMaxDeterminizedStates($searcher->config->get('CirrusSearchRegexMaxDeterminizedStates'));
                if (isset($regex['max_ngrams_extracted'])) {
                    $filter->setMaxNgramExtracted($regex['max_ngrams_extracted']);
                }
                $filter->setCaseSensitive(!$insensitive);
                $filter->setLocale($this->config->get('LanguageCode'));
                $filterDestination[] = $filter;
            } else {
                // Without the extra plugin we need to use groovy to attempt the regex.
                // Its less good but its something.
                $script = <<<GROOVY
import org.apache.lucene.util.automaton.*;
sourceText = _source.get("source_text");
if (sourceText == null) {
\tfalse;
} else {
\tif (automaton == null) {
\t\tif (insensitive) {
\t\t\tlocale = new Locale(language);
\t\t\tpattern = pattern.toLowerCase(locale);
\t\t}
\t\tregexp = new RegExp(pattern, RegExp.ALL ^ RegExp.AUTOMATON);
\t\tautomaton = new CharacterRunAutomaton(regexp.toAutomaton());
\t}
\tif (insensitive) {
\t\tsourceText = sourceText.toLowerCase(locale);
\t}
\tautomaton.run(sourceText);
}

GROOVY;
                $filterDestination[] = new \Elastica\Filter\Script(new \Elastica\Script($script, array('pattern' => '.*(' . $matches['pattern'] . ').*', 'insensitive' => $insensitive, 'language' => $searcher->config->get('LanguageCode'), 'automaton' => null, 'locale' => null), 'groovy'));
            }
        });
        // Match filters that look like foobar:thing or foobar:"thing thing"
        // The {7,15} keeps this from having horrible performance on big strings
        $escaper = $this->escaper;
        $fuzzyQuery = $this->fuzzyQuery;
        $isEmptyQuery = false;
        $this->extractSpecialSyntaxFromTerm('/(?<key>[a-z\\-]{7,15}):\\s*(?<value>"(?<quoted>(?:[^"]|(?<=\\\\)")+)"|(?<unquoted>\\S+)) ?/', function ($matches) use($searcher, $escaper, &$filters, &$notFilters, &$boostTemplates, &$searchContainedSyntax, &$fuzzyQuery, &$highlightSource, &$isEmptyQuery) {
            $key = $matches['key'];
            $quotedValue = $matches['value'];
            $value = $matches['quoted'] !== '' ? str_replace('\\"', '"', $matches['quoted']) : $matches['unquoted'];
            $filterDestination =& $filters;
            $keepText = true;
            if ($key[0] === '-') {
                $key = substr($key, 1);
                $filterDestination =& $notFilters;
                $keepText = false;
            }
            switch ($key) {
                case 'boost-templates':
                    $boostTemplates = Searcher::parseBoostTemplates($value);
                    if ($boostTemplates === null) {
                        $boostTemplates = Searcher::getDefaultBoostTemplates();
                    }
                    $searchContainedSyntax = true;
                    return '';
                case 'hastemplate':
                    // We emulate template syntax here as best as possible,
                    // so things in NS_MAIN are prefixed with ":" and things
                    // in NS_TEMPLATE don't have a prefix at all. Since we
                    // don't actually index templates like that, munge the
                    // query here
                    if (strpos($value, ':') === 0) {
                        $value = substr($value, 1);
                    } else {
                        $title = Title::newFromText($value);
                        if ($title && $title->getNamespace() == NS_MAIN) {
                            $value = Title::makeTitle(NS_TEMPLATE, $title->getDBkey())->getPrefixedText();
                        }
                    }
                    $filterDestination[] = $searcher->matchPage('template', $value);
                    $searchContainedSyntax = true;
                    return '';
                case 'linksto':
                    $filterDestination[] = $searcher->matchPage('outgoing_link', $value, true);
                    $searchContainedSyntax = true;
                    return '';
                case 'incategory':
                    $categories = array_slice(explode('|', $value), 0, $searcher->config->get('CirrusSearchMaxIncategoryOptions'));
                    $categoryFilters = $searcher->matchPageCategories($categories);
                    if ($categoryFilters === null) {
                        $isEmptyQuery = true;
                    } else {
                        $filterDestination[] = $categoryFilters;
                    }
                    $searchContainedSyntax = true;
                    return '';
                case 'insource':
                    $updateReferences = Filters::insource($escaper, $searcher->getSearchContext(), $quotedValue);
                    $updateReferences($fuzzyQuery, $filterDestination, $highlightSource, $searchContainedSyntax);
                    return '';
                case 'intitle':
                    $updateReferences = Filters::intitle($escaper, $searcher->getSearchContext(), $quotedValue);
                    $updateReferences($fuzzyQuery, $filterDestination, $highlightSource, $searchContainedSyntax);
                    return $keepText ? "{$quotedValue} " : '';
                default:
                    return $matches[0];
            }
        });
        if ($isEmptyQuery) {
            return Status::newGood(new SearchResultSet(true));
        }
        $this->filters = $filters;
        $this->notFilters = $notFilters;
        $this->boostTemplates = $boostTemplates;
        $this->searchContext->setSearchContainedSyntax($searchContainedSyntax);
        $this->fuzzyQuery = $fuzzyQuery;
        $this->highlightSource = $highlightSource;
        $this->term = $this->escaper->escapeQuotes($this->term);
        $this->term = trim($this->term);
        // Match quoted phrases including those containing escaped quotes
        // Those phrases can optionally be followed by ~ then a number (this is the phrase slop)
        // That can optionally be followed by a ~ (this matches stemmed words in phrases)
        // The following all match: "a", "a boat", "a\"boat", "a boat"~, "a boat"~9, "a boat"~9~, -"a boat", -"a boat"~9~
        $slop = $this->config->get('CirrusSearchPhraseSlop');
        $query = self::replacePartsOfQuery($this->term, '/(?<![\\]])(?<negate>-|!)?(?<main>"((?:[^"]|(?<=\\\\)")+)"(?<slop>~\\d+)?)(?<fuzzy>~)?/', function ($matches) use($searcher, $escaper, $slop) {
            $negate = $matches['negate'][0] ? 'NOT ' : '';
            $main = $escaper->fixupQueryStringPart($matches['main'][0]);
            if (!$negate && !isset($matches['fuzzy']) && !isset($matches['slop']) && preg_match('/^"([^"*]+)[*]"/', $main, $matches)) {
                $phraseMatch = new Elastica\Query\Match();
                $phraseMatch->setFieldQuery("all.plain", $matches[1]);
                $phraseMatch->setFieldType("all.plain", "phrase_prefix");
                $this->nonTextQueries[] = $phraseMatch;
                $phraseHighlightMatch = new Elastica\Query\QueryString();
                $phraseHighlightMatch->setQuery($matches[1] . '*');
                $phraseHighlightMatch->setFields(array('all.plain'));
                $this->nonTextHighlightQueries[] = $phraseHighlightMatch;
                return array();
            }
            if (!isset($matches['fuzzy'])) {
                if (!isset($matches['slop'])) {
                    $main = $main . '~' . $slop['precise'];
                }
                // Got to collect phrases that don't use the all field so we can highlight them.
                // The highlighter locks phrases to the fields that specify them.  It doesn't do
                // that with terms.
                return array('escaped' => $negate . $searcher->switchSearchToExact($main, true), 'nonAll' => $negate . $searcher->switchSearchToExact($main, false));
            }
            return array('escaped' => $negate . $main);
        });
        // Find prefix matches and force them to only match against the plain analyzed fields.  This
        // prevents prefix matches from getting confused by stemming.  Users really don't expect stemming
        // in prefix queries.
        $query = self::replaceAllPartsOfQuery($query, '/\\w+\\*(?:\\w*\\*?)*/u', function ($matches) use($searcher, $escaper) {
            $term = $escaper->fixupQueryStringPart($matches[0][0]);
            return array('escaped' => $searcher->switchSearchToExactForWildcards($term), 'nonAll' => $searcher->switchSearchToExactForWildcards($term));
        });
        $escapedQuery = array();
        $nonAllQuery = array();
        $nearMatchQuery = array();
        foreach ($query as $queryPart) {
            if (isset($queryPart['escaped'])) {
                $escapedQuery[] = $queryPart['escaped'];
                if (isset($queryPart['nonAll'])) {
                    $nonAllQuery[] = $queryPart['nonAll'];
                } else {
                    $nonAllQuery[] = $queryPart['escaped'];
                }
                continue;
            }
            if (isset($queryPart['raw'])) {
                $fixed = $this->escaper->fixupQueryStringPart($queryPart['raw']);
                $escapedQuery[] = $fixed;
                $nonAllQuery[] = $fixed;
                $nearMatchQuery[] = $queryPart['raw'];
                continue;
            }
            LoggerFactory::getInstance('CirrusSearch')->warning('Unknown query part: {queryPart}', array('queryPart' => serialize($queryPart)));
        }
        // Actual text query
        list($queryStringQueryString, $this->fuzzyQuery) = $escaper->fixupWholeQueryString(implode(' ', $escapedQuery));
        // Note that no escaping is required for near_match's match query.
        $nearMatchQuery = implode(' ', $nearMatchQuery);
        if ($queryStringQueryString !== '') {
            if (preg_match('/(?<!\\\\)[?*+~"!|-]|AND|OR|NOT/', $queryStringQueryString)) {
                $this->searchContext->setSearchContainedSyntax(true);
                // We're unlikey to make good suggestions for query string with special syntax in them....
                $showSuggestion = false;
            }
            $fields = array_merge($this->buildFullTextSearchFields(1, '.plain', true), $this->buildFullTextSearchFields($this->config->get('CirrusSearchStemmedWeight'), '', true));
            $nearMatchFields = $this->buildFullTextSearchFields($this->config->get('CirrusSearchNearMatchWeight'), '.near_match', true);
            $this->query = $this->buildSearchTextQuery($fields, $nearMatchFields, $queryStringQueryString, $nearMatchQuery);
            // The highlighter doesn't know about the weightinging from the all fields so we have to send
            // it a query without the all fields.  This swaps one in.
            if ($this->config->getElement('CirrusSearchAllFields', 'use')) {
                $nonAllFields = array_merge($this->buildFullTextSearchFields(1, '.plain', false), $this->buildFullTextSearchFields($this->config->get('CirrusSearchStemmedWeight'), '', false));
                list($nonAllQueryString, ) = $escaper->fixupWholeQueryString(implode(' ', $nonAllQuery));
                $this->highlightQuery = $this->buildSearchTextQueryForFields($nonAllFields, $nonAllQueryString, 1, false, true);
            } else {
                $nonAllFields = $fields;
            }
            // Only do a phrase match rescore if the query doesn't include any quotes and has a space.
            // Queries without spaces are either single term or have a phrase query generated.
            // Queries with the quote already contain a phrase query and we can't build phrase queries
            // out of phrase queries at this point.
            if ($this->config->get('CirrusSearchPhraseRescoreBoost') > 1.0 && $this->config->get('CirrusSearchPhraseRescoreWindowSize') && !$this->searchContext->isSearchContainedSyntax() && strpos($queryStringQueryString, '"') === false && strpos($queryStringQueryString, ' ') !== false) {
                $rescoreFields = $fields;
                if (!$this->config->get('CirrusSearchAllFieldsForRescore')) {
                    $rescoreFields = $nonAllFields;
                }
                $this->rescore[] = array('window_size' => $this->config->get('CirrusSearchPhraseRescoreWindowSize'), 'query' => array('rescore_query' => $this->buildSearchTextQueryForFields($rescoreFields, '"' . $queryStringQueryString . '"', $this->config->getElement('CirrusSearchPhraseSlop', 'boost'), true), 'query_weight' => 1.0, 'rescore_query_weight' => $this->config->get('CirrusSearchPhraseRescoreBoost')));
            }
            $showSuggestion = $showSuggestion && $this->offset == 0;
            if ($showSuggestion) {
                $this->suggest = array('text' => $this->term, 'suggest' => $this->buildSuggestConfig('suggest'));
            }
            $result = $this->search($searchType, $originalTerm);
            if (!$result->isOK() && $this->isParseError($result)) {
                // Elasticsearch has reported a parse error and we've already logged it when we built the status
                // so at this point all we can do is retry the query as a simple query string query.
                $this->query = new \Elastica\Query\Simple(array('simple_query_string' => array('fields' => $fields, 'query' => $queryStringQueryString, 'default_operator' => 'AND')));
                $this->rescore = array();
                // Not worth trying in this state.
                $result = $this->search('degraded_full_text', $originalTerm);
                // If that doesn't work we're out of luck but it should.  There no guarantee it'll work properly
                // with the syntax we've built above but it'll do _something_ and we'll still work on fixing all
                // the parse errors that come in.
            }
        } else {
            $result = $this->search($searchType, $originalTerm);
            // No need to check for a parse error here because we don't actually create a query for
            // Elasticsearch to parse
        }
        return $result;
    }
 /**
  * Let Elasticsearch take a crack at getting near matches once mediawiki has tried all kinds of variants.
  * @param string $term the original search term and all language variants
  * @param null|Title $titleResult resulting match.  A Title if we found something, unchanged otherwise.
  * @return bool return false if we find something, true otherwise so mediawiki can try its default behavior
  */
 public static function onSearchGetNearMatch($term, &$titleResult)
 {
     global $wgContLang;
     $title = Title::newFromText($term);
     if ($title === null) {
         return false;
     }
     $user = RequestContext::getMain()->getUser();
     // Ask for the first 50 results we see.  If there are more than that too bad.
     $searcher = new Searcher(self::getConnection(), 0, 50, null, array($title->getNamespace()), $user);
     if ($title->getNamespace() === NS_MAIN) {
         $searcher->updateNamespacesFromQuery($term);
     } else {
         $term = $title->getText();
     }
     $searcher->setResultsType(new FancyTitleResultsType('near_match'));
     try {
         $status = $searcher->nearMatchTitleSearch($term);
     } catch (UsageException $e) {
         if (defined('MW_API')) {
             throw $e;
         }
         return true;
     }
     // There is no way to send errors or warnings back to the caller here so we have to make do with
     // only sending results back if there are results and relying on the logging done at the status
     // constrution site to log errors.
     if (!$status->isOK()) {
         return true;
     }
     $picker = new NearMatchPicker($wgContLang, $term, $status->getValue());
     $best = $picker->pickBest();
     if ($best) {
         $titleResult = $best;
         return false;
     }
     // Didn't find a result so let Mediawiki have a crack at it.
     return true;
 }
 /**
  * @param integer $maxDocs the number of docs in the index
  * @param array of key values, key is the template name, value the boost factor.
  *        Defaults to Searcher::getDefaultBoostTemplates()
  */
 public function __construct($maxDocs, $boostTemplates = null)
 {
     $this->maxDocs = $maxDocs;
     $this->boostTemplates = $boostTemplates ?: Searcher::getDefaultBoostTemplates();
     // We normalize incoming links according to the size of the index
     $this->incomingLinksNorm = (int) ($maxDocs * self::INCOMING_LINKS_MAX_DOCS_FACTOR);
     if ($this->incomingLinksNorm < 1) {
         // it's a very small wiki let's force the norm to 1
         $this->incomingLinksNorm = 1;
     }
 }
 private function searchTextReal($term, SearchConfig $config = null)
 {
     global $wgCirrusSearchInterwikiSources;
     // Convert the unicode character 'idiographic whitespace' into standard
     // whitespace.  Cirrussearch treats them both as normal whitespace, but
     // the preceding isn't appropriatly trimmed.
     $term = trim(str_replace(" ", " ", $term));
     // No searching for nothing!  That takes forever!
     if (!$term) {
         return null;
     }
     $context = RequestContext::getMain();
     $request = $context->getRequest();
     $user = $context->getUser();
     if ($config) {
         $this->indexBaseName = $config->getWikiId();
     }
     $searcher = new Searcher($this->connection, $this->offset, $this->limit, $config, $this->namespaces, $user, $this->indexBaseName);
     // Ignore leading ~ because it is used to force displaying search results but not to effect them
     if (substr($term, 0, 1) === '~') {
         $term = substr($term, 1);
         $searcher->addSuggestPrefix('~');
     }
     // TODO remove this when we no longer have to support core versions without
     // Ie946150c6796139201221dfa6f7750c210e97166
     if (method_exists($this, 'getSort')) {
         $searcher->setSort($this->getSort());
     }
     $dumpQuery = $request && $request->getVal('cirrusDumpQuery') !== null;
     $searcher->setReturnQuery($dumpQuery);
     $dumpResult = $request && $request->getVal('cirrusDumpResult') !== null;
     $searcher->setDumpResult($dumpResult);
     $returnExplain = $request && $request->getVal('cirrusExplain') !== null;
     $searcher->setReturnExplain($returnExplain);
     // Delegate to either searchText or moreLikeThisArticle and dump the result into $status
     if (substr($term, 0, strlen(self::MORE_LIKE_THIS_PREFIX)) === self::MORE_LIKE_THIS_PREFIX) {
         $term = substr($term, strlen(self::MORE_LIKE_THIS_PREFIX));
         $status = $this->moreLikeThis($term, $searcher, Searcher::MORE_LIKE_THESE_NONE);
     } else {
         if (substr($term, 0, strlen(self::MORE_LIKE_THIS_JUST_WIKIBASE_PREFIX)) === self::MORE_LIKE_THIS_JUST_WIKIBASE_PREFIX) {
             $term = substr($term, strlen(self::MORE_LIKE_THIS_JUST_WIKIBASE_PREFIX));
             $status = $this->moreLikeThis($term, $searcher, Searcher::MORE_LIKE_THESE_ONLY_WIKIBASE);
         } else {
             # Namespace lookup should not be done for morelike special syntax (T111244)
             if ($this->lastNamespacePrefix) {
                 $searcher->addSuggestPrefix($this->lastNamespacePrefix);
             } else {
                 $searcher->updateNamespacesFromQuery($term);
             }
             $highlightingConfig = FullTextResultsType::HIGHLIGHT_ALL;
             if ($request) {
                 if ($request->getVal('cirrusSuppressSuggest') !== null) {
                     $this->showSuggestion = false;
                 }
                 if ($request->getVal('cirrusSuppressTitleHighlight') !== null) {
                     $highlightingConfig ^= FullTextResultsType::HIGHLIGHT_TITLE;
                 }
                 if ($request->getVal('cirrusSuppressAltTitle') !== null) {
                     $highlightingConfig ^= FullTextResultsType::HIGHLIGHT_ALT_TITLE;
                 }
                 if ($request->getVal('cirrusSuppressSnippet') !== null) {
                     $highlightingConfig ^= FullTextResultsType::HIGHLIGHT_SNIPPET;
                 }
                 if ($request->getVal('cirrusHighlightDefaultSimilarity') === 'no') {
                     $highlightingConfig ^= FullTextResultsType::HIGHLIGHT_WITH_DEFAULT_SIMILARITY;
                 }
                 if ($request->getVal('cirrusHighlightAltTitleWithPostings') === 'no') {
                     $highlightingConfig ^= FullTextResultsType::HIGHLIGHT_ALT_TITLES_WITH_POSTINGS;
                 }
             }
             if ($this->namespaces && !in_array(NS_FILE, $this->namespaces)) {
                 $highlightingConfig ^= FullTextResultsType::HIGHLIGHT_FILE_TEXT;
             }
             $searcher->setResultsType(new FullTextResultsType($highlightingConfig, $config ? $config->getWikiCode() : ''));
             $status = $searcher->searchText($term, $this->showSuggestion);
         }
     }
     if ($dumpQuery || $dumpResult) {
         // When dumping the query we skip _everything_ but echoing the query.
         $context->getOutput()->disable();
         $request->response()->header('Content-type: application/json; charset=UTF-8');
         if ($status->getValue() === null) {
             echo '{}';
         } else {
             echo json_encode($status->getValue());
         }
         exit;
     }
     $this->lastSearchMetrics = $searcher->getSearchMetrics();
     // Add interwiki results, if we have a sane result
     // Note that we have no way of sending warning back to the user.  In this case all warnings
     // are logged when they are added to the status object so we just ignore them here....
     if ($status->isOK() && $wgCirrusSearchInterwikiSources && $status->getValue() && method_exists($status->getValue(), 'addInterwikiResults')) {
         // @todo @fixme: This should absolutely be a multisearch. I knew this when I
         // wrote the code but Searcher needs some refactoring first.
         foreach ($wgCirrusSearchInterwikiSources as $interwiki => $index) {
             $iwSearch = new InterwikiSearcher($this->connection, $this->namespaces, $user, $index, $interwiki);
             $interwikiResult = $iwSearch->getInterwikiResults($term);
             if ($interwikiResult) {
                 $status->getValue()->addInterwikiResults($interwikiResult);
             }
         }
     }
     // For historical reasons all callers of searchText interpret any Status return as an error
     // so we must unwrap all OK statuses.  Note that $status can be "good" and still contain null
     // since that is interpreted as no results.
     return $status->isOk() ? $status->getValue() : $status;
 }