MediaWiki REL1_37
ApiQuerySearch.php
Go to the documentation of this file.
1<?php
29 use SearchApi;
30
33
36
39
46 public function __construct(
47 ApiQuery $query,
48 $moduleName,
51 ) {
52 parent::__construct( $query, $moduleName, 'sr' );
53 // Services also needed in SearchApi trait
54 $this->searchEngineConfig = $searchEngineConfig;
55 $this->searchEngineFactory = $searchEngineFactory;
56 }
57
58 public function execute() {
59 $this->run();
60 }
61
62 public function executeGenerator( $resultPageSet ) {
63 $this->run( $resultPageSet );
64 }
65
70 private function run( $resultPageSet = null ) {
71 $params = $this->extractRequestParams();
72
73 // Extract parameters
74 $query = $params['search'];
75 $what = $params['what'];
76 $interwiki = $params['interwiki'];
77 $searchInfo = array_fill_keys( $params['info'], true );
78 $prop = array_fill_keys( $params['prop'], true );
79
80 // Create search engine instance and set options
81 $search = $this->buildSearchEngine( $params );
82 if ( isset( $params['sort'] ) ) {
83 $search->setSort( $params['sort'] );
84 }
85 $search->setFeatureData( 'rewrite', (bool)$params['enablerewrites'] );
86 $search->setFeatureData( 'interwiki', (bool)$interwiki );
87
88 $nquery = $search->replacePrefixes( $query );
89 if ( $nquery !== $query ) {
90 $query = $nquery;
91 wfDeprecatedMsg( 'SearchEngine::replacePrefixes() is overridden by ' .
92 get_class( $search ) . ', this was deprecated in MediaWiki 1.32',
93 '1.32' );
94 }
95 // Perform the actual search
96 if ( $what == 'text' ) {
97 $matches = $search->searchText( $query );
98 } elseif ( $what == 'title' ) {
99 $matches = $search->searchTitle( $query );
100 } elseif ( $what == 'nearmatch' ) {
101 // near matches must receive the user input as provided, otherwise
102 // the near matches within namespaces are lost.
103 $matches = $search->getNearMatcher( $this->getConfig() )
104 ->getNearMatchResultSet( $params['search'] );
105 } else {
106 // We default to title searches; this is a terrible legacy
107 // of the way we initially set up the MySQL fulltext-based
108 // search engine with separate title and text fields.
109 // In the future, the default should be for a combined index.
110 $what = 'title';
111 $matches = $search->searchTitle( $query );
112
113 // Not all search engines support a separate title search,
114 // for instance the Lucene-based engine we use on Wikipedia.
115 // In this case, fall back to full-text search (which will
116 // include titles in it!)
117 if ( $matches === null ) {
118 $what = 'text';
119 $matches = $search->searchText( $query );
120 }
121 }
122
123 if ( $matches instanceof Status ) {
124 $status = $matches;
125 $matches = $status->getValue();
126 } else {
127 $status = null;
128 }
129
130 if ( $status ) {
131 if ( $status->isOK() ) {
132 $this->getMain()->getErrorFormatter()->addMessagesFromStatus(
133 $this->getModuleName(),
134 $status
135 );
136 } else {
137 $this->dieStatus( $status );
138 }
139 } elseif ( $matches === null ) {
140 $this->dieWithError( [ 'apierror-searchdisabled', $what ], "search-{$what}-disabled" );
141 }
142
143 $apiResult = $this->getResult();
144 // Add search meta data to result
145 if ( isset( $searchInfo['totalhits'] ) ) {
146 $totalhits = $matches->getTotalHits();
147 if ( $totalhits !== null ) {
148 $apiResult->addValue( [ 'query', 'searchinfo' ],
149 'totalhits', $totalhits );
150 }
151 }
152 if ( isset( $searchInfo['suggestion'] ) && $matches->hasSuggestion() ) {
153 $apiResult->addValue( [ 'query', 'searchinfo' ],
154 'suggestion', $matches->getSuggestionQuery() );
155 $apiResult->addValue( [ 'query', 'searchinfo' ],
156 'suggestionsnippet', HtmlArmor::getHtml( $matches->getSuggestionSnippet() ) );
157 }
158 if ( isset( $searchInfo['rewrittenquery'] ) && $matches->hasRewrittenQuery() ) {
159 $apiResult->addValue( [ 'query', 'searchinfo' ],
160 'rewrittenquery', $matches->getQueryAfterRewrite() );
161 $apiResult->addValue( [ 'query', 'searchinfo' ],
162 'rewrittenquerysnippet', HtmlArmor::getHtml( $matches->getQueryAfterRewriteSnippet() ) );
163 }
164
165 $titles = [];
166 $data = [];
167 $count = 0;
168
169 if ( $matches->hasMoreResults() ) {
170 $this->setContinueEnumParameter( 'offset', $params['offset'] + $params['limit'] );
171 }
172
173 foreach ( $matches as $result ) {
174 $count++;
175 // Silently skip broken and missing titles
176 if ( $result->isBrokenTitle() || $result->isMissingRevision() ) {
177 continue;
178 }
179
180 $vals = $this->getSearchResultData( $result, $prop );
181
182 if ( $resultPageSet === null ) {
183 if ( $vals ) {
184 // Add item to results and see whether it fits
185 $fit = $apiResult->addValue( [ 'query', $this->getModuleName() ], null, $vals );
186 if ( !$fit ) {
187 $this->setContinueEnumParameter( 'offset', $params['offset'] + $count - 1 );
188 break;
189 }
190 }
191 } else {
192 $titles[] = $result->getTitle();
193 $data[] = $vals ?: [];
194 }
195 }
196
197 // Here we assume interwiki results do not count with
198 // regular search results. We may want to reconsider this
199 // if we ever return a lot of interwiki results or want pagination
200 // for them.
201 // Interwiki results inside main result set
202 $canAddInterwiki = (bool)$params['enablerewrites'] && ( $resultPageSet === null );
203 if ( $canAddInterwiki ) {
204 $this->addInterwikiResults( $matches, $apiResult, $prop, 'additional',
205 ISearchResultSet::INLINE_RESULTS );
206 }
207
208 // Interwiki results outside main result set
209 if ( $interwiki && $resultPageSet === null ) {
210 $this->addInterwikiResults( $matches, $apiResult, $prop, 'interwiki',
211 ISearchResultSet::SECONDARY_RESULTS );
212 }
213
214 if ( $resultPageSet === null ) {
215 $apiResult->addIndexedTagName( [
216 'query', $this->getModuleName()
217 ], 'p' );
218 } else {
219 $resultPageSet->setRedirectMergePolicy( static function ( $current, $new ) {
220 if ( !isset( $current['index'] ) || $new['index'] < $current['index'] ) {
221 $current['index'] = $new['index'];
222 }
223 return $current;
224 } );
225 $resultPageSet->populateFromTitles( $titles );
226 $offset = $params['offset'] + 1;
227 foreach ( $titles as $index => $title ) {
228 $resultPageSet->setGeneratorData(
229 $title,
230 $data[ $index ] + [ 'index' => $index + $offset ]
231 );
232 }
233 }
234 }
235
242 private function getSearchResultData( SearchResult $result, $prop ) {
243 // Silently skip broken and missing titles
244 if ( $result->isBrokenTitle() || $result->isMissingRevision() ) {
245 return null;
246 }
247
248 $vals = [];
249
250 $title = $result->getTitle();
252 $vals['pageid'] = $title->getArticleID();
253
254 if ( isset( $prop['size'] ) ) {
255 $vals['size'] = $result->getByteSize();
256 }
257 if ( isset( $prop['wordcount'] ) ) {
258 $vals['wordcount'] = $result->getWordCount();
259 }
260 if ( isset( $prop['snippet'] ) ) {
261 $vals['snippet'] = $result->getTextSnippet();
262 }
263 if ( isset( $prop['timestamp'] ) ) {
264 $vals['timestamp'] = wfTimestamp( TS_ISO_8601, $result->getTimestamp() );
265 }
266 if ( isset( $prop['titlesnippet'] ) ) {
267 $vals['titlesnippet'] = $result->getTitleSnippet();
268 }
269 if ( isset( $prop['categorysnippet'] ) ) {
270 $vals['categorysnippet'] = $result->getCategorySnippet();
271 }
272 if ( $result->getRedirectTitle() !== null ) {
273 if ( isset( $prop['redirecttitle'] ) ) {
274 $vals['redirecttitle'] = $result->getRedirectTitle()->getPrefixedText();
275 }
276 if ( isset( $prop['redirectsnippet'] ) ) {
277 $vals['redirectsnippet'] = $result->getRedirectSnippet();
278 }
279 }
280 if ( $result->getSectionTitle() !== null ) {
281 if ( isset( $prop['sectiontitle'] ) ) {
282 $vals['sectiontitle'] = $result->getSectionTitle()->getFragment();
283 }
284 if ( isset( $prop['sectionsnippet'] ) ) {
285 $vals['sectionsnippet'] = $result->getSectionSnippet();
286 }
287 }
288 if ( isset( $prop['isfilematch'] ) ) {
289 $vals['isfilematch'] = $result->isFileMatch();
290 }
291
292 if ( isset( $prop['extensiondata'] ) ) {
293 $extra = $result->getExtensionData();
294 // Add augmented data to the result. The data would be organized as a map:
295 // augmentorName => data
296 if ( $extra ) {
297 $vals['extensiondata'] = ApiResult::addMetadataToResultVars( $extra );
298 }
299 }
300
301 return $vals;
302 }
303
313 private function addInterwikiResults(
314 ISearchResultSet $matches, ApiResult $apiResult, $prop,
315 $section, $type
316 ) {
317 $totalhits = null;
318 if ( $matches->hasInterwikiResults( $type ) ) {
319 foreach ( $matches->getInterwikiResults( $type ) as $interwikiMatches ) {
320 // Include number of results if requested
321 $totalhits += $interwikiMatches->getTotalHits();
322
323 foreach ( $interwikiMatches as $result ) {
324 $title = $result->getTitle();
325 $vals = $this->getSearchResultData( $result, $prop );
326
327 $vals['namespace'] = $result->getInterwikiNamespaceText();
328 $vals['title'] = $title->getText();
329 $vals['url'] = $title->getFullURL();
330
331 // Add item to results and see whether it fits
332 $fit = $apiResult->addValue( [
333 'query',
334 $section . $this->getModuleName(),
335 $result->getInterwikiPrefix()
336 ], null, $vals );
337
338 if ( !$fit ) {
339 // We hit the limit. We can't really provide any meaningful
340 // pagination info so just bail out
341 break;
342 }
343 }
344 }
345 if ( $totalhits !== null ) {
346 $apiResult->addValue( [ 'query', $section . 'searchinfo' ], 'totalhits', $totalhits );
347 $apiResult->addIndexedTagName( [
348 'query', $section . $this->getModuleName()
349 ], 'p' );
350 }
351 }
352 return $totalhits;
353 }
354
355 public function getCacheMode( $params ) {
356 return 'public';
357 }
358
359 public function getAllowedParams() {
360 if ( $this->allowedParams !== null ) {
362 }
363
364 $this->allowedParams = $this->buildCommonApiParams() + [
365 'what' => [
367 'title',
368 'text',
369 'nearmatch',
370 ]
371 ],
372 'info' => [
373 ApiBase::PARAM_DFLT => 'totalhits|suggestion|rewrittenquery',
375 'totalhits',
376 'suggestion',
377 'rewrittenquery',
378 ],
380 ],
381 'prop' => [
382 ApiBase::PARAM_DFLT => 'size|wordcount|timestamp|snippet',
384 'size',
385 'wordcount',
386 'timestamp',
387 'snippet',
388 'titlesnippet',
389 'redirecttitle',
390 'redirectsnippet',
391 'sectiontitle',
392 'sectionsnippet',
393 'isfilematch',
394 'categorysnippet',
395 'score', // deprecated
396 'hasrelated', // deprecated
397 'extensiondata',
398 ],
402 'score' => true,
403 'hasrelated' => true
404 ],
405 ],
406 'interwiki' => false,
407 'enablerewrites' => false,
408 ];
409
410 // Generators only add info/properties if explicitly requested. T263841
411 if ( $this->isInGeneratorMode() ) {
412 $this->allowedParams['prop'][ApiBase::PARAM_DFLT] = '';
413 $this->allowedParams['info'][ApiBase::PARAM_DFLT] = '';
414 }
415
416 // If we have more than one engine the list of available sorts is
417 // difficult to represent. For now don't expose it.
418 $alternatives = $this->searchEngineConfig->getSearchTypes();
419 if ( count( $alternatives ) == 1 ) {
420 $this->allowedParams['sort'] = [
421 ApiBase::PARAM_DFLT => SearchEngine::DEFAULT_SORT,
422 ApiBase::PARAM_TYPE => $this->searchEngineFactory->create()->getValidSorts(),
423 ];
424 }
425
427 }
428
429 public function getSearchProfileParams() {
430 return [
431 'qiprofile' => [
432 'profile-type' => SearchEngine::FT_QUERY_INDEP_PROFILE_TYPE,
433 'help-message' => 'apihelp-query+search-param-qiprofile',
434 ],
435 ];
436 }
437
438 protected function getExamplesMessages() {
439 return [
440 'action=query&list=search&srsearch=meaning'
441 => 'apihelp-query+search-example-simple',
442 'action=query&list=search&srwhat=text&srsearch=meaning'
443 => 'apihelp-query+search-example-text',
444 'action=query&generator=search&gsrsearch=meaning&prop=info'
445 => 'apihelp-query+search-example-generator',
446 ];
447 }
448
449 public function getHelpUrls() {
450 return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Search';
451 }
452}
wfDeprecatedMsg( $msg, $version=false, $component=false, $callerOffset=2)
Log a deprecation warning with arbitrary message text.
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
buildSearchEngine(array $params=null)
Build the search engine to use.
buildCommonApiParams( $isScrollable=true)
The set of api parameters that are shared between api calls that call the SearchEngine.
Definition SearchApi.php:63
dieWithError( $msg, $code=null, $data=null, $httpCode=0)
Abort execution with an error.
Definition ApiBase.php:1436
const PARAM_DEPRECATED_VALUES
Definition ApiBase.php:129
getMain()
Get the main module.
Definition ApiBase.php:513
const PARAM_TYPE
Definition ApiBase.php:81
const PARAM_DFLT
Definition ApiBase.php:73
const PARAM_HELP_MSG_PER_VALUE
((string|array|Message)[]) When PARAM_TYPE is an array, this is an array mapping those values to $msg...
Definition ApiBase.php:195
getResult()
Get the result object.
Definition ApiBase.php:628
extractRequestParams( $options=[])
Using getAllowedParams(), this function makes an array of the values provided by the user,...
Definition ApiBase.php:764
getModuleName()
Get the name of the module being executed by this instance.
Definition ApiBase.php:497
dieStatus(StatusValue $status)
Throw an ApiUsageException based on the Status object.
Definition ApiBase.php:1495
const PARAM_ISMULTI
Definition ApiBase.php:77
static addTitleInfo(&$arr, $title, $prefix='')
Add information (title and namespace) about a Title object to a result array.
setContinueEnumParameter( $paramName, $paramValue)
Overridden to set the generator param if in generator mode.
isInGeneratorMode()
Indicate whether the module is in generator mode.
Query module to perform full text search within wiki titles and content.
run( $resultPageSet=null)
SearchEngineFactory $searchEngineFactory
execute()
Evaluates the parameters, performs the requested query, and sets up the result.
getHelpUrls()
Return links to more detailed help pages about the module.
getExamplesMessages()
Returns usage examples for this module.
getSearchResultData(SearchResult $result, $prop)
Assemble search result data.
__construct(ApiQuery $query, $moduleName, SearchEngineConfig $searchEngineConfig, SearchEngineFactory $searchEngineFactory)
getCacheMode( $params)
Get the cache mode for the data generated by this module.
getAllowedParams()
Returns an array of allowed parameters (parameter name) => (default value) or (parameter name) => (ar...
addInterwikiResults(ISearchResultSet $matches, ApiResult $apiResult, $prop, $section, $type)
Add interwiki results as a section in query results.
executeGenerator( $resultPageSet)
Execute this module as a generator.
SearchEngineConfig $searchEngineConfig
array $allowedParams
list of api allowed params
This is the main query class.
Definition ApiQuery.php:37
This class represents the result of the API operations.
Definition ApiResult.php:35
addIndexedTagName( $path, $tag)
Set the tag name for numeric-keyed values in XML format.
addValue( $path, $name, $value, $flags=0)
Add value to the output data at the given path.
Configuration handling class for SearchEngine.
Factory class for SearchEngine.
NOTE: this class is being refactored into an abstract base class.
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition Status.php:44
trait SearchApi
Traits for API components that use a SearchEngine.
Definition SearchApi.php:27
A set of SearchEngine results.
return true
Definition router.php:92