wikimedia/mediawiki-extensions-Wikibase

View on GitHub
repo/includes/Api/QuerySearchEntities.php

Summary

Maintainability
A
3 hrs
Test Coverage
<?php

declare( strict_types = 1 );

namespace Wikibase\Repo\Api;

use MediaWiki\Api\ApiPageSet;
use MediaWiki\Api\ApiQuery;
use MediaWiki\Api\ApiQueryGeneratorBase;
use MediaWiki\Api\ApiUsageException;
use MediaWiki\Cache\LinkBatchFactory;
use Wikibase\Lib\ContentLanguages;
use Wikibase\Lib\Interactors\TermSearchResult;
use Wikibase\Lib\SettingsArray;
use Wikibase\Lib\Store\EntityTitleLookup;
use Wikimedia\Assert\InvariantException;
use Wikimedia\ParamValidator\ParamValidator;

/**
 * API module to search for Wikibase entities that can be used as a generator.
 *
 * @license GPL-2.0-or-later
 * @author Bene* < benestar.wikimedia@gmail.com >
 */
class QuerySearchEntities extends ApiQueryGeneratorBase {

    private LinkBatchFactory $linkBatchFactory;

    private EntitySearchHelper $entitySearchHelper;

    private EntityTitleLookup $titleLookup;

    private ContentLanguages $termsLanguages;

    /**
     * @var string[] The supported entity types.
     * This should be initialized from {@link WikibaseRepo::getEnabledEntityTypes()},
     * <strong>not</strong> from {@link WikibaseRepo::getEnabledEntityTypesForSearch()} –
     * unlike {@link SearchEntities}, this module does not support additional entity types
     * that are not registered with Wikibase’s entity registration yet
     * (every search result’s {@link TermSearchResult::getEntityId() entity ID}
     * must be non-null so that we can use the {@link EntityTitleLookup}).
     */
    private array $entityTypes;

    /** @var (string|null)[] */
    private array $searchProfiles;

    public function __construct(
        ApiQuery $apiQuery,
        string $moduleName,
        LinkBatchFactory $linkBatchFactory,
        EntitySearchHelper $entitySearchHelper,
        EntityTitleLookup $titleLookup,
        ContentLanguages $termsLanguages,
        array $entityTypes,
        array $searchProfiles
    ) {
        parent::__construct( $apiQuery, $moduleName, 'wbs' );

        $this->linkBatchFactory = $linkBatchFactory;
        $this->entitySearchHelper = $entitySearchHelper;
        $this->titleLookup = $titleLookup;
        $this->termsLanguages = $termsLanguages;
        $this->entityTypes = $entityTypes;
        $this->searchProfiles = $searchProfiles;
    }

    public static function factory(
        ApiQuery $apiQuery,
        string $moduleName,
        LinkBatchFactory $linkBatchFactory,
        array $enabledEntityTypes,
        array $entitySearchHelperCallbacks,
        EntityTitleLookup $entityTitleLookup,
        SettingsArray $repoSettings,
        ContentLanguages $termsLanguages
    ): self {
        return new self(
            $apiQuery,
            $moduleName,
            $linkBatchFactory,
            new TypeDispatchingEntitySearchHelper(
                $entitySearchHelperCallbacks,
                $apiQuery->getRequest()
            ),
            $entityTitleLookup,
            $termsLanguages,
            $enabledEntityTypes,
            $repoSettings->getSetting( 'searchProfiles' )
        );
    }

    /**
     * @inheritDoc
     */
    public function execute(): void {
        $params = $this->extractRequestParams();
        $searchResults = $this->getSearchResults( $params );
        $result = $this->getResult();

        // prefetch page IDs
        $this->linkBatchFactory->newLinkBatch( array_map(
            fn ( TermSearchResult $match ) => $this->titleLookup->getTitleForId( $match->getEntityId() ),
            $searchResults
        ) )->execute();

        foreach ( $searchResults as $match ) {
            $title = $this->titleLookup->getTitleForId( $match->getEntityId() );

            $values = [
                'ns' => $title->getNamespace(),
                'title' => $title->getPrefixedText(),
                'pageid' => intval( $title->getArticleID() ),
                'displaytext' => $match->getMatchedTerm()->getText(),
            ];

            $result->addValue( [ 'query', $this->getModuleName() ], null, $values );
        }

        $result->addIndexedTagName(
            [ 'query', $this->getModuleName() ], $this->getModulePrefix()
        );
    }

