$search = trim( $search );
// preload the titles with LinkBatch
- $titles = $suggestions->map( function ( SearchSuggestion $sugg ) {
+ $lb = new LinkBatch( $suggestions->map( function ( SearchSuggestion $sugg ) {
return $sugg->getSuggestedTitle();
- } );
- $lb = new LinkBatch( $titles );
+ } ) );
$lb->setCaller( __METHOD__ );
$lb->execute();
+ $diff = $suggestions->filter( function ( SearchSuggestion $sugg ) {
+ return $sugg->getSuggestedTitle()->isKnown();
+ } );
+ if ( $diff > 0 ) {
+ MediaWikiServices::getInstance()->getStatsdDataFactory()
+ ->updateCount( 'search.completion.missing', $diff );
+ }
+
$results = $suggestions->map( function ( SearchSuggestion $sugg ) {
return $sugg->getSuggestedTitle()->getPrefixedText();
} );
return array_map( $callback, $this->suggestions );
}
+ /**
+ * Filter the suggestions array
+ * @param callback $callback Callable accepting single SearchSuggestion
+ * instance returning bool false to remove the item.
+ * @return int The number of suggestions removed
+ */
+ public function filter( $callback ) {
+ $before = count( $this->suggestions );
+ $this->suggestions = array_values( array_filter( $this->suggestions, $callback ) );
+ return $before - count( $this->suggestions );
+ }
+
/**
* Add a new suggestion at the end.
* If the score of the new suggestion is greater than the worst one,
* @covers ApiQueryPrefixSearch
*/
class ApiQueryPrefixSearchTest extends ApiTestCase {
+ const TEST_QUERY = 'unittest';
+
+ public function setUp() {
+ parent::setUp();
+ $this->setMwGlobals( [
+ 'wgSearchType' => MockCompletionSearchEngine::class,
+ ] );
+ MockCompletionSearchEngine::clearMockResults();
+ $results = [];
+ foreach ( range( 0, 10 ) as $i ) {
+ $title = "Search_Result_$i";
+ $results[] = $title;
+ $this->editPage( $title, 'hi there' );
+ }
+ MockCompletionSearchEngine::addMockResults( self::TEST_QUERY, $results );
+ }
+
public function offsetContinueProvider() {
return [
'no offset' => [ 2, 2, 0, 2 ],
* @dataProvider offsetContinueProvider
*/
public function testOffsetContinue( $expectedOffset, $expectedResults, $offset, $limit ) {
- $this->registerMockSearchEngine();
$response = $this->doApiRequest( [
'action' => 'query',
'list' => 'prefixsearch',
- 'pssearch' => 'example query terms',
+ 'pssearch' => self::TEST_QUERY,
'psoffset' => $offset,
'pslimit' => $limit,
] );
$this->assertEquals( $expectedOffset, $result['continue']['psoffset'] );
}
}
-
- private function registerMockSearchEngine() {
- $this->setMwGlobals( [
- 'wgSearchType' => MockCompletionSearchEngine::class,
- ] );
- }
}
$this->insertPage( 'Talk:Example' );
$this->insertPage( 'User:Example' );
+ $this->insertPage( 'Barcelona' );
+ $this->insertPage( 'Barbara' );
+ $this->insertPage( 'External' );
}
protected function setUp() {
],
] ],
[ [
- 'Exact match not on top (T72958)',
+ 'Exact match not in first result should be moved to the first result (T72958)',
'provision' => [
'Barcelona',
'Bar',
],
] ],
[ [
- 'Exact match missing (T72958)',
+ 'Exact match missing from results should be added as first result (T72958)',
'provision' => [
'Barcelona',
'Barbara',
],
] ],
[ [
- 'Exact match missing and not existing',
+ 'Exact match missing and not existing pages should be dropped',
'provision' => [
'Exile',
'Exist',
],
'query' => 'Ex',
'results' => [
- 'Exile',
- 'Exist',
'External',
],
] ],
} );
$rowAugmentors['testRow'] = $rowAugmentor;
}
+
+ public function testFiltersMissing() {
+ $availableResults = [];
+ foreach ( range( 0, 11 ) as $i ) {
+ $title = "Search_Result_$i";
+ $availableResults[] = $title;
+ // pages not created must be filtered
+ if ( $i % 2 == 0 ) {
+ $this->editPage( $title );
+ }
+ }
+ MockCompletionSearchEngine::addMockResults( 'foo', $availableResults );
+
+ $engine = new MockCompletionSearchEngine();
+ $engine->setLimitOffset( 10, 0 );
+ $results = $engine->completionSearch( 'foo' );
+ $this->assertEquals( 5, $results->getSize() );
+ $this->assertTrue( $results->hasMoreResults() );
+
+ $engine->setLimitOffset( 10, 10 );
+ $results = $engine->completionSearch( 'foo' );
+ $this->assertEquals( 1, $results->getSize() );
+ $this->assertFalse( $results->hasMoreResults() );
+ }
+
+ private function editPage( $title ) {
+ $page = WikiPage::factory( Title::newFromText( $title ) );
+ $page->doEditContent(
+ new WikitextContent( 'UTContent' ),
+ 'UTPageSummary',
+ EDIT_NEW | EDIT_SUPPRESS_RC
+ );
+ }
}
<?php
-use MediaWiki\MediaWikiServices;
-
/**
* SearchEngine implementation for returning mocked completion search results.
*/
class MockCompletionSearchEngine extends SearchEngine {
- private static $completionSearchResult = [];
+ /** @var string[][] */
+ private static $results = [];
+
+ /**
+ * Reset any mocked results
+ */
+ public static function clearMockResults() {
+ self::$results = [];
+ }
+
+ /**
+ * Allows returning arbitrary lists of titles for completion search.
+ * Provided results will be sliced based on offset/limit of query.
+ *
+ * For results to exit the search engine they must pass Title::isKnown.
+ * Injecting into link cache is not enough, as LinkBatch will mark them
+ * bad, they need to be injected into the DB.
+ *
+ * @param string $query Search term as seen in completionSearchBackend
+ * @param string[] $result List of titles to respond to query with
+ */
+ public static function addMockResults( $query, array $result ) {
+ // Leading : ensures we don't treat another : as a namespace separator
+ $normalized = Title::newFromText( ":$query" )->getText();
+ self::$results[$normalized] = $result;
+ }
public function completionSearchBackend( $search ) {
- if ( self::$completionSearchResult == null ) {
- self::$completionSearchResult = [];
- // TODO: Or does this have to be setup per-test?
- $lc = MediaWikiServices::getInstance()->getLinkCache();
- foreach ( range( 0, 10 ) as $i ) {
- $dbkey = "Search_Result_$i";
- $lc->addGoodLinkObj( 6543 + $i, new TitleValue( NS_MAIN, $dbkey ) );
- self::$completionSearchResult[] = "Search Result $i";
- }
+ if ( !isset( self::$results[$search] ) ) {
+ return SearchSuggestionSet::emptySuggestionSet();
}
- $results = array_slice( self::$completionSearchResult, $this->offset, $this->limit );
+ $results = array_slice( self::$results[$search], $this->offset, $this->limit );
return SearchSuggestionSet::fromStrings( $results );
}
-
}
self::$results[$query] = $results;
$lc = MediaWikiServices::getInstance()->getLinkCache();
foreach ( $results as $result ) {
- // TODO: better page ids?
+ // TODO: better page ids? Does it matter?
$lc->addGoodLinkObj( mt_rand(), $result->getTitle() );
}
}