Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
97.92% covered (success)
97.92%
47 / 48
85.71% covered (warning)
85.71%
6 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
HasDataForLangFeature
97.92% covered (success)
97.92%
47 / 48
85.71% covered (warning)
85.71%
6 / 7
18
0.00% covered (danger)
0.00%
0 / 1
 getKeywords
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 doApply
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
2
 makeQuery
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
4
 parseValue
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
5
 getFilterQuery
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 getFieldName
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3namespace Wikibase\Search\Elastic\Query;
4
5use CirrusSearch\Parser\AST\KeywordFeatureNode;
6use CirrusSearch\Query\Builder\QueryBuildingContext;
7use CirrusSearch\Query\FilterQueryFeature;
8use CirrusSearch\Query\SimpleKeywordFeature;
9use CirrusSearch\Search\SearchContext;
10use CirrusSearch\WarningCollector;
11use Elastica\Query\BoolQuery;
12use Elastica\Query\Exists;
13
14/**
15 * Abstract class supporting querying for the existence or nonexistence of term values by language.
16 * Currently supports descriptions (via 'hasdescription:' or 'hascaption:') and labels (via
17 * 'haslabel:').
18 * @see https://phabricator.wikimedia.org/T220282
19 */
20class HasDataForLangFeature extends SimpleKeywordFeature implements FilterQueryFeature {
21
22    /** @var int A limit to the number of fields that can be queried at once */
23    private const MAX_FIELDS = 30;
24
25    /** @var true[] Keyed by known language codes for set membership check */
26    private $validLangs;
27
28    /**
29     * @return string[]
30     */
31    protected function getKeywords() {
32        return [ 'hasdescription', 'haslabel', 'hascaption' ];
33    }
34
35    /**
36     * HasTermDataFeature constructor.
37     * @param string[] $languages list of languages indexed in elastic. Must all be lowercase.
38     */
39    public function __construct( $languages ) {
40        $this->validLangs = [];
41        foreach ( $languages as $lang ) {
42            $this->validLangs[$lang] = true;
43        }
44    }
45
46    /**
47     * @param SearchContext $context
48     * @param string $key The keyword
49     * @param string $value The value attached to the keyword with quotes stripped
50     * @param string $quotedValue The original value in the search string, including quotes if used
51     * @param bool $negated Is the search negated? Not used to generate the returned AbstractQuery,
52     *  that will be negated as necessary. Used for any other building/context necessary.
53     * @return array Two element array, first an AbstractQuery or null to apply to the
54     *  query. Second a boolean indicating if the quotedValue should be kept in the search
55     *  string.
56     */
57    protected function doApply( SearchContext $context, $key, $value, $quotedValue, $negated ) {
58        $langCodes = $this->parseValue(
59            $key,
60            $value,
61            $quotedValue,
62            '',
63            '',
64            $context
65        );
66        if ( $langCodes === [] ) {
67            $context->setResultsPossible( false );
68            return [ null, false ];
69        }
70        return [ $this->makeQuery( $key, $langCodes ), false ];
71    }
72
73    /**
74     * Builds a boolean query requiring the existence of a value in each query language for the
75     * specified field.
76     *
77     * @param string $key the search keywords
78     * @param array $langCodes valid language codes parsed from the query term
79     * @return BoolQuery
80     */
81    private function makeQuery( $key, array $langCodes ) {
82        $query = new BoolQuery();
83        if ( $langCodes === [ '__all__' ] ) {
84            if ( $key === 'haslabel' ) {
85                $field = 'labels_all.plain';
86            } else {
87                $field = $this->getFieldName( $key ) . '.*.plain';
88            }
89            $query->addShould( new Exists( $field ) );
90            return $query;
91        }
92        foreach ( $langCodes as $lang ) {
93            $query->addShould( new Exists( $this->getFieldName( $key ) . '.' . $lang . '.plain' ) );
94        }
95        return $query;
96    }
97
98    /**
99     * @param string $key
100     * @param string $value
101     * @param string $quotedValue
102     * @param string $valueDelimiter
103     * @param string $suffix
104     * @param WarningCollector $warningCollector
105     * @return string[] deduplicated list of fields to query for the existence of values
106     */
107    public function parseValue(
108        $key,
109        $value,
110        $quotedValue,
111        $valueDelimiter,
112        $suffix,
113        WarningCollector $warningCollector
114    ) {
115        if ( $value === '*' ) {
116            return [ '__all__' ];
117        }
118        $langCodes = [];
119
120        $langCodeCandidates = array_unique( array_map( 'mb_strtolower', explode( ',', $value ) ) );
121
122        foreach ( $langCodeCandidates as $candidate ) {
123            if ( isset( $this->validLangs[$candidate] ) ) {
124                $langCodes[] = $candidate;
125            } else {
126                $warningCollector->addWarning(
127                    'wikibasecirrus-keywordfeature-unknown-language-code',
128                    $key,
129                    $candidate
130                );
131            }
132        }
133
134        if ( count( $langCodes ) > self::MAX_FIELDS ) {
135            $warningCollector->addWarning( 'wikibasecirrus-keywordfeature-too-many-language-codes',
136                $key, self::MAX_FIELDS, count( $langCodes ) );
137            $langCodes = array_slice( $langCodes, 0, self::MAX_FIELDS );
138        }
139
140        return $langCodes;
141    }
142
143    /**
144     * @param KeywordFeatureNode $node
145     * @param QueryBuildingContext $context
146     * @return BoolQuery|null
147     */
148    public function getFilterQuery( KeywordFeatureNode $node, QueryBuildingContext $context ) {
149        $langCodes = $node->getParsedValue();
150        if ( $langCodes === [] ) {
151            return null;
152        }
153        return $this->makeQuery( $node->getKey(), $langCodes );
154    }
155
156    /**
157     * @param string $key the search keyword
158     * @return string field name to search
159     */
160    private function getFieldName( $key ) {
161        return $key === 'haslabel' ? 'labels' : 'descriptions';
162    }
163
164}