Merge "Type hint against LinkTarget in WatchedItemStore"
[lhc/web/wiklou.git] / includes / search / SearchResultSet.php
1 <?php
2 /**
3 * Search result sets
4 *
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.
9 *
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.
14 *
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
19 *
20 * @file
21 * @ingroup Search
22 */
23
24 /**
25 * @ingroup Search
26 */
27 class SearchResultSet implements ISearchResultSet {
28
29 protected $containedSyntax = false;
30
31 /**
32 * Cache of titles.
33 * Lists titles of the result set, in the same order as results.
34 * @var Title[]
35 */
36 private $titles;
37
38 /**
39 * Cache of results - serialization of the result iterator
40 * as an array.
41 * @var SearchResult[]
42 */
43 protected $results;
44
45 /**
46 * Set of result's extra data, indexed per result id
47 * and then per data item name.
48 * The structure is:
49 * PAGE_ID => [ augmentor name => data, ... ]
50 * @var array[]
51 */
52 protected $extraData = [];
53
54 /**
55 * @var boolean True when there are more pages of search results available.
56 */
57 private $hasMoreResults;
58
59 /**
60 * @var ArrayIterator|null Iterator supporting BC iteration methods
61 */
62 private $bcIterator;
63
64 /**
65 * @param bool $containedSyntax True when query is not requesting a simple
66 * term match
67 * @param bool $hasMoreResults True when there are more pages of search
68 * results available.
69 */
70 public function __construct( $containedSyntax = false, $hasMoreResults = false ) {
71 if ( static::class === self::class ) {
72 // This class will eventually be abstract. SearchEngine implementations
73 // already have to extend this class anyways to provide the actual
74 // search results.
75 wfDeprecated( __METHOD__, '1.32' );
76 }
77 $this->containedSyntax = $containedSyntax;
78 $this->hasMoreResults = $hasMoreResults;
79 }
80
81 /**
82 * Fetch an array of regular expression fragments for matching
83 * the search terms as parsed by this engine in a text extract.
84 * STUB
85 *
86 * @return string[]
87 * @deprecated since 1.34 (use SqlSearchResult)
88 */
89 function termMatches() {
90 return [];
91 }
92
93 function numRows() {
94 return $this->count();
95 }
96
97 final public function count() {
98 return count( $this->extractResults() );
99 }
100
101 /**
102 * Some search modes return a total hit count for the query
103 * in the entire article database. This may include pages
104 * in namespaces that would not be matched on the given
105 * settings.
106 *
107 * Return null if no total hits number is supported.
108 *
109 * @return int
110 */
111 function getTotalHits() {
112 return null;
113 }
114
115 /**
116 * Some search modes will run an alternative query that it thinks gives
117 * a better result than the provided search. Returns true if this has
118 * occurred.
119 *
120 * @return bool
121 */
122 function hasRewrittenQuery() {
123 return false;
124 }
125
126 /**
127 * @return string|null The search the query was internally rewritten to,
128 * or null when the result of the original query was returned.
129 */
130 function getQueryAfterRewrite() {
131 return null;
132 }
133
134 /**
135 * @return string|null Same as self::getQueryAfterRewrite(), but in HTML
136 * and with changes highlighted. Null when the query was not rewritten.
137 */
138 function getQueryAfterRewriteSnippet() {
139 return null;
140 }
141
142 /**
143 * Some search modes return a suggested alternate term if there are
144 * no exact hits. Returns true if there is one on this set.
145 *
146 * @return bool
147 */
148 function hasSuggestion() {
149 return false;
150 }
151
152 /**
153 * @return string|null Suggested query, null if none
154 */
155 function getSuggestionQuery() {
156 return null;
157 }
158
159 /**
160 * @return string HTML highlighted suggested query, '' if none
161 */
162 function getSuggestionSnippet() {
163 return '';
164 }
165
166 /**
167 * Return a result set of hits on other (multiple) wikis associated with this one
168 *
169 * @param int $type
170 * @return ISearchResultSet[]
171 */
172 function getInterwikiResults( $type = self::SECONDARY_RESULTS ) {
173 return null;
174 }
175
176 /**
177 * Check if there are results on other wikis
178 *
179 * @param int $type
180 * @return bool
181 */
182 function hasInterwikiResults( $type = self::SECONDARY_RESULTS ) {
183 return false;
184 }
185
186 /**
187 * Fetches next search result, or false.
188 * @deprecated since 1.32; Use self::extractResults() or foreach
189 * @return SearchResult|false
190 */
191 public function next() {
192 wfDeprecated( __METHOD__, '1.32' );
193 $it = $this->bcIterator();
194 $searchResult = $it->current();
195 $it->next();
196 return $searchResult ?? false;
197 }
198
199 /**
200 * Rewind result set back to beginning
201 * @deprecated since 1.32; Use self::extractResults() or foreach
202 */
203 public function rewind() {
204 wfDeprecated( __METHOD__, '1.32' );
205 $this->bcIterator()->rewind();
206 }
207
208 private function bcIterator() {
209 if ( $this->bcIterator === null ) {
210 $this->bcIterator = 'RECURSION';
211 $this->bcIterator = $this->getIterator();
212 } elseif ( $this->bcIterator === 'RECURSION' ) {
213 // Either next/rewind or extractResults must be implemented. This
214 // class was potentially instantiated directly. It should be
215 // abstract with abstract methods to enforce this but that's a
216 // breaking change...
217 wfDeprecated( static::class . ' without implementing extractResults', '1.32' );
218 $this->bcIterator = new ArrayIterator( [] );
219 }
220 return $this->bcIterator;
221 }
222
223 /**
224 * Frees the result set, if applicable.
225 * @deprecated noop since 1.34
226 */
227 function free() {
228 }
229
230 /**
231 * Did the search contain search syntax? If so, Special:Search won't offer
232 * the user a link to a create a page named by the search string because the
233 * name would contain the search syntax.
234 * @return bool
235 */
236 public function searchContainedSyntax() {
237 return $this->containedSyntax;
238 }
239
240 /**
241 * @return bool True when there are more pages of search results available.
242 */
243 public function hasMoreResults() {
244 return $this->hasMoreResults;
245 }
246
247 /**
248 * @param int $limit Shrink result set to $limit and flag
249 * if more results are available.
250 */
251 public function shrink( $limit ) {
252 if ( $this->count() > $limit ) {
253 $this->hasMoreResults = true;
254 // shrinking result set for implementations that
255 // have not implemented extractResults and use
256 // the default cache location. Other implementations
257 // must override this as well.
258 if ( is_array( $this->results ) ) {
259 $this->results = array_slice( $this->results, 0, $limit );
260 } else {
261 throw new \UnexpectedValueException(
262 "When overriding result store extending classes must "
263 . " also override " . __METHOD__ );
264 }
265 }
266 }
267
268 /**
269 * Extract all the results in the result set as array.
270 * @return SearchResult[]
271 */
272 public function extractResults() {
273 if ( is_null( $this->results ) ) {
274 $this->results = [];
275 if ( $this->numRows() == 0 ) {
276 // Don't bother if we've got empty result
277 return $this->results;
278 }
279 $this->rewind();
280 while ( ( $result = $this->next() ) != false ) {
281 $this->results[] = $result;
282 }
283 $this->rewind();
284 }
285 return $this->results;
286 }
287
288 /**
289 * Extract all the titles in the result set.
290 * @return Title[]
291 */
292 public function extractTitles() {
293 if ( is_null( $this->titles ) ) {
294 if ( $this->numRows() == 0 ) {
295 // Don't bother if we've got empty result
296 $this->titles = [];
297 } else {
298 $this->titles = array_map(
299 function ( SearchResult $result ) {
300 return $result->getTitle();
301 },
302 $this->extractResults() );
303 }
304 }
305 return $this->titles;
306 }
307
308 /**
309 * Sets augmented data for result set.
310 * @param string $name Extra data item name
311 * @param array[] $data Extra data as PAGEID => data
312 */
313 public function setAugmentedData( $name, $data ) {
314 foreach ( $data as $id => $resultData ) {
315 $this->extraData[$id][$name] = $resultData;
316 }
317 }
318
319 /**
320 * Returns extra data for specific result and store it in SearchResult object.
321 * @param SearchResult $result
322 */
323 public function augmentResult( SearchResult $result ) {
324 $id = $result->getTitle()->getArticleID();
325 if ( $id === -1 ) {
326 return;
327 }
328 $result->setExtensionData( function () use ( $id ) {
329 return $this->extraData[$id] ?? [];
330 } );
331 }
332
333 /**
334 * @return int|null The offset the current page starts at. Typically
335 * this should be null to allow the UI to decide on its own, but in
336 * special cases like interleaved AB tests specifying explicitly is
337 * necessary.
338 */
339 public function getOffset() {
340 return null;
341 }
342
343 final public function getIterator() {
344 return new ArrayIterator( $this->extractResults() );
345 }
346 }