Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 45
0.00% covered (danger)
0.00%
0 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
EntitySearchUtils
0.00% covered (danger)
0.00%
0 / 45
0.00% covered (danger)
0.00%
0 / 6
272
0.00% covered (danger)
0.00%
0 / 1
 makeConstScoreQuery
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 normalizeId
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 normalizeIdFromSearchQuery
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
30
 parseOrNull
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 entityIdParserNormalizer
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 findTermForDisplay
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
20
1<?php
2namespace Wikibase\Search\Elastic;
3
4use Elastica\Query\ConstantScore;
5use Elastica\Query\MatchQuery;
6use Wikibase\DataModel\Entity\EntityId;
7use Wikibase\DataModel\Entity\EntityIdParser;
8use Wikibase\DataModel\Entity\EntityIdParsingException;
9use Wikibase\DataModel\Term\Term;
10use Wikibase\Lib\TermLanguageFallbackChain;
11
12/**
13 * Utilities useful for entity searches.
14 */
15final class EntitySearchUtils {
16
17    /**
18     * Create constant score query for a field.
19     * @param string $field
20     * @param string|double $boost
21     * @param string $text
22     * @param string $matchOperator
23     * @return ConstantScore
24     */
25    public static function makeConstScoreQuery( $field, $boost, $text, $matchOperator = MatchQuery::OPERATOR_OR ) {
26        if ( $matchOperator === MatchQuery::OPERATOR_AND ) {
27            $filter = new MatchQuery( $field, [ 'query' => $text ] );
28            $filter->setFieldOperator( $field, $matchOperator );
29        } else {
30            $filter = new MatchQuery( $field, $text );
31        }
32
33        $csquery = new ConstantScore();
34        $csquery->setFilter( $filter );
35        $csquery->setBoost( $boost );
36        return $csquery;
37    }
38
39    /**
40     * If the text looks like ID, normalize it to ID title
41     * Cases handled:
42     * - q42
43     * - (q42)
44     * - leading/trailing spaces
45     * - http://www.wikidata.org/entity/Q42
46     * @param string $text
47     * @param EntityIdParser $idParser
48     * @return string Normalized ID or original string
49     */
50    public static function normalizeId( $text, EntityIdParser $idParser ) {
51        return self::normalizeIdFromSearchQuery( $text, self::entityIdParserNormalizer( $idParser ) );
52    }
53
54    /**
55     * If the text looks like ID, normalize it to ID title
56     * Cases handled:
57     * - q42
58     * - (q42)
59     * - leading/trailing spaces
60     * - http://www.wikidata.org/entity/Q42
61     * @param string $text
62     * @param callable(string):(string|null) $idNormalizer
63     * @return string Normalized ID or original string
64     */
65    public static function normalizeIdFromSearchQuery( $text, callable $idNormalizer ) {
66        // TODO: this is a bit hacky, better way would be to make the field case-insensitive
67        // or add new subfiled which is case-insensitive
68        $text = strtoupper( str_replace( [ '(', ')' ], '', trim( $text ) ) );
69        $id = $idNormalizer( $text );
70        if ( $id !== null ) {
71            return $id;
72        }
73        if ( preg_match( '/\b(\w+)$/', $text, $matches ) && $matches[1] ) {
74            $id = $idNormalizer( $matches[1] );
75            if ( $id !== null ) {
76                return $id;
77            }
78        }
79        return $text;
80    }
81
82    /**
83     * Parse entity ID or return null
84     * @param string $text
85     * @param EntityIdParser $idParser
86     * @return ?EntityId
87     */
88    public static function parseOrNull( $text, EntityIdParser $idParser ): ?EntityId {
89        try {
90            $id = $idParser->parse( $text );
91        } catch ( EntityIdParsingException $ex ) {
92            return null;
93        }
94        return $id;
95    }
96
97    /**
98     * An id normalizer based on a given EntityIdParser.
99     *
100     * @param EntityIdParser $idParser
101     * @return callable(string):(string|null)
102     */
103    public static function entityIdParserNormalizer( EntityIdParser $idParser ): callable {
104        return static function ( string $text ) use ( $idParser ): ?string {
105            $id = EntitySearchUtils::parseOrNull( $text, $idParser );
106            if ( $id === null ) {
107                return null;
108            }
109            return $id->getSerialization();
110        };
111    }
112
113    /**
114     * Locate label for display among the source data, basing on fallback chain.
115     * @param array $sourceData
116     * @param string $field
117     * @param TermLanguageFallbackChain $termFallbackChain
118     * @return null|Term
119     */
120    public static function findTermForDisplay( $sourceData, $field, TermLanguageFallbackChain $termFallbackChain ) {
121        if ( empty( $sourceData[$field] ) ) {
122            return null;
123        }
124
125        $data = $sourceData[$field];
126        $first = reset( $data );
127        if ( is_array( $first ) ) {
128            // If we have multiple, like for labels, extract the first one
129            $labels_data = array_map(
130                static function ( $data ) {
131                    return $data[0] ?? null;
132                },
133                $data
134            );
135        } else {
136            $labels_data = $data;
137        }
138        // Drop empty ones
139        $labels_data = array_filter( $labels_data );
140
141        $preferredValue = $termFallbackChain->extractPreferredValueOrAny( $labels_data );
142        if ( $preferredValue ) {
143            return new Term( $preferredValue['language'], $preferredValue['value'] );
144        }
145
146        return null;
147    }
148
149}