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