|
1
|
|
|
<?php |
|
2
|
|
|
/** |
|
3
|
|
|
* Basic search engine |
|
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
|
|
|
* @defgroup Search Search |
|
26
|
|
|
*/ |
|
27
|
|
|
|
|
28
|
|
|
use MediaWiki\MediaWikiServices; |
|
29
|
|
|
|
|
30
|
|
|
/** |
|
31
|
|
|
* Contain a class for special pages |
|
32
|
|
|
* @ingroup Search |
|
33
|
|
|
*/ |
|
34
|
|
|
abstract class SearchEngine { |
|
35
|
|
|
/** @var string */ |
|
36
|
|
|
public $prefix = ''; |
|
37
|
|
|
|
|
38
|
|
|
/** @var int[]|null */ |
|
39
|
|
|
public $namespaces = [ NS_MAIN ]; |
|
40
|
|
|
|
|
41
|
|
|
/** @var int */ |
|
42
|
|
|
protected $limit = 10; |
|
43
|
|
|
|
|
44
|
|
|
/** @var int */ |
|
45
|
|
|
protected $offset = 0; |
|
46
|
|
|
|
|
47
|
|
|
/** @var array|string */ |
|
48
|
|
|
protected $searchTerms = []; |
|
49
|
|
|
|
|
50
|
|
|
/** @var bool */ |
|
51
|
|
|
protected $showSuggestion = true; |
|
52
|
|
|
private $sort = 'relevance'; |
|
53
|
|
|
|
|
54
|
|
|
/** @var array Feature values */ |
|
55
|
|
|
protected $features = []; |
|
56
|
|
|
|
|
57
|
|
|
/** @const string profile type for completionSearch */ |
|
58
|
|
|
const COMPLETION_PROFILE_TYPE = 'completionSearchProfile'; |
|
59
|
|
|
|
|
60
|
|
|
/** @const string profile type for query independent ranking features */ |
|
61
|
|
|
const FT_QUERY_INDEP_PROFILE_TYPE = 'fulltextQueryIndepProfile'; |
|
62
|
|
|
|
|
63
|
|
|
/** |
|
64
|
|
|
* Perform a full text search query and return a result set. |
|
65
|
|
|
* If full text searches are not supported or disabled, return null. |
|
66
|
|
|
* STUB |
|
67
|
|
|
* |
|
68
|
|
|
* @param string $term Raw search term |
|
69
|
|
|
* @return SearchResultSet|Status|null |
|
70
|
|
|
*/ |
|
71
|
|
|
function searchText( $term ) { |
|
72
|
|
|
return null; |
|
73
|
|
|
} |
|
74
|
|
|
|
|
75
|
|
|
/** |
|
76
|
|
|
* Perform a title-only search query and return a result set. |
|
77
|
|
|
* If title searches are not supported or disabled, return null. |
|
78
|
|
|
* STUB |
|
79
|
|
|
* |
|
80
|
|
|
* @param string $term Raw search term |
|
81
|
|
|
* @return SearchResultSet|null |
|
82
|
|
|
*/ |
|
83
|
|
|
function searchTitle( $term ) { |
|
84
|
|
|
return null; |
|
85
|
|
|
} |
|
86
|
|
|
|
|
87
|
|
|
/** |
|
88
|
|
|
* @since 1.18 |
|
89
|
|
|
* @param string $feature |
|
90
|
|
|
* @return bool |
|
91
|
|
|
*/ |
|
92
|
|
|
public function supports( $feature ) { |
|
93
|
|
|
switch ( $feature ) { |
|
94
|
|
|
case 'search-update': |
|
95
|
|
|
return true; |
|
96
|
|
|
case 'title-suffix-filter': |
|
97
|
|
|
default: |
|
98
|
|
|
return false; |
|
99
|
|
|
} |
|
100
|
|
|
} |
|
101
|
|
|
|
|
102
|
|
|
/** |
|
103
|
|
|
* Way to pass custom data for engines |
|
104
|
|
|
* @since 1.18 |
|
105
|
|
|
* @param string $feature |
|
106
|
|
|
* @param mixed $data |
|
107
|
|
|
* @return bool |
|
108
|
|
|
*/ |
|
109
|
|
|
public function setFeatureData( $feature, $data ) { |
|
110
|
|
|
$this->features[$feature] = $data; |
|
111
|
|
|
} |
|
112
|
|
|
|
|
113
|
|
|
/** |
|
114
|
|
|
* When overridden in derived class, performs database-specific conversions |
|
115
|
|
|
* on text to be used for searching or updating search index. |
|
116
|
|
|
* Default implementation does nothing (simply returns $string). |
|
117
|
|
|
* |
|
118
|
|
|
* @param string $string String to process |
|
119
|
|
|
* @return string |
|
120
|
|
|
*/ |
|
121
|
|
|
public function normalizeText( $string ) { |
|
122
|
|
|
global $wgContLang; |
|
123
|
|
|
|
|
124
|
|
|
// Some languages such as Chinese require word segmentation |
|
125
|
|
|
return $wgContLang->segmentByWord( $string ); |
|
126
|
|
|
} |
|
127
|
|
|
|
|
128
|
|
|
/** |
|
129
|
|
|
* Transform search term in cases when parts of the query came as different |
|
130
|
|
|
* GET params (when supported), e.g. for prefix queries: |
|
131
|
|
|
* search=test&prefix=Main_Page/Archive -> test prefix:Main Page/Archive |
|
132
|
|
|
* @param string $term |
|
133
|
|
|
* @return string |
|
134
|
|
|
*/ |
|
135
|
|
|
public function transformSearchTerm( $term ) { |
|
136
|
|
|
return $term; |
|
137
|
|
|
} |
|
138
|
|
|
|
|
139
|
|
|
/** |
|
140
|
|
|
* Get service class to finding near matches. |
|
141
|
|
|
* @param Config $config Configuration to use for the matcher. |
|
142
|
|
|
* @return SearchNearMatcher |
|
143
|
|
|
*/ |
|
144
|
|
|
public function getNearMatcher( Config $config ) { |
|
145
|
|
|
global $wgContLang; |
|
146
|
|
|
return new SearchNearMatcher( $config, $wgContLang ); |
|
147
|
|
|
} |
|
148
|
|
|
|
|
149
|
|
|
/** |
|
150
|
|
|
* Get near matcher for default SearchEngine. |
|
151
|
|
|
* @return SearchNearMatcher |
|
152
|
|
|
*/ |
|
153
|
|
|
protected static function defaultNearMatcher() { |
|
154
|
|
|
$config = MediaWikiServices::getInstance()->getMainConfig(); |
|
155
|
|
|
return MediaWikiServices::getInstance()->newSearchEngine()->getNearMatcher( $config ); |
|
156
|
|
|
} |
|
157
|
|
|
|
|
158
|
|
|
/** |
|
159
|
|
|
* If an exact title match can be found, or a very slightly close match, |
|
160
|
|
|
* return the title. If no match, returns NULL. |
|
161
|
|
|
* @deprecated since 1.27; Use SearchEngine::getNearMatcher() |
|
162
|
|
|
* @param string $searchterm |
|
163
|
|
|
* @return Title |
|
164
|
|
|
*/ |
|
165
|
|
|
public static function getNearMatch( $searchterm ) { |
|
166
|
|
|
return static::defaultNearMatcher()->getNearMatch( $searchterm ); |
|
167
|
|
|
} |
|
168
|
|
|
|
|
169
|
|
|
/** |
|
170
|
|
|
* Do a near match (see SearchEngine::getNearMatch) and wrap it into a |
|
171
|
|
|
* SearchResultSet. |
|
172
|
|
|
* @deprecated since 1.27; Use SearchEngine::getNearMatcher() |
|
173
|
|
|
* @param string $searchterm |
|
174
|
|
|
* @return SearchResultSet |
|
175
|
|
|
*/ |
|
176
|
|
|
public static function getNearMatchResultSet( $searchterm ) { |
|
177
|
|
|
return static::defaultNearMatcher()->getNearMatchResultSet( $searchterm ); |
|
178
|
|
|
} |
|
179
|
|
|
|
|
180
|
|
|
/** |
|
181
|
|
|
* Get chars legal for search. |
|
182
|
|
|
* NOTE: usage as static is deprecated and preserved only as BC measure |
|
183
|
|
|
* @return string |
|
184
|
|
|
*/ |
|
185
|
|
|
public static function legalSearchChars() { |
|
186
|
|
|
return "A-Za-z_'.0-9\\x80-\\xFF\\-"; |
|
187
|
|
|
} |
|
188
|
|
|
|
|
189
|
|
|
/** |
|
190
|
|
|
* Set the maximum number of results to return |
|
191
|
|
|
* and how many to skip before returning the first. |
|
192
|
|
|
* |
|
193
|
|
|
* @param int $limit |
|
194
|
|
|
* @param int $offset |
|
195
|
|
|
*/ |
|
196
|
|
|
function setLimitOffset( $limit, $offset = 0 ) { |
|
197
|
|
|
$this->limit = intval( $limit ); |
|
198
|
|
|
$this->offset = intval( $offset ); |
|
199
|
|
|
} |
|
200
|
|
|
|
|
201
|
|
|
/** |
|
202
|
|
|
* Set which namespaces the search should include. |
|
203
|
|
|
* Give an array of namespace index numbers. |
|
204
|
|
|
* |
|
205
|
|
|
* @param int[]|null $namespaces |
|
206
|
|
|
*/ |
|
207
|
|
|
function setNamespaces( $namespaces ) { |
|
208
|
|
|
if ( $namespaces ) { |
|
209
|
|
|
// Filter namespaces to only keep valid ones |
|
210
|
|
|
$validNs = $this->searchableNamespaces(); |
|
|
|
|
|
|
211
|
|
|
$namespaces = array_filter( $namespaces, function( $ns ) use( $validNs ) { |
|
212
|
|
|
return $ns < 0 || isset( $validNs[$ns] ); |
|
213
|
|
|
} ); |
|
214
|
|
|
} else { |
|
215
|
|
|
$namespaces = []; |
|
216
|
|
|
} |
|
217
|
|
|
$this->namespaces = $namespaces; |
|
218
|
|
|
} |
|
219
|
|
|
|
|
220
|
|
|
/** |
|
221
|
|
|
* Set whether the searcher should try to build a suggestion. Note: some searchers |
|
222
|
|
|
* don't support building a suggestion in the first place and others don't respect |
|
223
|
|
|
* this flag. |
|
224
|
|
|
* |
|
225
|
|
|
* @param bool $showSuggestion Should the searcher try to build suggestions |
|
226
|
|
|
*/ |
|
227
|
|
|
function setShowSuggestion( $showSuggestion ) { |
|
228
|
|
|
$this->showSuggestion = $showSuggestion; |
|
229
|
|
|
} |
|
230
|
|
|
|
|
231
|
|
|
/** |
|
232
|
|
|
* Get the valid sort directions. All search engines support 'relevance' but others |
|
233
|
|
|
* might support more. The default in all implementations should be 'relevance.' |
|
234
|
|
|
* |
|
235
|
|
|
* @since 1.25 |
|
236
|
|
|
* @return array(string) the valid sort directions for setSort |
|
|
|
|
|
|
237
|
|
|
*/ |
|
238
|
|
|
public function getValidSorts() { |
|
239
|
|
|
return [ 'relevance' ]; |
|
240
|
|
|
} |
|
241
|
|
|
|
|
242
|
|
|
/** |
|
243
|
|
|
* Set the sort direction of the search results. Must be one returned by |
|
244
|
|
|
* SearchEngine::getValidSorts() |
|
245
|
|
|
* |
|
246
|
|
|
* @since 1.25 |
|
247
|
|
|
* @throws InvalidArgumentException |
|
248
|
|
|
* @param string $sort sort direction for query result |
|
249
|
|
|
*/ |
|
250
|
|
|
public function setSort( $sort ) { |
|
251
|
|
|
if ( !in_array( $sort, $this->getValidSorts() ) ) { |
|
252
|
|
|
throw new InvalidArgumentException( "Invalid sort: $sort. " . |
|
253
|
|
|
"Must be one of: " . implode( ', ', $this->getValidSorts() ) ); |
|
254
|
|
|
} |
|
255
|
|
|
$this->sort = $sort; |
|
256
|
|
|
} |
|
257
|
|
|
|
|
258
|
|
|
/** |
|
259
|
|
|
* Get the sort direction of the search results |
|
260
|
|
|
* |
|
261
|
|
|
* @since 1.25 |
|
262
|
|
|
* @return string |
|
263
|
|
|
*/ |
|
264
|
|
|
public function getSort() { |
|
265
|
|
|
return $this->sort; |
|
266
|
|
|
} |
|
267
|
|
|
|
|
268
|
|
|
/** |
|
269
|
|
|
* Parse some common prefixes: all (search everything) |
|
270
|
|
|
* or namespace names |
|
271
|
|
|
* |
|
272
|
|
|
* @param string $query |
|
273
|
|
|
* @return string |
|
274
|
|
|
*/ |
|
275
|
|
|
function replacePrefixes( $query ) { |
|
276
|
|
|
global $wgContLang; |
|
277
|
|
|
|
|
278
|
|
|
$parsed = $query; |
|
279
|
|
|
if ( strpos( $query, ':' ) === false ) { // nothing to do |
|
280
|
|
|
return $parsed; |
|
281
|
|
|
} |
|
282
|
|
|
|
|
283
|
|
|
$allkeyword = wfMessage( 'searchall' )->inContentLanguage()->text() . ":"; |
|
284
|
|
|
if ( strncmp( $query, $allkeyword, strlen( $allkeyword ) ) == 0 ) { |
|
285
|
|
|
$this->namespaces = null; |
|
286
|
|
|
$parsed = substr( $query, strlen( $allkeyword ) ); |
|
287
|
|
|
} elseif ( strpos( $query, ':' ) !== false ) { |
|
288
|
|
|
$prefix = str_replace( ' ', '_', substr( $query, 0, strpos( $query, ':' ) ) ); |
|
289
|
|
|
$index = $wgContLang->getNsIndex( $prefix ); |
|
290
|
|
|
if ( $index !== false ) { |
|
291
|
|
|
$this->namespaces = [ $index ]; |
|
292
|
|
|
$parsed = substr( $query, strlen( $prefix ) + 1 ); |
|
293
|
|
|
} |
|
294
|
|
|
} |
|
295
|
|
|
if ( trim( $parsed ) == '' ) { |
|
296
|
|
|
$parsed = $query; // prefix was the whole query |
|
297
|
|
|
} |
|
298
|
|
|
|
|
299
|
|
|
return $parsed; |
|
300
|
|
|
} |
|
301
|
|
|
|
|
302
|
|
|
/** |
|
303
|
|
|
* Find snippet highlight settings for all users |
|
304
|
|
|
* @return array Contextlines, contextchars |
|
305
|
|
|
*/ |
|
306
|
|
|
public static function userHighlightPrefs() { |
|
307
|
|
|
$contextlines = 2; // Hardcode this. Old defaults sucked. :) |
|
308
|
|
|
$contextchars = 75; // same as above.... :P |
|
309
|
|
|
return [ $contextlines, $contextchars ]; |
|
310
|
|
|
} |
|
311
|
|
|
|
|
312
|
|
|
/** |
|
313
|
|
|
* Create or update the search index record for the given page. |
|
314
|
|
|
* Title and text should be pre-processed. |
|
315
|
|
|
* STUB |
|
316
|
|
|
* |
|
317
|
|
|
* @param int $id |
|
318
|
|
|
* @param string $title |
|
319
|
|
|
* @param string $text |
|
320
|
|
|
*/ |
|
321
|
|
|
function update( $id, $title, $text ) { |
|
322
|
|
|
// no-op |
|
323
|
|
|
} |
|
324
|
|
|
|
|
325
|
|
|
/** |
|
326
|
|
|
* Update a search index record's title only. |
|
327
|
|
|
* Title should be pre-processed. |
|
328
|
|
|
* STUB |
|
329
|
|
|
* |
|
330
|
|
|
* @param int $id |
|
331
|
|
|
* @param string $title |
|
332
|
|
|
*/ |
|
333
|
|
|
function updateTitle( $id, $title ) { |
|
334
|
|
|
// no-op |
|
335
|
|
|
} |
|
336
|
|
|
|
|
337
|
|
|
/** |
|
338
|
|
|
* Delete an indexed page |
|
339
|
|
|
* Title should be pre-processed. |
|
340
|
|
|
* STUB |
|
341
|
|
|
* |
|
342
|
|
|
* @param int $id Page id that was deleted |
|
343
|
|
|
* @param string $title Title of page that was deleted |
|
344
|
|
|
*/ |
|
345
|
|
|
function delete( $id, $title ) { |
|
346
|
|
|
// no-op |
|
347
|
|
|
} |
|
348
|
|
|
|
|
349
|
|
|
/** |
|
350
|
|
|
* Get OpenSearch suggestion template |
|
351
|
|
|
* |
|
352
|
|
|
* @deprecated since 1.25 |
|
353
|
|
|
* @return string |
|
354
|
|
|
*/ |
|
355
|
|
|
public static function getOpenSearchTemplate() { |
|
356
|
|
|
wfDeprecated( __METHOD__, '1.25' ); |
|
357
|
|
|
return ApiOpenSearch::getOpenSearchTemplate( 'application/x-suggestions+json' ); |
|
358
|
|
|
} |
|
359
|
|
|
|
|
360
|
|
|
/** |
|
361
|
|
|
* Get the raw text for updating the index from a content object |
|
362
|
|
|
* Nicer search backends could possibly do something cooler than |
|
363
|
|
|
* just returning raw text |
|
364
|
|
|
* |
|
365
|
|
|
* @todo This isn't ideal, we'd really like to have content-specific handling here |
|
366
|
|
|
* @param Title $t Title we're indexing |
|
367
|
|
|
* @param Content $c Content of the page to index |
|
368
|
|
|
* @return string |
|
369
|
|
|
*/ |
|
370
|
|
|
public function getTextFromContent( Title $t, Content $c = null ) { |
|
371
|
|
|
return $c ? $c->getTextForSearchIndex() : ''; |
|
372
|
|
|
} |
|
373
|
|
|
|
|
374
|
|
|
/** |
|
375
|
|
|
* If an implementation of SearchEngine handles all of its own text processing |
|
376
|
|
|
* in getTextFromContent() and doesn't require SearchUpdate::updateText()'s |
|
377
|
|
|
* rather silly handling, it should return true here instead. |
|
378
|
|
|
* |
|
379
|
|
|
* @return bool |
|
380
|
|
|
*/ |
|
381
|
|
|
public function textAlreadyUpdatedForIndex() { |
|
382
|
|
|
return false; |
|
383
|
|
|
} |
|
384
|
|
|
|
|
385
|
|
|
/** |
|
386
|
|
|
* Makes search simple string if it was namespaced. |
|
387
|
|
|
* Sets namespaces of the search to namespaces extracted from string. |
|
388
|
|
|
* @param string $search |
|
389
|
|
|
* @return string Simplified search string |
|
390
|
|
|
*/ |
|
391
|
|
|
protected function normalizeNamespaces( $search ) { |
|
392
|
|
|
// Find a Title which is not an interwiki and is in NS_MAIN |
|
393
|
|
|
$title = Title::newFromText( $search ); |
|
394
|
|
|
$ns = $this->namespaces; |
|
395
|
|
|
if ( $title && !$title->isExternal() ) { |
|
396
|
|
|
$ns = [ $title->getNamespace() ]; |
|
397
|
|
|
$search = $title->getText(); |
|
398
|
|
View Code Duplication |
if ( $ns[0] == NS_MAIN ) { |
|
399
|
|
|
$ns = $this->namespaces; // no explicit prefix, use default namespaces |
|
400
|
|
|
Hooks::run( 'PrefixSearchExtractNamespace', [ &$ns, &$search ] ); |
|
401
|
|
|
} |
|
402
|
|
|
} else { |
|
403
|
|
|
$title = Title::newFromText( $search . 'Dummy' ); |
|
404
|
|
View Code Duplication |
if ( $title && $title->getText() == 'Dummy' |
|
405
|
|
|
&& $title->getNamespace() != NS_MAIN |
|
406
|
|
|
&& !$title->isExternal() ) |
|
407
|
|
|
{ |
|
408
|
|
|
$ns = [ $title->getNamespace() ]; |
|
409
|
|
|
$search = ''; |
|
410
|
|
|
} else { |
|
411
|
|
|
Hooks::run( 'PrefixSearchExtractNamespace', [ &$ns, &$search ] ); |
|
412
|
|
|
} |
|
413
|
|
|
} |
|
414
|
|
|
|
|
415
|
|
|
$ns = array_map( function( $space ) { |
|
416
|
|
|
return $space == NS_MEDIA ? NS_FILE : $space; |
|
417
|
|
|
}, $ns ); |
|
418
|
|
|
|
|
419
|
|
|
$this->setNamespaces( $ns ); |
|
420
|
|
|
return $search; |
|
421
|
|
|
} |
|
422
|
|
|
|
|
423
|
|
|
/** |
|
424
|
|
|
* Perform a completion search. |
|
425
|
|
|
* Does not resolve namespaces and does not check variants. |
|
426
|
|
|
* Search engine implementations may want to override this function. |
|
427
|
|
|
* @param string $search |
|
428
|
|
|
* @return SearchSuggestionSet |
|
429
|
|
|
*/ |
|
430
|
|
|
protected function completionSearchBackend( $search ) { |
|
431
|
|
|
$results = []; |
|
432
|
|
|
|
|
433
|
|
|
$search = trim( $search ); |
|
434
|
|
|
|
|
435
|
|
|
if ( !in_array( NS_SPECIAL, $this->namespaces ) && // We do not run hook on Special: search |
|
436
|
|
|
!Hooks::run( 'PrefixSearchBackend', |
|
437
|
|
|
[ $this->namespaces, $search, $this->limit, &$results, $this->offset ] |
|
438
|
|
|
) ) { |
|
439
|
|
|
// False means hook worked. |
|
440
|
|
|
// FIXME: Yes, the API is weird. That's why it is going to be deprecated. |
|
441
|
|
|
|
|
442
|
|
|
return SearchSuggestionSet::fromStrings( $results ); |
|
443
|
|
|
} else { |
|
444
|
|
|
// Hook did not do the job, use default simple search |
|
445
|
|
|
$results = $this->simplePrefixSearch( $search ); |
|
446
|
|
|
return SearchSuggestionSet::fromTitles( $results ); |
|
447
|
|
|
} |
|
448
|
|
|
} |
|
449
|
|
|
|
|
450
|
|
|
/** |
|
451
|
|
|
* Perform a completion search. |
|
452
|
|
|
* @param string $search |
|
453
|
|
|
* @return SearchSuggestionSet |
|
454
|
|
|
*/ |
|
455
|
|
|
public function completionSearch( $search ) { |
|
456
|
|
|
if ( trim( $search ) === '' ) { |
|
457
|
|
|
return SearchSuggestionSet::emptySuggestionSet(); // Return empty result |
|
458
|
|
|
} |
|
459
|
|
|
$search = $this->normalizeNamespaces( $search ); |
|
460
|
|
|
return $this->processCompletionResults( $search, $this->completionSearchBackend( $search ) ); |
|
461
|
|
|
} |
|
462
|
|
|
|
|
463
|
|
|
/** |
|
464
|
|
|
* Perform a completion search with variants. |
|
465
|
|
|
* @param string $search |
|
466
|
|
|
* @return SearchSuggestionSet |
|
467
|
|
|
*/ |
|
468
|
|
|
public function completionSearchWithVariants( $search ) { |
|
469
|
|
|
if ( trim( $search ) === '' ) { |
|
470
|
|
|
return SearchSuggestionSet::emptySuggestionSet(); // Return empty result |
|
471
|
|
|
} |
|
472
|
|
|
$search = $this->normalizeNamespaces( $search ); |
|
473
|
|
|
|
|
474
|
|
|
$results = $this->completionSearchBackend( $search ); |
|
475
|
|
|
$fallbackLimit = $this->limit - $results->getSize(); |
|
476
|
|
|
if ( $fallbackLimit > 0 ) { |
|
477
|
|
|
global $wgContLang; |
|
478
|
|
|
|
|
479
|
|
|
$fallbackSearches = $wgContLang->autoConvertToAllVariants( $search ); |
|
480
|
|
|
$fallbackSearches = array_diff( array_unique( $fallbackSearches ), [ $search ] ); |
|
481
|
|
|
|
|
482
|
|
|
foreach ( $fallbackSearches as $fbs ) { |
|
483
|
|
|
$this->setLimitOffset( $fallbackLimit ); |
|
484
|
|
|
$fallbackSearchResult = $this->completionSearch( $fbs ); |
|
485
|
|
|
$results->appendAll( $fallbackSearchResult ); |
|
486
|
|
|
$fallbackLimit -= count( $fallbackSearchResult ); |
|
487
|
|
|
if ( $fallbackLimit <= 0 ) { |
|
488
|
|
|
break; |
|
489
|
|
|
} |
|
490
|
|
|
} |
|
491
|
|
|
} |
|
492
|
|
|
return $this->processCompletionResults( $search, $results ); |
|
493
|
|
|
} |
|
494
|
|
|
|
|
495
|
|
|
/** |
|
496
|
|
|
* Extract titles from completion results |
|
497
|
|
|
* @param SearchSuggestionSet $completionResults |
|
498
|
|
|
* @return Title[] |
|
499
|
|
|
*/ |
|
500
|
|
|
public function extractTitles( SearchSuggestionSet $completionResults ) { |
|
501
|
|
|
return $completionResults->map( function( SearchSuggestion $sugg ) { |
|
502
|
|
|
return $sugg->getSuggestedTitle(); |
|
503
|
|
|
} ); |
|
504
|
|
|
} |
|
505
|
|
|
|
|
506
|
|
|
/** |
|
507
|
|
|
* Process completion search results. |
|
508
|
|
|
* Resolves the titles and rescores. |
|
509
|
|
|
* @param SearchSuggestionSet $suggestions |
|
510
|
|
|
* @return SearchSuggestionSet |
|
511
|
|
|
*/ |
|
512
|
|
|
protected function processCompletionResults( $search, SearchSuggestionSet $suggestions ) { |
|
513
|
|
|
$search = trim( $search ); |
|
514
|
|
|
// preload the titles with LinkBatch |
|
515
|
|
|
$titles = $suggestions->map( function( SearchSuggestion $sugg ) { |
|
516
|
|
|
return $sugg->getSuggestedTitle(); |
|
517
|
|
|
} ); |
|
518
|
|
|
$lb = new LinkBatch( $titles ); |
|
519
|
|
|
$lb->setCaller( __METHOD__ ); |
|
520
|
|
|
$lb->execute(); |
|
521
|
|
|
|
|
522
|
|
|
$results = $suggestions->map( function( SearchSuggestion $sugg ) { |
|
523
|
|
|
return $sugg->getSuggestedTitle()->getPrefixedText(); |
|
524
|
|
|
} ); |
|
525
|
|
|
|
|
526
|
|
|
// Rescore results with an exact title match |
|
527
|
|
|
// NOTE: in some cases like cross-namespace redirects |
|
528
|
|
|
// (frequently used as shortcuts e.g. WP:WP on huwiki) some |
|
529
|
|
|
// backends like Cirrus will return no results. We should still |
|
530
|
|
|
// try an exact title match to workaround this limitation |
|
531
|
|
|
$rescorer = new SearchExactMatchRescorer(); |
|
532
|
|
|
$rescoredResults = $rescorer->rescore( $search, $this->namespaces, $results, $this->limit ); |
|
|
|
|
|
|
533
|
|
|
|
|
534
|
|
|
if ( count( $rescoredResults ) > 0 ) { |
|
535
|
|
|
$found = array_search( $rescoredResults[0], $results ); |
|
536
|
|
|
if ( $found === false ) { |
|
537
|
|
|
// If the first result is not in the previous array it |
|
538
|
|
|
// means that we found a new exact match |
|
539
|
|
|
$exactMatch = SearchSuggestion::fromTitle( 0, Title::newFromText( $rescoredResults[0] ) ); |
|
|
|
|
|
|
540
|
|
|
$suggestions->prepend( $exactMatch ); |
|
541
|
|
|
$suggestions->shrink( $this->limit ); |
|
542
|
|
|
} else { |
|
543
|
|
|
// if the first result is not the same we need to rescore |
|
544
|
|
|
if ( $found > 0 ) { |
|
545
|
|
|
$suggestions->rescore( $found ); |
|
546
|
|
|
} |
|
547
|
|
|
} |
|
548
|
|
|
} |
|
549
|
|
|
|
|
550
|
|
|
return $suggestions; |
|
551
|
|
|
} |
|
552
|
|
|
|
|
553
|
|
|
/** |
|
554
|
|
|
* Simple prefix search for subpages. |
|
555
|
|
|
* @param string $search |
|
556
|
|
|
* @return Title[] |
|
557
|
|
|
*/ |
|
558
|
|
|
public function defaultPrefixSearch( $search ) { |
|
559
|
|
|
if ( trim( $search ) === '' ) { |
|
560
|
|
|
return []; |
|
561
|
|
|
} |
|
562
|
|
|
|
|
563
|
|
|
$search = $this->normalizeNamespaces( $search ); |
|
564
|
|
|
return $this->simplePrefixSearch( $search ); |
|
565
|
|
|
} |
|
566
|
|
|
|
|
567
|
|
|
/** |
|
568
|
|
|
* Call out to simple search backend. |
|
569
|
|
|
* Defaults to TitlePrefixSearch. |
|
570
|
|
|
* @param string $search |
|
571
|
|
|
* @return Title[] |
|
572
|
|
|
*/ |
|
573
|
|
|
protected function simplePrefixSearch( $search ) { |
|
574
|
|
|
// Use default database prefix search |
|
575
|
|
|
$backend = new TitlePrefixSearch; |
|
|
|
|
|
|
576
|
|
|
return $backend->defaultSearchBackend( $this->namespaces, $search, $this->limit, $this->offset ); |
|
|
|
|
|
|
577
|
|
|
} |
|
578
|
|
|
|
|
579
|
|
|
/** |
|
580
|
|
|
* Make a list of searchable namespaces and their canonical names. |
|
581
|
|
|
* @deprecated since 1.27; use SearchEngineConfig::searchableNamespaces() |
|
582
|
|
|
* @return array |
|
583
|
|
|
*/ |
|
584
|
|
|
public static function searchableNamespaces() { |
|
585
|
|
|
return MediaWikiServices::getInstance()->getSearchEngineConfig()->searchableNamespaces(); |
|
586
|
|
|
} |
|
587
|
|
|
|
|
588
|
|
|
/** |
|
589
|
|
|
* Extract default namespaces to search from the given user's |
|
590
|
|
|
* settings, returning a list of index numbers. |
|
591
|
|
|
* @deprecated since 1.27; use SearchEngineConfig::userNamespaces() |
|
592
|
|
|
* @param user $user |
|
593
|
|
|
* @return array |
|
594
|
|
|
*/ |
|
595
|
|
|
public static function userNamespaces( $user ) { |
|
596
|
|
|
return MediaWikiServices::getInstance()->getSearchEngineConfig()->userNamespaces( $user ); |
|
597
|
|
|
} |
|
598
|
|
|
|
|
599
|
|
|
/** |
|
600
|
|
|
* An array of namespaces indexes to be searched by default |
|
601
|
|
|
* @deprecated since 1.27; use SearchEngineConfig::defaultNamespaces() |
|
602
|
|
|
* @return array |
|
603
|
|
|
*/ |
|
604
|
|
|
public static function defaultNamespaces() { |
|
605
|
|
|
return MediaWikiServices::getInstance()->getSearchEngineConfig()->defaultNamespaces(); |
|
606
|
|
|
} |
|
607
|
|
|
|
|
608
|
|
|
/** |
|
609
|
|
|
* Get a list of namespace names useful for showing in tooltips |
|
610
|
|
|
* and preferences |
|
611
|
|
|
* @deprecated since 1.27; use SearchEngineConfig::namespacesAsText() |
|
612
|
|
|
* @param array $namespaces |
|
613
|
|
|
* @return array |
|
614
|
|
|
*/ |
|
615
|
|
|
public static function namespacesAsText( $namespaces ) { |
|
616
|
|
|
return MediaWikiServices::getInstance()->getSearchEngineConfig()->namespacesAsText( $namespaces ); |
|
617
|
|
|
} |
|
618
|
|
|
|
|
619
|
|
|
/** |
|
620
|
|
|
* Load up the appropriate search engine class for the currently |
|
621
|
|
|
* active database backend, and return a configured instance. |
|
622
|
|
|
* @deprecated since 1.27; Use SearchEngineFactory::create |
|
623
|
|
|
* @param string $type Type of search backend, if not the default |
|
624
|
|
|
* @return SearchEngine |
|
625
|
|
|
*/ |
|
626
|
|
|
public static function create( $type = null ) { |
|
627
|
|
|
return MediaWikiServices::getInstance()->getSearchEngineFactory()->create( $type ); |
|
628
|
|
|
} |
|
629
|
|
|
|
|
630
|
|
|
/** |
|
631
|
|
|
* Return the search engines we support. If only $wgSearchType |
|
632
|
|
|
* is set, it'll be an array of just that one item. |
|
633
|
|
|
* @deprecated since 1.27; use SearchEngineConfig::getSearchTypes() |
|
634
|
|
|
* @return array |
|
635
|
|
|
*/ |
|
636
|
|
|
public static function getSearchTypes() { |
|
637
|
|
|
return MediaWikiServices::getInstance()->getSearchEngineConfig()->getSearchTypes(); |
|
638
|
|
|
} |
|
639
|
|
|
|
|
640
|
|
|
/** |
|
641
|
|
|
* Get a list of supported profiles. |
|
642
|
|
|
* Some search engine implementations may expose specific profiles to fine-tune |
|
643
|
|
|
* its behaviors. |
|
644
|
|
|
* The profile can be passed as a feature data with setFeatureData( $profileType, $profileName ) |
|
645
|
|
|
* The array returned by this function contains the following keys: |
|
646
|
|
|
* - name: the profile name to use with setFeatureData |
|
647
|
|
|
* - desc-message: the i18n description |
|
648
|
|
|
* - default: set to true if this profile is the default |
|
649
|
|
|
* |
|
650
|
|
|
* @since 1.28 |
|
651
|
|
|
* @param $profileType the type of profiles |
|
652
|
|
|
* @return array|null the list of profiles or null if none available |
|
653
|
|
|
*/ |
|
654
|
|
|
public function getProfiles( $profileType ) { |
|
655
|
|
|
return null; |
|
656
|
|
|
} |
|
657
|
|
|
|
|
658
|
|
|
} |
|
659
|
|
|
|
|
660
|
|
|
/** |
|
661
|
|
|
* Dummy class to be used when non-supported Database engine is present. |
|
662
|
|
|
* @todo FIXME: Dummy class should probably try something at least mildly useful, |
|
663
|
|
|
* such as a LIKE search through titles. |
|
664
|
|
|
* @ingroup Search |
|
665
|
|
|
*/ |
|
666
|
|
|
class SearchEngineDummy extends SearchEngine { |
|
667
|
|
|
// no-op |
|
668
|
|
|
} |
|
669
|
|
|
|
This method has been deprecated. The supplier of the class has supplied an explanatory message.
The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.