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