Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
100.00% |
49 / 49 |
|
100.00% |
7 / 7 |
CRAP | |
100.00% |
1 / 1 |
WbStatementQuantityFeature | |
100.00% |
49 / 49 |
|
100.00% |
7 / 7 |
14 | |
100.00% |
1 / 1 |
getKeywords | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
doApply | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
2 | |||
createFilters | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
2 | |||
parseValue | |
100.00% |
15 / 15 |
|
100.00% |
1 / 1 |
4 | |||
getQueryStrings | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getPattern | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
1 | |||
getFilterQuery | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
3 |
1 | <?php |
2 | |
3 | namespace Wikibase\Search\Elastic\Query; |
4 | |
5 | use CirrusSearch\Extra\Query\TermFreq; |
6 | use CirrusSearch\Parser\AST\KeywordFeatureNode; |
7 | use CirrusSearch\Query\Builder\QueryBuildingContext; |
8 | use CirrusSearch\Query\FilterQueryFeature; |
9 | use CirrusSearch\Query\SimpleKeywordFeature; |
10 | use CirrusSearch\Search\Filters; |
11 | use CirrusSearch\Search\SearchContext; |
12 | use CirrusSearch\WarningCollector; |
13 | use Elastica\Query\AbstractQuery; |
14 | use Wikibase\Search\Elastic\Fields\StatementQuantityField; |
15 | use Wikibase\Search\Elastic\Fields\StatementsField; |
16 | |
17 | /** |
18 | * Handles the search keyword 'wbstatementquantity:' |
19 | * |
20 | * Allows the user to search for pages/items that have wikibase statements associated with them, and |
21 | * specify quantities of those statements |
22 | * |
23 | * If a file page has the statement 'P180=Q5' with the qualifier 'P1114=5' (meaning 'depicts human, |
24 | * quantity 5' in wikidata) associated then it can be found using any of the following search |
25 | * queries: |
26 | * |
27 | * - wbstatementquantity:P180=Q5<6 |
28 | * - wbstatementquantity:P180=Q5<=5 |
29 | * - wbstatementquantity:P180=Q5>=5 |
30 | * - wbstatementquantity:P180=Q5>4 |
31 | * - wbstatementquantity:P180=Q5=5 |
32 | * |
33 | * Statements can be combined using logical OR by separating them using a pipe |
34 | * e.g. wbstatementquantity:P999=Q888>5|P999=Q888<8 |
35 | * |
36 | * Statements can be combined using logical AND by using two separate wbstatementquantity queries |
37 | * e.g. wbstatementquantity:P999=Q888>5 wbstatementquantity:P999=Q888<8 (a range search) |
38 | * e.g. wbstatementquantity:P999=Q888>5 wbstatementquantity:P999=Q777<8 |
39 | * |
40 | * Note that NOT ALL STATEMENTS ARE INDEXED. Searching for a statement about a property that has |
41 | * not been indexed will give an empty result set. |
42 | * |
43 | * @uses CirrusSearch |
44 | * @see https://phabricator.wikimedia.org/T190022 |
45 | */ |
46 | class WbStatementQuantityFeature extends SimpleKeywordFeature implements FilterQueryFeature { |
47 | |
48 | /** |
49 | * @return string[] |
50 | */ |
51 | protected function getKeywords() { |
52 | return [ 'wbstatementquantity' ]; |
53 | } |
54 | |
55 | /** |
56 | * @param SearchContext $context |
57 | * @param string $key The keyword |
58 | * @param string $value The value attached to the keyword with quotes stripped |
59 | * @param string $quotedValue The original value in the search string, including quotes if used |
60 | * @param bool $negated Is the search negated? Not used to generate the returned AbstractQuery, |
61 | * that will be negated as necessary. Used for any other building/context necessary. |
62 | * @return array Two element array, first an AbstractQuery or null to apply to the |
63 | * query. Second a boolean indicating if the quotedValue should be kept in the search |
64 | * string. |
65 | */ |
66 | protected function doApply( SearchContext $context, $key, $value, $quotedValue, $negated ) { |
67 | $params = $this->parseValue( |
68 | $key, |
69 | $value, |
70 | $quotedValue, |
71 | '', |
72 | '', |
73 | $context |
74 | ); |
75 | if ( count( $params['statements'] ) == 0 ) { |
76 | $context->setResultsPossible( false ); |
77 | return [ null, false ]; |
78 | } |
79 | |
80 | return [ $this->createFilters( $params ), false ]; |
81 | } |
82 | |
83 | private function createFilters( array $params ) { |
84 | $filters = []; |
85 | foreach ( $params[ 'statements' ] as $index => $statement ) { |
86 | $filters[] = new TermFreq( |
87 | StatementQuantityField::NAME, |
88 | $statement, |
89 | $params['operators'][$index], |
90 | $params['numbers'][$index] |
91 | ); |
92 | } |
93 | return Filters::booleanOr( $filters ); |
94 | } |
95 | |
96 | /** |
97 | * @param string $key |
98 | * @param string $value |
99 | * @param string $quotedValue |
100 | * @param string $valueDelimiter |
101 | * @param string $suffix |
102 | * @param WarningCollector $warningCollector |
103 | * @return array |
104 | */ |
105 | public function parseValue( |
106 | $key, |
107 | $value, |
108 | $quotedValue, |
109 | $valueDelimiter, |
110 | $suffix, |
111 | WarningCollector $warningCollector |
112 | ) { |
113 | $statements = $operators = $numbers = []; |
114 | $queryStrings = $this->getQueryStrings( $value ); |
115 | $pattern = $this->getPattern(); |
116 | foreach ( $queryStrings as $queryString ) { |
117 | $queryParts = []; |
118 | if ( preg_match( $pattern, $queryString, $queryParts ) ) { |
119 | $statements[] = $queryParts['statement']; |
120 | $operators[] = $queryParts['operator']; |
121 | $numbers[] = (int)$queryParts['number']; |
122 | } |
123 | } |
124 | |
125 | if ( count( $statements ) == 0 ) { |
126 | $warningCollector->addWarning( |
127 | 'wikibasecirrus-wbstatementquantity-feature-no-valid-statements', |
128 | $key |
129 | ); |
130 | } |
131 | return [ 'statements' => $statements, 'operators' => $operators, 'numbers' => $numbers ]; |
132 | } |
133 | |
134 | /** |
135 | * @param string $value |
136 | * @return string[] |
137 | */ |
138 | private function getQueryStrings( $value ) { |
139 | return explode( '|', $value ); |
140 | } |
141 | |
142 | /** |
143 | * Construct a regex pattern with which to parse the query string into a statement, an operator, |
144 | * and a number. |
145 | * |
146 | * @return string |
147 | */ |
148 | private function getPattern() { |
149 | $statementPattern = '(?<statement>'; |
150 | $statementPattern .= 'P[1-9]\d{0,9}' . |
151 | preg_quote( StatementsField::STATEMENT_SEPARATOR, '/' ) . |
152 | '.+)'; |
153 | $operatorPattern = '(?<operator>>=?|<=?|=)'; |
154 | $numberPattern = '(?<number>\d+)'; |
155 | |
156 | return '/^' . $statementPattern . $operatorPattern . $numberPattern . '$/U'; |
157 | } |
158 | |
159 | /** |
160 | * @param KeywordFeatureNode $node |
161 | * @param QueryBuildingContext $context |
162 | * @return AbstractQuery|null |
163 | */ |
164 | public function getFilterQuery( KeywordFeatureNode $node, QueryBuildingContext $context ) { |
165 | $params = $node->getParsedValue(); |
166 | if ( $params === null || $params['statements'] === [] ) { |
167 | return null; |
168 | } |
169 | return $this->createFilters( $params ); |
170 | } |
171 | |
172 | } |