Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
91.49% covered (success)
91.49%
43 / 47
33.33% covered (danger)
33.33%
1 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
EntitySearchElastic
91.49% covered (success)
91.49%
43 / 47
33.33% covered (danger)
33.33%
1 / 3
9.05
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 getElasticSearchQuery
85.71% covered (warning)
85.71%
18 / 21
0.00% covered (danger)
0.00%
0 / 1
2.01
 getRankedSearchResults
95.00% covered (success)
95.00%
19 / 20
0.00% covered (danger)
0.00%
0 / 1
4
1<?php
2
3namespace Wikibase\Search\Elastic;
4
5use CirrusSearch\CirrusDebugOptions;
6use CirrusSearch\Search\SearchContext;
7use Elastica\Query\AbstractQuery;
8use Elastica\Query\MatchNone;
9use MediaWiki\Language\Language;
10use MediaWiki\Request\FauxRequest;
11use MediaWiki\Request\WebRequest;
12use Wikibase\DataModel\Entity\EntityIdParser;
13use Wikibase\Lib\LanguageFallbackChainFactory;
14use Wikibase\Repo\Api\EntitySearchException;
15use Wikibase\Repo\Api\EntitySearchHelper;
16use Wikibase\Search\Elastic\Query\LabelsCompletionQuery;
17
18/**
19 * Entity search implementation using ElasticSearch.
20 * Requires CirrusSearch extension and $wgEntitySearchUseCirrus to be on.
21 *
22 * @license GPL-2.0-or-later
23 * @author Stas Malyshev
24 */
25class EntitySearchElastic implements EntitySearchHelper {
26    /**
27     * Default rescore profile
28     */
29    public const DEFAULT_RESCORE_PROFILE = 'wikibase_prefix';
30
31    /**
32     * Name of the context for profile name resolution
33     */
34    public const CONTEXT_WIKIBASE_PREFIX = 'wikibase_prefix_search';
35
36    /**
37     * Name of the context for profile name resolution
38     */
39    public const CONTEXT_WIKIBASE_FULLTEXT = 'wikibase_fulltext_search';
40
41    /**
42     * Name of the context for profile name resolution
43     */
44    public const CONTEXT_WIKIBASE_IN_LABEL = 'wikibase_in_label_search';
45
46    /**
47     * Name of the profile type used to build the elastic query
48     */
49    public const WIKIBASE_PREFIX_QUERY_BUILDER = 'wikibase_prefix_querybuilder';
50
51    /**
52     * Name of the profile type used to build the elastic query
53     */
54    public const WIKIBASE_IN_LABEL_QUERY_BUILDER = 'wikibase_in_label_querybuilder';
55
56    /**
57     * Default query builder profile for prefix searches
58     */
59    public const DEFAULT_QUERY_BUILDER_PROFILE = 'default';
60
61    /**
62     * Default query builder profile for fulltext searches
63     *
64     */
65    public const DEFAULT_FULL_TEXT_QUERY_BUILDER_PROFILE = 'wikibase';
66
67    /**
68     * Replacement syntax for statement boosting
69     * @see \CirrusSearch\Profile\SearchProfileRepositoryTransformer
70     * and repo/config/ElasticSearchRescoreFunctions.php
71     */
72    public const STMT_BOOST_PROFILE_REPL = 'functions.*[type=term_boost].params[statement_keywords=_statementBoost_].statement_keywords';
73
74    /**
75     * @var LanguageFallbackChainFactory
76     */
77    private $languageChainFactory;
78
79    /**
80     * @var EntityIdParser
81     */
82    private $idParser;
83
84    /**
85     * @var string[]
86     */
87    private $contentModelMap;
88
89    /**
90     * Web request context.
91     * Used for implementing debug features such as cirrusDumpQuery.
92     * @var WebRequest
93     */
94    private $request;
95
96    /**
97     * @var Language User language for display.
98     */
99    private $userLang;
100
101    /**
102     * @var CirrusDebugOptions
103     */
104    private $debugOptions;
105
106    /**
107     * @param LanguageFallbackChainFactory $languageChainFactory
108     * @param EntityIdParser $idParser
109     * @param Language $userLang
110     * @param array $contentModelMap Maps entity type => content model name
111     * @param WebRequest|null $request Web request context
112     * @param CirrusDebugOptions|null $options
113     */
114    public function __construct(
115        LanguageFallbackChainFactory $languageChainFactory,
116        EntityIdParser $idParser,
117        Language $userLang,
118        array $contentModelMap,
119        ?WebRequest $request = null,
120        ?CirrusDebugOptions $options = null
121    ) {
122        $this->languageChainFactory = $languageChainFactory;
123        $this->idParser = $idParser;
124        $this->userLang = $userLang;
125        $this->contentModelMap = $contentModelMap;
126        $this->request = $request ?: new FauxRequest();
127        $this->debugOptions = $options ?: CirrusDebugOptions::fromRequest( $this->request );
128    }
129
130    /**
131     * Produce ES query that matches the arguments.
132     *
133     * @param string $text
134     * @param string $languageCode
135     * @param string $entityType
136     * @param bool $strictLanguage
137     * @param SearchContext $context
138     *
139     * @return AbstractQuery
140     */
141    protected function getElasticSearchQuery(
142        $text,
143        $languageCode,
144        $entityType,
145        $strictLanguage,
146        SearchContext $context
147    ) {
148        $context->setOriginalSearchTerm( $text );
149        if ( empty( $this->contentModelMap[$entityType] ) ) {
150            $context->setResultsPossible( false );
151            $context->addWarning( 'wikibasecirrus-search-bad-entity-type', $entityType );
152            return new MatchNone();
153        }
154        $profile = LabelsCompletionQuery::loadProfile(
155            $context->getConfig()->getProfileService(),
156            $this->languageChainFactory,
157            self::WIKIBASE_PREFIX_QUERY_BUILDER,
158            $context->getProfileContext(),
159            $context->getProfileContextParams(),
160            $languageCode
161        );
162        return LabelsCompletionQuery::build(
163            $text,
164            $profile,
165            $this->contentModelMap[$entityType],
166            $languageCode,
167            $strictLanguage,
168            EntitySearchUtils::entityIdParserNormalizer( $this->idParser )
169        );
170    }
171
172    /**
173     * @inheritDoc
174     */
175    public function getRankedSearchResults(
176        $text,
177        $languageCode,
178        $entityType,
179        $limit,
180        $strictLanguage,
181        ?string $profileContext = null
182    ) {
183        $profileContext ??= self::CONTEXT_WIKIBASE_PREFIX;
184        $searcher = new WikibaseEntitySearcher( 0, $limit, 'wikibase_prefix', 'wikibase-prefix', $this->debugOptions );
185        $searcher->getSearchContext()->setProfileContext(
186            $profileContext,
187            [ 'language' => $languageCode ] );
188        $query = $this->getElasticSearchQuery( $text, $languageCode, $entityType, $strictLanguage,
189                $searcher->getSearchContext() );
190
191        $searcher->setResultsType( new EntityElasticTermResult(
192            $this->idParser,
193            $query instanceof LabelsCompletionQuery ? $query->getSearchLanguageCodes() : [],
194            'prefix',
195            $this->languageChainFactory->newFromLanguage( $this->userLang )
196        ) );
197
198        $result = $searcher->performSearch( $query );
199
200        if ( $result->isOK() ) {
201            $result = $result->getValue();
202        } else {
203            throw new EntitySearchException( $result );
204        }
205
206        if ( $searcher->isReturnRaw() ) {
207            $result = $searcher->processRawReturn( $result, $this->request );
208        }
209
210        return $result;
211    }
212
213}