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