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 | } |