277 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			277 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
<?php
 | 
						|
 | 
						|
namespace WapplerSystems\Meilisearch\Domain\Search\Suggest;
 | 
						|
 | 
						|
/***************************************************************
 | 
						|
 *  Copyright notice
 | 
						|
 *
 | 
						|
 *  (c) 2017 Franz Saris <frans.saris@beech.it>
 | 
						|
 *  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 <frans.saris@beech.it>
 | 
						|
 * @author Timo Hund <timo.hund@dkd.de>
 | 
						|
 */
 | 
						|
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];
 | 
						|
    }
 | 
						|
 | 
						|
 | 
						|
}
 |