Include completion search into SearchEngine
By default it still uses PrefixSearch and supports PrefixSearchBackend but it can be deprecated and phased out and SearchEngine extensions used instead. New APIs: - SearchEngine public function defaultPrefixSearch( $search ); public function completionSearch( $search ); public function completionSearchWithVariants( $search ); Search engines should override: protected function completionSearchBackend( $search ); Bug: T121430 Change-Id: Ie78649591dff94d21b72fad8e4e5eab010a461df
This commit is contained in:
parent
8aa6c9f43e
commit
027972a20f
19 changed files with 1099 additions and 82 deletions
|
|
@ -1120,6 +1120,8 @@ $wgAutoloadLocalClasses = array(
|
|||
'SearchResult' => __DIR__ . '/includes/search/SearchResult.php',
|
||||
'SearchResultSet' => __DIR__ . '/includes/search/SearchResultSet.php',
|
||||
'SearchSqlite' => __DIR__ . '/includes/search/SearchSqlite.php',
|
||||
'SearchSuggestion' => __DIR__ . '/includes/search/SearchSuggestion.php',
|
||||
'SearchSuggestionSet' => __DIR__ . '/includes/search/SearchSuggestionSet.php',
|
||||
'SearchUpdate' => __DIR__ . '/includes/deferred/SearchUpdate.php',
|
||||
'SectionProfileCallback' => __DIR__ . '/includes/profiler/SectionProfiler.php',
|
||||
'SectionProfiler' => __DIR__ . '/includes/profiler/SectionProfiler.php',
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@
|
|||
/**
|
||||
* Handles searching prefixes of titles and finding any page
|
||||
* names that match. Used largely by the OpenSearch implementation.
|
||||
* @deprecated Since 1.27, Use SearchEngine::prefixSearchSubpages or SearchEngine::completionSearch
|
||||
*
|
||||
* @ingroup Search
|
||||
*/
|
||||
|
|
@ -259,14 +260,17 @@ abstract class PrefixSearch {
|
|||
* @param int $offset Number of items to skip
|
||||
* @return array Array of Title objects
|
||||
*/
|
||||
protected function defaultSearchBackend( $namespaces, $search, $limit, $offset ) {
|
||||
public function defaultSearchBackend( $namespaces, $search, $limit, $offset ) {
|
||||
$ns = array_shift( $namespaces ); // support only one namespace
|
||||
if ( in_array( NS_MAIN, $namespaces ) ) {
|
||||
if ( is_null( $ns ) || in_array( NS_MAIN, $namespaces ) ) {
|
||||
$ns = NS_MAIN; // if searching on many always default to main
|
||||
}
|
||||
|
||||
$t = Title::newFromText( $search, $ns );
|
||||
if ( $ns == NS_SPECIAL ) {
|
||||
return $this->specialSearch( $search, $limit, $offset );
|
||||
}
|
||||
|
||||
$t = Title::newFromText( $search, $ns );
|
||||
$prefix = $t ? $t->getDBkey() : '';
|
||||
$dbr = wfGetDB( DB_SLAVE );
|
||||
$res = $dbr->select( 'page',
|
||||
|
|
@ -318,6 +322,7 @@ abstract class PrefixSearch {
|
|||
|
||||
/**
|
||||
* Performs prefix search, returning Title objects
|
||||
* @deprecated Since 1.27, Use SearchEngine::prefixSearchSubpages or SearchEngine::completionSearch
|
||||
* @ingroup Search
|
||||
*/
|
||||
class TitlePrefixSearch extends PrefixSearch {
|
||||
|
|
@ -337,6 +342,7 @@ class TitlePrefixSearch extends PrefixSearch {
|
|||
|
||||
/**
|
||||
* Performs prefix search, returning strings
|
||||
* @deprecated Since 1.27, Use SearchEngine::prefixSearchSubpages or SearchEngine::completionSearch
|
||||
* @ingroup Search
|
||||
*/
|
||||
class StringPrefixSearch extends PrefixSearch {
|
||||
|
|
|
|||
|
|
@ -123,9 +123,12 @@ class ApiOpenSearch extends ApiBase {
|
|||
* @param array &$results Put results here. Keys have to be integers.
|
||||
*/
|
||||
protected function search( $search, $limit, $namespaces, $resolveRedir, &$results ) {
|
||||
// Find matching titles as Title objects
|
||||
$searcher = new TitlePrefixSearch;
|
||||
$titles = $searcher->searchWithVariants( $search, $limit, $namespaces );
|
||||
|
||||
$searchEngine = SearchEngine::create();
|
||||
$searchEngine->setLimitOffset( $limit );
|
||||
$searchEngine->setNamespaces( $namespaces );
|
||||
$titles = $searchEngine->extractTitles( $searchEngine->completionSearchWithVariants( $search ) );
|
||||
|
||||
if ( !$titles ) {
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,8 +45,11 @@ class ApiQueryPrefixSearch extends ApiQueryGeneratorBase {
|
|||
$namespaces = $params['namespace'];
|
||||
$offset = $params['offset'];
|
||||
|
||||
$searcher = new TitlePrefixSearch;
|
||||
$titles = $searcher->searchWithVariants( $search, $limit + 1, $namespaces, $offset );
|
||||
$searchEngine = SearchEngine::create();
|
||||
$searchEngine->setLimitOffset( $limit + 1, $offset );
|
||||
$searchEngine->setNamespaces( $namespaces );
|
||||
$titles = $searchEngine->extractTitles( $searchEngine->completionSearchWithVariants( $search ) );
|
||||
|
||||
if ( $resultPageSet ) {
|
||||
$resultPageSet->setRedirectMergePolicy( function( array $current, array $new ) {
|
||||
if ( !isset( $current['index'] ) || $new['index'] < $current['index'] ) {
|
||||
|
|
|
|||
|
|
@ -296,6 +296,15 @@ class SearchEngine {
|
|||
* @param int[]|null $namespaces
|
||||
*/
|
||||
function setNamespaces( $namespaces ) {
|
||||
if ( $namespaces ) {
|
||||
// Filter namespaces to only keep valid ones
|
||||
$validNs = $this->searchableNamespaces();
|
||||
$namespaces = array_filter( $namespaces, function( $ns ) use( $validNs ) {
|
||||
return $ns < 0 || isset( $validNs[$ns] );
|
||||
} );
|
||||
} else {
|
||||
$namespaces = array();
|
||||
}
|
||||
$this->namespaces = $namespaces;
|
||||
}
|
||||
|
||||
|
|
@ -570,6 +579,201 @@ class SearchEngine {
|
|||
public function textAlreadyUpdatedForIndex() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes search simple string if it was namespaced.
|
||||
* Sets namespaces of the search to namespaces extracted from string.
|
||||
* @param string $search
|
||||
* @return $string Simplified search string
|
||||
*/
|
||||
protected function normalizeNamespaces( $search ) {
|
||||
// Find a Title which is not an interwiki and is in NS_MAIN
|
||||
$title = Title::newFromText( $search );
|
||||
$ns = $this->namespaces;
|
||||
if ( $title && !$title->isExternal() ) {
|
||||
$ns = array( $title->getNamespace() );
|
||||
$search = $title->getText();
|
||||
if ( $ns[0] == NS_MAIN ) {
|
||||
$ns = $this->namespaces; // no explicit prefix, use default namespaces
|
||||
Hooks::run( 'PrefixSearchExtractNamespace', array( &$ns, &$search ) );
|
||||
}
|
||||
} else {
|
||||
$title = Title::newFromText( $search . 'Dummy' );
|
||||
if ( $title && $title->getText() == 'Dummy'
|
||||
&& $title->getNamespace() != NS_MAIN
|
||||
&& !$title->isExternal() )
|
||||
{
|
||||
$ns = array( $title->getNamespace() );
|
||||
$search = '';
|
||||
} else {
|
||||
Hooks::run( 'PrefixSearchExtractNamespace', array( &$ns, &$search ) );
|
||||
}
|
||||
}
|
||||
|
||||
$ns = array_map( function( $space ) {
|
||||
return $space == NS_MEDIA ? NS_FILE : $space;
|
||||
}, $ns );
|
||||
|
||||
$this->setNamespaces( $ns );
|
||||
return $search;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a completion search.
|
||||
* Does not resolve namespaces and does not check variants.
|
||||
* Search engine implementations may want to override this function.
|
||||
* @param string $search
|
||||
* @return SearchSuggestionSet
|
||||
*/
|
||||
protected function completionSearchBackend( $search ) {
|
||||
$results = array();
|
||||
|
||||
$search = trim( $search );
|
||||
|
||||
if ( !in_array( NS_SPECIAL, $this->namespaces ) && // We do not run hook on Special: search
|
||||
!Hooks::run( 'PrefixSearchBackend',
|
||||
array( $this->namespaces, $search, $this->limit, &$results, $this->offset )
|
||||
) ) {
|
||||
// False means hook worked.
|
||||
// FIXME: Yes, the API is weird. That's why it is going to be deprecated.
|
||||
|
||||
return SearchSuggestionSet::fromStrings( $results );
|
||||
} else {
|
||||
// Hook did not do the job, use default simple search
|
||||
$results = $this->simplePrefixSearch( $search );
|
||||
return SearchSuggestionSet::fromTitles( $results );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a completion search.
|
||||
* @param string $search
|
||||
* @return SearchSuggestionSet
|
||||
*/
|
||||
public function completionSearch( $search ) {
|
||||
if ( trim( $search ) === '' ) {
|
||||
return SearchSuggestionSet::emptySuggestionSet(); // Return empty result
|
||||
}
|
||||
$search = $this->normalizeNamespaces( $search );
|
||||
return $this->processCompletionResults( $search, $this->completionSearchBackend( $search ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a completion search with variants.
|
||||
* @param string $search
|
||||
* @return SearchSuggestionSet
|
||||
*/
|
||||
public function completionSearchWithVariants( $search ) {
|
||||
if ( trim( $search ) === '' ) {
|
||||
return SearchSuggestionSet::emptySuggestionSet(); // Return empty result
|
||||
}
|
||||
$search = $this->normalizeNamespaces( $search );
|
||||
|
||||
$results = $this->completionSearchBackend( $search );
|
||||
$fallbackLimit = $this->limit - $results->getSize();
|
||||
if ( $fallbackLimit > 0 ) {
|
||||
global $wgContLang;
|
||||
|
||||
$fallbackSearches = $wgContLang->autoConvertToAllVariants( $search );
|
||||
$fallbackSearches = array_diff( array_unique( $fallbackSearches ), array( $search ) );
|
||||
|
||||
foreach ( $fallbackSearches as $fbs ) {
|
||||
$this->setLimitOffset( $fallbackLimit );
|
||||
$fallbackSearchResult = $this->completionSearch( $fbs );
|
||||
$results->appendAll( $fallbackSearchResult );
|
||||
$fallbackLimit -= count( $fallbackSearchResult );
|
||||
if ( $fallbackLimit <= 0 ) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $this->processCompletionResults( $search, $results );
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract titles from completion results
|
||||
* @param SearchSuggestionSet $completionResults
|
||||
* @return Title[]
|
||||
*/
|
||||
public function extractTitles( SearchSuggestionSet $completionResults ) {
|
||||
return $completionResults->map( function( SearchSuggestion $sugg ) {
|
||||
return $sugg->getSuggestedTitle();
|
||||
} );
|
||||
}
|
||||
|
||||
/**
|
||||
* Process completion search results.
|
||||
* Resolves the titles and rescores.
|
||||
* @param SearchSuggestionSet $suggestions
|
||||
* @return SearchSuggestionSet
|
||||
*/
|
||||
protected function processCompletionResults( $search, SearchSuggestionSet $suggestions ) {
|
||||
if ( $suggestions->getSize() == 0 ) {
|
||||
// If we don't have anything, don't bother
|
||||
return $suggestions;
|
||||
}
|
||||
$search = trim( $search );
|
||||
// preload the titles with LinkBatch
|
||||
$titles = $suggestions->map( function( SearchSuggestion $sugg ) {
|
||||
return $sugg->getSuggestedTitle();
|
||||
} );
|
||||
$lb = new LinkBatch( $titles );
|
||||
$lb->setCaller( __METHOD__ );
|
||||
$lb->execute();
|
||||
|
||||
$results = $suggestions->map( function( SearchSuggestion $sugg ) {
|
||||
return $sugg->getSuggestedTitle()->getPrefixedText();
|
||||
} );
|
||||
|
||||
// Rescore results with an exact title match
|
||||
$rescorer = new SearchExactMatchRescorer();
|
||||
$rescoredResults = $rescorer->rescore( $search, $this->namespaces, $results, $this->limit );
|
||||
|
||||
if ( count( $rescoredResults ) > 0 ) {
|
||||
$found = array_search( $rescoredResults[0], $results );
|
||||
if ( $found === false ) {
|
||||
// If the first result is not in the previous array it
|
||||
// means that we found a new exact match
|
||||
$exactMatch = SearchSuggestion::fromTitle( 0, Title::newFromText( $rescoredResults[0] ) );
|
||||
$suggestions->prepend( $exactMatch );
|
||||
$suggestions->shrink( $this->limit );
|
||||
} else {
|
||||
// if the first result is not the same we need to rescore
|
||||
if ( $found > 0 ) {
|
||||
$suggestions->rescore( $found );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $suggestions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple prefix search for subpages.
|
||||
* @param string $search
|
||||
* @return Title[]
|
||||
*/
|
||||
public function defaultPrefixSearch( $search ) {
|
||||
if ( trim( $search ) === '' ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$search = $this->normalizeNamespaces( $search );
|
||||
return $this->simplePrefixSearch( $search );
|
||||
}
|
||||
|
||||
/**
|
||||
* Call out to simple search backend.
|
||||
* Defaults to TitlePrefixSearch.
|
||||
* @param string $search
|
||||
* @return Title[]
|
||||
*/
|
||||
protected function simplePrefixSearch( $search ) {
|
||||
// Use default database prefix search
|
||||
$backend = new TitlePrefixSearch;
|
||||
return $backend->defaultSearchBackend( $this->namespaces, $search, $this->limit, $this->offset );
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
187
includes/search/SearchSuggestion.php
Normal file
187
includes/search/SearchSuggestion.php
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Search suggestion
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
* http://www.gnu.org/copyleft/gpl.html
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* A search suggestion
|
||||
*
|
||||
*/
|
||||
class SearchSuggestion {
|
||||
/**
|
||||
* @var string the suggestion
|
||||
*/
|
||||
private $text;
|
||||
|
||||
/**
|
||||
* @var string the suggestion URL
|
||||
*/
|
||||
private $url;
|
||||
|
||||
/**
|
||||
* @var Title|null the suggested title
|
||||
*/
|
||||
private $suggestedTitle;
|
||||
|
||||
/**
|
||||
* NOTE: even if suggestedTitle is a redirect suggestedTitleID
|
||||
* is the ID of the target page.
|
||||
* @var int|null the suggested title ID
|
||||
*/
|
||||
private $suggestedTitleID;
|
||||
|
||||
/**
|
||||
* @var float|null The suggestion score
|
||||
*/
|
||||
private $score;
|
||||
|
||||
/**
|
||||
* Construct a new suggestion
|
||||
* @param float $score the suggestion score
|
||||
* @param string $text|null the suggestion text
|
||||
* @param Title|null $suggestedTitle the suggested title
|
||||
* @param int|null $suggestedTitleID the suggested title ID
|
||||
*/
|
||||
public function __construct( $score, $text = null, Title $suggestedTitle = null,
|
||||
$suggestedTitleID = null ) {
|
||||
$this->score = $score;
|
||||
$this->text = $text;
|
||||
if ( $suggestedTitle ) {
|
||||
$this->setSuggestedTitle( $suggestedTitle );
|
||||
}
|
||||
$this->suggestedTitleID = $suggestedTitleID;
|
||||
}
|
||||
|
||||
/**
|
||||
* The suggestion text
|
||||
* @return string
|
||||
*/
|
||||
public function getText() {
|
||||
return $this->text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the suggestion text.
|
||||
* @param string $text
|
||||
* @param bool $setTitle Should we also update the title?
|
||||
*/
|
||||
public function setText( $text, $setTitle = true ) {
|
||||
$this->text = $text;
|
||||
if ( $setTitle && $text ) {
|
||||
$this->setSuggestedTitle( Title::makeTitle( 0, $text ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Title object in the case this suggestion is based on a title.
|
||||
* May return null if the suggestion is not a Title.
|
||||
* @return Title|null
|
||||
*/
|
||||
public function getSuggestedTitle() {
|
||||
return $this->suggestedTitle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the suggested title
|
||||
* @param Title|null $title
|
||||
*/
|
||||
public function setSuggestedTitle( Title $title = null ) {
|
||||
$this->suggestedTitle = $title;
|
||||
if ( $title !== null ) {
|
||||
$this->url = wfExpandUrl( $title->getFullURL(), PROTO_CURRENT );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Title ID in the case this suggestion is based on a title.
|
||||
* May return null if the suggestion is not a Title.
|
||||
* @return int|null
|
||||
*/
|
||||
public function getSuggestedTitleID() {
|
||||
return $this->suggestedTitleID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the suggested title ID
|
||||
* @param int|null $suggestedTitleID
|
||||
*/
|
||||
public function setSuggestedTitleID( $suggestedTitleID = null ) {
|
||||
$this->suggestedTitleID = $suggestedTitleID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Suggestion score
|
||||
* @return float Suggestion score
|
||||
*/
|
||||
public function getScore() {
|
||||
return $this->score;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the suggestion score
|
||||
* @param float $score
|
||||
*/
|
||||
public function setScore( $score ) {
|
||||
$this->score = $score;
|
||||
}
|
||||
|
||||
/**
|
||||
* Suggestion URL, can be the link to the Title or maybe in the
|
||||
* future a link to the search results for this search suggestion.
|
||||
* @return string Suggestion URL
|
||||
*/
|
||||
public function getURL() {
|
||||
return $this->url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the suggestion URL
|
||||
* @param string $url
|
||||
*/
|
||||
public function setURL( $url ) {
|
||||
$this->url = $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create suggestion from Title
|
||||
* @param float $score Suggestions score
|
||||
* @param Title $title
|
||||
* @return SearchSuggestion
|
||||
*/
|
||||
public static function fromTitle( $score, Title $title ) {
|
||||
return new self( $score, $title->getPrefixedText(), $title, $title->getArticleID() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Create suggestion from text
|
||||
* Will also create a title if text if not empty.
|
||||
* @param float $score Suggestions score
|
||||
* @param string $text
|
||||
* @return SearchSuggestion
|
||||
*/
|
||||
public static function fromText( $score, $text ) {
|
||||
$suggestion = new self( $score, $text );
|
||||
if ( $text ) {
|
||||
$suggestion->setSuggestedTitle( Title::makeTitle( 0, $text ) );
|
||||
}
|
||||
return $suggestion;
|
||||
}
|
||||
|
||||
}
|
||||
213
includes/search/SearchSuggestionSet.php
Normal file
213
includes/search/SearchSuggestionSet.php
Normal file
|
|
@ -0,0 +1,213 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Search suggestion sets
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
* http://www.gnu.org/copyleft/gpl.html
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* A set of search suggestions.
|
||||
* The set is always ordered by score, with the best match first.
|
||||
*/
|
||||
class SearchSuggestionSet {
|
||||
/**
|
||||
* @var SearchSuggestion[]
|
||||
*/
|
||||
private $suggestions = array();
|
||||
|
||||
/**
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $pageMap = array();
|
||||
|
||||
/**
|
||||
* Builds a new set of suggestions.
|
||||
*
|
||||
* NOTE: the array should be sorted by score (higher is better),
|
||||
* in descending order.
|
||||
* SearchSuggestionSet will not try to re-order this input array.
|
||||
* Providing an unsorted input array is a mistake and will lead to
|
||||
* unexpected behaviors.
|
||||
*
|
||||
* @param SearchSuggestion[] $suggestions (must be sorted by score)
|
||||
*/
|
||||
public function __construct( array $suggestions ) {
|
||||
foreach ( $suggestions as $suggestion ) {
|
||||
$pageID = $suggestion->getSuggestedTitleID();
|
||||
if ( $pageID && empty( $this->pageMap[$pageID] ) ) {
|
||||
$this->pageMap[$pageID] = true;
|
||||
}
|
||||
$this->suggestions[] = $suggestion;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of suggestions.
|
||||
* @return SearchSuggestion[]
|
||||
*/
|
||||
public function getSuggestions() {
|
||||
return $this->suggestions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call array_map on the suggestions array
|
||||
* @param callback $callback
|
||||
* @return array
|
||||
*/
|
||||
public function map( $callback ) {
|
||||
return array_map( $callback, $this->suggestions );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new suggestion at the end.
|
||||
* If the score of the new suggestion is greater than the worst one,
|
||||
* the new suggestion score will be updated (worst - 1).
|
||||
*
|
||||
* @param SearchSuggestion $suggestion
|
||||
*/
|
||||
public function append( SearchSuggestion $suggestion ) {
|
||||
$pageID = $suggestion->getSuggestedTitleID();
|
||||
if ( $pageID && isset( $this->pageMap[$pageID] ) ) {
|
||||
return;
|
||||
}
|
||||
if ( $this->getSize() > 0 && $suggestion->getScore() >= $this->getWorstScore() ) {
|
||||
$suggestion->setScore( $this->getWorstScore() - 1 );
|
||||
}
|
||||
$this->suggestions[] = $suggestion;
|
||||
if ( $pageID ) {
|
||||
$this->pageMap[$pageID] = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add suggestion set to the end of the current one.
|
||||
* @param SearchSuggestionSet $set
|
||||
*/
|
||||
public function appendAll( SearchSuggestionSet $set ) {
|
||||
foreach ( $set->getSuggestions() as $sugg ) {
|
||||
$this->append( $sugg );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the suggestion at index $key to the first position
|
||||
*/
|
||||
public function rescore( $key ) {
|
||||
$removed = array_splice( $this->suggestions, $key, 1 );
|
||||
unset( $this->pageMap[$removed[0]->getSuggestedTitleID()] );
|
||||
$this->prepend( $removed[0] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new suggestion at the top. If the new suggestion score
|
||||
* is lower than the best one its score will be updated (best + 1)
|
||||
* @param SearchSuggestion $suggestion
|
||||
*/
|
||||
public function prepend( SearchSuggestion $suggestion ) {
|
||||
$pageID = $suggestion->getSuggestedTitleID();
|
||||
if ( $pageID && isset( $this->pageMap[$pageID] ) ) {
|
||||
return;
|
||||
}
|
||||
if ( $this->getSize() > 0 && $suggestion->getScore() <= $this->getBestScore() ) {
|
||||
$suggestion->setScore( $this->getBestScore() + 1 );
|
||||
}
|
||||
array_unshift( $this->suggestions, $suggestion );
|
||||
if ( $pageID ) {
|
||||
$this->pageMap[$pageID] = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return float the best score in this suggestion set
|
||||
*/
|
||||
public function getBestScore() {
|
||||
if ( empty( $this->suggestions ) ) {
|
||||
return 0;
|
||||
}
|
||||
return $this->suggestions[0]->getScore();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return float the worst score in this set
|
||||
*/
|
||||
public function getWorstScore() {
|
||||
if ( empty( $this->suggestions ) ) {
|
||||
return 0;
|
||||
}
|
||||
return end( $this->suggestions )->getScore();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int the number of suggestion in this set
|
||||
*/
|
||||
public function getSize() {
|
||||
return count( $this->suggestions );
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove any extra elements in the suggestions set
|
||||
* @param int $limit the max size of this set.
|
||||
*/
|
||||
public function shrink( $limit ) {
|
||||
if ( count( $this->suggestions ) > $limit ) {
|
||||
$this->suggestions = array_slice( $this->suggestions, 0, $limit );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a new set of suggestion based on a title array.
|
||||
* Useful when using a backend that supports only Titles.
|
||||
*
|
||||
* NOTE: Suggestion scores will be generated.
|
||||
*
|
||||
* @param Title[] $titles
|
||||
* @return SearchSuggestionSet
|
||||
*/
|
||||
public static function fromTitles( array $titles ) {
|
||||
$score = count( $titles );
|
||||
$suggestions = array_map( function( $title ) use ( &$score ) {
|
||||
return SearchSuggestion::fromTitle( $score--, $title );
|
||||
}, $titles );
|
||||
return new SearchSuggestionSet( $suggestions );
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a new set of suggestion based on a string array.
|
||||
*
|
||||
* NOTE: Suggestion scores will be generated.
|
||||
*
|
||||
* @param string[] $titles
|
||||
* @return SearchSuggestionSet
|
||||
*/
|
||||
public static function fromStrings( array $titles ) {
|
||||
$score = count( $titles );
|
||||
$suggestions = array_map( function( $title ) use ( &$score ) {
|
||||
return SearchSuggestion::fromText( $score--, $title );
|
||||
}, $titles );
|
||||
return new SearchSuggestionSet( $suggestions );
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return SearchSuggestionSet an empty suggestion set
|
||||
*/
|
||||
public static function emptySuggestionSet() {
|
||||
return new SearchSuggestionSet( array() );
|
||||
}
|
||||
}
|
||||
|
|
@ -328,6 +328,29 @@ class SpecialPage {
|
|||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a regular substring search for prefixSearchSubpages
|
||||
* @param string $search Prefix to search for
|
||||
* @param int $limit Maximum number of results to return (usually 10)
|
||||
* @param int $offset Number of results to skip (usually 0)
|
||||
* @return string[] Matching subpages
|
||||
*/
|
||||
protected function prefixSearchString( $search, $limit, $offset ) {
|
||||
$title = Title::newFromText( $search );
|
||||
if ( !$title || !$title->canExist() ) {
|
||||
// No prefix suggestion in special and media namespace
|
||||
return array();
|
||||
}
|
||||
|
||||
$search = SearchEngine::create();
|
||||
$search->setLimitOffset( $limit, $offset );
|
||||
$search->setNamespaces( array() );
|
||||
$result = $search->defaultPrefixSearch( $search );
|
||||
return array_map( function( Title $t ) {
|
||||
return $t->getPrefixedText();
|
||||
}, $result );
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function for implementations of prefixSearchSubpages() that
|
||||
* filter the values in memory (as opposed to making a query).
|
||||
|
|
|
|||
|
|
@ -365,15 +365,7 @@ class SpecialAllPages extends IncludableSpecialPage {
|
|||
* @return string[] Matching subpages
|
||||
*/
|
||||
public function prefixSearchSubpages( $search, $limit, $offset ) {
|
||||
$title = Title::newFromText( $search );
|
||||
if ( !$title || !$title->canExist() ) {
|
||||
// No prefix suggestion in special and media namespace
|
||||
return array();
|
||||
}
|
||||
// Autocomplete subpage the same as a normal search
|
||||
$prefixSearcher = new StringPrefixSearch;
|
||||
$result = $prefixSearcher->search( $search, $limit, array(), $offset );
|
||||
return $result;
|
||||
return $this->prefixSearchString( $search, $limit, $offset );
|
||||
}
|
||||
|
||||
protected function getGroupName() {
|
||||
|
|
|
|||
|
|
@ -234,15 +234,7 @@ class SpecialChangeContentModel extends FormSpecialPage {
|
|||
* @return string[] Matching subpages
|
||||
*/
|
||||
public function prefixSearchSubpages( $search, $limit, $offset ) {
|
||||
$title = Title::newFromText( $search );
|
||||
if ( !$title || !$title->canExist() ) {
|
||||
// No prefix suggestion in special and media namespace
|
||||
return array();
|
||||
}
|
||||
// Autocomplete subpage the same as a normal search
|
||||
$prefixSearcher = new StringPrefixSearch;
|
||||
$result = $prefixSearcher->search( $search, $limit, array(), $offset );
|
||||
return $result;
|
||||
return $this->prefixSearchString( $search, $limit, $offset );
|
||||
}
|
||||
|
||||
protected function getGroupName() {
|
||||
|
|
|
|||
|
|
@ -246,9 +246,11 @@ class FileDuplicateSearchPage extends QueryPage {
|
|||
// No prefix suggestion outside of file namespace
|
||||
return array();
|
||||
}
|
||||
$search = SearchEngine::create();
|
||||
$search->setLimitOffset( $limit, $offset );
|
||||
// Autocomplete subpage the same as a normal search, but just for files
|
||||
$prefixSearcher = new TitlePrefixSearch;
|
||||
$result = $prefixSearcher->search( $search, $limit, array( NS_FILE ), $offset );
|
||||
$search->setNamespaces( array( NS_FILE ) );
|
||||
$result = $search->defaultPrefixSearch( $search );
|
||||
|
||||
return array_map( function ( Title $t ) {
|
||||
// Remove namespace in search suggestion
|
||||
|
|
|
|||
|
|
@ -820,15 +820,7 @@ class MovePageForm extends UnlistedSpecialPage {
|
|||
* @return string[] Matching subpages
|
||||
*/
|
||||
public function prefixSearchSubpages( $search, $limit, $offset ) {
|
||||
$title = Title::newFromText( $search );
|
||||
if ( !$title || !$title->canExist() ) {
|
||||
// No prefix suggestion in special and media namespace
|
||||
return array();
|
||||
}
|
||||
// Autocomplete subpage the same as a normal search
|
||||
$prefixSearcher = new StringPrefixSearch;
|
||||
$result = $prefixSearcher->search( $search, $limit, array(), $offset );
|
||||
return $result;
|
||||
return $this->prefixSearchString( $search, $limit, $offset );
|
||||
}
|
||||
|
||||
protected function getGroupName() {
|
||||
|
|
|
|||
|
|
@ -214,15 +214,7 @@ class SpecialPageLanguage extends FormSpecialPage {
|
|||
* @return string[] Matching subpages
|
||||
*/
|
||||
public function prefixSearchSubpages( $search, $limit, $offset ) {
|
||||
$title = Title::newFromText( $search );
|
||||
if ( !$title || !$title->canExist() ) {
|
||||
// No prefix suggestion in special and media namespace
|
||||
return array();
|
||||
}
|
||||
// Autocomplete subpage the same as a normal search
|
||||
$prefixSearcher = new StringPrefixSearch;
|
||||
$result = $prefixSearcher->search( $search, $limit, array(), $offset );
|
||||
return $result;
|
||||
return $this->prefixSearchString( $search, $limit, $offset );
|
||||
}
|
||||
|
||||
protected function getGroupName() {
|
||||
|
|
|
|||
|
|
@ -303,15 +303,7 @@ class SpecialPrefixindex extends SpecialAllPages {
|
|||
* @return string[] Matching subpages
|
||||
*/
|
||||
public function prefixSearchSubpages( $search, $limit, $offset ) {
|
||||
$title = Title::newFromText( $search );
|
||||
if ( !$title || !$title->canExist() ) {
|
||||
// No prefix suggestion in special and media namespace
|
||||
return array();
|
||||
}
|
||||
// Autocomplete subpage the same as a normal search
|
||||
$prefixSearcher = new StringPrefixSearch;
|
||||
$result = $prefixSearcher->search( $search, $limit, array(), $offset );
|
||||
return $result;
|
||||
return $this->prefixSearchString( $search, $limit, $offset );
|
||||
}
|
||||
|
||||
protected function getGroupName() {
|
||||
|
|
|
|||
|
|
@ -273,14 +273,6 @@ class SpecialRecentChangesLinked extends SpecialRecentChanges {
|
|||
* @return string[] Matching subpages
|
||||
*/
|
||||
public function prefixSearchSubpages( $search, $limit, $offset ) {
|
||||
$title = Title::newFromText( $search );
|
||||
if ( !$title || !$title->canExist() ) {
|
||||
// No prefix suggestion in special and media namespace
|
||||
return array();
|
||||
}
|
||||
// Autocomplete subpage the same as a normal search
|
||||
$prefixSearcher = new StringPrefixSearch;
|
||||
$result = $prefixSearcher->search( $search, $limit, array(), $offset );
|
||||
return $result;
|
||||
return $this->prefixSearchString( $search, $limit, $offset );
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1709,15 +1709,7 @@ class SpecialUndelete extends SpecialPage {
|
|||
* @return string[] Matching subpages
|
||||
*/
|
||||
public function prefixSearchSubpages( $search, $limit, $offset ) {
|
||||
$title = Title::newFromText( $search );
|
||||
if ( !$title || !$title->canExist() ) {
|
||||
// No prefix suggestion in special and media namespace
|
||||
return array();
|
||||
}
|
||||
// Autocomplete subpage the same as a normal search
|
||||
$prefixSearcher = new StringPrefixSearch;
|
||||
$result = $prefixSearcher->search( $search, $limit, array(), $offset );
|
||||
return $result;
|
||||
return $this->prefixSearchString( $search, $limit, $offset );
|
||||
}
|
||||
|
||||
protected function getGroupName() {
|
||||
|
|
|
|||
|
|
@ -548,15 +548,7 @@ class SpecialWhatLinksHere extends IncludableSpecialPage {
|
|||
* @return string[] Matching subpages
|
||||
*/
|
||||
public function prefixSearchSubpages( $search, $limit, $offset ) {
|
||||
$title = Title::newFromText( $search );
|
||||
if ( !$title || !$title->canExist() ) {
|
||||
// No prefix suggestion in special and media namespace
|
||||
return array();
|
||||
}
|
||||
// Autocomplete subpage the same as a normal search
|
||||
$prefixSearcher = new StringPrefixSearch;
|
||||
$result = $prefixSearcher->search( $search, $limit, array(), $offset );
|
||||
return $result;
|
||||
return $this->prefixSearchString( $search, $limit, $offset );
|
||||
}
|
||||
|
||||
protected function getGroupName() {
|
||||
|
|
|
|||
334
tests/phpunit/includes/search/SearchEnginePrefixTest.php
Normal file
334
tests/phpunit/includes/search/SearchEnginePrefixTest.php
Normal file
|
|
@ -0,0 +1,334 @@
|
|||
<?php
|
||||
/**
|
||||
* @group Search
|
||||
* @group Database
|
||||
*/
|
||||
class SearchEnginePrefixTest extends MediaWikiLangTestCase {
|
||||
|
||||
/**
|
||||
* @var SearchEngine
|
||||
*/
|
||||
private $search;
|
||||
|
||||
public function addDBData() {
|
||||
if ( !$this->isWikitextNS( NS_MAIN ) ) {
|
||||
// tests are skipped if NS_MAIN is not wikitext
|
||||
return;
|
||||
}
|
||||
|
||||
$this->insertPage( 'Sandbox' );
|
||||
$this->insertPage( 'Bar' );
|
||||
$this->insertPage( 'Example' );
|
||||
$this->insertPage( 'Example Bar' );
|
||||
$this->insertPage( 'Example Foo' );
|
||||
$this->insertPage( 'Example Foo/Bar' );
|
||||
$this->insertPage( 'Example/Baz' );
|
||||
$this->insertPage( 'Redirect test', '#REDIRECT [[Redirect Test]]' );
|
||||
$this->insertPage( 'Redirect Test' );
|
||||
$this->insertPage( 'Redirect Test Worse Result' );
|
||||
$this->insertPage( 'Redirect test2', '#REDIRECT [[Redirect Test2]]' );
|
||||
$this->insertPage( 'Redirect TEST2', '#REDIRECT [[Redirect Test2]]' );
|
||||
$this->insertPage( 'Redirect Test2' );
|
||||
$this->insertPage( 'Redirect Test2 Worse Result' );
|
||||
|
||||
$this->insertPage( 'Talk:Sandbox' );
|
||||
$this->insertPage( 'Talk:Example' );
|
||||
|
||||
$this->insertPage( 'User:Example' );
|
||||
}
|
||||
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
if ( !$this->isWikitextNS( NS_MAIN ) ) {
|
||||
$this->markTestSkipped( 'Main namespace does not support wikitext.' );
|
||||
}
|
||||
|
||||
// Avoid special pages from extensions interferring with the tests
|
||||
$this->setMwGlobals( 'wgSpecialPages', array() );
|
||||
$this->search = SearchEngine::create();
|
||||
$this->search->setNamespaces( array() );
|
||||
}
|
||||
|
||||
protected function searchProvision( Array $results = null ) {
|
||||
if ( $results === null ) {
|
||||
$this->setMwGlobals( 'wgHooks', array() );
|
||||
} else {
|
||||
$this->setMwGlobals( 'wgHooks', array(
|
||||
'PrefixSearchBackend' => array(
|
||||
function ( $namespaces, $search, $limit, &$srchres ) use ( $results ) {
|
||||
$srchres = $results;
|
||||
return false;
|
||||
}
|
||||
),
|
||||
) );
|
||||
}
|
||||
}
|
||||
|
||||
public static function provideSearch() {
|
||||
return array(
|
||||
array( array(
|
||||
'Empty string',
|
||||
'query' => '',
|
||||
'results' => array(),
|
||||
) ),
|
||||
array( array(
|
||||
'Main namespace with title prefix',
|
||||
'query' => 'Ex',
|
||||
'results' => array(
|
||||
'Example',
|
||||
'Example/Baz',
|
||||
'Example Bar',
|
||||
),
|
||||
// Third result when testing offset
|
||||
'offsetresult' => array(
|
||||
'Example Foo',
|
||||
),
|
||||
) ),
|
||||
array( array(
|
||||
'Talk namespace prefix',
|
||||
'query' => 'Talk:',
|
||||
'results' => array(
|
||||
'Talk:Example',
|
||||
'Talk:Sandbox',
|
||||
),
|
||||
) ),
|
||||
array( array(
|
||||
'User namespace prefix',
|
||||
'query' => 'User:',
|
||||
'results' => array(
|
||||
'User:Example',
|
||||
),
|
||||
) ),
|
||||
array( array(
|
||||
'Special namespace prefix',
|
||||
'query' => 'Special:',
|
||||
'results' => array(
|
||||
'Special:ActiveUsers',
|
||||
'Special:AllMessages',
|
||||
'Special:AllMyFiles',
|
||||
),
|
||||
// Third result when testing offset
|
||||
'offsetresult' => array(
|
||||
'Special:AllMyUploads',
|
||||
),
|
||||
) ),
|
||||
array( array(
|
||||
'Special namespace with prefix',
|
||||
'query' => 'Special:Un',
|
||||
'results' => array(
|
||||
'Special:Unblock',
|
||||
'Special:UncategorizedCategories',
|
||||
'Special:UncategorizedFiles',
|
||||
),
|
||||
// Third result when testing offset
|
||||
'offsetresult' => array(
|
||||
'Special:UncategorizedImages',
|
||||
),
|
||||
) ),
|
||||
array( array(
|
||||
'Special page name',
|
||||
'query' => 'Special:EditWatchlist',
|
||||
'results' => array(
|
||||
'Special:EditWatchlist',
|
||||
),
|
||||
) ),
|
||||
array( array(
|
||||
'Special page subpages',
|
||||
'query' => 'Special:EditWatchlist/',
|
||||
'results' => array(
|
||||
'Special:EditWatchlist/clear',
|
||||
'Special:EditWatchlist/raw',
|
||||
),
|
||||
) ),
|
||||
array( array(
|
||||
'Special page subpages with prefix',
|
||||
'query' => 'Special:EditWatchlist/cl',
|
||||
'results' => array(
|
||||
'Special:EditWatchlist/clear',
|
||||
),
|
||||
) ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideSearch
|
||||
* @covers SearchEngine::defaultPrefixSearch
|
||||
*/
|
||||
public function testSearch( Array $case ) {
|
||||
$this->search->setLimitOffset( 3 );
|
||||
$results = $this->search->defaultPrefixSearch( $case['query'] );
|
||||
$results = array_map( function( Title $t ) {
|
||||
return $t->getPrefixedText();
|
||||
}, $results );
|
||||
$this->assertEquals(
|
||||
$case['results'],
|
||||
$results,
|
||||
$case[0]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideSearch
|
||||
* @covers SearchEngine::defaultPrefixSearch
|
||||
*/
|
||||
public function testSearchWithOffset( Array $case ) {
|
||||
$this->search->setLimitOffset( 3, 1 );
|
||||
$results = $this->search->defaultPrefixSearch( $case['query'] );
|
||||
$results = array_map( function( Title $t ) {
|
||||
return $t->getPrefixedText();
|
||||
}, $results );
|
||||
|
||||
// We don't expect the first result when offsetting
|
||||
array_shift( $case['results'] );
|
||||
// And sometimes we expect a different last result
|
||||
$expected = isset( $case['offsetresult'] ) ?
|
||||
array_merge( $case['results'], $case['offsetresult'] ) :
|
||||
$case['results'];
|
||||
|
||||
$this->assertEquals(
|
||||
$expected,
|
||||
$results,
|
||||
$case[0]
|
||||
);
|
||||
}
|
||||
|
||||
public static function provideSearchBackend() {
|
||||
return array(
|
||||
array( array(
|
||||
'Simple case',
|
||||
'provision' => array(
|
||||
'Bar',
|
||||
'Barcelona',
|
||||
'Barbara',
|
||||
),
|
||||
'query' => 'Bar',
|
||||
'results' => array(
|
||||
'Bar',
|
||||
'Barcelona',
|
||||
'Barbara',
|
||||
),
|
||||
) ),
|
||||
array( array(
|
||||
'Exact match not on top (bug 70958)',
|
||||
'provision' => array(
|
||||
'Barcelona',
|
||||
'Bar',
|
||||
'Barbara',
|
||||
),
|
||||
'query' => 'Bar',
|
||||
'results' => array(
|
||||
'Bar',
|
||||
'Barcelona',
|
||||
'Barbara',
|
||||
),
|
||||
) ),
|
||||
array( array(
|
||||
'Exact match missing (bug 70958)',
|
||||
'provision' => array(
|
||||
'Barcelona',
|
||||
'Barbara',
|
||||
'Bart',
|
||||
),
|
||||
'query' => 'Bar',
|
||||
'results' => array(
|
||||
'Bar',
|
||||
'Barcelona',
|
||||
'Barbara',
|
||||
),
|
||||
) ),
|
||||
array( array(
|
||||
'Exact match missing and not existing',
|
||||
'provision' => array(
|
||||
'Exile',
|
||||
'Exist',
|
||||
'External',
|
||||
),
|
||||
'query' => 'Ex',
|
||||
'results' => array(
|
||||
'Exile',
|
||||
'Exist',
|
||||
'External',
|
||||
),
|
||||
) ),
|
||||
array( array(
|
||||
"Exact match shouldn't override already found match if " .
|
||||
"exact is redirect and found isn't",
|
||||
'provision' => array(
|
||||
// Target of the exact match is low in the list
|
||||
'Redirect Test Worse Result',
|
||||
'Redirect Test',
|
||||
),
|
||||
'query' => 'redirect test',
|
||||
'results' => array(
|
||||
// Redirect target is pulled up and exact match isn't added
|
||||
'Redirect Test',
|
||||
'Redirect Test Worse Result',
|
||||
),
|
||||
) ),
|
||||
array( array(
|
||||
"Exact match shouldn't override already found match if " .
|
||||
"both exact match and found match are redirect",
|
||||
'provision' => array(
|
||||
// Another redirect to the same target as the exact match
|
||||
// is low in the list
|
||||
'Redirect Test2 Worse Result',
|
||||
'Redirect test2',
|
||||
),
|
||||
'query' => 'redirect TEST2',
|
||||
'results' => array(
|
||||
// Found redirect is pulled to the top and exact match isn't
|
||||
// added
|
||||
'Redirect test2',
|
||||
'Redirect Test2 Worse Result',
|
||||
),
|
||||
) ),
|
||||
array( array(
|
||||
"Exact match should override any already found matches that " .
|
||||
"are redirects to it",
|
||||
'provision' => array(
|
||||
// Another redirect to the same target as the exact match
|
||||
// is low in the list
|
||||
'Redirect Test Worse Result',
|
||||
'Redirect test',
|
||||
),
|
||||
'query' => 'Redirect Test',
|
||||
'results' => array(
|
||||
// Found redirect is pulled to the top and exact match isn't
|
||||
// added
|
||||
'Redirect Test',
|
||||
'Redirect Test Worse Result',
|
||||
'Redirect test',
|
||||
),
|
||||
) ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideSearchBackend
|
||||
* @covers PrefixSearch::searchBackend
|
||||
*/
|
||||
public function testSearchBackend( Array $case ) {
|
||||
$search = $stub = $this->getMockBuilder( 'SearchEngine' )
|
||||
->setMethods( array( 'completionSearchBackend' ) )->getMock();
|
||||
|
||||
$return = SearchSuggestionSet::fromStrings( $case['provision'] );
|
||||
|
||||
$search->expects( $this->any() )
|
||||
->method( 'completionSearchBackend' )
|
||||
->will( $this->returnValue( $return ) );
|
||||
|
||||
$search->setLimitOffset( 3 );
|
||||
$results = $search->completionSearch( $case['query'] );
|
||||
|
||||
$results = $results->map( function( SearchSuggestion $s ) {
|
||||
return $s->getText();
|
||||
} );
|
||||
|
||||
$this->assertEquals(
|
||||
$case['results'],
|
||||
$results,
|
||||
$case[0]
|
||||
);
|
||||
}
|
||||
}
|
||||
104
tests/phpunit/includes/search/SearchSuggestionSetTest.php
Normal file
104
tests/phpunit/includes/search/SearchSuggestionSetTest.php
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Test for filter utilities.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
* http://www.gnu.org/copyleft/gpl.html
|
||||
*/
|
||||
|
||||
class SearchSuggestionSetTest extends \PHPUnit_Framework_TestCase {
|
||||
/**
|
||||
* Test that adding a new suggestion at the end
|
||||
* will keep proper score ordering
|
||||
*/
|
||||
public function testAppend() {
|
||||
$set = SearchSuggestionSet::emptySuggestionSet();
|
||||
$this->assertEquals( 0, $set->getSize() );
|
||||
$set->append( new SearchSuggestion( 3 ) );
|
||||
$this->assertEquals( 3, $set->getWorstScore() );
|
||||
$this->assertEquals( 3, $set->getBestScore() );
|
||||
|
||||
$suggestion = new SearchSuggestion( 4 );
|
||||
$set->append( $suggestion );
|
||||
$this->assertEquals( 2, $set->getWorstScore() );
|
||||
$this->assertEquals( 3, $set->getBestScore() );
|
||||
$this->assertEquals( 2, $suggestion->getScore() );
|
||||
|
||||
$suggestion = new SearchSuggestion( 2 );
|
||||
$set->append( $suggestion );
|
||||
$this->assertEquals( 1, $set->getWorstScore() );
|
||||
$this->assertEquals( 3, $set->getBestScore() );
|
||||
$this->assertEquals( 1, $suggestion->getScore() );
|
||||
|
||||
$scores = $set->map( function( $s ) {
|
||||
return $s->getScore();
|
||||
} );
|
||||
$sorted = $scores;
|
||||
asort( $sorted );
|
||||
$this->assertEquals( $sorted, $scores );
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that adding a new best suggestion will keep proper score
|
||||
* ordering
|
||||
*/
|
||||
public function testInsertBest() {
|
||||
$set = SearchSuggestionSet::emptySuggestionSet();
|
||||
$this->assertEquals( 0, $set->getSize() );
|
||||
$set->prepend( new SearchSuggestion( 3 ) );
|
||||
$this->assertEquals( 3, $set->getWorstScore() );
|
||||
$this->assertEquals( 3, $set->getBestScore() );
|
||||
|
||||
$suggestion = new SearchSuggestion( 4 );
|
||||
$set->prepend( $suggestion );
|
||||
$this->assertEquals( 3, $set->getWorstScore() );
|
||||
$this->assertEquals( 4, $set->getBestScore() );
|
||||
$this->assertEquals( 4, $suggestion->getScore() );
|
||||
|
||||
$suggestion = new SearchSuggestion( 0 );
|
||||
$set->prepend( $suggestion );
|
||||
$this->assertEquals( 3, $set->getWorstScore() );
|
||||
$this->assertEquals( 5, $set->getBestScore() );
|
||||
$this->assertEquals( 5, $suggestion->getScore() );
|
||||
|
||||
$suggestion = new SearchSuggestion( 2 );
|
||||
$set->prepend( $suggestion );
|
||||
$this->assertEquals( 3, $set->getWorstScore() );
|
||||
$this->assertEquals( 6, $set->getBestScore() );
|
||||
$this->assertEquals( 6, $suggestion->getScore() );
|
||||
|
||||
$scores = $set->map( function( $s ) {
|
||||
return $s->getScore();
|
||||
} );
|
||||
$sorted = $scores;
|
||||
asort( $sorted );
|
||||
$this->assertEquals( $sorted, $scores );
|
||||
}
|
||||
|
||||
public function testShrink() {
|
||||
$set = SearchSuggestionSet::emptySuggestionSet();
|
||||
for ( $i = 0; $i < 100; $i++ ) {
|
||||
$set->append( new SearchSuggestion( 0 ) );
|
||||
}
|
||||
$set->shrink( 10 );
|
||||
$this->assertEquals( 10, $set->getSize() );
|
||||
|
||||
$set->shrink( 0 );
|
||||
$this->assertEquals( 0, $set->getSize() );
|
||||
}
|
||||
|
||||
// TODO: test for fromTitles
|
||||
}
|
||||
Loading…
Reference in a new issue