MediaWiki  REL1_23
ApiQuerySearch.php
Go to the documentation of this file.
00001 <?php
00032 class ApiQuerySearch extends ApiQueryGeneratorBase {
00033 
00040     const BACKEND_NULL_PARAM = 'database-backed';
00041 
00042     public function __construct( $query, $moduleName ) {
00043         parent::__construct( $query, $moduleName, 'sr' );
00044     }
00045 
00046     public function execute() {
00047         $this->run();
00048     }
00049 
00050     public function executeGenerator( $resultPageSet ) {
00051         $this->run( $resultPageSet );
00052     }
00053 
00058     private function run( $resultPageSet = null ) {
00059         global $wgContLang;
00060         $params = $this->extractRequestParams();
00061 
00062         // Extract parameters
00063         $limit = $params['limit'];
00064         $query = $params['search'];
00065         $what = $params['what'];
00066         $interwiki = $params['interwiki'];
00067         $searchInfo = array_flip( $params['info'] );
00068         $prop = array_flip( $params['prop'] );
00069 
00070         // Create search engine instance and set options
00071         $search = isset( $params['backend'] ) && $params['backend'] != self::BACKEND_NULL_PARAM ?
00072             SearchEngine::create( $params['backend'] ) : SearchEngine::create();
00073         $search->setLimitOffset( $limit + 1, $params['offset'] );
00074         $search->setNamespaces( $params['namespace'] );
00075 
00076         $query = $search->transformSearchTerm( $query );
00077         $query = $search->replacePrefixes( $query );
00078 
00079         // Perform the actual search
00080         if ( $what == 'text' ) {
00081             $matches = $search->searchText( $query );
00082         } elseif ( $what == 'title' ) {
00083             $matches = $search->searchTitle( $query );
00084         } elseif ( $what == 'nearmatch' ) {
00085             $matches = SearchEngine::getNearMatchResultSet( $query );
00086         } else {
00087             // We default to title searches; this is a terrible legacy
00088             // of the way we initially set up the MySQL fulltext-based
00089             // search engine with separate title and text fields.
00090             // In the future, the default should be for a combined index.
00091             $what = 'title';
00092             $matches = $search->searchTitle( $query );
00093 
00094             // Not all search engines support a separate title search,
00095             // for instance the Lucene-based engine we use on Wikipedia.
00096             // In this case, fall back to full-text search (which will
00097             // include titles in it!)
00098             if ( is_null( $matches ) ) {
00099                 $what = 'text';
00100                 $matches = $search->searchText( $query );
00101             }
00102         }
00103         if ( is_null( $matches ) ) {
00104             $this->dieUsage( "{$what} search is disabled", "search-{$what}-disabled" );
00105         } elseif ( $matches instanceof Status && !$matches->isGood() ) {
00106             $this->dieUsage( $matches->getWikiText(), 'search-error' );
00107         }
00108 
00109         $apiResult = $this->getResult();
00110         // Add search meta data to result
00111         if ( isset( $searchInfo['totalhits'] ) ) {
00112             $totalhits = $matches->getTotalHits();
00113             if ( $totalhits !== null ) {
00114                 $apiResult->addValue( array( 'query', 'searchinfo' ),
00115                     'totalhits', $totalhits );
00116             }
00117         }
00118         if ( isset( $searchInfo['suggestion'] ) && $matches->hasSuggestion() ) {
00119             $apiResult->addValue( array( 'query', 'searchinfo' ),
00120                 'suggestion', $matches->getSuggestionQuery() );
00121         }
00122 
00123         // Add the search results to the result
00124         $terms = $wgContLang->convertForSearchResult( $matches->termMatches() );
00125         $titles = array();
00126         $count = 0;
00127         $result = $matches->next();
00128 
00129         while ( $result ) {
00130             if ( ++$count > $limit ) {
00131                 // We've reached the one extra which shows that there are
00132                 // additional items to be had. Stop here...
00133                 $this->setContinueEnumParameter( 'offset', $params['offset'] + $params['limit'] );
00134                 break;
00135             }
00136 
00137             // Silently skip broken and missing titles
00138             if ( $result->isBrokenTitle() || $result->isMissingRevision() ) {
00139                 $result = $matches->next();
00140                 continue;
00141             }
00142 
00143             $title = $result->getTitle();
00144             if ( is_null( $resultPageSet ) ) {
00145                 $vals = array();
00146                 ApiQueryBase::addTitleInfo( $vals, $title );
00147 
00148                 if ( isset( $prop['snippet'] ) ) {
00149                     $vals['snippet'] = $result->getTextSnippet( $terms );
00150                 }
00151                 if ( isset( $prop['size'] ) ) {
00152                     $vals['size'] = $result->getByteSize();
00153                 }
00154                 if ( isset( $prop['wordcount'] ) ) {
00155                     $vals['wordcount'] = $result->getWordCount();
00156                 }
00157                 if ( isset( $prop['timestamp'] ) ) {
00158                     $vals['timestamp'] = wfTimestamp( TS_ISO_8601, $result->getTimestamp() );
00159                 }
00160                 if ( !is_null( $result->getScore() ) && isset( $prop['score'] ) ) {
00161                     $vals['score'] = $result->getScore();
00162                 }
00163                 if ( isset( $prop['titlesnippet'] ) ) {
00164                     $vals['titlesnippet'] = $result->getTitleSnippet( $terms );
00165                 }
00166                 if ( !is_null( $result->getRedirectTitle() ) ) {
00167                     if ( isset( $prop['redirecttitle'] ) ) {
00168                         $vals['redirecttitle'] = $result->getRedirectTitle();
00169                     }
00170                     if ( isset( $prop['redirectsnippet'] ) ) {
00171                         $vals['redirectsnippet'] = $result->getRedirectSnippet( $terms );
00172                     }
00173                 }
00174                 if ( !is_null( $result->getSectionTitle() ) ) {
00175                     if ( isset( $prop['sectiontitle'] ) ) {
00176                         $vals['sectiontitle'] = $result->getSectionTitle()->getFragment();
00177                     }
00178                     if ( isset( $prop['sectionsnippet'] ) ) {
00179                         $vals['sectionsnippet'] = $result->getSectionSnippet();
00180                     }
00181                 }
00182                 if ( isset( $prop['hasrelated'] ) && $result->hasRelated() ) {
00183                     $vals['hasrelated'] = '';
00184                 }
00185 
00186                 // Add item to results and see whether it fits
00187                 $fit = $apiResult->addValue( array( 'query', $this->getModuleName() ),
00188                     null, $vals );
00189                 if ( !$fit ) {
00190                     $this->setContinueEnumParameter( 'offset', $params['offset'] + $count - 1 );
00191                     break;
00192                 }
00193             } else {
00194                 $titles[] = $title;
00195             }
00196 
00197             $result = $matches->next();
00198         }
00199 
00200         $hasInterwikiResults = false;
00201         if ( $interwiki && $resultPageSet === null && $matches->hasInterwikiResults() ) {
00202             $matches = $matches->getInterwikiResults();
00203             $iwprefixes = array();
00204             $hasInterwikiResults = true;
00205 
00206             // Include number of results if requested
00207             if ( isset( $searchInfo['totalhits'] ) ) {
00208                 $totalhits = $matches->getTotalHits();
00209                 if ( $totalhits !== null ) {
00210                     $apiResult->addValue( array( 'query', 'interwikisearchinfo' ),
00211                         'totalhits', $totalhits );
00212                 }
00213             }
00214 
00215             $result = $matches->next();
00216             while ( $result ) {
00217                 $title = $result->getTitle();
00218                 $vals = array(
00219                     'namespace' => $result->getInterwikiNamespaceText(),
00220                     'title' => $title->getText(),
00221                     'url' => $title->getFullUrl(),
00222                 );
00223 
00224                 // Add item to results and see whether it fits
00225                 $fit = $apiResult->addValue( array( 'query', 'interwiki' . $this->getModuleName(), $result->getInterwikiPrefix()  ),
00226                     null, $vals );
00227                 if ( !$fit ) {
00228                     // We hit the limit. We can't really provide any meaningful
00229                     // pagination info so just bail out
00230                     break;
00231                 }
00232 
00233                 $result = $matches->next();
00234             }
00235         }
00236 
00237         if ( is_null( $resultPageSet ) ) {
00238             $apiResult->setIndexedTagName_internal( array(
00239                 'query', $this->getModuleName()
00240             ), 'p' );
00241             if ( $hasInterwikiResults ) {
00242                 $apiResult->setIndexedTagName_internal( array(
00243                     'query', 'interwiki' . $this->getModuleName()
00244                 ), 'p' );
00245             }
00246         } else {
00247             $resultPageSet->populateFromTitles( $titles );
00248         }
00249     }
00250 
00251     public function getCacheMode( $params ) {
00252         return 'public';
00253     }
00254 
00255     public function getAllowedParams() {
00256         global $wgSearchType;
00257 
00258         $params = array(
00259             'search' => array(
00260                 ApiBase::PARAM_TYPE => 'string',
00261                 ApiBase::PARAM_REQUIRED => true
00262             ),
00263             'namespace' => array(
00264                 ApiBase::PARAM_DFLT => NS_MAIN,
00265                 ApiBase::PARAM_TYPE => 'namespace',
00266                 ApiBase::PARAM_ISMULTI => true,
00267             ),
00268             'what' => array(
00269                 ApiBase::PARAM_DFLT => null,
00270                 ApiBase::PARAM_TYPE => array(
00271                     'title',
00272                     'text',
00273                     'nearmatch',
00274                 )
00275             ),
00276             'info' => array(
00277                 ApiBase::PARAM_DFLT => 'totalhits|suggestion',
00278                 ApiBase::PARAM_TYPE => array(
00279                     'totalhits',
00280                     'suggestion',
00281                 ),
00282                 ApiBase::PARAM_ISMULTI => true,
00283             ),
00284             'prop' => array(
00285                 ApiBase::PARAM_DFLT => 'size|wordcount|timestamp|snippet',
00286                 ApiBase::PARAM_TYPE => array(
00287                     'size',
00288                     'wordcount',
00289                     'timestamp',
00290                     'score',
00291                     'snippet',
00292                     'titlesnippet',
00293                     'redirecttitle',
00294                     'redirectsnippet',
00295                     'sectiontitle',
00296                     'sectionsnippet',
00297                     'hasrelated',
00298                 ),
00299                 ApiBase::PARAM_ISMULTI => true,
00300             ),
00301             'offset' => 0,
00302             'limit' => array(
00303                 ApiBase::PARAM_DFLT => 10,
00304                 ApiBase::PARAM_TYPE => 'limit',
00305                 ApiBase::PARAM_MIN => 1,
00306                 ApiBase::PARAM_MAX => ApiBase::LIMIT_SML1,
00307                 ApiBase::PARAM_MAX2 => ApiBase::LIMIT_SML2
00308             ),
00309             'interwiki' => false,
00310         );
00311 
00312         $alternatives = SearchEngine::getSearchTypes();
00313         if ( count( $alternatives ) > 1 ) {
00314             if ( $alternatives[0] === null ) {
00315                 $alternatives[0] = self::BACKEND_NULL_PARAM;
00316             }
00317             $params['backend'] = array(
00318                 ApiBase::PARAM_DFLT => $wgSearchType,
00319                 ApiBase::PARAM_TYPE => $alternatives,
00320             );
00321         }
00322 
00323         return $params;
00324     }
00325 
00326     public function getParamDescription() {
00327         $descriptions = array(
00328             'search' => 'Search for all page titles (or content) that has this value',
00329             'namespace' => 'The namespace(s) to enumerate',
00330             'what' => 'Search inside the text or titles',
00331             'info' => 'What metadata to return',
00332             'prop' => array(
00333                 'What properties to return',
00334                 ' size             - Adds the size of the page in bytes',
00335                 ' wordcount        - Adds the word count of the page',
00336                 ' timestamp        - Adds the timestamp of when the page was last edited',
00337                 ' score            - Adds the score (if any) from the search engine',
00338                 ' snippet          - Adds a parsed snippet of the page',
00339                 ' titlesnippet     - Adds a parsed snippet of the page title',
00340                 ' redirectsnippet  - Adds a parsed snippet of the redirect title',
00341                 ' redirecttitle    - Adds the title of the matching redirect',
00342                 ' sectionsnippet   - Adds a parsed snippet of the matching section title',
00343                 ' sectiontitle     - Adds the title of the matching section',
00344                 ' hasrelated       - Indicates whether a related search is available',
00345             ),
00346             'offset' => 'Use this value to continue paging (return by query)',
00347             'limit' => 'How many total pages to return',
00348             'interwiki' => 'Include interwiki results in the search, if available'
00349         );
00350 
00351         if ( count( SearchEngine::getSearchTypes() ) > 1 ) {
00352             $descriptions['backend'] = 'Which search backend to use, if not the default';
00353         }
00354 
00355         return $descriptions;
00356     }
00357 
00358     public function getResultProperties() {
00359         return array(
00360             '' => array(
00361                 'ns' => 'namespace',
00362                 'title' => 'string'
00363             ),
00364             'snippet' => array(
00365                 'snippet' => 'string'
00366             ),
00367             'size' => array(
00368                 'size' => 'integer'
00369             ),
00370             'wordcount' => array(
00371                 'wordcount' => 'integer'
00372             ),
00373             'timestamp' => array(
00374                 'timestamp' => 'timestamp'
00375             ),
00376             'score' => array(
00377                 'score' => array(
00378                     ApiBase::PROP_TYPE => 'string',
00379                     ApiBase::PROP_NULLABLE => true
00380                 )
00381             ),
00382             'titlesnippet' => array(
00383                 'titlesnippet' => 'string'
00384             ),
00385             'redirecttitle' => array(
00386                 'redirecttitle' => array(
00387                     ApiBase::PROP_TYPE => 'string',
00388                     ApiBase::PROP_NULLABLE => true
00389                 )
00390             ),
00391             'redirectsnippet' => array(
00392                 'redirectsnippet' => array(
00393                     ApiBase::PROP_TYPE => 'string',
00394                     ApiBase::PROP_NULLABLE => true
00395                 )
00396             ),
00397             'sectiontitle' => array(
00398                 'sectiontitle' => array(
00399                     ApiBase::PROP_TYPE => 'string',
00400                     ApiBase::PROP_NULLABLE => true
00401                 )
00402             ),
00403             'sectionsnippet' => array(
00404                 'sectionsnippet' => array(
00405                     ApiBase::PROP_TYPE => 'string',
00406                     ApiBase::PROP_NULLABLE => true
00407                 )
00408             ),
00409             'hasrelated' => array(
00410                 'hasrelated' => 'boolean'
00411             )
00412         );
00413     }
00414 
00415     public function getDescription() {
00416         return 'Perform a full text search.';
00417     }
00418 
00419     public function getPossibleErrors() {
00420         return array_merge( parent::getPossibleErrors(), array(
00421             array( 'code' => 'search-text-disabled', 'info' => 'text search is disabled' ),
00422             array( 'code' => 'search-title-disabled', 'info' => 'title search is disabled' ),
00423             array( 'code' => 'search-error', 'info' => 'search error has occurred' ),
00424         ) );
00425     }
00426 
00427     public function getExamples() {
00428         return array(
00429             'api.php?action=query&list=search&srsearch=meaning',
00430             'api.php?action=query&list=search&srwhat=text&srsearch=meaning',
00431             'api.php?action=query&generator=search&gsrsearch=meaning&prop=info',
00432         );
00433     }
00434 
00435     public function getHelpUrls() {
00436         return 'https://www.mediawiki.org/wiki/API:Search';
00437     }
00438 }