Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 59
0.00% covered (danger)
0.00%
0 / 23
CRAP
0.00% covered (danger)
0.00%
0 / 1
BaseCirrusSearchResultSet
0.00% covered (danger)
0.00%
0 / 59
0.00% covered (danger)
0.00%
0 / 23
1406
0.00% covered (danger)
0.00%
0 / 1
 transformOneResult
n/a
0 / 0
n/a
0 / 0
0
 hasMoreResults
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setSuggestionQuery
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 preCacheContainedTitles
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
42
 emptyResultSet
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 shrink
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 extractResults
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
30
 extractTitles
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 addInterwikiResults
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getInterwikiResults
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 hasInterwikiResults
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setRewrittenQuery
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 hasRewrittenQuery
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getQueryAfterRewrite
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getQueryAfterRewriteSnippet
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 hasSuggestion
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getSuggestionQuery
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getSuggestionSnippet
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 count
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 numRows
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getTotalHits
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 isApproximateTotalHits
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 getElasticResponse
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 getTitleHelper
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2
3namespace CirrusSearch\Search;
4
5use MediaWiki\MediaWikiServices;
6use MediaWiki\Search\BaseSearchResultSet;
7use MediaWiki\Search\ISearchResultSet;
8use MediaWiki\Search\SearchResult;
9use MediaWiki\Search\SearchResultSetTrait;
10use MediaWiki\Title\Title;
11use Wikimedia\Assert\Assert;
12use Wikimedia\HtmlArmor\HtmlArmor;
13
14/**
15 * Base class to represent a CirrusSearchResultSet
16 * Extensions willing to feed Cirrus with a CirrusSearchResultSet must extend this class.
17 */
18abstract class BaseCirrusSearchResultSet extends BaseSearchResultSet implements CirrusSearchResultSet {
19    use SearchResultSetTrait;
20
21    /** @var bool */
22    private $hasMoreResults = false;
23
24    /**
25     * @var CirrusSearchResult[]|null
26     */
27    private $results;
28
29    /**
30     * @var string|null
31     */
32    private $suggestionQuery;
33
34    /**
35     * @var HtmlArmor|string|null
36     */
37    private $suggestionSnippet;
38
39    /**
40     * @var array<int,array<string,CirrusSearchResultSet>>
41     */
42    private $interwikiResults = [];
43
44    /**
45     * @var string|null
46     */
47    private $rewrittenQuery;
48
49    /**
50     * @var HtmlArmor|string|null
51     */
52    private $rewrittenQuerySnippet;
53
54    /**
55     * @var TitleHelper
56     */
57    private $titleHelper;
58
59    /**
60     * @param \Elastica\Result $result Result from search engine
61     * @return CirrusSearchResult|null Elasticsearch result transformed into mediawiki
62     *  search result object.
63     */
64    abstract protected function transformOneResult( \Elastica\Result $result );
65
66    /**
67     * @return bool True when there are more pages of search results available.
68     */
69    final public function hasMoreResults() {
70        return $this->hasMoreResults;
71    }
72
73    /**
74     * @param string $suggestionQuery
75     * @param HtmlArmor|string|null $suggestionSnippet
76     */
77    final public function setSuggestionQuery( string $suggestionQuery, $suggestionSnippet = null ) {
78        $this->suggestionQuery = $suggestionQuery;
79        $this->suggestionSnippet = $suggestionSnippet ?? $suggestionQuery;
80    }
81
82    /**
83     * Loads the result set into the mediawiki LinkCache via a
84     * batch query. By pre-caching this we ensure methods such as
85     * Result::isMissingRevision() don't trigger a query for each and
86     * every search result.
87     *
88     * @param \Elastica\ResultSet $resultSet Result set from which the titles come
89     */
90    private function preCacheContainedTitles( \Elastica\ResultSet $resultSet ) {
91        // We can only pull in information about the local wiki
92        $lb = MediaWikiServices::getInstance()->getLinkBatchFactory()->newLinkBatch();
93        foreach ( $resultSet->getResults() as $result ) {
94            if ( !$this->getTitleHelper()->isExternal( $result )
95                && isset( $result->namespace )
96                && isset( $result->title )
97            ) {
98                $lb->add( $result->namespace, $result->title );
99            }
100        }
101        if ( !$lb->isEmpty() ) {
102            $lb->setCaller( __METHOD__ );
103            $lb->execute();
104        }
105    }
106
107    /**
108     * @param bool $searchContainedSyntax
109     * @return CirrusSearchResultSet an empty result set
110     */
111    final public static function emptyResultSet( $searchContainedSyntax = false ) {
112        return new EmptySearchResultSet( $searchContainedSyntax );
113    }
114
115    /**
116     * @param int $limit Shrink result set to $limit and flag
117     *  if more results are available.
118     */
119    final public function shrink( $limit ) {
120        if ( $this->count() > $limit ) {
121            Assert::precondition( $this->results !== null, "results not initialized" );
122            $this->results = array_slice( $this->results, 0, $limit );
123            $this->hasMoreResults = true;
124        }
125    }
126
127    /**
128     * @return CirrusSearchResult[]|SearchResult[]
129     */
130    final public function extractResults() {
131        if ( $this->results === null ) {
132            $this->results = [];
133            $elasticaResults = $this->getElasticaResultSet();
134            if ( $elasticaResults !== null ) {
135                $this->preCacheContainedTitles( $elasticaResults );
136                foreach ( $elasticaResults->getResults() as $result ) {
137                    $transformed = $this->transformOneResult( $result );
138                    if ( $transformed !== null ) {
139                        $this->augmentResult( $transformed );
140                        $this->results[] = $transformed;
141                    }
142                }
143            }
144        }
145        return $this->results;
146    }
147
148    /**
149     * Extract all the titles in the result set.
150     * @return Title[]
151     */
152    final public function extractTitles() {
153        return array_map(
154            static function ( SearchResult $result ) {
155                return $result->getTitle();
156            },
157            $this->extractResults() );
158    }
159
160    /**
161     * @param CirrusSearchResultSet $res
162     * @param int $type One of the ISearchResultSet::…_RESULTS constants
163     * @param string $interwiki
164     */
165    final public function addInterwikiResults( CirrusSearchResultSet $res, $type, $interwiki ) {
166        $this->interwikiResults[$type][$interwiki] = $res;
167    }
168
169    /**
170     * @param int $type One of the ISearchResultSet::…_RESULTS constants
171     * @return ISearchResultSet[]
172     */
173    final public function getInterwikiResults( $type = self::SECONDARY_RESULTS ) {
174        return $this->interwikiResults[$type] ?? [];
175    }
176
177    /**
178     * @param int $type One of the ISearchResultSet::…_RESULTS constants
179     * @return bool
180     */
181    final public function hasInterwikiResults( $type = self::SECONDARY_RESULTS ): bool {
182        return (bool)( $this->interwikiResults[$type] ?? [] );
183    }
184
185    /**
186     * @param string $newQuery
187     * @param HtmlArmor|string|null $newQuerySnippet
188     */
189    final public function setRewrittenQuery( string $newQuery, $newQuerySnippet = null ) {
190        $this->rewrittenQuery = $newQuery;
191        $this->rewrittenQuerySnippet = $newQuerySnippet ?? $newQuery;
192    }
193
194    /**
195     * @return bool
196     */
197    final public function hasRewrittenQuery() {
198        return $this->rewrittenQuery !== null;
199    }
200
201    /**
202     * @return string|null
203     */
204    final public function getQueryAfterRewrite() {
205        return $this->rewrittenQuery;
206    }
207
208    /**
209     * @return HtmlArmor|string|null
210     */
211    final public function getQueryAfterRewriteSnippet() {
212        return $this->rewrittenQuerySnippet;
213    }
214
215    /**
216     * @return bool
217     */
218    final public function hasSuggestion() {
219        return $this->suggestionQuery !== null;
220    }
221
222    /**
223     * @return string|null
224     */
225    final public function getSuggestionQuery() {
226        return $this->suggestionQuery;
227    }
228
229    /**
230     * @return string|null
231     */
232    final public function getSuggestionSnippet() {
233        return $this->suggestionSnippet;
234    }
235
236    /**
237     * Count elements of an object
238     * @link https://php.net/manual/en/countable.count.php
239     * @return int The custom count as an integer.
240     * @since 5.1.0
241     */
242    final public function count(): int {
243        return count( $this->extractResults() );
244    }
245
246    /**
247     * @return int
248     */
249    final public function numRows() {
250        return $this->count();
251    }
252
253    /**
254     * Some search modes return a total hit count for the query
255     * in the entire article database. This may include pages
256     * in namespaces that would not be matched on the given
257     * settings.
258     *
259     * Return null if no total hits number is supported.
260     *
261     * @return int|null
262     */
263    final public function getTotalHits() {
264        $elasticaResultSet = $this->getElasticaResultSet();
265        if ( $elasticaResultSet !== null ) {
266            return $elasticaResultSet->getTotalHits();
267        }
268        return 0;
269    }
270
271    /**
272     * @inheritDoc
273     */
274    public function isApproximateTotalHits(): bool {
275        $elasticaResultSet = $this->getElasticaResultSet();
276        if ( $elasticaResultSet !== null ) {
277            return $elasticaResultSet->getTotalHitsRelation() !== 'eq';
278        }
279        return false;
280    }
281
282    /**
283     * @return \Elastica\Response|null
284     */
285    final public function getElasticResponse() {
286        $elasticaResultSet = $this->getElasticaResultSet();
287        return $elasticaResultSet != null ? $elasticaResultSet->getResponse() : null;
288    }
289
290    /**
291     * Useful to inject your own TitleHelper during tests
292     */
293    protected function getTitleHelper(): TitleHelper {
294        if ( $this->titleHelper === null ) {
295            $this->titleHelper = new TitleHelper();
296        }
297        return $this->titleHelper;
298    }
299}