35 private $searchEngineFactory;
38 private $searchEngineConfig;
41 private $permissionManager;
44 private $redirectLookup;
50 private $titleFormatter;
73 private const LIMIT = 50;
76 private const MAX_LIMIT = 100;
79 private const OFFSET = 0;
87 private $completionCacheExpiry;
107 $this->searchEngineFactory = $searchEngineFactory;
108 $this->searchEngineConfig = $searchEngineConfig;
109 $this->permissionManager = $permissionManager;
110 $this->redirectLookup = $redirectLookup;
111 $this->pageStore = $pageStore;
112 $this->titleFormatter = $titleFormatter;
121 if ( !in_array( $this->mode, self::SUPPORTED_MODES ) ) {
122 throw new InvalidArgumentException(
123 "Unsupported search mode `{$this->mode}` configured. Supported modes: " .
124 implode(
', ', self::SUPPORTED_MODES )
132 private function createSearchEngine() {
135 $searchEngine = $this->searchEngineFactory->create();
136 $searchEngine->setNamespaces( $this->searchEngineConfig->defaultNamespaces() );
137 $searchEngine->setLimitOffset( $limit, self::OFFSET );
138 return $searchEngine;
151 private function getSearchResultsOrThrow( $results ) {
153 if ( $results instanceof
Status ) {
155 if ( !$status->isOK() ) {
156 list( $error ) = $status->splitByErrorType();
157 if ( $error->getErrors() ) {
158 $errorMessages = $error->getMessage();
160 new MessageValue(
"rest-search-error", [ $errorMessages->getKey() ] )
164 $statusValue = $status->getValue();
166 return $statusValue->extractResults();
169 return $results->extractResults();
182 private function doSearch( $searchEngine ) {
185 if ( $this->mode == self::COMPLETION_MODE ) {
186 $completionSearch = $searchEngine->completionSearchWithVariants( $query );
187 return $this->buildPageObjects( $completionSearch->getSuggestions() );
189 $titleSearch = $searchEngine->searchTitle( $query );
190 $textSearch = $searchEngine->searchText( $query );
192 $titleSearchResults = $this->getSearchResultsOrThrow( $titleSearch );
193 $textSearchResults = $this->getSearchResultsOrThrow( $textSearch );
195 $mergedResults = array_merge( $titleSearchResults, $textSearchResults );
196 return $this->buildPageObjects( $mergedResults );
212 private function buildPageObjects( array $searchResponse ): array {
214 foreach ( $searchResponse as $response ) {
216 if ( $isSearchResult ) {
217 if ( $response->isBrokenTitle() || $response->isMissingRevision() ) {
220 $title = $response->getTitle();
222 $title = $response->getSuggestedTitle();
224 $pageObj = $this->buildSinglePage(
$title, $response );
226 $pageNsAndID = CacheKeyHelper::getKeyForPage( $pageObj[
'pageIdentity'] );
229 if ( isset( $pageInfos[$pageNsAndID] ) ) {
230 if ( $pageInfos[$pageNsAndID][
'redirect'] !==
null ) {
231 $pageInfos[$pageNsAndID][
'result'] = $isSearchResult ? $response :
null;
232 $pageInfos[$pageNsAndID][
'suggestion'] = $isSearchResult ? null : $response;
236 $pageInfos[$pageNsAndID] = $pageObj;
255 private function buildSinglePage(
$title, $result ) {
256 $redirectTarget =
$title->canExist() ? $this->redirectLookup->getRedirectTarget(
$title ) :
null;
259 if ( $redirectTarget && $redirectTarget->getNamespace() > -1 && !$redirectTarget->isExternal() ) {
261 $title = $this->pageStore->getPageForLink( $redirectTarget );
263 $redirectSource =
null;
271 'result' => $result instanceof
SearchResult ? $result :
null,
272 'redirect' => $redirectSource
288 private function buildResultFromPageInfos( array $pageInfos, array $thumbsAndDesc ): array {
290 foreach ( $pageInfos as $pageInfo ) {
292 'pageIdentity' => $page,
293 'suggestion' => $sugg,
295 'redirect' => $redirect
297 $excerpt = $sugg ? $sugg->getText() : $result->getTextSnippet();
298 $id = ( $page instanceof PageIdentity && $page->canExist() ) ? $page->getId() : 0;
301 'key' => $this->titleFormatter->getPrefixedDBkey( $page ),
302 'title' => $this->titleFormatter->getPrefixedText( $page ),
303 'excerpt' => $excerpt ?:
null,
304 'matched_title' => $redirect ? $this->titleFormatter->getPrefixedText( $redirect ) :
null,
305 'description' => $id > 0 ? $thumbsAndDesc[$id][
'description'] :
null,
306 'thumbnail' => $id > 0 ? $thumbsAndDesc[$id][
'thumbnail'] :
null,
319 private function serializeThumbnail( ?SearchResultThumbnail $thumbnail ): ?array {
320 if ( $thumbnail == null ) {
325 'mimetype' => $thumbnail->getMimeType(),
326 'size' => $thumbnail->getSize(),
327 'width' => $thumbnail->getWidth(),
328 'height' => $thumbnail->getHeight(),
329 'duration' => $thumbnail->getDuration(),
330 'url' => $thumbnail->getUrl(),
344 private function buildDescriptionsFromPageIdentities( array $pageIdentities ) {
345 $descriptions = array_fill_keys( array_keys( $pageIdentities ),
null );
347 $this->getHookRunner()->onSearchResultProvideDescription( $pageIdentities, $descriptions );
349 return array_map(
static function ( $description ) {
350 return [
'description' => $description ];
365 private function buildThumbnailsFromPageIdentities( array $pageIdentities ) {
366 $thumbnails = array_fill_keys( array_keys( $pageIdentities ),
null );
368 $this->getHookRunner()->onSearchResultProvideThumbnail( $pageIdentities, $thumbnails );
370 return array_map(
function ( $thumbnail ) {
371 return [
'thumbnail' => $this->serializeThumbnail( $thumbnail ) ];
380 $searchEngine = $this->createSearchEngine();
381 $pageInfos = $this->doSearch( $searchEngine );
384 $pageIdentities = array_reduce(
385 array_values( $pageInfos ),
386 static function ( $realPages, $item ) {
387 $page = $item[
'pageIdentity'];
389 $realPages[$item[
'pageIdentity']->getId()] = $item[
'pageIdentity'];
395 $descriptions = $this->buildDescriptionsFromPageIdentities( $pageIdentities );
396 $thumbs = $this->buildThumbnailsFromPageIdentities( $pageIdentities );
398 $thumbsAndDescriptions = [];
399 foreach ( $descriptions as $pageId => $description ) {
400 $thumbsAndDescriptions[$pageId] = $description + $thumbs[$pageId];
403 $result = $this->buildResultFromPageInfos( $pageInfos, $thumbsAndDescriptions );
405 $response = $this->getResponseFactory()->createJson( [
'pages' => $result ] );
407 if ( $this->mode === self::COMPLETION_MODE && $this->completionCacheExpiry ) {
411 if ( $this->permissionManager->isEveryoneAllowed(
'read' ) ) {
412 $response->setHeader(
'Cache-Control',
'public, max-age=' . $this->completionCacheExpiry );
414 $response->setHeader(
'Cache-Control',
'no-store, max-age=0' );
424 self::PARAM_SOURCE =>
'query',
425 ParamValidator::PARAM_TYPE =>
'string',
426 ParamValidator::PARAM_REQUIRED =>
true,
429 self::PARAM_SOURCE =>
'query',
430 ParamValidator::PARAM_TYPE =>
'integer',
431 ParamValidator::PARAM_REQUIRED =>
false,
432 ParamValidator::PARAM_DEFAULT => self::LIMIT,
433 IntegerDef::PARAM_MIN => 1,
434 IntegerDef::PARAM_MAX => self::MAX_LIMIT,