    /**
     * @param ApiPageSet $resultPageSet
     */
    public function executeGenerator( $resultPageSet ): void {
        $params = $this->extractRequestParams();
        $searchResults = $this->getSearchResults( $params );
        $titles = [];

        foreach ( $searchResults as $match ) {
            $title = $this->titleLookup->getTitleForId( $match->getEntityId() );
            $titles[] = $title;
            $resultPageSet->setGeneratorData( $title, [ 'displaytext' => $match->getMatchedTerm()->getText() ] );
        }

        $resultPageSet->populateFromTitles( $titles );
    }

    /**
     * @param array $params
     *
     * @return TermSearchResult[]
     * @throws ApiUsageException
     */
    private function getSearchResults( array $params ): array {
        try {
            return $this->entitySearchHelper->getRankedSearchResults(
                $params['search'],
                $params['language'] ?: $this->getLanguage()->getCode(),
                $params['type'],
                $params['limit'],
                $params['strictlanguage'],
                $this->searchProfiles[$params['profile']]
            );
        } catch ( EntitySearchException $ese ) {
            $this->dieStatus( $ese->getStatus() );

            // @phan-suppress-next-line PhanPluginUnreachableCode Wanted
            throw new InvariantException( "dieStatus() must throw an exception" );
        }
    }

    /**
     * @see ApiQueryBase::getCacheMode
     *
     * @param array $params
     * @return string
     */
    public function getCacheMode( $params ): string {
        return 'public';
    }

    /**
     * @inheritDoc
     */
    public function isInternal(): bool {
        return true; // mark this api module as internal until we settled on a solution for search
    }

    /**
     * @inheritDoc
     */
    protected function getAllowedParams(): array {
        return [
            'search' => [
                ParamValidator::PARAM_TYPE => 'string',
                ParamValidator::PARAM_REQUIRED => true,
            ],
            'language' => [
                ParamValidator::PARAM_TYPE => $this->termsLanguages->getLanguages(),
            ],
            'strictlanguage' => [
                ParamValidator::PARAM_TYPE => 'boolean',
                ParamValidator::PARAM_DEFAULT => false,
            ],
            'type' => [
                ParamValidator::PARAM_TYPE => $this->entityTypes,
                ParamValidator::PARAM_DEFAULT => 'item',
            ],
            'limit' => [
                ParamValidator::PARAM_TYPE => 'limit',
                ParamValidator::PARAM_DEFAULT => 7,
                self::PARAM_MAX => self::LIMIT_SML1,
                self::PARAM_MAX2 => self::LIMIT_SML2,
                self::PARAM_MIN => 0,
            ],
            'profile' => [
                ParamValidator::PARAM_TYPE => array_keys( $this->searchProfiles ),
                ParamValidator::PARAM_DEFAULT => array_key_first( $this->searchProfiles ),
                self::PARAM_HELP_MSG_PER_VALUE => [],
            ],
        ];
    }

    /**
     * @inheritDoc
     */
    protected function getExamplesMessages(): array {
        return [
            'action=query&list=wbsearch&wbssearch=abc&wbslanguage=en' => 'apihelp-query+wbsearch-example-1',
            'action=query&list=wbsearch&wbssearch=abc&wbslanguage=en&wbslimit=50' => 'apihelp-query+wbsearch-example-2',
            'action=query&list=wbsearch&wbssearch=alphabet&wbslanguage=en&wbstype=property' => 'apihelp-query+wbsearch-example-3',
            'action=query&generator=wbsearch&gwbssearch=alphabet&gwbslanguage=en' => 'apihelp-query+wbsearch-example-4',
        ];
    }

}