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