Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
49 / 49
100.00% covered (success)
100.00%
7 / 7
CRAP
100.00% covered (success)
100.00%
1 / 1
WbStatementQuantityFeature
100.00% covered (success)
100.00%
49 / 49
100.00% covered (success)
100.00%
7 / 7
14
100.00% covered (success)
100.00%
1 / 1
 getKeywords
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 doApply
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
2
 createFilters
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
2
 parseValue
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
4
 getQueryStrings
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getPattern
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 getFilterQuery
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2
3namespace Wikibase\Search\Elastic\Query;
4
5use CirrusSearch\Extra\Query\TermFreq;
6use CirrusSearch\Parser\AST\KeywordFeatureNode;
7use CirrusSearch\Query\Builder\QueryBuildingContext;
8use CirrusSearch\Query\FilterQueryFeature;
9use CirrusSearch\Query\SimpleKeywordFeature;
10use CirrusSearch\Search\Filters;
11use CirrusSearch\Search\SearchContext;
12use CirrusSearch\WarningCollector;
13use Elastica\Query\AbstractQuery;
14use Wikibase\Search\Elastic\Fields\StatementQuantityField;
15use 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 */
46class 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}