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
697.69
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 / 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 * 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                    if ( $transformed != null ) {
186                        $this->augmentResult( $transformed );
187                        $this->results[] = $transformed;
188                    }
189                }
190            }
191        }
192        return $this->results;
193    }
194
195    /**
196     * @param \Elastica\Result $result Result from search engine
197     * @return CirrusSearchResult Elasticsearch result transformed into mediawiki
198     *  search result object.
199     */
200    protected function transformOneResult( \Elastica\Result $result ) {
201        return $this->resultBuilder->build( $result );
202    }
203
204    /**
205     * @param CirrusSearchResultSet $res
206     * @param int $type One of SearchResultSet::* constants
207     * @param string $interwiki
208     */
209    public function addInterwikiResults( CirrusSearchResultSet $res, $type, $interwiki ) {
210        $this->interwikiResults[$type][$interwiki] = $res;
211    }
212
213    /**
214     * @param int $type
215     * @return ISearchResultSet[]
216     */
217    public function getInterwikiResults( $type = self::SECONDARY_RESULTS ) {
218        return $this->interwikiResults[$type] ?? [];
219    }
220
221    /**
222     * @param int $type
223     * @return bool
224     */
225    public function hasInterwikiResults( $type = self::SECONDARY_RESULTS ) {
226        return isset( $this->interwikiResults[$type] );
227    }
228
229    /**
230     * @param string $newQuery
231     * @param HtmlArmor|string|null $newQuerySnippet
232     */
233    public function setRewrittenQuery( string $newQuery, $newQuerySnippet = null ) {
234        $this->rewrittenQuery = $newQuery;
235        $this->rewrittenQuerySnippet = $newQuerySnippet ?? $newQuery;
236    }
237
238    /**
239     * @return bool
240     */
241    public function hasRewrittenQuery() {
242        return $this->rewrittenQuery !== null;
243    }
244
245    /**
246     * @return string|null
247     */
248    public function getQueryAfterRewrite() {
249        return $this->rewrittenQuery;
250    }
251
252    /**
253     * @return HtmlArmor|string|null
254     */
255    public function getQueryAfterRewriteSnippet() {
256        return $this->rewrittenQuerySnippet;
257    }
258
259    /**
260     * @return \Elastica\Response|null
261     */
262    public function getElasticResponse() {
263        return $this->result != null ? $this->result->getResponse() : null;
264    }
265
266    /**
267     * @return \Elastica\ResultSet|null
268     */
269    public function getElasticaResultSet() {
270        return $this->result;
271    }
272
273    /**
274     * Count elements of an object
275     * @link https://php.net/manual/en/countable.count.php
276     * @return int The custom count as an integer.
277     * @since 5.1.0
278     */
279    public function count(): int {
280        return count( $this->extractResults() );
281    }
282
283    /**
284     * @return int
285     */
286    public function numRows() {
287        return $this->count();
288    }
289
290    /**
291     * Did the search contain search syntax?  If so, Special:Search won't offer
292     * the user a link to a create a page named by the search string because the
293     * name would contain the search syntax.
294     * @return bool
295     */
296    public function searchContainedSyntax() {
297        return $this->searchContainedSyntax;
298    }
299
300    /**
301     * @return bool True when there are more pages of search results available.
302     */
303    public function hasMoreResults() {
304        return $this->hasMoreResults;
305    }
306
307    /**
308     * @param int $limit Shrink result set to $limit and flag
309     *  if more results are available.
310     */
311    public function shrink( $limit ) {
312        if ( $this->count() > $limit ) {
313            Assert::precondition( $this->results !== null, "results not initialized" );
314            $this->results = array_slice( $this->results, 0, $limit );
315            $this->hasMoreResults = true;
316        }
317    }
318
319    /**
320     * Extract all the titles in the result set.
321     * @return Title[]
322     */
323    public function extractTitles() {
324        return array_map(
325            static function ( SearchResult $result ) {
326                return $result->getTitle();
327            },
328            $this->extractResults() );
329    }
330}