Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
76.24% |
77 / 101 |
|
44.44% |
4 / 9 |
CRAP | |
0.00% |
0 / 1 |
SchemaTreeSuggester | |
76.24% |
77 / 101 |
|
44.44% |
4 / 9 |
35.07 | |
0.00% |
0 / 1 |
setDeprecatedPropertyIds | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setClassifyingPropertyIds | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setSchemaTreeSuggesterUrl | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setEventLogger | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
__construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getSuggestions | |
86.84% |
33 / 38 |
|
0.00% |
0 / 1 |
7.11 | |||
suggestByPropertyIds | |
92.31% |
12 / 13 |
|
0.00% |
0 / 1 |
1.00 | |||
suggestByItem | |
54.29% |
19 / 35 |
|
0.00% |
0 / 1 |
11.68 | |||
buildResult | |
90.00% |
9 / 10 |
|
0.00% |
0 / 1 |
6.04 |
1 | <?php |
2 | |
3 | namespace PropertySuggester\Suggesters; |
4 | |
5 | use Exception; |
6 | use InvalidArgumentException; |
7 | use LogicException; |
8 | use MediaWiki\Http\HttpRequestFactory; |
9 | use PropertySuggester\EventLogger; |
10 | use Wikibase\DataModel\Entity\EntityIdValue; |
11 | use Wikibase\DataModel\Entity\Item; |
12 | use Wikibase\DataModel\Entity\ItemId; |
13 | use Wikibase\DataModel\Entity\NumericPropertyId; |
14 | use Wikibase\DataModel\Snak\PropertyValueSnak; |
15 | |
16 | /** |
17 | * a Suggester implementation that creates suggestions using |
18 | * the SchemaTree suggester. Requires the PropertySuggesterSchemaTreeUrl |
19 | * to be defined in the configuration. |
20 | * |
21 | * @license GPL-2.0-or-later |
22 | */ |
23 | class SchemaTreeSuggester implements SuggesterEngine { |
24 | |
25 | /** |
26 | * @var int[] |
27 | */ |
28 | private $deprecatedPropertyIds = []; |
29 | |
30 | /** |
31 | * @var array Numeric property ids as keys, values are meaningless. |
32 | */ |
33 | private $classifyingPropertyIds = []; |
34 | |
35 | /** |
36 | * @var string |
37 | */ |
38 | private $schemaTreeSuggesterUrl; |
39 | |
40 | /** |
41 | * @var EventLogger|null |
42 | */ |
43 | private $eventLogger; |
44 | |
45 | /** |
46 | * @param int[] $deprecatedPropertyIds |
47 | */ |
48 | public function setDeprecatedPropertyIds( array $deprecatedPropertyIds ) { |
49 | $this->deprecatedPropertyIds = $deprecatedPropertyIds; |
50 | } |
51 | |
52 | /** |
53 | * @param int[] $classifyingPropertyIds |
54 | */ |
55 | public function setClassifyingPropertyIds( array $classifyingPropertyIds ) { |
56 | $this->classifyingPropertyIds = array_flip( $classifyingPropertyIds ); |
57 | } |
58 | |
59 | /** |
60 | * @param string $schemaTreeSuggesterUrl |
61 | */ |
62 | public function setSchemaTreeSuggesterUrl( string $schemaTreeSuggesterUrl ) { |
63 | $this->schemaTreeSuggesterUrl = $schemaTreeSuggesterUrl; |
64 | } |
65 | |
66 | /** |
67 | * @var HttpRequestFactory |
68 | */ |
69 | private $httpFactory; |
70 | |
71 | /** |
72 | * @param EventLogger $eventLogger |
73 | */ |
74 | public function setEventLogger( EventLogger $eventLogger ) { |
75 | $this->eventLogger = $eventLogger; |
76 | } |
77 | |
78 | public function __construct( HttpRequestFactory $httpFactory ) { |
79 | $this->httpFactory = $httpFactory; |
80 | } |
81 | |
82 | /** |
83 | * @param int[] $propertyIds |
84 | * @param int[] $typesIds |
85 | * @param int $limit |
86 | * @param float $minProbability |
87 | * @param string $include |
88 | * @return Suggestion[]|null |
89 | * @throws InvalidArgumentException |
90 | * @throws Exception |
91 | */ |
92 | private function getSuggestions( |
93 | array $propertyIds, |
94 | array $typesIds, |
95 | int $limit, |
96 | float $minProbability, |
97 | string $include |
98 | ) { |
99 | $this->eventLogger->setPropertySuggesterName( 'SchemaTreeSuggester' ); |
100 | $startTime = microtime( true ); |
101 | |
102 | if ( !in_array( $include, [ self::SUGGEST_ALL, self::SUGGEST_NEW ] ) ) { |
103 | throw new InvalidArgumentException( '$include must be one of the SUGGEST_* constants!' ); |
104 | } |
105 | $excludedIds = []; |
106 | if ( $include === self::SUGGEST_NEW ) { |
107 | $excludedIds = array_merge( $propertyIds, $this->deprecatedPropertyIds ); |
108 | } |
109 | $excludedIds = array_map( static function ( int $id ) { |
110 | return 'P' . $id; |
111 | }, $excludedIds ); |
112 | |
113 | $properties = []; |
114 | foreach ( $propertyIds as $id ) { |
115 | $properties[] = 'P' . $id; |
116 | } |
117 | |
118 | $types = []; |
119 | foreach ( $typesIds as $id ) { |
120 | $types[] = 'Q' . $id; |
121 | } |
122 | |
123 | $response = $this->httpFactory->post( |
124 | $this->schemaTreeSuggesterUrl, |
125 | [ |
126 | 'postData' => json_encode( [ |
127 | 'Properties' => $properties, |
128 | 'Types' => $types |
129 | ] ), |
130 | 'timeout' => 1 |
131 | ], |
132 | __METHOD__ |
133 | ); |
134 | |
135 | // if request fails fall back to original property suggester |
136 | if ( !$response ) { |
137 | $this->eventLogger->setRequestDuration( -1 ); |
138 | $this->eventLogger->logEvent(); |
139 | return null; |
140 | } |
141 | |
142 | $result = json_decode( $response, true ); |
143 | |
144 | $result = $result['recommendations'] ?? null; |
145 | if ( !is_array( $result ) ) { |
146 | return null; |
147 | } |
148 | |
149 | $results = $this->buildResult( $result, $minProbability, $excludedIds, $limit ); |
150 | $this->eventLogger->setRequestDuration( (int)( ( microtime( true ) - $startTime ) * 1000 ) ); |
151 | return $results; |
152 | } |
153 | |
154 | /** |
155 | * @see SuggesterEngine::suggestByPropertyIds |
156 | * |
157 | * @param NumericPropertyId[] $propertyIds |
158 | * @param ItemId[] $typesIds |
159 | * @param int $limit |
160 | * @param float $minProbability |
161 | * @param string $context |
162 | * @param string $include One of the self::SUGGEST_* constants |
163 | * @return Suggestion[]|null |
164 | */ |
165 | public function suggestByPropertyIds( |
166 | array $propertyIds, |
167 | array $typesIds, |
168 | $limit, |
169 | $minProbability, |
170 | $context, |
171 | $include |
172 | ) { |
173 | $numericIds = array_map( static function ( NumericPropertyId $propertyId ) { |
174 | return $propertyId->getNumericId(); |
175 | }, $propertyIds ); |
176 | |
177 | $numericTypeIds = array_map( static function ( ItemId $typeId ) { |
178 | return $typeId->getNumericId(); |
179 | }, $typesIds ); |
180 | |
181 | return $this->getSuggestions( |
182 | $numericIds, |
183 | $numericTypeIds, |
184 | $limit, |
185 | $minProbability, |
186 | $include |
187 | ); |
188 | } |
189 | |
190 | /** |
191 | * @see SuggesterEngine::suggestByEntity |
192 | * |
193 | * @param Item $item |
194 | * @param int $limit |
195 | * @param float $minProbability |
196 | * @param string $context |
197 | * @param string $include One of the self::SUGGEST_* constants |
198 | * @return Suggestion[]|null |
199 | * @throws LogicException|Exception |
200 | */ |
201 | public function suggestByItem( Item $item, $limit, $minProbability, $context, $include ) { |
202 | $ids = []; |
203 | $types = []; |
204 | |
205 | foreach ( $item->getStatements()->toArray() as $statement ) { |
206 | $mainSnak = $statement->getMainSnak(); |
207 | |
208 | $id = $mainSnak->getPropertyId(); |
209 | if ( !( $id instanceof NumericPropertyId ) ) { |
210 | throw new LogicException( 'PropertySuggester is incompatible with non-numeric Property IDs' ); |
211 | } |
212 | |
213 | $numericPropertyId = $id->getNumericId(); |
214 | $ids[] = $numericPropertyId; |
215 | |
216 | if ( isset( $this->classifyingPropertyIds[$numericPropertyId] ) |
217 | && ( $mainSnak instanceof PropertyValueSnak ) ) { |
218 | $dataValue = $mainSnak->getDataValue(); |
219 | |
220 | if ( !( $dataValue instanceof EntityIdValue ) ) { |
221 | throw new LogicException( |
222 | "Property $numericPropertyId in wgPropertySuggesterClassifyingPropertyIds" |
223 | . ' does not have value type wikibase-entityid' |
224 | ); |
225 | } |
226 | |
227 | $entityId = $dataValue->getEntityId(); |
228 | |
229 | if ( !( $entityId instanceof ItemId ) ) { |
230 | throw new LogicException( |
231 | "PropertyValueSnak for $numericPropertyId, configured in " . |
232 | ' wgPropertySuggesterClassifyingPropertyIds, has an unexpected value ' . |
233 | 'and data type (not wikibase-item).' |
234 | ); |
235 | } |
236 | $numericEntityId = $entityId->getNumericId(); |
237 | $types[] = $numericEntityId; |
238 | } |
239 | } |
240 | |
241 | $this->eventLogger->setExistingProperties( array_map( 'strval', $ids ) ); |
242 | $this->eventLogger->setExistingTypes( array_map( 'strval', $types ) ); |
243 | |
244 | return $this->getSuggestions( |
245 | $ids, |
246 | $types, |
247 | $limit, |
248 | $minProbability, |
249 | $include |
250 | ); |
251 | } |
252 | |
253 | /** |
254 | * Converts the JSON object results to Suggestion objects |
255 | * @param array $response |
256 | * @param float $minProbability |
257 | * @param array $excludedIds |
258 | * @param int $limit |
259 | * @return Suggestion[] |
260 | */ |
261 | private function buildResult( array $response, float $minProbability, array $excludedIds, int $limit ): array { |
262 | $resultArray = []; |
263 | foreach ( $response as $pos => $res ) { |
264 | if ( $pos > $limit ) { |
265 | break; |
266 | } |
267 | if ( $res['probability'] > $minProbability && strpos( $res['property'], 'P' ) === 0 ) { |
268 | if ( !in_array( $res['property'], $excludedIds ) ) { |
269 | $pid = new NumericPropertyId( $res['property'] ); |
270 | $suggestion = new Suggestion( $pid, $res["probability"] ); |
271 | $resultArray[] = $suggestion; |
272 | } |
273 | } |
274 | } |
275 | return $resultArray; |
276 | } |
277 | } |