* All rights reserved * * This script is part of the TYPO3 project. The TYPO3 project is * free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * The GNU General Public License can be found at * http://www.gnu.org/copyleft/gpl.html. * * This script is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * This copyright notice MUST APPEAR in all copies of the script! ***************************************************************/ use WapplerSystems\Meilisearch\ConnectionManager; use WapplerSystems\Meilisearch\Domain\Search\Query\QueryBuilder; use WapplerSystems\Meilisearch\Domain\Search\Query\SuggestQuery; use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Result\SearchResult; use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Result\SearchResultCollection; use WapplerSystems\Meilisearch\Domain\Search\ResultSet\SearchResultSet; use WapplerSystems\Meilisearch\Domain\Search\ResultSet\SearchResultSetService; use WapplerSystems\Meilisearch\Domain\Search\SearchRequest; use WapplerSystems\Meilisearch\Search; use WapplerSystems\Meilisearch\System\Configuration\TypoScriptConfiguration; use WapplerSystems\Meilisearch\System\Meilisearch\ParsingUtil; use WapplerSystems\Meilisearch\Util; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController; /** * Class SuggestService * * @author Frans Saris * @author Timo Hund */ class SuggestService { /** * @var TypoScriptFrontendController */ protected $tsfe; /** * @var SearchResultSetService */ protected $searchService; /** * @var TypoScriptConfiguration */ protected $typoScriptConfiguration; /** * @var QueryBuilder */ protected $queryBuilder; /** * SuggestService constructor. * @param TypoScriptFrontendController $tsfe * @param SearchResultSetService $searchResultSetService * @param QueryBuilder|null $queryBuilder */ public function __construct(TypoScriptFrontendController $tsfe, SearchResultSetService $searchResultSetService, TypoScriptConfiguration $typoScriptConfiguration, QueryBuilder $queryBuilder = null) { $this->tsfe = $tsfe; $this->searchService = $searchResultSetService; $this->typoScriptConfiguration = $typoScriptConfiguration; $this->queryBuilder = $queryBuilder ?? GeneralUtility::makeInstance(QueryBuilder::class, /** @scrutinizer ignore-type */ $typoScriptConfiguration); } /** * Build an array structure of the suggestions. * * @param SearchRequest $searchRequest * @param array $additionalFilters * @return array */ public function getSuggestions(SearchRequest $searchRequest, array $additionalFilters = []) : array { $requestId = (int)$this->tsfe->getRequestedId(); $groupList = Util::getFrontendUserGroupsList(); $suggestQuery = $this->queryBuilder->buildSuggestQuery($searchRequest->getRawUserQuery(), $additionalFilters, $requestId, $groupList); $meilisearchSuggestions = $this->getMeilisearchSuggestions($suggestQuery); if ($meilisearchSuggestions === []) { return ['status' => false]; } $maxSuggestions = $this->typoScriptConfiguration->getSuggestNumberOfSuggestions(); $showTopResults = $this->typoScriptConfiguration->getSuggestShowTopResults(); $suggestions = $this->getSuggestionArray($suggestQuery, $meilisearchSuggestions, $maxSuggestions); if (!$showTopResults) { return $this->getResultArray($searchRequest, $suggestions, [], false); } return $this->addTopResultsToSuggestions($searchRequest, $suggestions, $additionalFilters); } /** * Determines the top results and adds them to the suggestions. * * @param SearchRequest $searchRequest * @param array $suggestions * @param array $additionalFilters * @return array */ protected function addTopResultsToSuggestions(SearchRequest $searchRequest, $suggestions, array $additionalFilters) : array { $maxDocuments = $this->typoScriptConfiguration->getSuggestNumberOfTopResults(); // perform the current search. $searchRequest->setResultsPerPage($maxDocuments); $searchRequest->setAdditionalFilters($additionalFilters); $didASecondSearch = false; $documents = []; $searchResultSet = $this->doASearch($searchRequest); $results = $searchResultSet->getSearchResults(); if (count($results) > 0) { $documents = $this->addDocumentsWhenLimitNotReached($documents, $results, $maxDocuments); } $suggestionKeys = array_keys($suggestions); $bestSuggestion = reset($suggestionKeys); $bestSuggestionRequest = $searchRequest->getCopyForSubRequest(); $bestSuggestionRequest->setRawQueryString($bestSuggestion); $bestSuggestionRequest->setResultsPerPage($maxDocuments); $bestSuggestionRequest->setAdditionalFilters($additionalFilters); // No results found, use first proposed suggestion to perform the search if (count($documents) === 0 && !empty($suggestions) && ($searchResultSet = $this->doASearch($bestSuggestionRequest)) && count($searchResultSet->getSearchResults()) > 0) { $didASecondSearch = true; $documentsToAdd = $searchResultSet->getSearchResults(); $documents = $this->addDocumentsWhenLimitNotReached($documents, $documentsToAdd, $maxDocuments); } return $this->getResultArray($searchRequest, $suggestions, $documents, $didASecondSearch); } /** * Retrieves the suggestions from the meilisearch server. * * @param SuggestQuery $suggestQuery * @return array */ protected function getMeilisearchSuggestions(SuggestQuery $suggestQuery) : array { $pageId = $this->tsfe->getRequestedId(); $languageId = Util::getLanguageUid(); $meilisearch = GeneralUtility::makeInstance(ConnectionManager::class)->getConnectionByPageId($pageId, $languageId); $search = GeneralUtility::makeInstance(Search::class, /** @scrutinizer ignore-type */ $meilisearch); $response = $search->search($suggestQuery, 0, 0); $rawResponse = $response->getRawResponse(); $results = json_decode($rawResponse); $suggestConfig = $this->typoScriptConfiguration->getObjectByPath('plugin.tx_meilisearch.suggest.'); $facetSuggestions = $results->facet_counts->facet_fields->{$suggestConfig['suggestField']}; $facetSuggestions = ParsingUtil::getMapArrayFromFlatArray($facetSuggestions); return $facetSuggestions ?? []; } /** * Extracts the suggestions from meilisearch as array. * * @param SuggestQuery $suggestQuery * @param array $meilisearchSuggestions * @param integer $maxSuggestions * @return array */ protected function getSuggestionArray(SuggestQuery $suggestQuery, $meilisearchSuggestions, $maxSuggestions) : array { $queryString = $suggestQuery->getQuery(); $suggestionCount = 0; $suggestions = []; foreach ($meilisearchSuggestions as $string => $count) { $suggestion = trim($queryString . ' ' . $string); $suggestions[$suggestion] = $count; $suggestionCount++; if ($suggestionCount === $maxSuggestions) { return $suggestions; } } return $suggestions; } /** * Adds documents from a collection to the result collection as soon as the limit is not reached. * * @param array $documents * @param SearchResultCollection $documentsToAdd * @param integer $maxDocuments * @return array */ protected function addDocumentsWhenLimitNotReached(array $documents, SearchResultCollection $documentsToAdd, int $maxDocuments) : array { $additionalTopResultsFields = $this->typoScriptConfiguration->getSuggestAdditionalTopResultsFields(); /** @var SearchResult $document */ foreach ($documentsToAdd as $document) { $documents[] = $this->getDocumentAsArray($document, $additionalTopResultsFields); if (count($documents) >= $maxDocuments) { return $documents; } } return $documents; } /** * Creates an array representation of the result and returns it. * * @param SearchResult $document * @param array $additionalTopResultsFields * @return array */ protected function getDocumentAsArray(SearchResult $document, $additionalTopResultsFields = []) : array { $fields = [ 'link' => $document->getUrl(), 'type' => $document['type_stringS'] ? $document['type_stringS'] : $document->getType(), 'title' => $document->getTitle(), 'content' => $document->getContent(), 'group' => $document->getHasGroupItem() ? $document->getGroupItem()->getGroupValue() : '', 'previewImage' => $document['image'] ? $document['image'] : '', ]; foreach ($additionalTopResultsFields as $additionalTopResultsField) { $fields[$additionalTopResultsField] = $document[$additionalTopResultsField] ? $document[$additionalTopResultsField] : ''; } return $fields; } /** * Runs a search and returns the results. * * @param SearchRequest $searchRequest * @return \WapplerSystems\Meilisearch\Domain\Search\ResultSet\SearchResultSet */ protected function doASearch($searchRequest) : SearchResultSet { return $this->searchService->search($searchRequest); } /** * Creates an result array with the required fields. * * @param SearchRequest $searchRequest * @param array $suggestions * @param array $documents * @param boolean $didASecondSearch * @return array */ protected function getResultArray(SearchRequest $searchRequest, $suggestions, $documents, $didASecondSearch) : array { return ['suggestions' => $suggestions, 'suggestion' => $searchRequest->getRawUserQuery(), 'documents' => $documents, 'didSecondSearch' => $didASecondSearch]; } }