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