5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
27 class SearchResultSet
implements Countable
, IteratorAggregate
{
30 * Identifier for interwiki results that are displayed only together with existing main wiki
33 const SECONDARY_RESULTS
= 0;
36 * Identifier for interwiki results that can be displayed even if no existing main wiki results
39 const INLINE_RESULTS
= 1;
41 protected $containedSyntax = false;
45 * Lists titles of the result set, in the same order as results.
51 * Cache of results - serialization of the result iterator
58 * Set of result's extra data, indexed per result id
59 * and then per data item name.
61 * PAGE_ID => [ augmentor name => data, ... ]
64 protected $extraData = [];
67 * @var boolean True when there are more pages of search results available.
69 private $hasMoreResults;
72 * @var ArrayIterator|null Iterator supporting BC iteration methods
77 * @param bool $containedSyntax True when query is not requesting a simple
79 * @param bool $hasMoreResults True when there are more pages of search
82 public function __construct( $containedSyntax = false, $hasMoreResults = false ) {
83 if ( static::class === self
::class ) {
84 // This class will eventually be abstract. SearchEngine implementations
85 // already have to extend this class anyways to provide the actual
87 wfDeprecated( __METHOD__
, 1.32 );
89 $this->containedSyntax
= $containedSyntax;
90 $this->hasMoreResults
= $hasMoreResults;
94 * Fetch an array of regular expression fragments for matching
95 * the search terms as parsed by this engine in a text extract.
100 function termMatches() {
105 return $this->count();
108 final public function count() {
109 return count( $this->extractResults() );
113 * Some search modes return a total hit count for the query
114 * in the entire article database. This may include pages
115 * in namespaces that would not be matched on the given
118 * Return null if no total hits number is supported.
122 function getTotalHits() {
127 * Some search modes will run an alternative query that it thinks gives
128 * a better result than the provided search. Returns true if this has
133 function hasRewrittenQuery() {
138 * @return string|null The search the query was internally rewritten to,
139 * or null when the result of the original query was returned.
141 function getQueryAfterRewrite() {
146 * @return string|null Same as self::getQueryAfterRewrite(), but in HTML
147 * and with changes highlighted. Null when the query was not rewritten.
149 function getQueryAfterRewriteSnippet() {
154 * Some search modes return a suggested alternate term if there are
155 * no exact hits. Returns true if there is one on this set.
159 function hasSuggestion() {
164 * @return string|null Suggested query, null if none
166 function getSuggestionQuery() {
171 * @return string HTML highlighted suggested query, '' if none
173 function getSuggestionSnippet() {
178 * Return a result set of hits on other (multiple) wikis associated with this one
181 * @return SearchResultSet[]
183 function getInterwikiResults( $type = self
::SECONDARY_RESULTS
) {
188 * Check if there are results on other wikis
193 function hasInterwikiResults( $type = self
::SECONDARY_RESULTS
) {
198 * Fetches next search result, or false.
199 * @deprecated since 1.32; Use self::extractResults() or foreach
200 * @return SearchResult|false
202 public function next() {
203 wfDeprecated( __METHOD__
, '1.32' );
204 $it = $this->bcIterator();
205 $searchResult = $it->current();
207 return $searchResult ??
false;
211 * Rewind result set back to beginning
212 * @deprecated since 1.32; Use self::extractResults() or foreach
214 public function rewind() {
215 wfDeprecated( __METHOD__
, '1.32' );
216 $this->bcIterator()->rewind();
219 private function bcIterator() {
220 if ( $this->bcIterator
=== null ) {
221 $this->bcIterator
= 'RECURSION';
222 $this->bcIterator
= $this->getIterator();
223 } elseif ( $this->bcIterator
=== 'RECURSION' ) {
224 // Either next/rewind or extractResults must be implemented. This
225 // class was potentially instantiated directly. It should be
226 // abstract with abstract methods to enforce this but that's a
227 // breaking change...
228 wfDeprecated( static::class . ' without implementing extractResults', '1.32' );
229 $this->bcIterator
= new ArrayIterator( [] );
231 return $this->bcIterator
;
235 * Frees the result set, if applicable.
242 * Did the search contain search syntax? If so, Special:Search won't offer
243 * the user a link to a create a page named by the search string because the
244 * name would contain the search syntax.
247 public function searchContainedSyntax() {
248 return $this->containedSyntax
;
252 * @return bool True when there are more pages of search results available.
254 public function hasMoreResults() {
255 return $this->hasMoreResults
;
259 * @param int $limit Shrink result set to $limit and flag
260 * if more results are available.
262 public function shrink( $limit ) {
263 if ( $this->count() > $limit ) {
264 $this->hasMoreResults
= true;
265 // shrinking result set for implementations that
266 // have not implemented extractResults and use
267 // the default cache location. Other implementations
268 // must override this as well.
269 if ( is_array( $this->results
) ) {
270 $this->results
= array_slice( $this->results
, 0, $limit );
272 throw new \
UnexpectedValueException(
273 "When overriding result store extending classes must "
274 . " also override " . __METHOD__
);
280 * Extract all the results in the result set as array.
281 * @return SearchResult[]
283 public function extractResults() {
284 if ( is_null( $this->results
) ) {
286 if ( $this->numRows() == 0 ) {
287 // Don't bother if we've got empty result
288 return $this->results
;
291 while ( ( $result = $this->next() ) != false ) {
292 $this->results
[] = $result;
296 return $this->results
;
300 * Extract all the titles in the result set.
303 public function extractTitles() {
304 if ( is_null( $this->titles
) ) {
305 if ( $this->numRows() == 0 ) {
306 // Don't bother if we've got empty result
309 $this->titles
= array_map(
310 function ( SearchResult
$result ) {
311 return $result->getTitle();
313 $this->extractResults() );
316 return $this->titles
;
320 * Sets augmented data for result set.
321 * @param string $name Extra data item name
322 * @param array[] $data Extra data as PAGEID => data
324 public function setAugmentedData( $name, $data ) {
325 foreach ( $data as $id => $resultData ) {
326 $this->extraData
[$id][$name] = $resultData;
331 * Returns extra data for specific result and store it in SearchResult object.
332 * @param SearchResult $result
334 public function augmentResult( SearchResult
$result ) {
335 $id = $result->getTitle()->getArticleID();
339 $result->setExtensionData( function () use ( $id ) {
340 return $this->extraData
[$id] ??
[];
345 * @return int|null The offset the current page starts at. Typically
346 * this should be null to allow the UI to decide on its own, but in
347 * special cases like interleaved AB tests specifying explicitly is
350 public function getOffset() {
354 final public function getIterator() {
355 return new ArrayIterator( $this->extractResults() );