wikimedia/mediawiki-extensions-CirrusSearch

View on GitHub
includes/SearchConfig.php

Summary

Maintainability
A
0 mins
Test Coverage
<?php

namespace CirrusSearch;

use CirrusSearch\Profile\SearchProfileService;
use CirrusSearch\Profile\SearchProfileServiceFactory;
use CirrusSearch\Profile\SearchProfileServiceFactoryFactory;
use LogicException;
use MediaWiki\Config\Config;
use MediaWiki\Config\GlobalVarConfig;
use MediaWiki\Context\RequestContext;
use MediaWiki\MainConfigNames;
use MediaWiki\MediaWikiServices;
use MediaWiki\WikiMap\WikiMap;
use Wikimedia\Assert\Assert;

/**
 * Configuration class encapsulating Searcher environment.
 * This config class can import settings from the environment globals,
 * or from specific wiki configuration.
 */
class SearchConfig implements Config {
    // Constants for referring to various config values. Helps prevent fat-fingers
    public const INDEX_BASE_NAME = 'CirrusSearchIndexBaseName';
    private const PREFIX_IDS = 'CirrusSearchPrefixIds';
    private const CIRRUS_VAR_PREFIX = 'wgCirrus';

    // Magic word to tell the SearchConfig to translate INDEX_BASE_NAME into WikiMap::getCurrentWikiId()
    public const WIKI_ID_MAGIC_WORD = '__wikiid__';

    /** Non cirrus vars to load when loading external wiki config */
    private const NON_CIRRUS_VARS = [
        MainConfigNames::LanguageCode,
        MainConfigNames::ContentNamespaces,
        MainConfigNames::NamespacesToBeSearchedDefault,
    ];

    /**
     * @var SearchConfig Configuration of host wiki.
     */
    private $hostConfig;

    /**
     * Override settings
     * @var Config
     */
    private $source;

    /**
     * Wiki id or null for current wiki
     * @var string|null
     */
    private $wikiId;

    /**
     * @var Assignment\ClusterAssignment|null
     */
    private $clusters;

    /**
     * @var SearchProfileService|null
     */
    private $profileService;

    /**
     * @var SearchProfileServiceFactoryFactory|null (lazy loaded)
     */
    private $searchProfileServiceFactoryFactory;

    /**
     * Create new search config for the current wiki.
     * @param SearchProfileServiceFactoryFactory|null $searchProfileServiceFactoryFactory
     */
    public function __construct( SearchProfileServiceFactoryFactory $searchProfileServiceFactoryFactory = null ) {
        $this->source = new GlobalVarConfig();
        $this->wikiId = WikiMap::getCurrentWikiId();
        // The only ability to mutate SearchConfig is via a protected method, setSource.
        // As long as we have an instance of SearchConfig it must then be the hostConfig.
        $this->hostConfig = static::class === self::class ? $this : new SearchConfig();
        $this->searchProfileServiceFactoryFactory = $searchProfileServiceFactoryFactory;
    }

    /**
     * This must be delayed until after construction is complete. Before then
     * subclasses could change out the configuration we see.
     *
     * @return Assignment\ClusterAssignment
     */
    private function createClusterAssignment(): Assignment\ClusterAssignment {
        // Configuring CirrusSearchServers enables "easy mode" which assumes
        // everything happens inside a single elasticsearch cluster.
        if ( $this->has( 'CirrusSearchServers' ) ) {
            return new Assignment\ConstantAssignment(
                $this->get( 'CirrusSearchServers' ) );
        } else {
            return new Assignment\MultiClusterAssignment( $this );
        }
    }

    public function getClusterAssignment(): Assignment\ClusterAssignment {
        if ( $this->clusters === null ) {
            $this->clusters = $this->createClusterAssignment();
        }
        return $this->clusters;
    }

    /**
     * Reset any cached state so testing can ensures changes to global state
     * are reflected here. Only public for use from phpunit.
     */
    public function clearCachesForTesting() {
        $this->profileService = null;
        $this->clusters = null;
    }

    /**
     * @return bool true if this config was built for this wiki.
     */
    public function isLocalWiki() {
        // FIXME: this test is somewhat obscure (very indirect to say the least)
        // problem is that testing $this->wikiId === WikiMap::getCurrentWikiId()
        // would not work properly during unit tests.
        return $this->source instanceof GlobalVarConfig;
    }

    /**
     * @return SearchConfig Configuration of the host wiki.
     */
    public function getHostWikiConfig(): SearchConfig {
        return $this->hostConfig;
    }

    /**
     * @param string $name
     * @return bool
     */
    public function has( $name ) {
        return $this->source->has( $name );
    }

    /**
     * @param string $name
     * @return mixed
     */
    public function get( $name ) {
        if ( !$this->source->has( $name ) ) {
            return null;
        }
        $value = $this->source->get( $name );
        if ( $name === self::INDEX_BASE_NAME && $value === self::WIKI_ID_MAGIC_WORD ) {
            return $this->getWikiId();
        }
        return $value;
    }

