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 ApiBase; |
6 | use ApiMain; |
7 | use 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 | /** |
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 | } |