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