36 private $searchEngineFactory;
39 private $searchEngineConfig;
42 private $searchResultThumbnailProvider;
45 private $permissionManager;
48 private $redirectLookup;
54 private $titleFormatter;
77 private const LIMIT = 50;
80 private const MAX_LIMIT = 100;
83 private const OFFSET = 0;
91 private $completionCacheExpiry;
113 $this->searchEngineFactory = $searchEngineFactory;
114 $this->searchEngineConfig = $searchEngineConfig;
115 $this->searchResultThumbnailProvider = $searchResultThumbnailProvider;
116 $this->permissionManager = $permissionManager;
117 $this->redirectLookup = $redirectLookup;
118 $this->pageStore = $pageStore;
119 $this->titleFormatter = $titleFormatter;
128 if ( !in_array( $this->mode, self::SUPPORTED_MODES ) ) {
129 throw new InvalidArgumentException(
130 "Unsupported search mode `{$this->mode}` configured. Supported modes: " .
131 implode(
', ', self::SUPPORTED_MODES )
139 private function createSearchEngine() {
142 $searchEngine = $this->searchEngineFactory->create();
143 $searchEngine->setNamespaces( $this->searchEngineConfig->defaultNamespaces() );
144 $searchEngine->setLimitOffset( $limit, self::OFFSET );
145 return $searchEngine;
158 private function getSearchResultsOrThrow( $results ) {
160 if ( $results instanceof
Status ) {
162 if ( !$status->isOK() ) {
163 [ $error ] = $status->splitByErrorType();
164 if ( $error->getErrors() ) {
165 $errorMessages = $error->getMessage();
167 new MessageValue(
"rest-search-error", [ $errorMessages->getKey() ] )
171 $statusValue = $status->getValue();
173 return $statusValue->extractResults();
176 return $results->extractResults();
189 private function doSearch( $searchEngine ) {
192 if ( $this->mode == self::COMPLETION_MODE ) {
193 $completionSearch = $searchEngine->completionSearchWithVariants( $query );
194 return $this->buildPageObjects( $completionSearch->getSuggestions() );
196 $titleSearch = $searchEngine->searchTitle( $query );
197 $textSearch = $searchEngine->searchText( $query );
199 $titleSearchResults = $this->getSearchResultsOrThrow( $titleSearch );
200 $textSearchResults = $this->getSearchResultsOrThrow( $textSearch );
202 $mergedResults = array_merge( $titleSearchResults, $textSearchResults );
203 return $this->buildPageObjects( $mergedResults );
219 private function buildPageObjects( array $searchResponse ): array {
221 foreach ( $searchResponse as $response ) {
223 if ( $isSearchResult ) {
224 if ( $response->isBrokenTitle() || $response->isMissingRevision() ) {
227 $title = $response->getTitle();
229 $title = $response->getSuggestedTitle();
231 $pageObj = $this->buildSinglePage( $title, $response );
233 $pageNsAndID = CacheKeyHelper::getKeyForPage( $pageObj[
'pageIdentity'] );
236 if ( isset( $pageInfos[$pageNsAndID] ) ) {
237 if ( $pageInfos[$pageNsAndID][
'redirect'] !==
null ) {
238 $pageInfos[$pageNsAndID][
'result'] = $isSearchResult ? $response :
null;
239 $pageInfos[$pageNsAndID][
'suggestion'] = $isSearchResult ? null : $response;
243 $pageInfos[$pageNsAndID] = $pageObj;
262 private function buildSinglePage( $title, $result ) {
263 $redirectTarget = $title->canExist() ? $this->redirectLookup->getRedirectTarget( $title ) :
null;
266 if ( $redirectTarget && $redirectTarget->getNamespace() > -1 && !$redirectTarget->isExternal() ) {
267 $redirectSource = $title;
268 $title = $this->pageStore->getPageForLink( $redirectTarget );
270 $redirectSource =
null;
272 if ( !$title || !$this->
getAuthority()->probablyCan(
'read', $title ) ) {
276 'pageIdentity' => $title,
278 'result' => $result instanceof
SearchResult ? $result :
null,
279 'redirect' => $redirectSource
295 private function buildResultFromPageInfos( array $pageInfos, array $thumbsAndDesc ): array {
297 foreach ( $pageInfos as $pageInfo ) {
299 'pageIdentity' => $page,
300 'suggestion' => $sugg,
302 'redirect' => $redirect
304 $excerpt = $sugg ? $sugg->getText() : $result->getTextSnippet();
305 $id = ( $page instanceof PageIdentity && $page->canExist() ) ? $page->getId() : 0;
308 'key' => $this->titleFormatter->getPrefixedDBkey( $page ),
309 'title' => $this->titleFormatter->getPrefixedText( $page ),
310 'excerpt' => $excerpt ?:
null,
311 'matched_title' => $redirect ? $this->titleFormatter->getPrefixedText( $redirect ) :
null,
312 'description' => $id > 0 ? $thumbsAndDesc[$id][
'description'] :
null,
313 'thumbnail' => $id > 0 ? $thumbsAndDesc[$id][
'thumbnail'] :
null,
326 private function serializeThumbnail( ?SearchResultThumbnail $thumbnail ): ?array {
327 if ( $thumbnail == null ) {
332 'mimetype' => $thumbnail->getMimeType(),
333 'width' => $thumbnail->getWidth(),
334 'height' => $thumbnail->getHeight(),
335 'duration' => $thumbnail->getDuration(),
336 'url' => $thumbnail->getUrl(),
350 private function buildDescriptionsFromPageIdentities( array $pageIdentities ) {
351 $descriptions = array_fill_keys( array_keys( $pageIdentities ),
null );
353 $this->getHookRunner()->onSearchResultProvideDescription( $pageIdentities, $descriptions );
355 return array_map(
static function ( $description ) {
356 return [
'description' => $description ];
371 private function buildThumbnailsFromPageIdentities( array $pageIdentities ) {
372 $thumbnails = $this->searchResultThumbnailProvider->getThumbnails( $pageIdentities );
373 $thumbnails += array_fill_keys( array_keys( $pageIdentities ),
null );
375 return array_map(
function ( $thumbnail ) {
376 return [
'thumbnail' => $this->serializeThumbnail( $thumbnail ) ];
385 $searchEngine = $this->createSearchEngine();
386 $pageInfos = $this->doSearch( $searchEngine );
389 $pageIdentities = array_reduce(
390 array_values( $pageInfos ),
391 static function ( $realPages, $item ) {
392 $page = $item[
'pageIdentity'];
394 $realPages[$item[
'pageIdentity']->getId()] = $item[
'pageIdentity'];
400 $descriptions = $this->buildDescriptionsFromPageIdentities( $pageIdentities );
401 $thumbs = $this->buildThumbnailsFromPageIdentities( $pageIdentities );
403 $thumbsAndDescriptions = [];
404 foreach ( $descriptions as $pageId => $description ) {
405 $thumbsAndDescriptions[$pageId] = $description + $thumbs[$pageId];
408 $result = $this->buildResultFromPageInfos( $pageInfos, $thumbsAndDescriptions );
410 $response = $this->getResponseFactory()->createJson( [
'pages' => $result ] );
412 if ( $this->mode === self::COMPLETION_MODE && $this->completionCacheExpiry ) {
416 if ( $this->permissionManager->isEveryoneAllowed(
'read' ) ) {
417 $response->setHeader(
'Cache-Control',
'public, max-age=' . $this->completionCacheExpiry );
419 $response->setHeader(
'Cache-Control',
'no-store, max-age=0' );
429 self::PARAM_SOURCE =>
'query',
430 ParamValidator::PARAM_TYPE =>
'string',
431 ParamValidator::PARAM_REQUIRED =>
true,
434 self::PARAM_SOURCE =>
'query',
435 ParamValidator::PARAM_TYPE =>
'integer',
436 ParamValidator::PARAM_REQUIRED =>
false,
437 ParamValidator::PARAM_DEFAULT => self::LIMIT,
438 IntegerDef::PARAM_MIN => 1,
439 IntegerDef::PARAM_MAX => self::MAX_LIMIT,