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 [ $error ] = $status->splitByErrorType();
142 if ( $error->getErrors() ) {
143 $this->throwExceptionForStatus( $status,
'rest-search-error', 500 );
146 $statusValue = $status->getValue();
148 return $statusValue->extractResults();
151 return $results->extractResults();
164 private function doSearch( $searchEngine ) {
167 if ( $this->mode == self::COMPLETION_MODE ) {
168 $completionSearch = $searchEngine->completionSearchWithVariants( $query );
169 return $this->buildPageObjects( $completionSearch->getSuggestions() );
171 $titleSearch = $searchEngine->searchTitle( $query );
172 $textSearch = $searchEngine->searchText( $query );
174 $titleSearchResults = $this->getSearchResultsOrThrow( $titleSearch );
175 $textSearchResults = $this->getSearchResultsOrThrow( $textSearch );
177 $mergedResults = array_merge( $titleSearchResults, $textSearchResults );
178 return $this->buildPageObjects( $mergedResults );
194 private function buildPageObjects( array $searchResponse ): array {
196 foreach ( $searchResponse as $response ) {
198 if ( $isSearchResult ) {
199 if ( $response->isBrokenTitle() || $response->isMissingRevision() ) {
202 $title = $response->getTitle();
204 $title = $response->getSuggestedTitle();
206 $pageObj = $this->buildSinglePage( $title, $response );
208 $pageNsAndID = CacheKeyHelper::getKeyForPage( $pageObj[
'pageIdentity'] );
211 if ( isset( $pageInfos[$pageNsAndID] ) ) {
212 if ( $pageInfos[$pageNsAndID][
'redirect'] !==
null ) {
213 $pageInfos[$pageNsAndID][
'result'] = $isSearchResult ? $response :
null;
214 $pageInfos[$pageNsAndID][
'suggestion'] = $isSearchResult ? null : $response;
218 $pageInfos[$pageNsAndID] = $pageObj;
237 private function buildSinglePage( $title, $result ) {
238 $redirectTarget = $title->canExist() ? $this->redirectLookup->getRedirectTarget( $title ) :
null;
241 if ( $redirectTarget && $redirectTarget->getNamespace() > -1 && !$redirectTarget->isExternal() ) {
242 $redirectSource = $title;
243 $title = $this->pageStore->getPageForLink( $redirectTarget );
245 $redirectSource =
null;
247 if ( !$title || !$this->
getAuthority()->probablyCan(
'read', $title ) ) {
251 'pageIdentity' => $title,
253 'result' => $result instanceof
SearchResult ? $result :
null,
254 'redirect' => $redirectSource
270 private function buildResultFromPageInfos( array $pageInfos, array $thumbsAndDesc ): array {
272 foreach ( $pageInfos as $pageInfo ) {
274 'pageIdentity' => $page,
275 'suggestion' => $sugg,
277 'redirect' => $redirect
279 $excerpt = $sugg ? $sugg->getText() : $result->getTextSnippet();
280 $id = ( $page instanceof PageIdentity && $page->canExist() ) ? $page->getId() : 0;
283 'key' => $this->titleFormatter->getPrefixedDBkey( $page ),
284 'title' => $this->titleFormatter->getPrefixedText( $page ),
285 'excerpt' => $excerpt ?:
null,
286 'matched_title' => $redirect ? $this->titleFormatter->getPrefixedText( $redirect ) :
null,
287 'description' => $id > 0 ? $thumbsAndDesc[$id][
'description'] :
null,
288 'thumbnail' => $id > 0 ? $thumbsAndDesc[$id][
'thumbnail'] :
null,
301 private function serializeThumbnail( ?SearchResultThumbnail $thumbnail ): ?array {
302 if ( $thumbnail == null ) {
307 'mimetype' => $thumbnail->getMimeType(),
308 'width' => $thumbnail->getWidth(),
309 'height' => $thumbnail->getHeight(),
310 'duration' => $thumbnail->getDuration(),
311 'url' => $thumbnail->getUrl(),
325 private function buildDescriptionsFromPageIdentities( array $pageIdentities ) {
326 $descriptions = array_fill_keys( array_keys( $pageIdentities ),
null );
328 $this->getHookRunner()->onSearchResultProvideDescription( $pageIdentities, $descriptions );
330 return array_map(
static function ( $description ) {
331 return [
'description' => $description ];
346 private function buildThumbnailsFromPageIdentities( array $pageIdentities ) {
347 $thumbnails = $this->searchResultThumbnailProvider->getThumbnails( $pageIdentities );
348 $thumbnails += array_fill_keys( array_keys( $pageIdentities ),
null );
350 return array_map(
function ( $thumbnail ) {
351 return [
'thumbnail' => $this->serializeThumbnail( $thumbnail ) ];
360 $searchEngine = $this->createSearchEngine();
361 $pageInfos = $this->doSearch( $searchEngine );
364 $pageIdentities = array_reduce(
365 array_values( $pageInfos ),
366 static function ( $realPages, $item ) {
367 $page = $item[
'pageIdentity'];
369 $realPages[$item[
'pageIdentity']->getId()] = $item[
'pageIdentity'];
375 $descriptions = $this->buildDescriptionsFromPageIdentities( $pageIdentities );
376 $thumbs = $this->buildThumbnailsFromPageIdentities( $pageIdentities );
378 $thumbsAndDescriptions = [];
379 foreach ( $descriptions as $pageId => $description ) {
380 $thumbsAndDescriptions[$pageId] = $description + $thumbs[$pageId];
383 $result = $this->buildResultFromPageInfos( $pageInfos, $thumbsAndDescriptions );
385 $response = $this->getResponseFactory()->createJson( [
'pages' => $result ] );
387 if ( $this->mode === self::COMPLETION_MODE && $this->completionCacheExpiry ) {
391 if ( $this->permissionManager->isEveryoneAllowed(
'read' ) ) {
392 $response->setHeader(
'Cache-Control',
'public, max-age=' . $this->completionCacheExpiry );
394 $response->setHeader(
'Cache-Control',
'no-store, max-age=0' );
404 self::PARAM_SOURCE =>
'query',
405 ParamValidator::PARAM_TYPE =>
'string',
406 ParamValidator::PARAM_REQUIRED =>
true,
409 self::PARAM_SOURCE =>
'query',
410 ParamValidator::PARAM_TYPE =>
'integer',
411 ParamValidator::PARAM_REQUIRED =>
false,
412 ParamValidator::PARAM_DEFAULT => self::LIMIT,
413 IntegerDef::PARAM_MIN => 1,
414 IntegerDef::PARAM_MAX => self::MAX_LIMIT,