Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
47.95% covered (danger)
47.95%
35 / 73
20.00% covered (danger)
20.00%
4 / 20
CRAP
0.00% covered (danger)
0.00%
0 / 1
Result
47.95% covered (danger)
47.95%
35 / 73
20.00% covered (danger)
20.00%
4 / 20
303.81
0.00% covered (danger)
0.00%
0 / 1
 __construct
80.65% covered (warning)
80.65%
25 / 31
0.00% covered (danger)
0.00%
0 / 1
14.23
 pickTextSnippet
31.58% covered (danger)
31.58%
6 / 19
0.00% covered (danger)
0.00%
0 / 1
34.95
 getTitleSnippet
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getRedirectTitle
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 clearRedirectTitle
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
20
 getRedirectSnippet
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
 getSectionSnippet
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getSectionTitle
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getCategorySnippet
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getWordCount
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getByteSize
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getTimestamp
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isFileMatch
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getInterwikiPrefix
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getInterwikiNamespaceText
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getDocId
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getScore
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getExplanation
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getTitleHelper
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3namespace CirrusSearch\Search;
4
5use CirrusSearch\Search\Fetch\HighlightingTrait;
6use MediaWiki\Title\Title;
7use MediaWiki\Utils\MWTimestamp;
8
9/**
10 * An individual search result from Elasticsearch.
11 *
12 * @license GPL-2.0-or-later
13 */
14class Result extends CirrusSearchResult {
15    use HighlightingTrait;
16
17    /** @var string */
18    private $titleSnippet = '';
19    /** @var Title|null */
20    private $redirectTitle = null;
21    /** @var string */
22    private $redirectSnippet = '';
23    /** @var Title|null */
24    private $sectionTitle = null;
25    /** @var string */
26    private $sectionSnippet = '';
27    /** @var string */
28    private $categorySnippet = '';
29    /** @var string */
30    private $textSnippet;
31    /** @var bool */
32    private $isFileMatch = false;
33    /** @var string */
34    private $namespaceText;
35    /** @var int */
36    private $wordCount;
37    /** @var int */
38    private $byteSize;
39    /** @var MWTimestamp */
40    private $timestamp;
41    /** @var string */
42    private $docId;
43    /** @var float */
44    private $score;
45    /** @var array */
46    private $explanation;
47    /** @var TitleHelper */
48    private $titleHelper;
49
50    /**
51     * Build the result.
52     *
53     * @param mixed $results Unused
54     * @param \Elastica\Result $result containing the given search result
55     * @param TitleHelper|null $titleHelper
56     */
57    public function __construct( $results, $result, ?TitleHelper $titleHelper = null ) {
58        $this->titleHelper = $titleHelper ?? new TitleHelper();
59        parent::__construct( $this->titleHelper->makeTitle( $result ) );
60        $this->namespaceText = $result->namespace_text;
61        $this->docId = $result->getId();
62
63        $fields = $result->getFields();
64        // Not all results requested a word count. Just pretend we have none if so
65        $this->wordCount = isset( $fields['text.word_count'] ) ? $fields['text.word_count'][ 0 ] : 0;
66        $this->byteSize = $result->text_bytes;
67        $this->timestamp = new MWTimestamp( $result->timestamp );
68        $highlights = $result->getHighlights();
69        // Evil hax to not special case .plain fields for intitle regex
70        foreach ( [ 'title', 'redirect.title' ] as $field ) {
71            if ( isset( $highlights["$field.plain"] ) && !isset( $highlights[$field] ) ) {
72                $highlights[$field] = $highlights["$field.plain"];
73                unset( $highlights["$field.plain"] );
74            }
75        }
76        if ( isset( $highlights[ 'title' ] ) ) {
77            $nstext = $this->getTitle()->getNamespace() === 0 ? '' :
78                $this->titleHelper->getNamespaceText( $this->getTitle() ) . ':';
79            $this->titleSnippet = $nstext . $this->escapeHighlightedText( $highlights[ 'title' ][ 0 ] );
80        } elseif ( $this->getTitle()->isExternal() ) {
81            // Interwiki searches are weird. They won't have title highlights by design, but
82            // if we don't return a title snippet we'll get weird display results.
83            $this->titleSnippet = $this->getTitle()->getText();
84        }
85
86        if ( !isset( $highlights[ 'title' ] ) && isset( $highlights[ 'redirect.title' ] ) ) {
87            // Make sure to find the redirect title before escaping because escaping breaks it....
88            $this->redirectTitle = $this->findRedirectTitle( $result, $highlights[ 'redirect.title' ][ 0 ] );
89            if ( $this->redirectTitle !== null ) {
90                $this->redirectSnippet = $this->escapeHighlightedText( $highlights[ 'redirect.title' ][ 0 ] );
91            }
92        }
93
94        $this->textSnippet = $this->escapeHighlightedText( $this->pickTextSnippet( $highlights ) );
95
96        if ( isset( $highlights[ 'heading' ] ) ) {
97            $this->sectionSnippet = $this->escapeHighlightedText( $highlights[ 'heading' ][ 0 ] );
98            $this->sectionTitle = $this->findSectionTitle( $highlights[ 'heading' ][ 0 ], $this->getTitle() );
99        }
100
101        if ( isset( $highlights[ 'category' ] ) ) {
102            $this->categorySnippet = $this->escapeHighlightedText( $highlights[ 'category' ][ 0 ] );
103        }
104        $this->score = $result->getScore();
105        $this->explanation = $result->getExplanation();
106    }
107
108    /**
109     * @param string[] $highlights
110     * @return string
111     */
112    private function pickTextSnippet( $highlights ) {
113        // This can get skipped if there the page was sent to Elasticsearch without text.
114        // This could be a bug or it could be that the page simply doesn't have any text.
115        $mainSnippet = '';
116        // Prefer source_text.plain it's likely a regex
117        // TODO: use the priority system from the FetchPhaseConfigBuilder
118        if ( isset( $highlights[ 'source_text.plain' ] ) ) {
119            $sourceSnippet = $highlights[ 'source_text.plain' ][ 0 ];
120            if ( $this->containsMatches( $sourceSnippet ) ) {
121                return $sourceSnippet;
122            }
123        }
124        if ( isset( $highlights[ 'text' ] ) ) {
125            $mainSnippet = $highlights[ 'text' ][ 0 ];
126            if ( $this->containsMatches( $mainSnippet ) ) {
127                return $mainSnippet;
128            }
129        }
130        if ( isset( $highlights[ 'auxiliary_text' ] ) ) {
131            $auxSnippet = $highlights[ 'auxiliary_text' ][ 0 ];
132            if ( $this->containsMatches( $auxSnippet ) ) {
133                return $auxSnippet;
134            }
135        }
136        if ( isset( $highlights[ 'file_text' ] ) ) {
137            $fileSnippet = $highlights[ 'file_text' ][ 0 ];
138            if ( $this->containsMatches( $fileSnippet ) ) {
139                $this->isFileMatch = true;
140                return $fileSnippet;
141            }
142        }
143        return $mainSnippet;
144    }
145
146    /**
147     * @return string
148     */
149    public function getTitleSnippet() {
150        return $this->titleSnippet;
151    }
152
153    /**
154     * @return Title|null
155     */
156    public function getRedirectTitle() {
157        return $this->redirectTitle;
158    }
159
160    protected function clearRedirectTitle(): bool {
161        $this->redirectTitle = null;
162        $this->redirectSnippet = '';
163
164        return !$this->containsHighlight( $this->textSnippet )
165            && $this->titleSnippet === ''
166            && $this->sectionSnippet === ''
167            && $this->categorySnippet === '';
168    }
169
170    /**
171     * @return string
172     */
173    public function getRedirectSnippet() {
174        return $this->redirectSnippet;
175    }
176
177    /**
178     * @param array $terms
179     * @return string|null
180     */
181    public function getTextSnippet( $terms = [] ) {
182        return $this->textSnippet;
183    }
184
185    /**
186     * @return string
187     */
188    public function getSectionSnippet() {
189        return $this->sectionSnippet;
190    }
191
192    /**
193     * @return Title|null
194     */
195    public function getSectionTitle() {
196        return $this->sectionTitle;
197    }
198
199    /**
200     * @return string
201     */
202    public function getCategorySnippet() {
203        return $this->categorySnippet;
204    }
205
206    /**
207     * @return int
208     */
209    public function getWordCount() {
210        return $this->wordCount;
211    }
212
213    /**
214     * @return int
215     */
216    public function getByteSize() {
217        return $this->byteSize;
218    }
219
220    /**
221     * @return string
222     */
223    public function getTimestamp() {
224        return $this->timestamp->getTimestamp( TS_MW );
225    }
226
227    /**
228     * @return bool
229     */
230    public function isFileMatch() {
231        return $this->isFileMatch;
232    }
233
234    /**
235     * @return string
236     */
237    public function getInterwikiPrefix() {
238        return $this->getTitle()->getInterwiki();
239    }
240
241    /**
242     * @return string
243     */
244    public function getInterwikiNamespaceText() {
245        // Seems to be only useful for API
246        return $this->namespaceText;
247    }
248
249    /**
250     * @return string
251     */
252    public function getDocId() {
253        return $this->docId;
254    }
255
256    /**
257     * @return float the score
258     */
259    public function getScore() {
260        return $this->score;
261    }
262
263    /**
264     * @return array lucene score explanation
265     */
266    public function getExplanation() {
267        return $this->explanation;
268    }
269
270    protected function getTitleHelper(): TitleHelper {
271        return $this->titleHelper;
272    }
273}