Code Coverage
 
Classes and Traits
Functions and Methods
Lines
Total
0.00% covered (danger)
0.00%
0 / 1
91.30% covered (success)
91.30%
21 / 23
CRAP
97.04% covered (success)
97.04%
131 / 135
SearchQueryBuilder
0.00% covered (danger)
0.00%
0 / 1
91.30% covered (success)
91.30%
21 / 23
29
97.04% covered (success)
97.04%
131 / 135
 newFTSearchQueryBuilder
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
19 / 19
 forCrossProjectSearch
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
7 / 7
 copyQueryForCrossSearch
100.00% covered (success)
100.00%
1 / 1
4
100.00% covered (success)
100.00%
26 / 26
 forCrossLanguageSearch
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
7 / 7
 forRewrittenQuery
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
19 / 19
 build
100.00% covered (success)
100.00%
1 / 1
3
100.00% covered (success)
100.00%
21 / 21
 addContextualFilter
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
4 / 4
 setInitialNamespaces
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
2 / 2
 setCrossProjectSearch
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
2 / 2
 setCrossLanguageSearch
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
2 / 2
 setSearchEngineEntryPoint
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 setSort
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
2 / 2
 setOffset
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
2 / 2
 setLimit
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
2 / 2
 setRandomSeed
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
2 / 2
 setDebugOptions
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
2 / 2
 setWithDYMSuggestion
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
2 / 2
 setExtraIndicesSearch
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
2 / 2
 addForcedProfile
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
2 / 2
 setAllowRewrite
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
2 / 2
 addProfileContextParameter
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
2 / 2
 setExtraFieldsToExtract
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
2 / 2
 setProvideAllSnippets
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
<?php
namespace CirrusSearch\Search;
use CirrusSearch\CirrusDebugOptions;
use CirrusSearch\CirrusSearchHookRunner;
use CirrusSearch\CrossSearchStrategy;
use CirrusSearch\HashSearchConfig;
use CirrusSearch\Parser\AST\ParsedQuery;
use CirrusSearch\Parser\NamespacePrefixParser;
use CirrusSearch\Parser\QueryParserFactory;
use CirrusSearch\Query\Builder\ContextualFilter;
use CirrusSearch\SearchConfig;
use Wikimedia\Assert\Assert;
/**
 * Builder for SearchQuery
 */
