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