MediaWiki master
PrefixSearch.php
Go to the documentation of this file.
1<?php
9namespace MediaWiki\Search;
10
17
26abstract class PrefixSearch {
36 public function search( $search, $limit, $namespaces = [], $offset = 0 ) {
37 $search = trim( $search );
38 if ( $search == '' ) {
39 return []; // Return empty result
40 }
41
42 $hasNamespace = SearchEngine::parseNamespacePrefixes( $search, false, true );
43 if ( $hasNamespace !== false ) {
44 [ $search, $namespaces ] = $hasNamespace;
45 }
46
47 return $this->searchBackend( $namespaces, $search, $limit, $offset );
48 }
49
59 public function searchWithVariants( $search, $limit, array $namespaces, $offset = 0 ) {
60 $searches = $this->search( $search, $limit, $namespaces, $offset );
61
62 // if the content language has variants, try to retrieve fallback results
63 $fallbackLimit = $limit - count( $searches );
64 if ( $fallbackLimit > 0 ) {
66 $fallbackSearches = $services->getLanguageConverterFactory()
67 ->getLanguageConverter( $services->getContentLanguage() )
68 ->autoConvertToAllVariants( $search );
69 $fallbackSearches = array_diff( array_unique( $fallbackSearches ), [ $search ] );
70
71 foreach ( $fallbackSearches as $fbs ) {
72 $fallbackSearchResult = $this->search( $fbs, $fallbackLimit, $namespaces );
73 $searches = array_merge( $searches, $fallbackSearchResult );
74 $fallbackLimit -= count( $fallbackSearchResult );
75
76 if ( $fallbackLimit == 0 ) {
77 break;
78 }
79 }
80 }
81 return $searches;
82 }
83
91 abstract protected function titles( array $titles );
92
100 abstract protected function strings( array $strings );
101
110 protected function searchBackend( $namespaces, $search, $limit, $offset ) {
111 if ( count( $namespaces ) == 1 ) {
112 $ns = $namespaces[0];
113 if ( $ns == NS_MEDIA ) {
114 $namespaces = [ NS_FILE ];
115 } elseif ( $ns == NS_SPECIAL ) {
116 return $this->titles( $this->specialSearch( $search, $limit, $offset ) );
117 }
118 }
119 $srchres = [];
120 if ( ( new HookRunner( MediaWikiServices::getInstance()->getHookContainer() ) )->onPrefixSearchBackend(
121 $namespaces, $search, $limit, $srchres, $offset )
122 ) {
123 return $this->titles( $this->defaultSearchBackend( $namespaces, $search, $limit, $offset ) );
124 }
125 return $this->strings(
126 $this->handleResultFromHook( $srchres, $namespaces, $search, $limit, $offset ) );
127 }
128
129 private function handleResultFromHook(
130 array $srchres, array $namespaces, string $search, int $limit, int $offset
131 ): array {
132 if ( $offset === 0 ) {
133 // Only perform exact db match if offset === 0
134 // This is still far from perfect but at least we avoid returning the
135 // same title again and again when the user is scrolling with a query
136 // that matches a title in the db.
137 $rescorer = new SearchExactMatchRescorer();
138 $srchres = $rescorer->rescore( $search, $namespaces, $srchres, $limit );
139 }
140 return $srchres;
141 }
142
151 protected function specialSearch( $search, $limit, $offset ) {
152 $searchParts = explode( '/', $search, 2 );
153 $searchKey = $searchParts[0];
154 $subpageSearch = $searchParts[1] ?? null;
155
156 // Handle subpage search separately.
157 $spFactory = MediaWikiServices::getInstance()->getSpecialPageFactory();
158 if ( $subpageSearch !== null ) {
159 // Try matching the full search string as a page name
160 $specialTitle = Title::makeTitleSafe( NS_SPECIAL, $searchKey );
161 if ( !$specialTitle ) {
162 return [];
163 }
164 $special = $spFactory->getPage( $specialTitle->getText() );
165 if ( $special ) {
166 $subpages = $special->prefixSearchSubpages( $subpageSearch, $limit, $offset );
167 return array_map( $specialTitle->getSubpage( ... ), $subpages );
168 } else {
169 return [];
170 }
171 }
172
173 # normalize searchKey, so aliases with spaces can be found - T27675
174 $contLang = MediaWikiServices::getInstance()->getContentLanguage();
175 $searchKey = str_replace( ' ', '_', $searchKey );
176 $searchKey = $contLang->caseFold( $searchKey );
177
178 // Unlike SpecialPage itself, we want the canonical forms of both
179 // canonical and alias title forms...
180 $keys = [];
181 $listedPages = $spFactory->getListedPages();
182 foreach ( $listedPages as $page => $_obj ) {
183 $keys[$contLang->caseFold( $page )] = [ 'page' => $page, 'rank' => 0 ];
184 }
185
186 foreach ( $contLang->getSpecialPageAliases() as $page => $aliases ) {
187 // Exclude localisation aliases for pages that are not defined (T22885),
188 // e.g. if an extension registers a page based on site configuration.
189 if ( !in_array( $page, $spFactory->getNames() ) ) {
190 continue;
191 }
192 // Exclude aliases for unlisted pages
193 if ( !isset( $listedPages[$page] ) ) {
194 continue;
195 }
196
197 foreach ( $aliases as $key => $alias ) {
198 $keys[$contLang->caseFold( $alias )] = [ 'page' => $alias, 'rank' => $key ];
199 }
200 }
201 ksort( $keys );
202
203 $matches = [];
204 foreach ( $keys as $pageKey => $page ) {
205 if ( $searchKey === '' || str_starts_with( $pageKey, $searchKey ) ) {
206 // T29671: Don't use SpecialPage::getTitleFor() here because it
207 // localizes its input leading to searches for e.g. Special:All
208 // returning Spezial:MediaWiki-Systemnachrichten and returning
209 // Spezial:Alle_Seiten twice when $wgLanguageCode == 'de'
210 $matches[$page['rank']][] = Title::makeTitleSafe( NS_SPECIAL, $page['page'] );
211
212 if ( isset( $matches[0] ) && count( $matches[0] ) >= $limit + $offset ) {
213 // We have enough items in primary rank, no use to continue
214 break;
215 }
216 }
217
218 }
219
220 // Ensure keys are in order
221 ksort( $matches );
222 // Flatten the array
223 $matches = array_reduce( $matches, 'array_merge', [] );
224
225 return array_slice( $matches, $offset, $limit );
226 }
227
240 public function defaultSearchBackend( $namespaces, $search, $limit, $offset ) {
241 if ( !$namespaces ) {
242 $namespaces = [ NS_MAIN ];
243 }
244
245 if ( in_array( NS_SPECIAL, $namespaces ) ) {
246 // For now, if special is included, ignore the other namespaces
247 return $this->specialSearch( $search, $limit, $offset );
248 }
249
250 // Construct suitable prefix for each namespace. They differ in cases where
251 // some namespaces always capitalize and some don't.
252 $prefixes = [];
253 // Allow to do a prefix search for e.g. "Talk:"
254 if ( $search === '' ) {
255 $prefixes[$search] = $namespaces;
256 } else {
257 // Don't just ignore input like "[[Foo]]", but try to search for "Foo"
258 $search = preg_replace( TitleParser::getTitleInvalidRegex(), '', $search );
259 foreach ( $namespaces as $namespace ) {
260 $title = Title::makeTitleSafe( $namespace, $search );
261 if ( $title ) {
262 $prefixes[$title->getDBkey()][] = $namespace;
263 }
264 }
265 }
266 if ( !$prefixes ) {
267 return [];
268 }
269
270 $services = MediaWikiServices::getInstance();
271 $dbr = $services->getConnectionProvider()->getReplicaDatabase();
272 // Often there is only one prefix that applies to all requested namespaces,
273 // but sometimes there are two if some namespaces do not always capitalize.
274 $conds = [];
275 foreach ( $prefixes as $prefix => $namespaces ) {
276 $expr = $dbr->expr( 'page_namespace', '=', $namespaces );
277 if ( $prefix !== '' ) {
278 $expr = $expr->and(
279 'page_title',
280 IExpression::LIKE,
281 new LikeValue( (string)$prefix, $dbr->anyString() )
282 );
283 }
284 $conds[] = $expr;
285 }
286
287 $queryBuilder = $dbr->newSelectQueryBuilder()
288 ->select( [ 'page_id', 'page_namespace', 'page_title' ] )
289 ->from( 'page' )
290 ->where( $dbr->orExpr( $conds ) )
291 ->orderBy( [ 'page_title', 'page_namespace' ] )
292 ->limit( $limit )
293 ->offset( $offset );
294 $res = $queryBuilder->caller( __METHOD__ )->fetchResultSet();
295
296 return iterator_to_array( $services->getTitleFactory()->newTitleArrayFromResult( $res ) );
297 }
298}
299
301class_alias( PrefixSearch::class, 'PrefixSearch' );
const NS_FILE
Definition Defines.php:57
const NS_MAIN
Definition Defines.php:51
const NS_SPECIAL
Definition Defines.php:40
const NS_MEDIA
Definition Defines.php:39
if(!defined('MW_SETUP_CALLBACK'))
Definition WebStart.php:68
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
Service locator for MediaWiki core services.
static getInstance()
Returns the global default instance of the top level service locator.
Handles searching prefixes of titles and finding any page names that match.
strings(array $strings)
When implemented in a descendant class, receives an array of titles as strings and returns either an ...
titles(array $titles)
When implemented in a descendant class, receives an array of Title objects and returns either an unmo...
defaultSearchBackend( $namespaces, $search, $limit, $offset)
Unless overridden by PrefixSearchBackend hook... This is case-sensitive (First character may be autom...
searchWithVariants( $search, $limit, array $namespaces, $offset=0)
Do a prefix search for all possible variants of the prefix.
searchBackend( $namespaces, $search, $limit, $offset)
Do a prefix search of titles and return a list of matching page names.
specialSearch( $search, $limit, $offset)
Prefix search special-case for Special: namespace.
search( $search, $limit, $namespaces=[], $offset=0)
Do a prefix search of titles and return a list of matching page names.
static parseNamespacePrefixes( $query, $withAllKeyword=true, $withPrefixSearchExtractNamespaceHook=false)
Parse some common prefixes: all (search everything) or namespace names.
An utility class to rescore search results by looking for an exact match in the db and add the page f...
A title parser service for MediaWiki.
Represents a title within MediaWiki.
Definition Title.php:69
Content of like value.
Definition LikeValue.php:14
Definition of a mapping for the search index field.