Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 77
0.00% covered (danger)
0.00%
0 / 14
CRAP
0.00% covered (danger)
0.00%
0 / 1
EntityResult
0.00% covered (danger)
0.00%
0 / 77
0.00% covered (danger)
0.00%
0 / 14
870
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 31
0.00% covered (danger)
0.00%
0 / 1
56
 getHighlightOrField
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
20
 getSourceField
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 processHighlighting
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 getLabelData
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getDescriptionData
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getLabelHighlightedData
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getDescriptionHighlightedData
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getTitleSnippet
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getTextSnippet
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getExtraDisplay
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getStatementCount
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getSitelinkCount
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getExtensionData
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2namespace Wikibase\Search\Elastic;
3
4use CirrusSearch\Search\Result;
5use CirrusSearch\Searcher;
6use HtmlArmor;
7use Wikibase\Lib\TermLanguageFallbackChain;
8use Wikibase\Repo\Search\ExtendedResult;
9
10/**
11 * Single result for entity search.
12 */
13class EntityResult extends Result implements ExtendedResult {
14    /**
15     * Key which holds wikibase data for result extra data.
16     */
17    private const WIKIBASE_EXTRA_DATA = 'wikibase';
18
19    /**
20     * Label data with highlighting.
21     * ['language' => LANG, 'value' => TEXT]
22     * @var string[]
23     */
24    private $labelHighlightedData;
25    /**
26     * Raw label data from source.
27     * ['language' => LANG, 'value' => TEXT]
28     * @var string[]
29     */
30    private $labelData;
31    /**
32     * Description data with highlighting.
33     * ['language' => LANG, 'value' => TEXT]
34     * @var string[]
35     */
36    private $descriptionData;
37    /**
38     * Description data from source.
39     * ['language' => LANG, 'value' => TEXT]
40     * @var string[]
41     */
42    private $descriptionHighlightedData;
43
44    /**
45     * Did we capture actual match in display of
46     * label or description?
47     * @var bool
48     */
49    private $haveMatch;
50    /**
51     * Extra display field for match.
52     * ['language' => LANG, 'value' => TEXT]
53     * @var array
54     */
55    private $extraDisplay;
56    /**
57     * Display language
58     * @var string
59     */
60    private $displayLanguage;
61    /**
62     * Original source data
63     * @var array
64     */
65    private $sourceData;
66
67    /**
68     * @param string $displayLanguage
69     * @param TermLanguageFallbackChain $displayFallbackChain
70     * @param \Elastica\Result $result
71     */
72    public function __construct( $displayLanguage, TermLanguageFallbackChain $displayFallbackChain,
73                                    $result ) {
74        // Let Cirrus\Result class handle the boring stuff
75        parent::__construct( null, $result );
76        // FIXME: null is not nice, but Result doesn't really need it...
77        // Think how to fix this.
78        $this->displayLanguage = $displayLanguage;
79
80        $this->sourceData = $result->getSource();
81        $highlightData = $result->getHighlights();
82
83        // If our highlight hit is on alias, we have to put real label into the field,
84        // and alias in extra
85        $isAlias = false;
86        if ( !empty( $highlightData["labels.{$displayLanguage}.plain"] ) ) {
87            $hlLabel = $highlightData["labels.{$displayLanguage}.plain"][0];
88            if ( preg_match( ElasticTermResult::HIGHLIGHT_PATTERN, $hlLabel, $match ) ) {
89                $isAlias = ( $hlLabel[0] !== '0' );
90            }
91        }
92
93        $this->labelData = $this->getSourceField(
94            'labels', $displayLanguage, $displayFallbackChain, $this->sourceData );
95        if ( $isAlias ) {
96            // We have matched alias for highlighting, so we will show regular label and
97            // alias as extra data
98            $this->labelHighlightedData = $this->labelData;
99            $this->extraDisplay = $this->getHighlightOrField( 'labels', $displayLanguage,
100                $displayFallbackChain, $highlightData, $this->sourceData, true );
101            // Since isAlias can be true only if labels is in highlight data, the above will
102            // also set $this->haveMatch
103        } else {
104            $this->labelHighlightedData = $this->getHighlightOrField( 'labels', $displayLanguage,
105                $displayFallbackChain, $highlightData, $this->sourceData, true );
106        }
107
108        $this->descriptionHighlightedData = $this->getHighlightOrField(
109            'descriptions', $displayLanguage, $displayFallbackChain,
110            $highlightData, $this->sourceData );
111        $this->descriptionData = $this->getSourceField(
112            'descriptions', $displayLanguage, $displayFallbackChain, $this->sourceData );
113
114        if ( !$this->haveMatch ) {
115            reset( $highlightData );
116            $key = key( $highlightData );
117            if ( $key && preg_match( '/^(\w+)\.([^.]+)\.plain$/', $key, $match ) ) {
118                $this->extraDisplay = [
119                    'language' => $match[2],
120                    'value' => new HtmlArmor( $this->processHighlighting( $highlightData[$key][0],
121                        $match[1] === 'labels' ) )
122                ];
123            }
124        }
125    }
126
127    /**
128     * Extract field value from highlighting or source data.
129     * @param string $field
130     * @param string $displayLanguage
131     * @param TermLanguageFallbackChain $displayFallbackChain
132     * @param array $highlightData
133     * @param array $sourceData
134     * @param bool $useOffsets Is highlighter using offsets option?
135     * @return array [ $language, $text ] or [null, null] if nothing found
136     */
137    private function getHighlightOrField( $field, $displayLanguage,
138            TermLanguageFallbackChain $displayFallbackChain,
139            $highlightData, $sourceData, $useOffsets = false
140    ) {
141        // Try highlights first, if we have needed language there, use highlighted data
142        if ( !empty( $highlightData["{$field}.{$displayLanguage}.plain"] ) ) {
143            $this->haveMatch = true;
144            return [
145                'language' => $displayLanguage,
146                'value' => new HtmlArmor( $this->processHighlighting( $highlightData["{$field}.{$displayLanguage}.plain"][0],
147                    $useOffsets ) )
148            ];
149        }
150        // If that failed, try source data
151        $source = $this->getSourceField( $field, $displayLanguage, $displayFallbackChain, $sourceData );
152        // But if we actually have highlight for this one, use it!
153        if ( $source && !empty( $highlightData["{$field}.{$source['language']}.plain"] ) ) {
154            $this->haveMatch = true;
155            return [
156                'language' => $source['language'],
157                'value' => new HtmlArmor( $this->processHighlighting( $highlightData["{$field}.{$source['language']}.plain"][0],
158                    $useOffsets ) )
159            ];
160        }
161        return $source;
162    }
163
164    /**
165     * Get data from source fields, using fallback chain if necessary.
166     * @param string $field Field in source data where we're looking.
167     *                      The field will contain subfield by language names.
168     * @param string $displayLanguage
169     * @param TermLanguageFallbackChain $displayFallbackChain
170     * @param array $sourceData The source data as returned by Elastic.
171     * @return array
172     */
173    private function getSourceField( $field, $displayLanguage,
174            TermLanguageFallbackChain $displayFallbackChain,
175            $sourceData
176    ) {
177        $term = EntitySearchUtils::findTermForDisplay( $sourceData, $field, $displayFallbackChain );
178        if ( $term ) {
179            return [ 'language' => $term->getLanguageCode(), 'value' => $term->getText() ];
180        }
181        // OK, we don't have much here
182        return [ 'language' => $displayLanguage, 'value' => '' ];
183    }
184
185    /**
186     * Process highlighted string from search results.
187     * @param string $snippet
188     * @param bool $useOffsets Is highlighter using offsets option?
189     * @return string Highlighted and HTML-encoded string
190     */
191    private function processHighlighting( $snippet, $useOffsets = false ) {
192        if ( $useOffsets && preg_match( ElasticTermResult::HIGHLIGHT_PATTERN, $snippet, $match ) ) {
193            $snippet = $match[1];
194        }
195        return strtr( htmlspecialchars( $snippet ), [
196            Searcher::HIGHLIGHT_PRE_MARKER => Searcher::HIGHLIGHT_PRE,
197            Searcher::HIGHLIGHT_POST_MARKER => Searcher::HIGHLIGHT_POST
198        ] );
199    }
200
201    /**
202     * @return string[] ['language' => LANG, 'value' => TEXT]
203     */
204    public function getLabelData() {
205        return $this->labelData;
206    }
207
208    /**
209     * @return string[] ['language' => LANG, 'value' => TEXT]
210     */
211    public function getDescriptionData() {
212        return $this->descriptionData;
213    }
214
215    /**
216     * @return string[] ['language' => LANG, 'value' => TEXT]
217     */
218    public function getLabelHighlightedData() {
219        return $this->labelHighlightedData;
220    }
221
222    /**
223     * @return string[] ['language' => LANG, 'value' => TEXT]
224     */
225    public function getDescriptionHighlightedData() {
226        return $this->descriptionHighlightedData;
227    }
228
229    /**
230     * @return string
231     */
232    public function getTitleSnippet() {
233        return HtmlArmor::getHtml( $this->labelHighlightedData['value'] );
234    }
235
236    /**
237     * @param array $terms
238     * @return string
239     */
240    public function getTextSnippet( $terms = [] ) {
241        return HtmlArmor::getHtml( $this->descriptionHighlightedData['value'] );
242    }
243
244    /**
245     * @return string[] ['language' => LANG, 'value' => TEXT]
246     */
247    public function getExtraDisplay() {
248        return $this->extraDisplay;
249    }
250
251    /**
252     * Get number of statements
253     * @return int
254     */
255    public function getStatementCount() {
256        if ( !isset( $this->sourceData['statement_count'] ) ) {
257            return 0;
258        }
259        return (int)$this->sourceData['statement_count'];
260    }
261
262    /**
263     * Get number of sitelinks
264     * @return int
265     */
266    public function getSitelinkCount() {
267        if ( !isset( $this->sourceData['sitelink_count'] ) ) {
268            return 0;
269        }
270        return (int)$this->sourceData['sitelink_count'];
271    }
272
273    /**
274     * Augment extension data with extraDisplay data.
275     * @return array[]
276     */
277    public function getExtensionData() {
278        $data = parent::getExtensionData();
279        if ( $this->extraDisplay ) {
280            $data[self::WIKIBASE_EXTRA_DATA] = [
281                'extrasnippet' => HtmlArmor::getHtml( $this->extraDisplay['value'] ),
282                'extrasnippet-language' => $this->extraDisplay['language'],
283            ];
284        }
285        return $data;
286    }
287
288}