MediaWiki master
SearchEngine.php
Go to the documentation of this file.
1<?php
10namespace MediaWiki\Search;
11
12use Closure;
13use InvalidArgumentException;
23use SplObjectStorage;
26
35abstract class SearchEngine {
36 public const DEFAULT_SORT = 'relevance';
37
39 public $prefix = '';
40
42 public $namespaces = [ NS_MAIN ];
43
45 protected $limit = 10;
46
48 protected $offset = 0;
49
54 protected $searchTerms = [];
55
57 protected $showSuggestion = true;
59 private $sort = self::DEFAULT_SORT;
60
62 protected $features = [];
63
65 private $hookContainer;
66
68 private $hookRunner;
69
71 private ?SpecialPageSuggester $specialPageSuggester = null;
72
74 public const COMPLETION_PROFILE_TYPE = 'completionSearchProfile';
75
77 public const FT_QUERY_INDEP_PROFILE_TYPE = 'fulltextQueryIndepProfile';
78
80 public const FT_QUERY_DEP_PROFILE_TYPE = 'fulltextQueryDepProfile';
81
83 protected const CHARS_ALL = 1;
84
86 protected const CHARS_NO_SYNTAX = 2;
87
93 public const SEARCH_ID = 'searchId';
94
105 public function searchText( $term ) {
106 return $this->maybePaginate( function () use ( $term ) {
107 return $this->doSearchText( $term );
108 } );
109 }
110
120 protected function doSearchText( $term ) {
121 return null;
122 }
123
138 public function searchArchiveTitle( $term ) {
139 return $this->doSearchArchiveTitle( $term );
140 }
141
151 protected function doSearchArchiveTitle( $term ) {
152 return Status::newGood( [] );
153 }
154
166 public function searchTitle( $term ) {
167 return $this->maybePaginate( function () use ( $term ) {
168 return $this->doSearchTitle( $term );
169 } );
170 }
171
181 protected function doSearchTitle( $term ) {
182 return null;
183 }
184
193 private function maybePaginate( Closure $fn ) {
194 if ( $this instanceof PaginatingSearchEngine ) {
195 return $fn();
196 }
197 $this->limit++;
198 try {
199 $resultSetOrStatus = $fn();
200 } finally {
201 $this->limit--;
202 }
203
204 $resultSet = null;
205 if ( $resultSetOrStatus instanceof ISearchResultSet ) {
206 $resultSet = $resultSetOrStatus;
207 } elseif ( $resultSetOrStatus instanceof Status &&
208 $resultSetOrStatus->getValue() instanceof ISearchResultSet
209 ) {
210 $resultSet = $resultSetOrStatus->getValue();
211 }
212 if ( $resultSet ) {
213 $resultSet->shrink( $this->limit );
214 }
215
216 return $resultSetOrStatus;
217 }
218
226 public function supports( $feature ) {
227 switch ( $feature ) {
228 case 'search-update':
229 return true;
230 case 'title-suffix-filter':
231 default:
232 return false;
233 }
234 }
235
242 public function setFeatureData( $feature, $data ) {
243 $this->features[$feature] = $data;
244 }
245
253 public function getFeatureData( $feature ) {
254 return $this->features[$feature] ?? null;
255 }
256
265 public function normalizeText( $string ) {
266 // Some languages such as Chinese require word segmentation
267 return MediaWikiServices::getInstance()->getContentLanguage()->segmentByWord( $string );
268 }
269
276 public function getNearMatcher( Config $config ) {
277 return MediaWikiServices::getInstance()->getTitleMatcher();
278 }
279
286 protected static function defaultNearMatcher() {
287 wfDeprecated( __METHOD__, '1.40' );
288 return MediaWikiServices::getInstance()->getTitleMatcher();
289 }
290
297 public function legalSearchChars( $type = self::CHARS_ALL ) {
298 return "A-Za-z_'.0-9\\x80-\\xFF\\-";
299 }
300
308 public function setLimitOffset( $limit, $offset = 0 ) {
309 $this->limit = intval( $limit );
310 $this->offset = intval( $offset );
311 }
312
319 public function setNamespaces( $namespaces ) {
320 if ( $namespaces ) {
321 // Filter namespaces to only keep valid ones
322 $validNs = MediaWikiServices::getInstance()->getSearchEngineConfig()->searchableNamespaces();
323 $namespaces = array_filter( $namespaces,
324 static fn ( $id ) => $id < 0 || isset( $validNs[$id] )
325 );
326 } else {
327 $namespaces = [];
328 }
329 $this->namespaces = $namespaces;
330 }
331
340 $this->showSuggestion = $showSuggestion;
341 }
342
352 public function getValidSorts() {
353 return [ self::DEFAULT_SORT ];
354 }
355
363 public function setSort( $sort ) {
364 if ( !in_array( $sort, $this->getValidSorts() ) ) {
365 throw new InvalidArgumentException( "Invalid sort: $sort. " .
366 "Must be one of: " . implode( ', ', $this->getValidSorts() ) );
367 }
368 $this->sort = $sort;
369 }
370
377 public function getSort() {
378 return $this->sort;
379 }
380
394 public static function parseNamespacePrefixes(
395 $query,
396 $withAllKeyword = true,
397 $withPrefixSearchExtractNamespaceHook = false
398 ) {
399 $parsed = $query;
400 if ( !str_contains( $query, ':' ) ) { // nothing to do
401 return false;
402 }
403 $extractedNamespace = null;
404
405 $allQuery = false;
406 if ( $withAllKeyword ) {
407 $allkeywords = [];
408
409 $allkeywords[] = wfMessage( 'searchall' )->inContentLanguage()->text() . ":";
410 // force all: so that we have a common syntax for all the wikis
411 if ( !in_array( 'all:', $allkeywords ) ) {
412 $allkeywords[] = 'all:';
413 }
414
415 foreach ( $allkeywords as $kw ) {
416 if ( str_starts_with( $query, $kw ) ) {
417 $parsed = substr( $query, strlen( $kw ) );
418 $allQuery = true;
419 break;
420 }
421 }
422 }
423
424 if ( !$allQuery && str_contains( $query, ':' ) ) {
425 $colonPos = strpos( $query, ':' );
426 // Trim whitespace around the prefix so that "User : Foo" is treated the same as
427 // "User:Foo"; Title parsing canonicalizes the colon the same way.
428 $prefix = str_replace( ' ', '_', trim( substr( $query, 0, $colonPos ) ) );
429 $services = MediaWikiServices::getInstance();
430 $index = $services->getContentLanguage()->getNsIndex( $prefix );
431 if ( $index !== false ) {
432 $extractedNamespace = [ $index ];
433 $parsed = ltrim( substr( $query, $colonPos + 1 ) );
434 } elseif ( $withPrefixSearchExtractNamespaceHook ) {
435 $hookNamespaces = [ NS_MAIN ];
436 $hookQuery = $query;
437 ( new HookRunner( $services->getHookContainer() ) )
438 ->onPrefixSearchExtractNamespace( $hookNamespaces, $hookQuery );
439 if ( $hookQuery !== $query ) {
440 $parsed = $hookQuery;
441 $extractedNamespace = $hookNamespaces;
442 } else {
443 return false;
444 }
445 } else {
446 return false;
447 }
448 }
449
450 return [ $parsed, $extractedNamespace ];
451 }
452
460 public static function userHighlightPrefs() {
463 return [ $contextlines, $contextchars ];
464 }
465
475 public function update( $id, $title, $text ) {
476 // no-op
477 }
478
487 public function updateTitle( $id, $title ) {
488 // no-op
489 }
490
499 public function delete( $id, $title ) {
500 // no-op
501 }
502
509 protected function normalizeNamespaces( $search ) {
510 $queryAndNs = self::parseNamespacePrefixes( $search, false, true );
511 if ( $queryAndNs !== false ) {
512 $this->setNamespaces( $queryAndNs[1] );
513 return $queryAndNs[0];
514 }
515 return $search;
516 }
517
525 protected function completionSearchBackendOverfetch( $search ) {
526 $this->limit++;
527 try {
528 return $this->completionSearchBackend( $search );
529 } finally {
530 $this->limit--;
531 }
532 }
533
544 protected function completionSearchBackend( $search ) {
545 $search = trim( $search );
546 $results = $this->simplePrefixSearch( $search );
547 return SearchSuggestionSet::fromTitles( $results );
548 }
549
555 public function completionSearch( $search ) {
556 if ( trim( $search ) === '' ) {
557 return SearchSuggestionSet::emptySuggestionSet(); // Return empty result
558 }
559 $search = $this->normalizeNamespaces( $search );
560 $suggestions = $this->completionSearchBackendOverfetch( $search );
561 return $this->processCompletionResults( $search, $suggestions );
562 }
563
571 public function completionSearchWithVariants( $search ) {
572 if ( trim( $search ) === '' ) {
573 return SearchSuggestionSet::emptySuggestionSet(); // Return empty result
574 }
575 $search = $this->normalizeNamespaces( $search );
576
577 $results = $this->completionSearchBackendOverfetch( $search );
578 $fallbackLimit = 1 + $this->limit - $results->getSize();
579 if ( $fallbackLimit > 0 ) {
580 $services = MediaWikiServices::getInstance();
581 $fallbackSearches = $services->getLanguageConverterFactory()
582 ->getLanguageConverter( $services->getContentLanguage() )
583 ->autoConvertToAllVariants( $search );
584 $fallbackSearches = array_diff( array_unique( $fallbackSearches ), [ $search ] );
585
586 $origLimit = $this->limit;
587 $origOffset = $this->offset;
588 foreach ( $fallbackSearches as $fbs ) {
589 try {
590 $this->setLimitOffset( $fallbackLimit );
591 $fallbackSearchResult = $this->completionSearch( $fbs );
592 $results->appendAll( $fallbackSearchResult );
593 $fallbackLimit -= $fallbackSearchResult->getSize();
594 } finally {
595 $this->setLimitOffset( $origLimit, $origOffset );
596 }
597 if ( $fallbackLimit <= 0 ) {
598 break;
599 }
600 }
601 }
602 return $this->processCompletionResults( $search, $results );
603 }
604
610 public function extractTitles( SearchSuggestionSet $completionResults ) {
611 return $completionResults->map( static function ( SearchSuggestion $sugg ) {
612 return $sugg->getSuggestedTitle();
613 } );
614 }
615
623 protected function processCompletionResults( $search, SearchSuggestionSet $suggestions ) {
624 // We over-fetched to determine pagination. Shrink back down if we have extra results
625 // and mark if pagination is possible
626 $suggestions->shrink( $this->limit );
627
628 $search = trim( $search );
629 // preload the titles with LinkBatch
630 $suggestedTitles = $suggestions->map( static function ( SearchSuggestion $sugg ) {
631 return $sugg->getSuggestedTitle();
632 } );
633 $linkBatchFactory = MediaWikiServices::getInstance()->getLinkBatchFactory();
634 $linkBatchFactory->newLinkBatch( $suggestedTitles )
635 ->setCaller( __METHOD__ )
636 ->execute();
637
638 $diff = $suggestions->filter( static function ( SearchSuggestion $sugg ) {
639 return $sugg->getSuggestedTitle()->isKnown();
640 } );
641 if ( $diff > 0 ) {
642 $statsFactory = MediaWikiServices::getInstance()->getStatsFactory();
643 $statsFactory->getCounter( 'search_completion_missing_total' )
644 ->incrementBy( (float)$diff );
645 }
646
647 // SearchExactMatchRescorer should probably be refactored to work directly on top of a SearchSuggestionSet
648 // instead of converting it to array and trying to infer if it has re-scored anything by inspected the head
649 // of the returned array.
650 $results = $suggestions->map( static function ( SearchSuggestion $sugg ) {
651 return $sugg->getSuggestedTitle()->getPrefixedText();
652 } );
653
654 $rescorer = new SearchExactMatchRescorer();
655 if ( $this->offset === 0 ) {
656 // Rescore results with an exact title match
657 // NOTE: in some cases like cross-namespace redirects
658 // (frequently used as shortcuts e.g. WP:WP on huwiki) some
659 // backends like Cirrus will return no results. We should still
660 // try an exact title match to workaround this limitation
661 $rescoredResults = $rescorer->rescore( $search, $this->namespaces, $results, $this->limit );
662 } else {
663 // No need to rescore if offset is not 0
664 // The exact match must have been returned at position 0
665 // if it existed.
666 $rescoredResults = $results;
667 }
668
669 if ( count( $rescoredResults ) > 0 ) {
670 $found = array_search( $rescoredResults[0], $results );
671 if ( $found === false ) {
672 // If the first result is not in the previous array it
673 // means that we found a new exact match
674 $exactMatch = SearchSuggestion::fromTitle( 0, Title::newFromText( $rescoredResults[0] ) );
675 $suggestions->prepend( $exactMatch );
676 if ( $rescorer->getReplacedRedirect() !== null ) {
677 // the exact match rescorer replaced one of the suggestion found by the search engine
678 // let's remove it from our suggestions set to avoid showing duplicates
679 $suggestions->remove( SearchSuggestion::fromTitle( 0,
680 Title::newFromText( $rescorer->getReplacedRedirect() ) ) );
681 }
682 $suggestions->shrink( $this->limit );
683 } else {
684 // if the first result is not the same we need to rescore
685 if ( $found > 0 ) {
686 $suggestions->rescore( $found );
687 }
688 }
689 }
690
691 return $suggestions;
692 }
693
699 public function defaultPrefixSearch( $search ) {
700 if ( trim( $search ) === '' ) {
701 return [];
702 }
703
704 $search = $this->normalizeNamespaces( $search );
705 return $this->simplePrefixSearch( $search );
706 }
707
714 protected function simplePrefixSearch( $search ) {
716 if ( !$namespaces ) {
717 $namespaces = [ NS_MAIN ];
718 }
719
720 if ( in_array( NS_SPECIAL, $namespaces ) ) {
721 // For now, if special is included, ignore the other namespaces
722 return $this->getSpecialPageSuggester()->suggest( $search, $this->limit, $this->offset );
723 }
724
725 // Construct suitable prefix for each namespace. They differ in cases where
726 // some namespaces always capitalize and some don't.
727 $prefixes = [];
728 // Allow to do a prefix search for e.g. "Talk:"
729 if ( $search === '' ) {
730 $prefixes[$search] = $namespaces;
731 } else {
732 // Don't just ignore input like "[[Foo]]", but try to search for "Foo"
733 $search = preg_replace( TitleParser::getTitleInvalidRegex(), '', $search );
734 foreach ( $namespaces as $namespace ) {
735 $title = Title::makeTitleSafe( $namespace, $search );
736 if ( $title ) {
737 $prefixes[$title->getDBkey()][] = $namespace;
738 }
739 }
740 }
741 if ( !$prefixes ) {
742 return [];
743 }
744
745 $services = MediaWikiServices::getInstance();
746 $dbr = $services->getConnectionProvider()->getReplicaDatabase();
747 // Often there is only one prefix that applies to all requested namespaces,
748 // but sometimes there are two if some namespaces do not always capitalize.
749 $conds = [];
750 foreach ( $prefixes as $prefix => $namespaces ) {
751 $expr = $dbr->expr( 'page_namespace', '=', $namespaces );
752 if ( $prefix !== '' ) {
753 $expr = $expr->and(
754 'page_title',
755 IExpression::LIKE,
756 new LikeValue( (string)$prefix, $dbr->anyString() )
757 );
758 }
759 $conds[] = $expr;
760 }
761
762 $queryBuilder = $dbr->newSelectQueryBuilder()
763 ->select( [ 'page_id', 'page_namespace', 'page_title' ] )
764 ->from( 'page' )
765 ->where( $dbr->orExpr( $conds ) )
766 ->orderBy( [ 'page_title', 'page_namespace' ] )
767 ->limit( $this->limit )
768 ->offset( $this->offset );
769 $res = $queryBuilder->caller( __METHOD__ )->fetchResultSet();
770
771 return iterator_to_array( $services->getTitleFactory()->newTitleArrayFromResult( $res ) );
772 }
773
792 public function getProfiles( $profileType, ?User $user = null ) {
793 return null;
794 }
795
806 public function makeSearchFieldMapping( $name, $type ) {
807 return new NullIndexField();
808 }
809
815 public function getSearchIndexFields() {
816 $models = MediaWikiServices::getInstance()->getContentHandlerFactory()->getContentModels();
817 $fields = [];
818 $seenHandlers = new SplObjectStorage();
819 foreach ( $models as $model ) {
820 try {
822 ->getContentHandlerFactory()
823 ->getContentHandler( $model );
824 } catch ( UnknownContentModelException ) {
825 // If we can find no handler, ignore it
826 continue;
827 }
828 // Several models can have the same handler, so avoid processing it repeatedly
829 if ( $seenHandlers->offsetExists( $handler ) ) {
830 // We already did this one
831 continue;
832 }
833 $seenHandlers->offsetSet( $handler );
834 $handlerFields = $handler->getFieldsForSearchIndex( $this );
835 foreach ( $handlerFields as $fieldName => $fieldData ) {
836 if ( empty( $fields[$fieldName] ) ) {
837 $fields[$fieldName] = $fieldData;
838 } else {
839 // TODO: do we allow some clashes with the same type or reject all of them?
840 $mergeDef = $fields[$fieldName]->merge( $fieldData );
841 if ( !$mergeDef ) {
842 throw new InvalidArgumentException( "Duplicate field $fieldName for model $model" );
843 }
844 $fields[$fieldName] = $mergeDef;
845 }
846 }
847 }
848 // Hook to allow extensions to produce search mapping fields
849 $this->getHookRunner()->onSearchIndexFields( $fields, $this );
850 return $fields;
851 }
852
856 public function augmentSearchResults( ISearchResultSet $resultSet ) {
857 $setAugmentors = [];
858 $rowAugmentors = [];
859 $this->getHookRunner()->onSearchResultsAugment( $setAugmentors, $rowAugmentors );
860 if ( !$setAugmentors && !$rowAugmentors ) {
861 // We're done here
862 return;
863 }
864
865 // Convert row augmenters to set augmentor
866 foreach ( $rowAugmentors as $name => $row ) {
867 if ( isset( $setAugmentors[$name] ) ) {
868 throw new InvalidArgumentException( "Both row and set augmentors are defined for $name" );
869 }
870 $setAugmentors[$name] = new PerRowAugmentor( $row );
871 }
872
877 foreach ( $setAugmentors as $name => $augmentor ) {
878 $data = $augmentor->augmentAll( $resultSet );
879 if ( $data ) {
880 $resultSet->setAugmentedData( $name, $data );
881 }
882 }
883 }
884
890 public function setHookContainer( HookContainer $hookContainer ) {
891 $this->hookContainer = $hookContainer;
892 $this->hookRunner = new HookRunner( $hookContainer );
893 }
894
901 protected function getHookContainer(): HookContainer {
902 if ( !$this->hookContainer ) {
903 // This shouldn't be hit in core, but it is needed for CirrusSearch
904 // which commonly creates a CirrusSearch object without cirrus being
905 // configured in $wgSearchType/$wgSearchTypeAlternatives.
906 $this->hookContainer = MediaWikiServices::getInstance()->getHookContainer();
907 }
908 return $this->hookContainer;
909 }
910
919 protected function getHookRunner(): HookRunner {
920 if ( !$this->hookRunner ) {
921 $this->hookRunner = new HookRunner( $this->getHookContainer() );
922 }
923 return $this->hookRunner;
924 }
925
933 if ( !$this->specialPageSuggester ) {
934 $this->specialPageSuggester = new SpecialPageSuggester(
935 MediaWikiServices::getInstance()->getSpecialPageFactory(),
936 MediaWikiServices::getInstance()->getContentLanguage(),
937 );
938 }
939 return $this->specialPageSuggester;
940 }
941}
942
944class_alias( SearchEngine::class, 'SearchEngine' );
const NS_MAIN
Definition Defines.php:51
const NS_SPECIAL
Definition Defines.php:40
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that a deprecated feature was used.
if(!defined('MW_SETUP_CALLBACK'))
Definition WebStart.php:71
Exception thrown when an unregistered content model is requested.
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
Service locator for MediaWiki core services.
static getInstance()
Returns the global default instance of the top level service locator.
Null index field - means search engine does not implement this field.
Perform augmentation of each row and return composite result, indexed by ID.
updateTitle( $id, $title)
Update a search index record's title only.
setLimitOffset( $limit, $offset=0)
Set the maximum number of results to return and how many to skip before returning the first.
const COMPLETION_PROFILE_TYPE
Profile type for completionSearch.
getHookContainer()
Get a HookContainer, for running extension hooks or for hook metadata.
augmentSearchResults(ISearchResultSet $resultSet)
Augment search results with extra data.
processCompletionResults( $search, SearchSuggestionSet $suggestions)
Process completion search results.
setSort( $sort)
Set the sort direction of the search results.
static defaultNearMatcher()
Get near matcher for default SearchEngine.
setNamespaces( $namespaces)
Set which namespaces the search should include.
simplePrefixSearch( $search)
Call out to simple search backend.
completionSearchBackend( $search)
Perform a completion search.
searchTitle( $term)
Perform a title-only search query and return a result set.
const SEARCH_ID
Name of the feature data entry holding an identifier generated by some search backends.
setShowSuggestion( $showSuggestion)
Set whether the searcher should try to build a suggestion.
completionSearchBackendOverfetch( $search)
Perform an overfetch of completion search results.
doSearchText( $term)
Perform a full text search query and return a result set.
completionSearchWithVariants( $search)
Perform a completion search with variants.
doSearchArchiveTitle( $term)
Perform a title search in the article archive.
searchArchiveTitle( $term)
Perform a title search in the article archive.
extractTitles(SearchSuggestionSet $completionResults)
Extract titles from completion results.
setFeatureData( $feature, $data)
Way to pass custom data for engines.
getFeatureData( $feature)
Way to retrieve custom data set by setFeatureData or by the engine itself.
update( $id, $title, $text)
Create or update the search index record for the given page.
legalSearchChars( $type=self::CHARS_ALL)
Get chars legal for search.
normalizeNamespaces( $search)
Makes search simple string if it was namespaced.
getSearchIndexFields()
Get fields for search index.
const FT_QUERY_DEP_PROFILE_TYPE
Profile type for query dependent ranking features (ex: field weights)
static parseNamespacePrefixes( $query, $withAllKeyword=true, $withPrefixSearchExtractNamespaceHook=false)
Parse some common prefixes: all (search everything) or namespace names.
const FT_QUERY_INDEP_PROFILE_TYPE
Profile type for query independent ranking features (ex: article popularity)
defaultPrefixSearch( $search)
Simple prefix search for subpages.
getSpecialPageSuggester()
Utility to suggest special pages by prefix.
array $features
Feature values.
setHookContainer(HookContainer $hookContainer)
static userHighlightPrefs()
Find snippet highlight settings for all users.
makeSearchFieldMapping( $name, $type)
Create a search field definition.
getProfiles( $profileType, ?User $user=null)
Get a list of supported profiles.
completionSearch( $search)
Perform a completion search.
getSort()
Get the sort direction of the search results.
getValidSorts()
Get the valid sort directions.
getHookRunner()
Get a HookRunner for running core hooks.
getNearMatcher(Config $config)
Get service class to finding near matches.
normalizeText( $string)
When overridden in derived class, performs database-specific conversions on text to be used for searc...
const CHARS_NO_SYNTAX
Integer flag for legalSearchChars: includes all chars allowed in a search term.
searchText( $term)
Perform a full text search query and return a result set.
const CHARS_ALL
Integer flag for legalSearchChars: includes all chars allowed in a search query.
doSearchTitle( $term)
Perform a title-only search query and return a result set.
An utility class to rescore search results by looking for an exact match in the db and add the page f...
filter( $callback)
Filter the suggestions array.
rescore( $key)
Move the suggestion at index $key to the first position.
map( $callback)
Call array_map on the suggestions array.
static fromTitles(array $titles, $hasMoreResults=false)
Builds a new set of suggestion based on a title array.
shrink( $limit)
Remove any extra elements in the suggestions set.
remove(SearchSuggestion $suggestion)
Remove a suggestion from the set.
prepend(SearchSuggestion $suggestion)
Add a new suggestion at the top.
getSuggestedTitle()
Title object in the case this suggestion is based on a title.
static fromTitle( $score, Title $title)
Create suggestion from Title.
Handles searching prefixes of titles and finding any page names that match.
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition Status.php:44
A title parser service for MediaWiki.
Represents a title within MediaWiki.
Definition Title.php:69
User class for the MediaWiki software.
Definition User.php:129
Content of like value.
Definition LikeValue.php:14
Interface for configuration instances.
Definition Config.php:18
A set of SearchEngine results.
setAugmentedData( $name, $data)
Sets augmented data for result set.
Marker class for search engines that can handle their own pagination, by reporting in their ISearchRe...