Add Special:Search sort parameter without ui
[lhc/web/wiklou.git] / includes / search / SearchEngine.php
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 const DEFAULT_SORT = 'relevance';
36
37 /** @var string */
38 public $prefix = '';
39
40 /** @var int[]|null */
41 public $namespaces = [ NS_MAIN ];
42
43 /** @var int */
44 protected $limit = 10;
45
46 /** @var int */
47 protected $offset = 0;
48
49 /** @var array|string */
50 protected $searchTerms = [];
51
52 /** @var bool */
53 protected $showSuggestion = true;
54 private $sort = self::DEFAULT_SORT;
55
56 /** @var array Feature values */
57 protected $features = [];
58
59 /** @const string profile type for completionSearch */
60 const COMPLETION_PROFILE_TYPE = 'completionSearchProfile';
61
62 /** @const string profile type for query independent ranking features */
63 const FT_QUERY_INDEP_PROFILE_TYPE = 'fulltextQueryIndepProfile';
64
65 /** @const int flag for legalSearchChars: includes all chars allowed in a search query */
66 const CHARS_ALL = 1;
67
68 /** @const int flag for legalSearchChars: includes all chars allowed in a search term */
69 const CHARS_NO_SYNTAX = 2;
70
71 /**
72 * Perform a full text search query and return a result set.
73 * If full text searches are not supported or disabled, return null.
74 *
75 * As of 1.32 overriding this function is deprecated. It will
76 * be converted to final in 1.34. Override self::doSearchText().
77 *
78 * @param string $term Raw search term
79 * @return SearchResultSet|Status|null
80 */
81 public function searchText( $term ) {
82 return $this->maybePaginate( function () use ( $term ) {
83 return $this->doSearchText( $term );
84 } );
85 }
86
87 /**
88 * Perform a full text search query and return a result set.
89 *
90 * @param string $term Raw search term
91 * @return SearchResultSet|Status|null
92 * @since 1.32
93 */
94 protected function doSearchText( $term ) {
95 return null;
96 }
97
98 /**
99 * Perform a title search in the article archive.
100 * NOTE: these results still should be filtered by
101 * matching against PageArchive, permissions checks etc
102 * The results returned by this methods are only sugegstions and
103 * may not end up being shown to the user.
104 *
105 * As of 1.32 overriding this function is deprecated. It will
106 * be converted to final in 1.34. Override self::doSearchArchiveTitle().
107 *
108 * @param string $term Raw search term
109 * @return Status<Title[]>
110 * @since 1.29
111 */
112 public function searchArchiveTitle( $term ) {
113 return $this->doSearchArchiveTitle( $term );
114 }
115
116 /**
117 * Perform a title search in the article archive.
118 *
119 * @param string $term Raw search term
120 * @return Status<Title[]>
121 * @since 1.32
122 */
123 protected function doSearchArchiveTitle( $term ) {
124 return Status::newGood( [] );
125 }
126
127 /**
128 * Perform a title-only search query and return a result set.
129 * If title searches are not supported or disabled, return null.
130 * STUB
131 *
132 * As of 1.32 overriding this function is deprecated. It will
133 * be converted to final in 1.34. Override self::doSearchTitle().
134 *
135 * @param string $term Raw search term
136 * @return SearchResultSet|null
137 */
138 public function searchTitle( $term ) {
139 return $this->maybePaginate( function () use ( $term ) {
140 return $this->doSearchTitle( $term );
141 } );
142 }
143
144 /**
145 * Perform a title-only search query and return a result set.
146 *
147 * @param string $term Raw search term
148 * @return SearchResultSet|null
149 * @since 1.32
150 */
151 protected function doSearchTitle( $term ) {
152 return null;
153 }
154
155 /**
156 * Performs an overfetch and shrink operation to determine if
157 * the next page is available for search engines that do not
158 * explicitly implement their own pagination.
159 *
160 * @param Closure $fn Takes no arguments
161 * @return SearchResultSet|Status<SearchResultSet>|null Result of calling $fn
162 */
163 private function maybePaginate( Closure $fn ) {
164 if ( $this instanceof PaginatingSearchEngine ) {
165 return $fn();
166 }
167 $this->limit++;
168 try {
169 $resultSetOrStatus = $fn();
170 } finally {
171 $this->limit--;
172 }
173
174 $resultSet = null;
175 if ( $resultSetOrStatus instanceof SearchResultSet ) {
176 $resultSet = $resultSetOrStatus;
177 } elseif ( $resultSetOrStatus instanceof Status &&
178 $resultSetOrStatus->getValue() instanceof SearchResultSet
179 ) {
180 $resultSet = $resultSetOrStatus->getValue();
181 }
182 if ( $resultSet ) {
183 $resultSet->shrink( $this->limit );
184 }
185
186 return $resultSetOrStatus;
187 }
188
189 /**
190 * @since 1.18
191 * @param string $feature
192 * @return bool
193 */
194 public function supports( $feature ) {
195 switch ( $feature ) {
196 case 'search-update':
197 return true;
198 case 'title-suffix-filter':
199 default:
200 return false;
201 }
202 }
203
204 /**
205 * Way to pass custom data for engines
206 * @since 1.18
207 * @param string $feature
208 * @param mixed $data
209 */
210 public function setFeatureData( $feature, $data ) {
211 $this->features[$feature] = $data;
212 }
213
214 /**
215 * Way to retrieve custom data set by setFeatureData
216 * or by the engine itself.
217 * @since 1.29
218 * @param string $feature feature name
219 * @return mixed the feature value or null if unset
220 */
221 public function getFeatureData( $feature ) {
222 if ( isset( $this->features[$feature] ) ) {
223 return $this->features[$feature];
224 }
225 return null;
226 }
227
228 /**
229 * When overridden in derived class, performs database-specific conversions
230 * on text to be used for searching or updating search index.
231 * Default implementation does nothing (simply returns $string).
232 *
233 * @param string $string String to process
234 * @return string
235 */
236 public function normalizeText( $string ) {
237 global $wgContLang;
238
239 // Some languages such as Chinese require word segmentation
240 return $wgContLang->segmentByWord( $string );
241 }
242
243 /**
244 * Transform search term in cases when parts of the query came as different
245 * GET params (when supported), e.g. for prefix queries:
246 * search=test&prefix=Main_Page/Archive -> test prefix:Main Page/Archive
247 * @param string $term
248 * @return string
249 * @deprecated since 1.32 this should now be handled internally by the
250 * search engine
251 */
252 public function transformSearchTerm( $term ) {
253 return $term;
254 }
255
256 /**
257 * Get service class to finding near matches.
258 * @param Config $config Configuration to use for the matcher.
259 * @return SearchNearMatcher
260 */
261 public function getNearMatcher( Config $config ) {
262 global $wgContLang;
263 return new SearchNearMatcher( $config, $wgContLang );
264 }
265
266 /**
267 * Get near matcher for default SearchEngine.
268 * @return SearchNearMatcher
269 */
270 protected static function defaultNearMatcher() {
271 $config = MediaWikiServices::getInstance()->getMainConfig();
272 return MediaWikiServices::getInstance()->newSearchEngine()->getNearMatcher( $config );
273 }
274
275 /**
276 * If an exact title match can be found, or a very slightly close match,
277 * return the title. If no match, returns NULL.
278 * @deprecated since 1.27; Use SearchEngine::getNearMatcher()
279 * @param string $searchterm
280 * @return Title
281 */
282 public static function getNearMatch( $searchterm ) {
283 return static::defaultNearMatcher()->getNearMatch( $searchterm );
284 }
285
286 /**
287 * Do a near match (see SearchEngine::getNearMatch) and wrap it into a
288 * SearchResultSet.
289 * @deprecated since 1.27; Use SearchEngine::getNearMatcher()
290 * @param string $searchterm
291 * @return SearchResultSet
292 */
293 public static function getNearMatchResultSet( $searchterm ) {
294 return static::defaultNearMatcher()->getNearMatchResultSet( $searchterm );
295 }
296
297 /**
298 * Get chars legal for search
299 * NOTE: usage as static is deprecated and preserved only as BC measure
300 * @param int $type type of search chars (see self::CHARS_ALL
301 * and self::CHARS_NO_SYNTAX). Defaults to CHARS_ALL
302 * @return string
303 */
304 public static function legalSearchChars( $type = self::CHARS_ALL ) {
305 return "A-Za-z_'.0-9\\x80-\\xFF\\-";
306 }
307
308 /**
309 * Set the maximum number of results to return
310 * and how many to skip before returning the first.
311 *
312 * @param int $limit
313 * @param int $offset
314 */
315 function setLimitOffset( $limit, $offset = 0 ) {
316 $this->limit = intval( $limit );
317 $this->offset = intval( $offset );
318 }
319
320 /**
321 * Set which namespaces the search should include.
322 * Give an array of namespace index numbers.
323 *
324 * @param int[]|null $namespaces
325 */
326 function setNamespaces( $namespaces ) {
327 if ( $namespaces ) {
328 // Filter namespaces to only keep valid ones
329 $validNs = $this->searchableNamespaces();
330 $namespaces = array_filter( $namespaces, function ( $ns ) use( $validNs ) {
331 return $ns < 0 || isset( $validNs[$ns] );
332 } );
333 } else {
334 $namespaces = [];
335 }
336 $this->namespaces = $namespaces;
337 }
338
339 /**
340 * Set whether the searcher should try to build a suggestion. Note: some searchers
341 * don't support building a suggestion in the first place and others don't respect
342 * this flag.
343 *
344 * @param bool $showSuggestion Should the searcher try to build suggestions
345 */
346 function setShowSuggestion( $showSuggestion ) {
347 $this->showSuggestion = $showSuggestion;
348 }
349
350 /**
351 * Get the valid sort directions. All search engines support 'relevance' but others
352 * might support more. The default in all implementations must be 'relevance.'
353 *
354 * @since 1.25
355 * @return string[] the valid sort directions for setSort
356 */
357 public function getValidSorts() {
358 return [ self::DEFAULT_SORT ];
359 }
360
361 /**
362 * Set the sort direction of the search results. Must be one returned by
363 * SearchEngine::getValidSorts()
364 *
365 * @since 1.25
366 * @throws InvalidArgumentException
367 * @param string $sort sort direction for query result
368 */
369 public function setSort( $sort ) {
370 if ( !in_array( $sort, $this->getValidSorts() ) ) {
371 throw new InvalidArgumentException( "Invalid sort: $sort. " .
372 "Must be one of: " . implode( ', ', $this->getValidSorts() ) );
373 }
374 $this->sort = $sort;
375 }
376
377 /**
378 * Get the sort direction of the search results
379 *
380 * @since 1.25
381 * @return string
382 */
383 public function getSort() {
384 return $this->sort;
385 }
386
387 /**
388 * Parse some common prefixes: all (search everything)
389 * or namespace names and set the list of namespaces
390 * of this class accordingly.
391 *
392 * @deprecated since 1.32; should be handled internally by the search engine
393 * @param string $query
394 * @return string
395 */
396 function replacePrefixes( $query ) {
397 return $query;
398 }
399
400 /**
401 * Parse some common prefixes: all (search everything)
402 * or namespace names
403 *
404 * @param string $query
405 * @param bool $withAllKeyword activate support of the "all:" keyword and its
406 * translations to activate searching on all namespaces.
407 * @param bool $withPrefixSearchExtractNamespaceHook call the PrefixSearchExtractNamespace hook
408 * if classic namespace identification did not match.
409 * @return false|array false if no namespace was extracted, an array
410 * with the parsed query at index 0 and an array of namespaces at index
411 * 1 (or null for all namespaces).
412 * @throws FatalError
413 * @throws MWException
414 */
415 public static function parseNamespacePrefixes(
416 $query,
417 $withAllKeyword = true,
418 $withPrefixSearchExtractNamespaceHook = false
419 ) {
420 global $wgContLang;
421
422 $parsed = $query;
423 if ( strpos( $query, ':' ) === false ) { // nothing to do
424 return false;
425 }
426 $extractedNamespace = null;
427
428 $allQuery = false;
429 if ( $withAllKeyword ) {
430 $allkeywords = [];
431
432 $allkeywords[] = wfMessage( 'searchall' )->inContentLanguage()->text() . ":";
433 // force all: so that we have a common syntax for all the wikis
434 if ( !in_array( 'all:', $allkeywords ) ) {
435 $allkeywords[] = 'all:';
436 }
437
438 foreach ( $allkeywords as $kw ) {
439 if ( strncmp( $query, $kw, strlen( $kw ) ) == 0 ) {
440 $extractedNamespace = null;
441 $parsed = substr( $query, strlen( $kw ) );
442 $allQuery = true;
443 break;
444 }
445 }
446 }
447
448 if ( !$allQuery && strpos( $query, ':' ) !== false ) {
449 $prefix = str_replace( ' ', '_', substr( $query, 0, strpos( $query, ':' ) ) );
450 $index = $wgContLang->getNsIndex( $prefix );
451 if ( $index !== false ) {
452 $extractedNamespace = [ $index ];
453 $parsed = substr( $query, strlen( $prefix ) + 1 );
454 } elseif ( $withPrefixSearchExtractNamespaceHook ) {
455 $hookNamespaces = [ NS_MAIN ];
456 $hookQuery = $query;
457 Hooks::run( 'PrefixSearchExtractNamespace', [ &$hookNamespaces, &$hookQuery ] );
458 if ( $hookQuery !== $query ) {
459 $parsed = $hookQuery;
460 $extractedNamespace = $hookNamespaces;
461 } else {
462 return false;
463 }
464 } else {
465 return false;
466 }
467 }
468
469 return [ $parsed, $extractedNamespace ];
470 }
471
472 /**
473 * Find snippet highlight settings for all users
474 * @return array Contextlines, contextchars
475 */
476 public static function userHighlightPrefs() {
477 $contextlines = 2; // Hardcode this. Old defaults sucked. :)
478 $contextchars = 75; // same as above.... :P
479 return [ $contextlines, $contextchars ];
480 }
481
482 /**
483 * Create or update the search index record for the given page.
484 * Title and text should be pre-processed.
485 * STUB
486 *
487 * @param int $id
488 * @param string $title
489 * @param string $text
490 */
491 function update( $id, $title, $text ) {
492 // no-op
493 }
494
495 /**
496 * Update a search index record's title only.
497 * Title should be pre-processed.
498 * STUB
499 *
500 * @param int $id
501 * @param string $title
502 */
503 function updateTitle( $id, $title ) {
504 // no-op
505 }
506
507 /**
508 * Delete an indexed page
509 * Title should be pre-processed.
510 * STUB
511 *
512 * @param int $id Page id that was deleted
513 * @param string $title Title of page that was deleted
514 */
515 function delete( $id, $title ) {
516 // no-op
517 }
518
519 /**
520 * Get the raw text for updating the index from a content object
521 * Nicer search backends could possibly do something cooler than
522 * just returning raw text
523 *
524 * @todo This isn't ideal, we'd really like to have content-specific handling here
525 * @param Title $t Title we're indexing
526 * @param Content|null $c Content of the page to index
527 * @return string
528 */
529 public function getTextFromContent( Title $t, Content $c = null ) {
530 return $c ? $c->getTextForSearchIndex() : '';
531 }
532
533 /**
534 * If an implementation of SearchEngine handles all of its own text processing
535 * in getTextFromContent() and doesn't require SearchUpdate::updateText()'s
536 * rather silly handling, it should return true here instead.
537 *
538 * @return bool
539 */
540 public function textAlreadyUpdatedForIndex() {
541 return false;
542 }
543
544 /**
545 * Makes search simple string if it was namespaced.
546 * Sets namespaces of the search to namespaces extracted from string.
547 * @param string $search
548 * @return string Simplified search string
549 */
550 protected function normalizeNamespaces( $search ) {
551 $queryAndNs = self::parseNamespacePrefixes( $search, false, true );
552 if ( $queryAndNs !== false ) {
553 $this->setNamespaces( $queryAndNs[1] );
554 return $queryAndNs[0];
555 }
556 return $search;
557 }
558
559 /**
560 * Perform an overfetch of completion search results. This allows
561 * determining if another page of results is available.
562 *
563 * @param string $search
564 * @return SearchSuggestionSet
565 */
566 protected function completionSearchBackendOverfetch( $search ) {
567 $this->limit++;
568 try {
569 return $this->completionSearchBackend( $search );
570 } finally {
571 $this->limit--;
572 }
573 }
574
575 /**
576 * Perform a completion search.
577 * Does not resolve namespaces and does not check variants.
578 * Search engine implementations may want to override this function.
579 * @param string $search
580 * @return SearchSuggestionSet
581 */
582 protected function completionSearchBackend( $search ) {
583 $results = [];
584
585 $search = trim( $search );
586
587 if ( !in_array( NS_SPECIAL, $this->namespaces ) && // We do not run hook on Special: search
588 !Hooks::run( 'PrefixSearchBackend',
589 [ $this->namespaces, $search, $this->limit, &$results, $this->offset ]
590 ) ) {
591 // False means hook worked.
592 // FIXME: Yes, the API is weird. That's why it is going to be deprecated.
593
594 return SearchSuggestionSet::fromStrings( $results );
595 } else {
596 // Hook did not do the job, use default simple search
597 $results = $this->simplePrefixSearch( $search );
598 return SearchSuggestionSet::fromTitles( $results );
599 }
600 }
601
602 /**
603 * Perform a completion search.
604 * @param string $search
605 * @return SearchSuggestionSet
606 */
607 public function completionSearch( $search ) {
608 if ( trim( $search ) === '' ) {
609 return SearchSuggestionSet::emptySuggestionSet(); // Return empty result
610 }
611 $search = $this->normalizeNamespaces( $search );
612 $suggestions = $this->completionSearchBackendOverfetch( $search );
613 return $this->processCompletionResults( $search, $suggestions );
614 }
615
616 /**
617 * Perform a completion search with variants.
618 * @param string $search
619 * @return SearchSuggestionSet
620 */
621 public function completionSearchWithVariants( $search ) {
622 if ( trim( $search ) === '' ) {
623 return SearchSuggestionSet::emptySuggestionSet(); // Return empty result
624 }
625 $search = $this->normalizeNamespaces( $search );
626
627 $results = $this->completionSearchBackendOverfetch( $search );
628 $fallbackLimit = 1 + $this->limit - $results->getSize();
629 if ( $fallbackLimit > 0 ) {
630 global $wgContLang;
631
632 $fallbackSearches = $wgContLang->autoConvertToAllVariants( $search );
633 $fallbackSearches = array_diff( array_unique( $fallbackSearches ), [ $search ] );
634
635 foreach ( $fallbackSearches as $fbs ) {
636 $this->setLimitOffset( $fallbackLimit );
637 $fallbackSearchResult = $this->completionSearch( $fbs );
638 $results->appendAll( $fallbackSearchResult );
639 $fallbackLimit -= $fallbackSearchResult->getSize();
640 if ( $fallbackLimit <= 0 ) {
641 break;
642 }
643 }
644 }
645 return $this->processCompletionResults( $search, $results );
646 }
647
648 /**
649 * Extract titles from completion results
650 * @param SearchSuggestionSet $completionResults
651 * @return Title[]
652 */
653 public function extractTitles( SearchSuggestionSet $completionResults ) {
654 return $completionResults->map( function ( SearchSuggestion $sugg ) {
655 return $sugg->getSuggestedTitle();
656 } );
657 }
658
659 /**
660 * Process completion search results.
661 * Resolves the titles and rescores.
662 * @param string $search
663 * @param SearchSuggestionSet $suggestions
664 * @return SearchSuggestionSet
665 */
666 protected function processCompletionResults( $search, SearchSuggestionSet $suggestions ) {
667 // We over-fetched to determine pagination. Shrink back down if we have extra results
668 // and mark if pagination is possible
669 $suggestions->shrink( $this->limit );
670
671 $search = trim( $search );
672 // preload the titles with LinkBatch
673 $lb = new LinkBatch( $suggestions->map( function ( SearchSuggestion $sugg ) {
674 return $sugg->getSuggestedTitle();
675 } ) );
676 $lb->setCaller( __METHOD__ );
677 $lb->execute();
678
679 $diff = $suggestions->filter( function ( SearchSuggestion $sugg ) {
680 return $sugg->getSuggestedTitle()->isKnown();
681 } );
682 if ( $diff > 0 ) {
683 MediaWikiServices::getInstance()->getStatsdDataFactory()
684 ->updateCount( 'search.completion.missing', $diff );
685 }
686
687 $results = $suggestions->map( function ( SearchSuggestion $sugg ) {
688 return $sugg->getSuggestedTitle()->getPrefixedText();
689 } );
690
691 if ( $this->offset === 0 ) {
692 // Rescore results with an exact title match
693 // NOTE: in some cases like cross-namespace redirects
694 // (frequently used as shortcuts e.g. WP:WP on huwiki) some
695 // backends like Cirrus will return no results. We should still
696 // try an exact title match to workaround this limitation
697 $rescorer = new SearchExactMatchRescorer();
698 $rescoredResults = $rescorer->rescore( $search, $this->namespaces, $results, $this->limit );
699 } else {
700 // No need to rescore if offset is not 0
701 // The exact match must have been returned at position 0
702 // if it existed.
703 $rescoredResults = $results;
704 }
705
706 if ( count( $rescoredResults ) > 0 ) {
707 $found = array_search( $rescoredResults[0], $results );
708 if ( $found === false ) {
709 // If the first result is not in the previous array it
710 // means that we found a new exact match
711 $exactMatch = SearchSuggestion::fromTitle( 0, Title::newFromText( $rescoredResults[0] ) );
712 $suggestions->prepend( $exactMatch );
713 $suggestions->shrink( $this->limit );
714 } else {
715 // if the first result is not the same we need to rescore
716 if ( $found > 0 ) {
717 $suggestions->rescore( $found );
718 }
719 }
720 }
721
722 return $suggestions;
723 }
724
725 /**
726 * Simple prefix search for subpages.
727 * @param string $search
728 * @return Title[]
729 */
730 public function defaultPrefixSearch( $search ) {
731 if ( trim( $search ) === '' ) {
732 return [];
733 }
734
735 $search = $this->normalizeNamespaces( $search );
736 return $this->simplePrefixSearch( $search );
737 }
738
739 /**
740 * Call out to simple search backend.
741 * Defaults to TitlePrefixSearch.
742 * @param string $search
743 * @return Title[]
744 */
745 protected function simplePrefixSearch( $search ) {
746 // Use default database prefix search
747 $backend = new TitlePrefixSearch;
748 return $backend->defaultSearchBackend( $this->namespaces, $search, $this->limit, $this->offset );
749 }
750
751 /**
752 * Make a list of searchable namespaces and their canonical names.
753 * @deprecated since 1.27; use SearchEngineConfig::searchableNamespaces()
754 * @return array
755 */
756 public static function searchableNamespaces() {
757 return MediaWikiServices::getInstance()->getSearchEngineConfig()->searchableNamespaces();
758 }
759
760 /**
761 * Extract default namespaces to search from the given user's
762 * settings, returning a list of index numbers.
763 * @deprecated since 1.27; use SearchEngineConfig::userNamespaces()
764 * @param user $user
765 * @return array
766 */
767 public static function userNamespaces( $user ) {
768 return MediaWikiServices::getInstance()->getSearchEngineConfig()->userNamespaces( $user );
769 }
770
771 /**
772 * An array of namespaces indexes to be searched by default
773 * @deprecated since 1.27; use SearchEngineConfig::defaultNamespaces()
774 * @return array
775 */
776 public static function defaultNamespaces() {
777 return MediaWikiServices::getInstance()->getSearchEngineConfig()->defaultNamespaces();
778 }
779
780 /**
781 * Get a list of namespace names useful for showing in tooltips
782 * and preferences
783 * @deprecated since 1.27; use SearchEngineConfig::namespacesAsText()
784 * @param array $namespaces
785 * @return array
786 */
787 public static function namespacesAsText( $namespaces ) {
788 return MediaWikiServices::getInstance()->getSearchEngineConfig()->namespacesAsText( $namespaces );
789 }
790
791 /**
792 * Load up the appropriate search engine class for the currently
793 * active database backend, and return a configured instance.
794 * @deprecated since 1.27; Use SearchEngineFactory::create
795 * @param string $type Type of search backend, if not the default
796 * @return SearchEngine
797 */
798 public static function create( $type = null ) {
799 return MediaWikiServices::getInstance()->getSearchEngineFactory()->create( $type );
800 }
801
802 /**
803 * Return the search engines we support. If only $wgSearchType
804 * is set, it'll be an array of just that one item.
805 * @deprecated since 1.27; use SearchEngineConfig::getSearchTypes()
806 * @return array
807 */
808 public static function getSearchTypes() {
809 return MediaWikiServices::getInstance()->getSearchEngineConfig()->getSearchTypes();
810 }
811
812 /**
813 * Get a list of supported profiles.
814 * Some search engine implementations may expose specific profiles to fine-tune
815 * its behaviors.
816 * The profile can be passed as a feature data with setFeatureData( $profileType, $profileName )
817 * The array returned by this function contains the following keys:
818 * - name: the profile name to use with setFeatureData
819 * - desc-message: the i18n description
820 * - default: set to true if this profile is the default
821 *
822 * @since 1.28
823 * @param string $profileType the type of profiles
824 * @param User|null $user the user requesting the list of profiles
825 * @return array|null the list of profiles or null if none available
826 */
827 public function getProfiles( $profileType, User $user = null ) {
828 return null;
829 }
830
831 /**
832 * Create a search field definition.
833 * Specific search engines should override this method to create search fields.
834 * @param string $name
835 * @param int $type One of the types in SearchIndexField::INDEX_TYPE_*
836 * @return SearchIndexField
837 * @since 1.28
838 */
839 public function makeSearchFieldMapping( $name, $type ) {
840 return new NullIndexField();
841 }
842
843 /**
844 * Get fields for search index
845 * @since 1.28
846 * @return SearchIndexField[] Index field definitions for all content handlers
847 */
848 public function getSearchIndexFields() {
849 $models = ContentHandler::getContentModels();
850 $fields = [];
851 $seenHandlers = new SplObjectStorage();
852 foreach ( $models as $model ) {
853 try {
854 $handler = ContentHandler::getForModelID( $model );
855 }
856 catch ( MWUnknownContentModelException $e ) {
857 // If we can find no handler, ignore it
858 continue;
859 }
860 // Several models can have the same handler, so avoid processing it repeatedly
861 if ( $seenHandlers->contains( $handler ) ) {
862 // We already did this one
863 continue;
864 }
865 $seenHandlers->attach( $handler );
866 $handlerFields = $handler->getFieldsForSearchIndex( $this );
867 foreach ( $handlerFields as $fieldName => $fieldData ) {
868 if ( empty( $fields[$fieldName] ) ) {
869 $fields[$fieldName] = $fieldData;
870 } else {
871 // TODO: do we allow some clashes with the same type or reject all of them?
872 $mergeDef = $fields[$fieldName]->merge( $fieldData );
873 if ( !$mergeDef ) {
874 throw new InvalidArgumentException( "Duplicate field $fieldName for model $model" );
875 }
876 $fields[$fieldName] = $mergeDef;
877 }
878 }
879 }
880 // Hook to allow extensions to produce search mapping fields
881 Hooks::run( 'SearchIndexFields', [ &$fields, $this ] );
882 return $fields;
883 }
884
885 /**
886 * Augment search results with extra data.
887 *
888 * @param SearchResultSet $resultSet
889 */
890 public function augmentSearchResults( SearchResultSet $resultSet ) {
891 $setAugmentors = [];
892 $rowAugmentors = [];
893 Hooks::run( "SearchResultsAugment", [ &$setAugmentors, &$rowAugmentors ] );
894 if ( !$setAugmentors && !$rowAugmentors ) {
895 // We're done here
896 return;
897 }
898
899 // Convert row augmentors to set augmentor
900 foreach ( $rowAugmentors as $name => $row ) {
901 if ( isset( $setAugmentors[$name] ) ) {
902 throw new InvalidArgumentException( "Both row and set augmentors are defined for $name" );
903 }
904 $setAugmentors[$name] = new PerRowAugmentor( $row );
905 }
906
907 foreach ( $setAugmentors as $name => $augmentor ) {
908 $data = $augmentor->augmentAll( $resultSet );
909 if ( $data ) {
910 $resultSet->setAugmentedData( $name, $data );
911 }
912 }
913 }
914 }
915
916 /**
917 * Dummy class to be used when non-supported Database engine is present.
918 * @todo FIXME: Dummy class should probably try something at least mildly useful,
919 * such as a LIKE search through titles.
920 * @ingroup Search
921 */
922 class SearchEngineDummy extends SearchEngine {
923 // no-op
924 }