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