65 private const LIMIT = 50;
68 private const MAX_LIMIT = 100;
71 private const OFFSET = 0;
79 private $completionCacheExpiry;
91 $this->searchEngineFactory = $searchEngineFactory;
92 $this->searchEngineConfig = $searchEngineConfig;
93 $this->searchResultThumbnailProvider = $searchResultThumbnailProvider;
94 $this->permissionManager = $permissionManager;
95 $this->redirectLookup = $redirectLookup;
96 $this->pageStore = $pageStore;
97 $this->titleFormatter = $titleFormatter;
106 if ( !in_array( $this->mode, self::SUPPORTED_MODES ) ) {
107 throw new InvalidArgumentException(
108 "Unsupported search mode `{$this->mode}` configured. Supported modes: " .
109 implode(
', ', self::SUPPORTED_MODES )
117 private function createSearchEngine() {
120 $searchEngine = $this->searchEngineFactory->create();
121 $searchEngine->setNamespaces( $this->searchEngineConfig->defaultNamespaces() );
122 $searchEngine->setLimitOffset( $limit, self::OFFSET );
123 return $searchEngine;
136 private function getSearchResultsOrThrow( $results ) {
140 if ( !$status->isOK() ) {
141 if ( $status->getMessages(
'error' ) ) {
142 $this->throwExceptionForStatus( $status,
'rest-search-error', 500 );
145 $statusValue = $status->getValue();
147 return $statusValue->extractResults();
150 return $results->extractResults();
163 private function doSearch( $searchEngine ) {
166 if ( $this->mode == self::COMPLETION_MODE ) {
167 $completionSearch = $searchEngine->completionSearchWithVariants( $query );
168 return $this->buildPageObjects( $completionSearch->getSuggestions() );
170 $titleSearch = $searchEngine->searchTitle( $query );
171 $textSearch = $searchEngine->searchText( $query );
173 $titleSearchResults = $this->getSearchResultsOrThrow( $titleSearch );
174 $textSearchResults = $this->getSearchResultsOrThrow( $textSearch );
176 $mergedResults = array_merge( $titleSearchResults, $textSearchResults );
177 return $this->buildPageObjects( $mergedResults );
193 private function buildPageObjects( array $searchResponse ): array {
195 foreach ( $searchResponse as $response ) {
197 if ( $isSearchResult ) {
198 if ( $response->isBrokenTitle() || $response->isMissingRevision() ) {
201 $title = $response->getTitle();
203 $title = $response->getSuggestedTitle();
205 $pageObj = $this->buildSinglePage( $title, $response );
207 $pageNsAndID = CacheKeyHelper::getKeyForPage( $pageObj[
'pageIdentity'] );
210 if ( isset( $pageInfos[$pageNsAndID] ) ) {
211 if ( $pageInfos[$pageNsAndID][
'redirect'] !==
null ) {
212 $pageInfos[$pageNsAndID][
'result'] = $isSearchResult ? $response :
null;
213 $pageInfos[$pageNsAndID][
'suggestion'] = $isSearchResult ? null : $response;
217 $pageInfos[$pageNsAndID] = $pageObj;
236 private function buildSinglePage( $title, $result ) {
237 $redirectTarget = $title->canExist() ? $this->redirectLookup->getRedirectTarget( $title ) :
null;
240 if ( $redirectTarget && $redirectTarget->getNamespace() > -1 && !$redirectTarget->isExternal() ) {
241 $redirectSource = $title;
242 $title = $this->pageStore->getPageForLink( $redirectTarget );
244 $redirectSource =
null;
246 if ( !$title || !$this->
getAuthority()->probablyCan(
'read', $title ) ) {
250 'pageIdentity' => $title,
252 'result' => $result instanceof
SearchResult ? $result :
null,
253 'redirect' => $redirectSource
269 private function buildResultFromPageInfos( array $pageInfos, array $thumbsAndDesc ): array {
271 foreach ( $pageInfos as $pageInfo ) {
273 'pageIdentity' => $page,
274 'suggestion' => $sugg,
276 'redirect' => $redirect
278 $excerpt = $sugg ? $sugg->getText() : $result->getTextSnippet();
279 $id = ( $page instanceof PageIdentity && $page->canExist() ) ? $page->getId() : 0;
282 'key' => $this->titleFormatter->getPrefixedDBkey( $page ),
283 'title' => $this->titleFormatter->getPrefixedText( $page ),
284 'excerpt' => $excerpt ?:
null,
285 'matched_title' => $redirect ? $this->titleFormatter->getPrefixedText( $redirect ) :
null,
286 'description' => $id > 0 ? $thumbsAndDesc[$id][
'description'] :
null,
287 'thumbnail' => $id > 0 ? $thumbsAndDesc[$id][
'thumbnail'] :
null,
300 private function serializeThumbnail( ?SearchResultThumbnail $thumbnail ): ?array {
301 if ( $thumbnail == null ) {
306 'mimetype' => $thumbnail->getMimeType(),
307 'width' => $thumbnail->getWidth(),
308 'height' => $thumbnail->getHeight(),
309 'duration' => $thumbnail->getDuration(),
310 'url' => $thumbnail->getUrl(),
324 private function buildDescriptionsFromPageIdentities( array $pageIdentities ) {
325 $descriptions = array_fill_keys( array_keys( $pageIdentities ),
null );
327 $this->getHookRunner()->onSearchResultProvideDescription( $pageIdentities, $descriptions );
329 return array_map(
static function ( $description ) {
330 return [
'description' => $description ];
345 private function buildThumbnailsFromPageIdentities( array $pageIdentities ) {
346 $thumbnails = $this->searchResultThumbnailProvider->getThumbnails( $pageIdentities );
347 $thumbnails += array_fill_keys( array_keys( $pageIdentities ),
null );
349 return array_map(
function ( $thumbnail ) {
350 return [
'thumbnail' => $this->serializeThumbnail( $thumbnail ) ];
359 $searchEngine = $this->createSearchEngine();
360 $pageInfos = $this->doSearch( $searchEngine );
363 $pageIdentities = array_reduce(
364 array_values( $pageInfos ),
365 static function ( $realPages, $item ) {
366 $page = $item[
'pageIdentity'];
368 $realPages[$item[
'pageIdentity']->getId()] = $item[
'pageIdentity'];
374 $descriptions = $this->buildDescriptionsFromPageIdentities( $pageIdentities );
375 $thumbs = $this->buildThumbnailsFromPageIdentities( $pageIdentities );
377 $thumbsAndDescriptions = [];
378 foreach ( $descriptions as $pageId => $description ) {
379 $thumbsAndDescriptions[$pageId] = $description + $thumbs[$pageId];
382 $result = $this->buildResultFromPageInfos( $pageInfos, $thumbsAndDescriptions );
384 $response = $this->getResponseFactory()->createJson( [
'pages' => $result ] );
386 if ( $this->mode === self::COMPLETION_MODE && $this->completionCacheExpiry ) {
390 if ( $this->permissionManager->isEveryoneAllowed(
'read' ) ) {
391 $response->setHeader(
'Cache-Control',
'public, max-age=' . $this->completionCacheExpiry );
393 $response->setHeader(
'Cache-Control',
'no-store, max-age=0' );
403 self::PARAM_SOURCE =>
'query',
404 ParamValidator::PARAM_TYPE =>
'string',
405 ParamValidator::PARAM_REQUIRED =>
true,
408 self::PARAM_SOURCE =>
'query',
409 ParamValidator::PARAM_TYPE =>
'integer',
410 ParamValidator::PARAM_REQUIRED =>
false,
411 ParamValidator::PARAM_DEFAULT => self::LIMIT,
412 IntegerDef::PARAM_MIN => 1,
413 IntegerDef::PARAM_MAX => self::MAX_LIMIT,