final class SearchQueryBuilder {
    /**
     * @var ParsedQuery
     */
    private $parsedQuery;
    /**
     * @var int[]|null
     */
    private $initialNamespaces;
    /**
     * @var bool
     */
    private $crossProjectSearch = false;
    /**
     * @var bool
     */
    private $crossLanguageSearch = false;
    /**
     * @var bool
     */
    private $extraIndicesSearch = false;
    /**
     * @var ContextualFilter[]
     */
    private $contextualFilters = [];
    /**
     * @var string
     */
    private $searchEngineEntryPoint;
    /**
     * @var string
     */
    private $sort;
    /**
     * @var int|null
     */
    private $randomSeed;
    /**
     * @var string[]
     */
    private $forcedProfiles = [];
    /**
     * @var int
     */
    private $offset;
    /**
     * @var int
     */
    private $limit;
    /**
     * @var CirrusDebugOptions
     */
    private $debugOptions;
    /**
     * @var SearchConfig
     */
    private $searchConfig;
    /**
     * @var bool
     */
    private $withDYMSuggestion = false;
    /**
     * @var bool
     */
    private $allowRewrite = false;
    /**
     * @var string[] parameters for the SearchProfileService
     * @see \CirrusSearch\Profile\ContextualProfileOverride
     */
    private $profileContextParameters = [];
    /**
     * @var string[] list of extra fields to extract
     */
    private $extraFieldsToExtract = [];
    /**
     * @var bool When false the engine will decide the best fields to highlight, when
     *  true all fields will be highlighted (with increased computational cost).
     */
    private $provideAllSnippets = false;
    /**
     * Construct a new FT (FullText) SearchQueryBuilder using the config
     * and query string provided.
     *
     * NOTE: this method will parse the query string and set all builder attributes
     * to Fulltext search defaults.
     *
     * @param SearchConfig $config
     * @param string $queryString
     * @param NamespacePrefixParser $namespacePrefixParser
     * @param CirrusSearchHookRunner $cirrusSearchHookRunner
     * @return SearchQueryBuilder
     * @throws \CirrusSearch\Parser\ParsedQueryClassifierException
     * @throws \CirrusSearch\Parser\QueryStringRegex\SearchQueryParseException
     */
    public static function newFTSearchQueryBuilder(
        SearchConfig $config,
        $queryString,
        NamespacePrefixParser $namespacePrefixParser,
        CirrusSearchHookRunner $cirrusSearchHookRunner
    ): SearchQueryBuilder {
        $builder = new self();
        $builder->parsedQuery = QueryParserFactory::newFullTextQueryParser( $config,
            $namespacePrefixParser, $cirrusSearchHookRunner )->parse( $queryString );
        $builder->initialNamespaces = [ NS_MAIN ];
        $builder->sort = \SearchEngine::DEFAULT_SORT;
        $builder->randomSeed = null;
        $builder->debugOptions = CirrusDebugOptions::defaultOptions();
        $builder->limit = 10;
        $builder->offset = 0;
        $builder->searchConfig = $config;
        $builder->forcedProfiles = [];
        $builder->searchEngineEntryPoint = SearchQuery::SEARCH_TEXT;
        $builder->crossProjectSearch = true;
        $builder->crossLanguageSearch = true;
        $builder->extraIndicesSearch = true;
        $builder->withDYMSuggestion = true;
        $builder->allowRewrite = false;
        $builder->provideAllSnippets = false;
        return $builder;
    }
    /**
     * Recreate a SearchQueryBuilder using an existing query and the target wiki SearchConfig.
     *
     * @param SearchConfig $config
     * @param SearchQuery $query
     * @return SearchQueryBuilder
     */
    public static function forCrossProjectSearch( SearchConfig $config, SearchQuery $query ): SearchQueryBuilder {
        Assert::parameter( !$config->isLocalWiki(), '$config', 'must not be the local wiki config' );
        Assert::precondition( $query->getCrossSearchStrategy()->isCrossProjectSearchSupported(),
            'Trying to build a query for a cross-project search but the original query does not ' .
            'support such searches.' );
        $builder = self::copyQueryForCrossSearch( $config, $query );
        $builder->offset = 0;
        $builder->limit = $query->getSearchConfig()->get( 'CirrusSearchNumCrossProjectSearchResults' );
        return $builder;
    }
    /**
     * @param SearchConfig $config
     * @param SearchQuery $original
     * @return SearchQueryBuilder
     */
    private static function copyQueryForCrossSearch( SearchConfig $config, SearchQuery $original ): SearchQueryBuilder {
        Assert::precondition( $original->getContextualFilters() === [], 'The initial must not have contextual filters' );
        $builder = new self();
        $builder->parsedQuery = $original->getParsedQuery();
        $builder->searchEngineEntryPoint = $original->getSearchEngineEntryPoint();
        // Only allow core namespaces. We can't be sure any others exist
        // TODO: possibly move this later and try to detect if we run the default
        // profile, so that we could try to run the default profile on sister wikis
        $namespaces = $original->getInitialNamespaces();
        if ( $namespaces !== null ) {
            $namespaces = array_filter( $namespaces, static function ( $namespace ) {
                return $namespace <= NS_CATEGORY_TALK;
            } );
        }
        $builder->initialNamespaces = $namespaces;
        $builder->sort = $original->getSort();
        $builder->randomSeed = $original->getRandomSeed();
        $builder->debugOptions = $original->getDebugOptions();
        $builder->searchConfig = $config;
        $builder->profileContextParameters = $original->getProfileContextParameters();
        $forcedProfiles = [];
        // Copy forced profiles only if they exist on the target wiki.
        foreach ( $original->getForcedProfiles() as $type => $name ) {
            if ( $config->getProfileService()->hasProfile( $type, $name ) ) {
                $forcedProfiles[$type] = $name;
            }
        }
        // we do not copy extraFieldsToExtract as we have no way to know if they are available on a
        // target wiki
        $builder->extraFieldsToExtract = [];
        $builder->forcedProfiles = $forcedProfiles;
        // We force to false, during cross project/lang searches
        // and we explicitely disable DYM suggestions
        $builder->crossProjectSearch = false;
        $builder->crossLanguageSearch = false;
        $builder->extraIndicesSearch = false;
        $builder->withDYMSuggestion = false;
        $builder->allowRewrite = false;
        return $builder;
    }
    /**
     * @param SearchConfig $config
     * @param SearchQuery $original
     * @return SearchQueryBuilder
     */
    public static function forCrossLanguageSearch( SearchConfig $config, SearchQuery $original ) {
        Assert::parameter( !$config->isLocalWiki(), '$config', 'must not be the local wiki config' );
        Assert::precondition( $original->getCrossSearchStrategy()->isCrossLanguageSearchSupported(),
            'Trying to build a query for a cross-language search but the original query does not ' .
            'support such searches.' );
        $builder = self::copyQueryForCrossSearch( $config, $original );
        $builder->offset = $original->getOffset();
        $builder->limit = $original->getLimit();
        return $builder;
    }
    /**
     * @param SearchQuery $original
     * @param string $term
     * @param NamespacePrefixParser $namespacePrefixParser
     * @param CirrusSearchHookRunner $cirrusSearchHookRunner
     * @return SearchQueryBuilder
     * @throws \CirrusSearch\Parser\QueryStringRegex\SearchQueryParseException
     * @throws \MWException
     */
    public static function forRewrittenQuery(
        SearchQuery $original,
        $term,
        NamespacePrefixParser $namespacePrefixParser,
        CirrusSearchHookRunner $cirrusSearchHookRunner
    ): SearchQueryBuilder {
        Assert::precondition( $original->isAllowRewrite(), 'The original query must allow rewrites' );
        // Hack to prevent a second pass on this cleaning algo because its destructive
        $config = new HashSearchConfig( [ 'CirrusSearchStripQuestionMarks' => 'no' ],
            [ HashSearchConfig::FLAG_INHERIT ], $original->getSearchConfig() );
        $builder = self::newFTSearchQueryBuilder( $config, $term, $namespacePrefixParser, $cirrusSearchHookRunner );
        $builder->contextualFilters = $original->getContextualFilters();
        $builder->forcedProfiles = $original->getForcedProfiles();
        $builder->initialNamespaces = $original->getInitialNamespaces();
        $builder->sort = $original->getSort();
        $builder->randomSeed = $original->getRandomSeed();
        $builder->debugOptions = $original->getDebugOptions();
        $builder->limit = $original->getLimit();
        $builder->offset = $original->getOffset();
        $builder->crossProjectSearch = false;
        $builder->crossLanguageSearch = false;
        $builder->extraIndicesSearch = $original->getInitialCrossSearchStrategy()->isExtraIndicesSearchSupported();
        $builder->withDYMSuggestion = false;
        $builder->allowRewrite = false;
        $builder->extraFieldsToExtract = $original->getExtraFieldsToExtract();
        return $builder;
    }
    /**
     * @return SearchQuery
     */
    public function build(): SearchQuery {
        return new SearchQuery(
            $this->parsedQuery,
            $this->initialNamespaces,
            new CrossSearchStrategy(
                $this->crossProjectSearch && $this->searchConfig->isCrossProjectSearchEnabled(),
                $this->crossLanguageSearch && $this->searchConfig->isCrossLanguageSearchEnabled(),
                $this->extraIndicesSearch
            ),
            $this->contextualFilters,
            $this->searchEngineEntryPoint,
            $this->sort,
            $this->randomSeed,
            $this->forcedProfiles,
            $this->offset,
            $this->limit,
            $this->debugOptions ?? CirrusDebugOptions::defaultOptions(),
            $this->searchConfig,
            $this->withDYMSuggestion,
            $this->allowRewrite,
            $this->profileContextParameters,
            $this->extraFieldsToExtract,
            $this->provideAllSnippets
        );
    }
    /**
     * @param string $name
     * @param ContextualFilter $filter
     * @return SearchQueryBuilder
     */
    public function addContextualFilter( $name, ContextualFilter $filter ): SearchQueryBuilder {
        Assert::parameter( !array_key_exists( $name, $this->contextualFilters ),
            '$name', "context filter $name already set" );
        $this->contextualFilters[$name] = $filter;
        return $this;
    }
    /**
     * @param int[] $initialNamespaces
     * @return SearchQueryBuilder
     */
    public function setInitialNamespaces( array $initialNamespaces ): SearchQueryBuilder {
        $this->initialNamespaces = $initialNamespaces;
        return $this;
    }
    /**
     * @param bool $crossProjectSearch
     * @return SearchQueryBuilder
     */
    public function setCrossProjectSearch( $crossProjectSearch ): SearchQueryBuilder {
        $this->crossProjectSearch = $crossProjectSearch;
        return $this;
    }
    /**
     * @param bool $crossLanguageSearch
     * @return SearchQueryBuilder
     */
    public function setCrossLanguageSearch( $crossLanguageSearch ): SearchQueryBuilder {
        $this->crossLanguageSearch = $crossLanguageSearch;
        return $this;
    }
    /**
     * @param string $searchEngineEntryPoint
     * @return SearchQueryBuilder
     */
    public function setSearchEngineEntryPoint( $searchEngineEntryPoint ): SearchQueryBuilder {
        $this->searchEngineEntryPoint = $searchEngineEntryPoint;
        return $this;
    }
    /**
     * @param string $sort
     * @return SearchQueryBuilder
     */
    public function setSort( $sort ): SearchQueryBuilder {
        $this->sort = $sort;
        return $this;
    }
    /**
     * @param int $offset
     * @return SearchQueryBuilder
     */
    public function setOffset( $offset ): SearchQueryBuilder {
        $this->offset = $offset;
        return $this;
    }
    /**
     * @param int $limit
     * @return SearchQueryBuilder
     */
    public function setLimit( $limit ): SearchQueryBuilder {
        $this->limit = $limit;
        return $this;
    }
    /**
     * @param int|null $randomSeed
     * @return $this
     */
    public function setRandomSeed( ?int $randomSeed ): SearchQueryBuilder {
        $this->randomSeed = $randomSeed;
        return $this;
    }
    /**
     * @param CirrusDebugOptions $debugOptions
     * @return SearchQueryBuilder
     */
    public function setDebugOptions( CirrusDebugOptions $debugOptions ): SearchQueryBuilder {
        $this->debugOptions = $debugOptions;
        return $this;
    }
    /**
     * @param bool $withDYMSuggestion
     * @return SearchQueryBuilder
     */
    public function setWithDYMSuggestion( $withDYMSuggestion ): SearchQueryBuilder {
        $this->withDYMSuggestion = $withDYMSuggestion;
        return $this;
    }
    /**
     * @param bool $extraIndicesSearch
     * @return SearchQueryBuilder
     */
    public function setExtraIndicesSearch( $extraIndicesSearch ): SearchQueryBuilder {
        $this->extraIndicesSearch = $extraIndicesSearch;
        return $this;
    }
    /**
     * @param string $type
     * @param string $forcedProfile
     * @return SearchQueryBuilder
     */
    public function addForcedProfile( $type, $forcedProfile ): SearchQueryBuilder {
        $this->forcedProfiles[$type] = $forcedProfile;
        return $this;
    }
    /**
     * @param bool $allowRewrite
     * @return SearchQueryBuilder
     */
    public function setAllowRewrite( $allowRewrite ): SearchQueryBuilder {
        $this->allowRewrite = $allowRewrite;
        return $this;
    }
    /**
     * @param string $key
     * @param string $value
     * @return SearchQueryBuilder
     * @see \CirrusSearch\Profile\ContextualProfileOverride
     */
    public function addProfileContextParameter( $key, $value ): SearchQueryBuilder {
        $this->profileContextParameters[$key] = $value;
        return $this;
    }
    /**
     * @param string[] $fields
     * @return SearchQueryBuilder
     */
    public function setExtraFieldsToExtract( array $fields ): SearchQueryBuilder {
        $this->extraFieldsToExtract = $fields;
        return $this;
    }
    public function setProvideAllSnippets( bool $shouldProvide ): SearchQueryBuilder {
        $this->provideAllSnippets = $shouldProvide;
        return $this;
    }
}