37 private $searchEngineFactory;
40 private $searchEngineConfig;
43 private $searchResultThumbnailProvider;
46 private $permissionManager;
49 private $redirectLookup;
55 private $titleFormatter;
78 private const LIMIT = 50;
81 private const MAX_LIMIT = 100;
84 private const OFFSET = 0;
92 private $completionCacheExpiry;
114 $this->searchEngineFactory = $searchEngineFactory;
115 $this->searchEngineConfig = $searchEngineConfig;
116 $this->searchResultThumbnailProvider = $searchResultThumbnailProvider;
117 $this->permissionManager = $permissionManager;
118 $this->redirectLookup = $redirectLookup;
119 $this->pageStore = $pageStore;
120 $this->titleFormatter = $titleFormatter;
129 if ( !in_array( $this->mode, self::SUPPORTED_MODES ) ) {
130 throw new InvalidArgumentException(
131 "Unsupported search mode `{$this->mode}` configured. Supported modes: " .
132 implode(
', ', self::SUPPORTED_MODES )
140 private function createSearchEngine() {
143 $searchEngine = $this->searchEngineFactory->create();
144 $searchEngine->setNamespaces( $this->searchEngineConfig->defaultNamespaces() );
145 $searchEngine->setLimitOffset( $limit, self::OFFSET );
146 return $searchEngine;
159 private function getSearchResultsOrThrow( $results ) {
163 if ( !$status->isOK() ) {
164 [ $error ] = $status->splitByErrorType();
165 if ( $error->getErrors() ) {
166 $this->throwExceptionForStatus( $status,
'rest-search-error', 500 );
169 $statusValue = $status->getValue();
171 return $statusValue->extractResults();
174 return $results->extractResults();
187 private function doSearch( $searchEngine ) {
190 if ( $this->mode == self::COMPLETION_MODE ) {
191 $completionSearch = $searchEngine->completionSearchWithVariants( $query );
192 return $this->buildPageObjects( $completionSearch->getSuggestions() );
194 $titleSearch = $searchEngine->searchTitle( $query );
195 $textSearch = $searchEngine->searchText( $query );
197 $titleSearchResults = $this->getSearchResultsOrThrow( $titleSearch );
198 $textSearchResults = $this->getSearchResultsOrThrow( $textSearch );
200 $mergedResults = array_merge( $titleSearchResults, $textSearchResults );
201 return $this->buildPageObjects( $mergedResults );
217 private function buildPageObjects( array $searchResponse ): array {
219 foreach ( $searchResponse as $response ) {
221 if ( $isSearchResult ) {
222 if ( $response->isBrokenTitle() || $response->isMissingRevision() ) {
225 $title = $response->getTitle();
227 $title = $response->getSuggestedTitle();
229 $pageObj = $this->buildSinglePage( $title, $response );
231 $pageNsAndID = CacheKeyHelper::getKeyForPage( $pageObj[
'pageIdentity'] );
234 if ( isset( $pageInfos[$pageNsAndID] ) ) {
235 if ( $pageInfos[$pageNsAndID][
'redirect'] !==
null ) {
236 $pageInfos[$pageNsAndID][
'result'] = $isSearchResult ? $response :
null;
237 $pageInfos[$pageNsAndID][
'suggestion'] = $isSearchResult ? null : $response;
241 $pageInfos[$pageNsAndID] = $pageObj;
260 private function buildSinglePage( $title, $result ) {
261 $redirectTarget = $title->canExist() ? $this->redirectLookup->getRedirectTarget( $title ) :
null;
264 if ( $redirectTarget && $redirectTarget->getNamespace() > -1 && !$redirectTarget->isExternal() ) {
265 $redirectSource = $title;
266 $title = $this->pageStore->getPageForLink( $redirectTarget );
268 $redirectSource =
null;
270 if ( !$title || !$this->
getAuthority()->probablyCan(
'read', $title ) ) {
274 'pageIdentity' => $title,
276 'result' => $result instanceof
SearchResult ? $result :
null,
277 'redirect' => $redirectSource
293 private function buildResultFromPageInfos( array $pageInfos, array $thumbsAndDesc ): array {
295 foreach ( $pageInfos as $pageInfo ) {
297 'pageIdentity' => $page,
298 'suggestion' => $sugg,
300 'redirect' => $redirect
302 $excerpt = $sugg ? $sugg->getText() : $result->getTextSnippet();
303 $id = ( $page instanceof PageIdentity && $page->canExist() ) ? $page->getId() : 0;
306 'key' => $this->titleFormatter->getPrefixedDBkey( $page ),
307 'title' => $this->titleFormatter->getPrefixedText( $page ),
308 'excerpt' => $excerpt ?:
null,
309 'matched_title' => $redirect ? $this->titleFormatter->getPrefixedText( $redirect ) :
null,
310 'description' => $id > 0 ? $thumbsAndDesc[$id][
'description'] :
null,
311 'thumbnail' => $id > 0 ? $thumbsAndDesc[$id][
'thumbnail'] :
null,
324 private function serializeThumbnail( ?SearchResultThumbnail $thumbnail ): ?array {
325 if ( $thumbnail == null ) {
330 'mimetype' => $thumbnail->getMimeType(),
331 'width' => $thumbnail->getWidth(),
332 'height' => $thumbnail->getHeight(),
333 'duration' => $thumbnail->getDuration(),
334 'url' => $thumbnail->getUrl(),
348 private function buildDescriptionsFromPageIdentities( array $pageIdentities ) {
349 $descriptions = array_fill_keys( array_keys( $pageIdentities ),
null );
351 $this->getHookRunner()->onSearchResultProvideDescription( $pageIdentities, $descriptions );
353 return array_map(
static function ( $description ) {
354 return [
'description' => $description ];
369 private function buildThumbnailsFromPageIdentities( array $pageIdentities ) {
370 $thumbnails = $this->searchResultThumbnailProvider->getThumbnails( $pageIdentities );
371 $thumbnails += array_fill_keys( array_keys( $pageIdentities ),
null );
373 return array_map(
function ( $thumbnail ) {
374 return [
'thumbnail' => $this->serializeThumbnail( $thumbnail ) ];
383 $searchEngine = $this->createSearchEngine();
384 $pageInfos = $this->doSearch( $searchEngine );
387 $pageIdentities = array_reduce(
388 array_values( $pageInfos ),
389 static function ( $realPages, $item ) {
390 $page = $item[
'pageIdentity'];
392 $realPages[$item[
'pageIdentity']->getId()] = $item[
'pageIdentity'];
398 $descriptions = $this->buildDescriptionsFromPageIdentities( $pageIdentities );
399 $thumbs = $this->buildThumbnailsFromPageIdentities( $pageIdentities );
401 $thumbsAndDescriptions = [];
402 foreach ( $descriptions as $pageId => $description ) {
403 $thumbsAndDescriptions[$pageId] = $description + $thumbs[$pageId];
406 $result = $this->buildResultFromPageInfos( $pageInfos, $thumbsAndDescriptions );
408 $response = $this->getResponseFactory()->createJson( [
'pages' => $result ] );
410 if ( $this->mode === self::COMPLETION_MODE && $this->completionCacheExpiry ) {
414 if ( $this->permissionManager->isEveryoneAllowed(
'read' ) ) {
415 $response->setHeader(
'Cache-Control',
'public, max-age=' . $this->completionCacheExpiry );
417 $response->setHeader(
'Cache-Control',
'no-store, max-age=0' );
427 self::PARAM_SOURCE =>
'query',
428 ParamValidator::PARAM_TYPE =>
'string',
429 ParamValidator::PARAM_REQUIRED =>
true,
432 self::PARAM_SOURCE =>
'query',
433 ParamValidator::PARAM_TYPE =>
'integer',
434 ParamValidator::PARAM_REQUIRED =>
false,
435 ParamValidator::PARAM_DEFAULT => self::LIMIT,
436 IntegerDef::PARAM_MIN => 1,
437 IntegerDef::PARAM_MAX => self::MAX_LIMIT,