]> scripts.mit.edu Git - autoinstalls/mediawiki.git/blob - includes/api/ApiQuerySearch.php
MediaWiki 1.30.2-scripts
[autoinstalls/mediawiki.git] / includes / api / ApiQuerySearch.php
1 <?php
2 /**
3  *
4  *
5  * Created on July 30, 2007
6  *
7  * Copyright © 2007 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
8  *
9  * This program is free software; you can redistribute it and/or modify
10  * it under the terms of the GNU General Public License as published by
11  * the Free Software Foundation; either version 2 of the License, or
12  * (at your option) any later version.
13  *
14  * This program is distributed in the hope that it will be useful,
15  * but WITHOUT ANY WARRANTY; without even the implied warranty of
16  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17  * GNU General Public License for more details.
18  *
19  * You should have received a copy of the GNU General Public License along
20  * with this program; if not, write to the Free Software Foundation, Inc.,
21  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
22  * http://www.gnu.org/copyleft/gpl.html
23  *
24  * @file
25  */
26
27 /**
28  * Query module to perform full text search within wiki titles and content
29  *
30  * @ingroup API
31  */
32 class ApiQuerySearch extends ApiQueryGeneratorBase {
33         use SearchApi;
34
35         /** @var array list of api allowed params */
36         private $allowedParams;
37
38         public function __construct( ApiQuery $query, $moduleName ) {
39                 parent::__construct( $query, $moduleName, 'sr' );
40         }
41
42         public function execute() {
43                 $this->run();
44         }
45
46         public function executeGenerator( $resultPageSet ) {
47                 $this->run( $resultPageSet );
48         }
49
50         /**
51          * @param ApiPageSet $resultPageSet
52          * @return void
53          */
54         private function run( $resultPageSet = null ) {
55                 global $wgContLang;
56                 $params = $this->extractRequestParams();
57
58                 // Extract parameters
59                 $query = $params['search'];
60                 $what = $params['what'];
61                 $interwiki = $params['interwiki'];
62                 $searchInfo = array_flip( $params['info'] );
63                 $prop = array_flip( $params['prop'] );
64
65                 // Create search engine instance and set options
66                 $search = $this->buildSearchEngine( $params );
67                 $search->setFeatureData( 'rewrite', (bool)$params['enablerewrites'] );
68                 $search->setFeatureData( 'interwiki', (bool)$interwiki );
69
70                 $query = $search->transformSearchTerm( $query );
71                 $query = $search->replacePrefixes( $query );
72
73                 // Perform the actual search
74                 if ( $what == 'text' ) {
75                         $matches = $search->searchText( $query );
76                 } elseif ( $what == 'title' ) {
77                         $matches = $search->searchTitle( $query );
78                 } elseif ( $what == 'nearmatch' ) {
79                         // near matches must receive the user input as provided, otherwise
80                         // the near matches within namespaces are lost.
81                         $matches = $search->getNearMatcher( $this->getConfig() )
82                                 ->getNearMatchResultSet( $params['search'] );
83                 } else {
84                         // We default to title searches; this is a terrible legacy
85                         // of the way we initially set up the MySQL fulltext-based
86                         // search engine with separate title and text fields.
87                         // In the future, the default should be for a combined index.
88                         $what = 'title';
89                         $matches = $search->searchTitle( $query );
90
91                         // Not all search engines support a separate title search,
92                         // for instance the Lucene-based engine we use on Wikipedia.
93                         // In this case, fall back to full-text search (which will
94                         // include titles in it!)
95                         if ( is_null( $matches ) ) {
96                                 $what = 'text';
97                                 $matches = $search->searchText( $query );
98                         }
99                 }
100
101                 if ( $matches instanceof Status ) {
102                         $status = $matches;
103                         $matches = $status->getValue();
104                 } else {
105                         $status = null;
106                 }
107
108                 if ( $status ) {
109                         if ( $status->isOK() ) {
110                                 $this->getMain()->getErrorFormatter()->addMessagesFromStatus(
111                                         $this->getModuleName(),
112                                         $status
113                                 );
114                         } else {
115                                 $this->dieStatus( $status );
116                         }
117                 } elseif ( is_null( $matches ) ) {
118                         $this->dieWithError( [ 'apierror-searchdisabled', $what ], "search-{$what}-disabled" );
119                 }
120
121                 if ( $resultPageSet === null ) {
122                         $apiResult = $this->getResult();
123                         // Add search meta data to result
124                         if ( isset( $searchInfo['totalhits'] ) ) {
125                                 $totalhits = $matches->getTotalHits();
126                                 if ( $totalhits !== null ) {
127                                         $apiResult->addValue( [ 'query', 'searchinfo' ],
128                                                 'totalhits', $totalhits );
129                                 }
130                         }
131                         if ( isset( $searchInfo['suggestion'] ) && $matches->hasSuggestion() ) {
132                                 $apiResult->addValue( [ 'query', 'searchinfo' ],
133                                         'suggestion', $matches->getSuggestionQuery() );
134                                 $apiResult->addValue( [ 'query', 'searchinfo' ],
135                                         'suggestionsnippet', $matches->getSuggestionSnippet() );
136                         }
137                         if ( isset( $searchInfo['rewrittenquery'] ) && $matches->hasRewrittenQuery() ) {
138                                 $apiResult->addValue( [ 'query', 'searchinfo' ],
139                                         'rewrittenquery', $matches->getQueryAfterRewrite() );
140                                 $apiResult->addValue( [ 'query', 'searchinfo' ],
141                                         'rewrittenquerysnippet', $matches->getQueryAfterRewriteSnippet() );
142                         }
143                 }
144
145                 // Add the search results to the result
146                 $terms = $wgContLang->convertForSearchResult( $matches->termMatches() );
147                 $titles = [];
148                 $count = 0;
149                 $result = $matches->next();
150                 $limit = $params['limit'];
151
152                 while ( $result ) {
153                         if ( ++$count > $limit ) {
154                                 // We've reached the one extra which shows that there are
155                                 // additional items to be had. Stop here...
156                                 $this->setContinueEnumParameter( 'offset', $params['offset'] + $params['limit'] );
157                                 break;
158                         }
159
160                         // Silently skip broken and missing titles
161                         if ( $result->isBrokenTitle() || $result->isMissingRevision() ) {
162                                 $result = $matches->next();
163                                 continue;
164                         }
165
166                         if ( $resultPageSet === null ) {
167                                 $vals = $this->getSearchResultData( $result, $prop, $terms );
168                                 if ( $vals ) {
169                                         // Add item to results and see whether it fits
170                                         $fit = $apiResult->addValue( [ 'query', $this->getModuleName() ], null, $vals );
171                                         if ( !$fit ) {
172                                                 $this->setContinueEnumParameter( 'offset', $params['offset'] + $count - 1 );
173                                                 break;
174                                         }
175                                 }
176                         } else {
177                                 $titles[] = $result->getTitle();
178                         }
179
180                         $result = $matches->next();
181                 }
182
183                 // Here we assume interwiki results do not count with
184                 // regular search results. We may want to reconsider this
185                 // if we ever return a lot of interwiki results or want pagination
186                 // for them.
187                 // Interwiki results inside main result set
188                 $canAddInterwiki = (bool)$params['enablerewrites'] && ( $resultPageSet === null );
189                 if ( $canAddInterwiki ) {
190                         $this->addInterwikiResults( $matches, $apiResult, $prop, $terms, 'additional',
191                                 SearchResultSet::INLINE_RESULTS );
192                 }
193
194                 // Interwiki results outside main result set
195                 if ( $interwiki && $resultPageSet === null ) {
196                         $this->addInterwikiResults( $matches, $apiResult, $prop, $terms, 'interwiki',
197                                 SearchResultSet::SECONDARY_RESULTS );
198                 }
199
200                 if ( $resultPageSet === null ) {
201                         $apiResult->addIndexedTagName( [
202                                 'query', $this->getModuleName()
203                         ], 'p' );
204                 } else {
205                         $resultPageSet->setRedirectMergePolicy( function ( $current, $new ) {
206                                 if ( !isset( $current['index'] ) || $new['index'] < $current['index'] ) {
207                                         $current['index'] = $new['index'];
208                                 }
209                                 return $current;
210                         } );
211                         $resultPageSet->populateFromTitles( $titles );
212                         $offset = $params['offset'] + 1;
213                         foreach ( $titles as $index => $title ) {
214                                 $resultPageSet->setGeneratorData( $title, [ 'index' => $index + $offset ] );
215                         }
216                 }
217         }
218
219         /**
220          * Assemble search result data.
221          * @param SearchResult $result Search result
222          * @param array        $prop Props to extract (as keys)
223          * @param array        $terms Terms list
224          * @return array|null Result data or null if result is broken in some way.
225          */
226         private function getSearchResultData( SearchResult $result, $prop, $terms ) {
227                 // Silently skip broken and missing titles
228                 if ( $result->isBrokenTitle() || $result->isMissingRevision() ) {
229                         return null;
230                 }
231
232                 $vals = [];
233
234                 $title = $result->getTitle();
235                 ApiQueryBase::addTitleInfo( $vals, $title );
236                 $vals['pageid'] = $title->getArticleID();
237
238                 if ( isset( $prop['size'] ) ) {
239                         $vals['size'] = $result->getByteSize();
240                 }
241                 if ( isset( $prop['wordcount'] ) ) {
242                         $vals['wordcount'] = $result->getWordCount();
243                 }
244                 if ( isset( $prop['snippet'] ) ) {
245                         $vals['snippet'] = $result->getTextSnippet( $terms );
246                 }
247                 if ( isset( $prop['timestamp'] ) ) {
248                         $vals['timestamp'] = wfTimestamp( TS_ISO_8601, $result->getTimestamp() );
249                 }
250                 if ( isset( $prop['titlesnippet'] ) ) {
251                         $vals['titlesnippet'] = $result->getTitleSnippet();
252                 }
253                 if ( isset( $prop['categorysnippet'] ) ) {
254                         $vals['categorysnippet'] = $result->getCategorySnippet();
255                 }
256                 if ( !is_null( $result->getRedirectTitle() ) ) {
257                         if ( isset( $prop['redirecttitle'] ) ) {
258                                 $vals['redirecttitle'] = $result->getRedirectTitle()->getPrefixedText();
259                         }
260                         if ( isset( $prop['redirectsnippet'] ) ) {
261                                 $vals['redirectsnippet'] = $result->getRedirectSnippet();
262                         }
263                 }
264                 if ( !is_null( $result->getSectionTitle() ) ) {
265                         if ( isset( $prop['sectiontitle'] ) ) {
266                                 $vals['sectiontitle'] = $result->getSectionTitle()->getFragment();
267                         }
268                         if ( isset( $prop['sectionsnippet'] ) ) {
269                                 $vals['sectionsnippet'] = $result->getSectionSnippet();
270                         }
271                 }
272                 if ( isset( $prop['isfilematch'] ) ) {
273                         $vals['isfilematch'] = $result->isFileMatch();
274                 }
275                 return $vals;
276         }
277
278         /**
279          * Add interwiki results as a section in query results.
280          * @param SearchResultSet $matches
281          * @param ApiResult       $apiResult
282          * @param array           $prop Props to extract (as keys)
283          * @param array           $terms Terms list
284          * @param string          $section Section name where results would go
285          * @param int             $type Interwiki result type
286          * @return int|null Number of total hits in the data or null if none was produced
287          */
288         private function addInterwikiResults(
289                 SearchResultSet $matches, ApiResult $apiResult, $prop,
290                 $terms, $section, $type
291         ) {
292                 $totalhits = null;
293                 if ( $matches->hasInterwikiResults( $type ) ) {
294                         foreach ( $matches->getInterwikiResults( $type ) as $interwikiMatches ) {
295                                 // Include number of results if requested
296                                 $totalhits += $interwikiMatches->getTotalHits();
297
298                                 $result = $interwikiMatches->next();
299                                 while ( $result ) {
300                                         $title = $result->getTitle();
301                                         $vals = $this->getSearchResultData( $result, $prop, $terms );
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                                         $result = $interwikiMatches->next();
321                                 }
322                         }
323                         if ( $totalhits !== null ) {
324                                 $apiResult->addValue( [ 'query', $section . 'searchinfo' ], 'totalhits', $totalhits );
325                                 $apiResult->addIndexedTagName( [
326                                         'query', $section . $this->getModuleName()
327                                 ], 'p' );
328                         }
329                 }
330                 return $totalhits;
331         }
332
333         public function getCacheMode( $params ) {
334                 return 'public';
335         }
336
337         public function getAllowedParams() {
338                 if ( $this->allowedParams !== null ) {
339                         return $this->allowedParams;
340                 }
341
342                 $this->allowedParams = $this->buildCommonApiParams() + [
343                         'what' => [
344                                 ApiBase::PARAM_TYPE => [
345                                         'title',
346                                         'text',
347                                         'nearmatch',
348                                 ]
349                         ],
350                         'info' => [
351                                 ApiBase::PARAM_DFLT => 'totalhits|suggestion|rewrittenquery',
352                                 ApiBase::PARAM_TYPE => [
353                                         'totalhits',
354                                         'suggestion',
355                                         'rewrittenquery',
356                                 ],
357                                 ApiBase::PARAM_ISMULTI => true,
358                         ],
359                         'prop' => [
360                                 ApiBase::PARAM_DFLT => 'size|wordcount|timestamp|snippet',
361                                 ApiBase::PARAM_TYPE => [
362                                         'size',
363                                         'wordcount',
364                                         'timestamp',
365                                         'snippet',
366                                         'titlesnippet',
367                                         'redirecttitle',
368                                         'redirectsnippet',
369                                         'sectiontitle',
370                                         'sectionsnippet',
371                                         'isfilematch',
372                                         'categorysnippet',
373                                         'score', // deprecated
374                                         'hasrelated', // deprecated
375                                 ],
376                                 ApiBase::PARAM_ISMULTI => true,
377                                 ApiBase::PARAM_HELP_MSG_PER_VALUE => [],
378                                 ApiBase::PARAM_DEPRECATED_VALUES => [
379                                         'score' => true,
380                                         'hasrelated' => true
381                                 ],
382                         ],
383                         'interwiki' => false,
384                         'enablerewrites' => false,
385                 ];
386
387                 return $this->allowedParams;
388         }
389
390         public function getSearchProfileParams() {
391                 return [
392                         'qiprofile' => [
393                                 'profile-type' => SearchEngine::FT_QUERY_INDEP_PROFILE_TYPE,
394                                 'help-message' => 'apihelp-query+search-param-qiprofile',
395                         ],
396                 ];
397         }
398
399         protected function getExamplesMessages() {
400                 return [
401                         'action=query&list=search&srsearch=meaning'
402                                 => 'apihelp-query+search-example-simple',
403                         'action=query&list=search&srwhat=text&srsearch=meaning'
404                                 => 'apihelp-query+search-example-text',
405                         'action=query&generator=search&gsrsearch=meaning&prop=info'
406                                 => 'apihelp-query+search-example-generator',
407                 ];
408         }
409
410         public function getHelpUrls() {
411                 return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Search';
412         }
413 }