Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
93.43% covered (success)
93.43%
185 / 198
50.00% covered (danger)
50.00%
3 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
GetSuggestions
93.43% covered (success)
93.43%
185 / 198
50.00% covered (danger)
50.00%
3 / 6
21.12
0.00% covered (danger)
0.00%
0 / 1
 __construct
96.30% covered (success)
96.30%
26 / 27
0.00% covered (danger)
0.00%
0 / 1
2
 execute
92.05% covered (success)
92.05%
81 / 88
0.00% covered (danger)
0.00%
0 / 1
15.11
 querySearchApi
100.00% covered (success)
100.00%
23 / 23
100.00% covered (success)
100.00%
1 / 1
1
 hasher
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 getAllowedParams
100.00% covered (success)
100.00%
43 / 43
100.00% covered (success)
100.00%
1 / 1
1
 getExamplesMessages
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3namespace PropertySuggester;
4
5use MediaWiki\Api\ApiBase;
6use MediaWiki\Api\ApiMain;
7use MediaWiki\Api\ApiResult;
8use MediaWiki\MediaWikiServices;
9use MediaWiki\Request\DerivativeRequest;
10use PropertySuggester\Suggesters\SchemaTreeSuggester;
11use PropertySuggester\Suggesters\SimpleSuggester;
12use PropertySuggester\Suggesters\SuggesterEngine;
13use Wikibase\DataAccess\PrefetchingTermLookup;
14use Wikibase\DataModel\Entity\Property;
15use Wikibase\DataModel\Services\Lookup\EntityLookup;
16use Wikibase\Lib\LanguageFallbackChainFactory;
17use Wikibase\Lib\Store\EntityTitleLookup;
18use Wikibase\Repo\Api\EntitySearchException;
19use Wikibase\Repo\Api\EntitySearchHelper;
20use Wikibase\Repo\Domains\Search\WbSearch;
21use Wikibase\Repo\WikibaseRepo;
22use Wikimedia\ParamValidator\ParamValidator;
23use Wikimedia\ParamValidator\TypeDef\IntegerDef;
24
25/**
26 * API module to get property suggestions.
27 *
28 * @author BP2013N2
29 * @license GPL-2.0-or-later
30 */
31class GetSuggestions extends ApiBase {
32
33    /**
34     * @var EntityLookup
35     */
36    private $entityLookup;
37
38    /**
39     * @var EntityTitleLookup
40     */
41    private $entityTitleLookup;
42
43    /**
44     * @var string[]
45     */
46    private $languageCodes;
47
48    /**
49     * @var SimpleSuggester
50     */
51    private $suggester;
52
53    /**
54     * @var SchemaTreeSuggester
55     */
56    private $schemaTreeSuggester;
57
58    /**
59     * @var SuggesterParamsParser
60     */
61    private $paramsParser;
62
63    /**
64     * @var EntitySearchHelper
65     */
66    private $entitySearchHelper;
67
68    /**
69     * @var PrefetchingTermLookup
70     */
71    private $prefetchingTermLookup;
72
73    /**
74     * @var LanguageFallbackChainFactory
75     */
76    private $languageFallbackChainFactory;
77
78    /**
79     * @var bool
80     */
81    private $abTestingState;
82
83    /**
84     * @var SimpleSuggester|SchemaTreeSuggester
85     */
86    private $defaultSuggester;
87
88    /**
89     * @var int
90     */
91    private $testingRatio;
92
93    public function __construct( ApiMain $main, string $name ) {
94        parent::__construct( $main, $name );
95        $config = $this->getConfig();
96
97        $mwServices = MediaWikiServices::getInstance();
98        $lb = $mwServices->getDBLoadBalancer();
99        $httpFactory = $mwServices->getHttpRequestFactory();
100
101        $this->prefetchingTermLookup = WikibaseRepo::getPrefetchingTermLookup( $mwServices );
102        $this->languageFallbackChainFactory = WikibaseRepo::getLanguageFallbackChainFactory( $mwServices );
103        $this->entitySearchHelper = WbSearch::getPropertySearchHelper( $mwServices );
104        $this->entityLookup = WikibaseRepo::getEntityLookup( $mwServices );
105        $this->entityTitleLookup = WikibaseRepo::getEntityTitleLookup( $mwServices );
106        $this->languageCodes = WikibaseRepo::getTermsLanguages( $mwServices )->getLanguages();
107        $this->abTestingState = $config->get( 'PropertySuggesterABTestingState' );
108
109        $deprecatedPropertyIds = $config->get( 'PropertySuggesterDeprecatedIds' );
110        $classifyingPropertyIds = $config->get( 'PropertySuggesterClassifyingPropertyIds' );
111
112        $this->suggester = new SimpleSuggester( $lb );
113        $this->suggester->setDeprecatedPropertyIds( $deprecatedPropertyIds );
114        $this->suggester->setClassifyingPropertyIds( $classifyingPropertyIds );
115        $this->suggester->setInitialSuggestions( $config->get( 'PropertySuggesterInitialSuggestions' ) );
116
117        $this->schemaTreeSuggester = new SchemaTreeSuggester( $httpFactory );
118        $this->schemaTreeSuggester->setSchemaTreeSuggesterUrl( $config->get( 'PropertySuggesterSchemaTreeUrl' ) );
119        $this->schemaTreeSuggester->setDeprecatedPropertyIds( $deprecatedPropertyIds );
120        $this->schemaTreeSuggester->setClassifyingPropertyIds( $classifyingPropertyIds );
121
122        if ( $config->get( 'PropertySuggesterDefaultSuggester' ) === 'PropertySuggester' ) {
123            $this->defaultSuggester = $this->suggester;
124        } else {
125            $this->defaultSuggester = $this->schemaTreeSuggester;
126        }
127
128        $this->testingRatio = $config->get( 'PropertySuggesterTestingRatio' );
129        $this->paramsParser = new SuggesterParamsParser( 500, $config->get( 'PropertySuggesterMinProbability' ) );
130    }
131
132    /**
133     * @see ApiBase::execute()
134     */
135    public function execute() {
136        $extracted = $this->extractRequestParams();
137        $paramsStatus = $this->paramsParser->parseAndValidate( $extracted );
138        if ( !$paramsStatus->isGood() ) {
139            $this->dieStatus( $paramsStatus );
140        }
141        /** @var SuggesterParams $params */
142        $params = $paramsStatus->getValue();
143
144        $eventLogger = new EventLogger(
145            $params->event,
146            $params->language
147        );
148
149        if ( $params->context === 'item' ) {
150            if ( $this->abTestingState && $params->entity !== null ) {
151                $hashId = $this->hasher( $params->entity );
152                if ( $hashId % $this->testingRatio === 0 ) {
153                    $suggester = $this->schemaTreeSuggester;
154                } else {
155                    $suggester = $this->suggester;
156                }
157            } else {
158                $suggester = $this->defaultSuggester;
159            }
160        } else {
161            $suggester = $this->suggester;
162        }
163
164        $suggester->setEventLogger( $eventLogger );
165        $this->suggester->setEventLogger( $eventLogger );
166
167        $suggestionGenerator = new SuggestionGenerator(
168            $this->entityLookup,
169            $this->entitySearchHelper,
170            $suggester,
171            // used in cases where schema tree recommender request fails
172            $this->suggester
173        );
174
175        $suggest = SuggesterEngine::SUGGEST_NEW;
176        if ( $params->include === 'all' ) {
177            $suggest = SuggesterEngine::SUGGEST_ALL;
178        }
179        if ( $params->entity !== null ) {
180            $suggestionsStatus = $suggestionGenerator->generateSuggestionsByItem(
181                $params->entity,
182                $params->suggesterLimit,
183                $params->minProbability,
184                $params->context,
185                $suggest
186            );
187        } else {
188            if ( $params->types === null ) {
189                $params->types = [];
190            }
191            $suggestionsStatus = $suggestionGenerator->generateSuggestionsByPropertyList(
192                $params->properties,
193                $params->types,
194                $params->suggesterLimit,
195                $params->minProbability,
196                $params->context,
197                $suggest
198            );
199        }
200        if ( !$suggestionsStatus->isGood() ) {
201            $this->dieStatus( $suggestionsStatus );
202        }
203        $suggestions = $suggestionsStatus->getValue();
204
205        try {
206            $suggestions = $suggestionGenerator->filterSuggestions(
207                $suggestions,
208                $params->search,
209                $params->language,
210                $params->resultSize
211            );
212        } catch ( EntitySearchException $ese ) {
213            $this->dieStatus( $ese->getStatus() );
214        }
215
216        $addSuggestions = [];
217        foreach ( $suggestions as $suggestion ) {
218            $addSuggestions[] = strval( $suggestion->getPropertyId()->getNumericId() );
219        }
220        $eventLogger->setAddSuggestions( $addSuggestions );
221        $eventLogger->logEvent();
222
223        // Build result array
224        $resultBuilder = new ResultBuilder(
225            $this->getResult(),
226            $this->prefetchingTermLookup,
227            $this->languageFallbackChainFactory,
228            $this->entityTitleLookup,
229            $params->search
230        );
231
232        $entries = $resultBuilder->createResultArray( $suggestions, $params->language );
233
234        // merge with search result if possible and necessary
235        if ( count( $entries ) < $params->resultSize && $params->search !== '' ) {
236            $searchResult = $this->querySearchApi(
237                $params->resultSize,
238                $params->search,
239                $params->language
240            );
241            $entries = $resultBuilder->mergeWithTraditionalSearchResults(
242                $entries,
243                $searchResult,
244                $params->resultSize
245            );
246        }
247
248        // Define Result
249        $slicedEntries = array_slice( $entries, $params->continue, $params->limit );
250        ApiResult::setIndexedTagName( $slicedEntries, 'search' );
251        $this->getResult()->addValue( null, 'search', $slicedEntries );
252
253        $this->getResult()->addValue( null, 'success', 1 );
254        if ( count( $entries ) >= $params->resultSize ) {
255            $this->getResult()->addValue( null, 'search-continue', $params->resultSize );
256        }
257        $this->getResult()->addValue( 'searchinfo', 'search', $params->search );
258    }
259
260    /**
261     * @param int $resultSize
262     * @param string $search
263     * @param string $language
264     * @return array[]
265     */
266    private function querySearchApi( $resultSize, $search, $language ) {
267        $searchEntitiesParameters = new DerivativeRequest(
268            $this->getRequest(),
269            [
270                'limit' => $resultSize + 1,
271                'continue' => 0,
272                'search' => $search,
273                'action' => 'wbsearchentities',
274                'language' => $language,
275                'uselang' => $language,
276                'type' => Property::ENTITY_TYPE
277            ]
278        );
279
280        $api = new ApiMain( $searchEntitiesParameters );
281        $api->execute();
282
283        $apiResult = $api->getResult()->getResultData(
284            null,
285            [
286                'BC' => [],
287                'Types' => [],
288                'Strip' => 'all'
289            ]
290        );
291
292        return $apiResult['search'];
293    }
294
295    /**
296     * Creates a numeric hash for the entity IDs
297     *
298     * @param string $code
299     * @return int a 64 bit decimal hash
300     */
301    private function hasher( $code ) {
302        $hex16 = substr( hash( 'sha256', $code ), 0, 16 );
303
304        $hexhi = substr( $hex16, 0, 8 );
305        $hexlo = substr( $hex16, 8, 8 );
306
307        $int = hexdec( $hexlo ) | ( hexdec( $hexhi ) << 32 );
308        return $int;
309    }
310
311    /**
312     * @inheritDoc
313     */
314    public function getAllowedParams() {
315        return [
316            'entity' => [
317                ParamValidator::PARAM_TYPE => 'string',
318            ],
319            'properties' => [
320                ParamValidator::PARAM_TYPE => 'string',
321                ParamValidator::PARAM_ISMULTI => true
322            ],
323            'types' => [
324                ParamValidator::PARAM_TYPE => 'string',
325                ParamValidator::PARAM_ISMULTI => true
326            ],
327            'limit' => [
328                ParamValidator::PARAM_TYPE => 'limit',
329                ParamValidator::PARAM_DEFAULT => 7,
330                IntegerDef::PARAM_MAX => ApiBase::LIMIT_SML1,
331                IntegerDef::PARAM_MAX2 => ApiBase::LIMIT_SML2,
332                IntegerDef::PARAM_MIN => 0,
333            ],
334            'continue' => [
335                ParamValidator::PARAM_TYPE => 'integer',
336            ],
337            'language' => [
338                ParamValidator::PARAM_TYPE => $this->languageCodes,
339                ParamValidator::PARAM_DEFAULT => $this->getContext()->getLanguage()->getCode(),
340            ],
341            'context' => [
342                ParamValidator::PARAM_TYPE => [ 'item', 'qualifier', 'reference' ],
343                ParamValidator::PARAM_DEFAULT => 'item',
344            ],
345            'include' => [
346                ParamValidator::PARAM_TYPE => [ '', 'all' ],
347                ParamValidator::PARAM_DEFAULT => '',
348            ],
349            'search' => [
350                ParamValidator::PARAM_TYPE => 'string',
351                ParamValidator::PARAM_DEFAULT => '',
352            ],
353            'event' => [
354                ParamValidator::PARAM_TYPE => 'string',
355                ParamValidator::PARAM_DEFAULT => '',
356            ],
357        ];
358    }
359
360    /**
361     * @inheritDoc
362     */
363    public function getExamplesMessages() {
364        return [
365            'action=wbsgetsuggestions&entity=Q4'
366            => 'apihelp-wbsgetsuggestions-example-1',
367            'action=wbsgetsuggestions&entity=Q4&continue=10&limit=5'
368            => 'apihelp-wbsgetsuggestions-example-2',
369            'action=wbsgetsuggestions&properties=P31|P21'
370            => 'apihelp-wbsgetsuggestions-example-3',
371            'action=wbsgetsuggestions&properties=P21&context=qualifier'
372            => 'apihelp-wbsgetsuggestions-example-4',
373            'action=wbsgetsuggestions&properties=P21&context=reference'
374            => 'apihelp-wbsgetsuggestions-example-5'
375        ];
376    }
377
378}