    /**
     * Produce new configuration from globals
     * @return SearchConfig
     */
    public static function newFromGlobals() {
        return new self();
    }

    /**
     * Return configured Wiki ID
     * @return string
     */
    public function getWikiId() {
        return $this->wikiId;
    }

    /**
     * @todo
     * The indices have to be rebuilt with new id's and we have to know when
     * generating queries if new style id's are being used, or old style. It
     * could plausibly be done with the metastore index, but that seems like
     * overkill because the knowledge is only necessary during transition, and
     * not post-transition.  Additionally this function would then need to know
     * the name of the index being queried, and that isn't always known when
     * building.
     *
     * @param string|int $pageId
     * @return string
     */
    public function makeId( $pageId ) {
        Assert::parameter( is_int( $pageId ) || ( is_string( $pageId ) && ctype_digit( $pageId ) ),
            '$pageId', "should be an integer or a string with digits, got [$pageId]." );
        $prefix = $this->get( self::PREFIX_IDS )
            ? $this->getWikiId()
            : null;

        if ( $prefix === null ) {
            return (string)$pageId;
        } else {
            return "{$prefix}|{$pageId}";
        }
    }

    /**
     * Convert an elasticsearch document id back into a mediawiki page id.
     *
     * @param string $docId Elasticsearch document id
     * @return int Related mediawiki page id
     * @throws \Exception
     */
    public function makePageId( $docId ) {
        if ( !$this->get( self::PREFIX_IDS ) ) {
            return (int)$docId;
        }

        $pieces = explode( '|', $docId );
        switch ( count( $pieces ) ) {
            case 2:
                return (int)$pieces[1];
            case 1:
                // Broken doc id...assume somehow this didn't get prefixed.
                // Attempt to continue on...but maybe should throw exception
                // instead?
                return (int)$docId;
            default:
                throw new LogicException( "Invalid document id: $docId" );
        }
    }

    /**
     * Get user's language
     * @return string User's language code
     */
    public function getUserLanguage() {
        // I suppose using $wgLang would've been more evil than this, but
        // only marginally so. Find some real context to use here.
        return RequestContext::getMain()->getLanguage()->getCode();
    }

    /**
     * Get chain of elements from config array
     * @param string $configName
     * @param string ...$path list of path elements
     * @return mixed Returns value or null if not present
     */
    public function getElement( $configName, ...$path ) {
        if ( !$this->has( $configName ) ) {
            return null;
        }
        $data = $this->get( $configName );
        foreach ( $path as $el ) {
            if ( !isset( $data[$el] ) ) {
                return null;
            }
            $data = $data[$el];
        }
        return $data;
    }

    /**
     * For Unit tests
     * @param Config $source Config override source
     */
    protected function setSource( Config $source ) {
        $this->source = $source;
        $this->clusters = null;
    }

    /**
     * for unit tests purpose only
     * @return string[] list of "non-cirrus" var names
     */
    public static function getNonCirrusConfigVarNames() {
        return self::NON_CIRRUS_VARS;
    }

    /**
     * @return bool if cross project (same language) is enabled
     */
    public function isCrossProjectSearchEnabled() {
        if ( $this->get( 'CirrusSearchEnableCrossProjectSearch' ) ) {
            return true;
        }
        return false;
    }

    /**
     * @return bool if cross language (same project) is enabled
     */
    public function isCrossLanguageSearchEnabled() {
        if ( $this->get( 'CirrusSearchEnableAltLanguage' ) ) {
            return true;
        }
        return false;
    }

    /**
     * Load the SearchProfileService suited for this SearchConfig.
     * The service is initialized thanks to SearchProfileServiceFactory
     * that will load CirrusSearch profiles and additional extension profiles
     *
     * <b>NOTE:</b> extension profiles are not loaded if this config is built
     * for a sister wiki.
     *
     * @return SearchProfileService
     * @see SearchProfileService
     * @see SearchProfileServiceFactory
     */
    public function getProfileService() {
        if ( $this->profileService === null ) {
            if ( $this->searchProfileServiceFactoryFactory === null ) {

                /** @var SearchProfileServiceFactory $factory */
                $factory = MediaWikiServices::getInstance()
                    ->getService( SearchProfileServiceFactory::SERVICE_NAME );
            } else {
                $factory = $this->searchProfileServiceFactoryFactory->getFactory( $this );
            }
            $this->profileService = $factory->loadService( $this );
        }
        return $this->profileService;
    }

    /**
     * @return bool true if the completion suggester is enabled
     */
    public function isCompletionSuggesterEnabled() {
        $useCompletion = $this->getElement( 'CirrusSearchUseCompletionSuggester' );
        if ( is_string( $useCompletion ) ) {
            return wfStringToBool( $useCompletion );
        }
        return $useCompletion === true;
    }
}