Failed Conditions
Push — release-11.5.x ( 71e6eb...3bfdb1 )
by Markus
27:37
created

SuggestService::getResultArray()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 5
dl 0
loc 11
ccs 6
cts 6
cp 1
rs 10
c 1
b 0
f 0
cc 1
nc 1
nop 4
crap 1
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the TYPO3 CMS project.
7
 *
8
 * It is free software; you can redistribute it and/or modify it under
9
 * the terms of the GNU General Public License, either version 2
10
 * of the License, or any later version.
11
 *
12
 * For the full copyright and license information, please read the
13
 * LICENSE.txt file that was distributed with this source code.
14
 *
15
 * The TYPO3 project - inspiring people to share!
16
 */
17
18
namespace ApacheSolrForTypo3\Solr\Domain\Search\Suggest;
19
20
use ApacheSolrForTypo3\Solr\ConnectionManager;
21
use ApacheSolrForTypo3\Solr\Domain\Search\Query\QueryBuilder;
22
use ApacheSolrForTypo3\Solr\Domain\Search\Query\SuggestQuery;
23
use ApacheSolrForTypo3\Solr\Domain\Search\ResultSet\Facets\InvalidFacetPackageException;
24
use ApacheSolrForTypo3\Solr\Domain\Search\ResultSet\Result\SearchResult;
25
use ApacheSolrForTypo3\Solr\Domain\Search\ResultSet\Result\SearchResultCollection;
26
use ApacheSolrForTypo3\Solr\Domain\Search\ResultSet\SearchResultSet;
27
use ApacheSolrForTypo3\Solr\Domain\Search\ResultSet\SearchResultSetService;
28
use ApacheSolrForTypo3\Solr\Domain\Search\SearchRequest;
29
use ApacheSolrForTypo3\Solr\NoSolrConnectionFoundException;
30
use ApacheSolrForTypo3\Solr\Search;
31
use ApacheSolrForTypo3\Solr\System\Configuration\TypoScriptConfiguration;
32
use ApacheSolrForTypo3\Solr\System\Solr\ParsingUtil;
33
use ApacheSolrForTypo3\Solr\Util;
34
use Exception;
35
use TYPO3\CMS\Core\Context\Exception\AspectNotFoundException;
36
use TYPO3\CMS\Core\Utility\GeneralUtility;
37
use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
38
39
/**
40
 * Class SuggestService
41
 *
42
 * @author Frans Saris <[email protected]>
43
 * @author Timo Hund <[email protected]>
44
 */
45
class SuggestService
46
{
47
    /**
48
     * @var TypoScriptFrontendController
49
     */
50
    protected TypoScriptFrontendController $tsfe;
51
52
    /**
53
     * @var SearchResultSetService
54
     */
55
    protected SearchResultSetService $searchService;
56
57
    /**
58
     * @var TypoScriptConfiguration
59
     */
60
    protected TypoScriptConfiguration $typoScriptConfiguration;
61
62
    /**
63
     * @var QueryBuilder
64
     */
65
    protected QueryBuilder $queryBuilder;
66
67
    /**
68
     * SuggestService constructor.
69
     * @param TypoScriptFrontendController $tsfe
70
     * @param SearchResultSetService $searchResultSetService
71
     * @param TypoScriptConfiguration $typoScriptConfiguration
72
     * @param QueryBuilder|null $queryBuilder
73
     */
74 6
    public function __construct(
75
        TypoScriptFrontendController $tsfe,
76
        SearchResultSetService $searchResultSetService,
77
        TypoScriptConfiguration $typoScriptConfiguration,
78
        QueryBuilder $queryBuilder = null
79
    ) {
80 6
        $this->tsfe = $tsfe;
81 6
        $this->searchService = $searchResultSetService;
82 6
        $this->typoScriptConfiguration = $typoScriptConfiguration;
83 6
        $this->queryBuilder = $queryBuilder ?? GeneralUtility::makeInstance(
84 6
            QueryBuilder::class,
85
            /** @scrutinizer ignore-type */
86 6
            $typoScriptConfiguration
87 6
        );
88
    }
89
90
    /**
91
     * Build an array structure of the suggestions.
92
     *
93
     * @param SearchRequest $searchRequest
94
     * @param array $additionalFilters
95
     * @return array
96
     * @throws AspectNotFoundException
97
     * @throws InvalidFacetPackageException
98
     * @throws NoSolrConnectionFoundException
99
     */
100 6
    public function getSuggestions(SearchRequest $searchRequest, array $additionalFilters = []): array
101
    {
102 6
        $requestId = (int)$this->tsfe->getRequestedId();
103 6
        $groupList = Util::getFrontendUserGroupsList();
104
105 6
        $suggestQuery = $this->queryBuilder->buildSuggestQuery($searchRequest->getRawUserQuery(), $additionalFilters, $requestId, $groupList);
106 6
        $solrSuggestions = $this->getSolrSuggestions($suggestQuery);
107
108 6
        if ($solrSuggestions === []) {
109
            return ['status' => false];
110
        }
111
112 6
        $maxSuggestions = $this->typoScriptConfiguration->getSuggestNumberOfSuggestions();
113 6
        $showTopResults = $this->typoScriptConfiguration->getSuggestShowTopResults();
114 6
        $suggestions    = $this->getSuggestionArray($suggestQuery, $solrSuggestions, $maxSuggestions);
115
116 6
        if (!$showTopResults) {
117
            return $this->getResultArray($searchRequest, $suggestions, [], false);
118
        }
119
120 6
        return $this->addTopResultsToSuggestions($searchRequest, $suggestions, $additionalFilters);
121
    }
122
123
    /**
124
     * Determines the top results and adds them to the suggestions.
125
     *
126
     * @param SearchRequest $searchRequest
127
     * @param array $suggestions
128
     * @param array $additionalFilters
129
     * @return array
130
     * @throws InvalidFacetPackageException
131
     */
132 6
    protected function addTopResultsToSuggestions(SearchRequest $searchRequest, array $suggestions, array $additionalFilters): array
133
    {
134 6
        $maxDocuments = $this->typoScriptConfiguration->getSuggestNumberOfTopResults();
135
136
        // perform the current search.
137 6
        $searchRequest->setResultsPerPage($maxDocuments);
138 6
        $searchRequest->setAdditionalFilters($additionalFilters);
139
140 6
        $didASecondSearch = false;
141 6
        $documents = [];
142
143 6
        $searchResultSet = $this->doASearch($searchRequest);
144 6
        $results = $searchResultSet->getSearchResults();
145 6
        if (count($results) > 0) {
146 2
            $documents = $this->addDocumentsWhenLimitNotReached($documents, $results, $maxDocuments);
147
        }
148
149 6
        $suggestionKeys = array_keys($suggestions);
150 6
        $bestSuggestion = (string)reset($suggestionKeys);
151 6
        $bestSuggestionRequest = $searchRequest->getCopyForSubRequest();
152 6
        $bestSuggestionRequest->setRawQueryString($bestSuggestion);
153 6
        $bestSuggestionRequest->setResultsPerPage($maxDocuments);
154 6
        $bestSuggestionRequest->setAdditionalFilters($additionalFilters);
155
156
        // No results found, use first proposed suggestion to perform the search
157 6
        if (count($documents) === 0 && !empty($suggestions) && ($searchResultSet = $this->doASearch($bestSuggestionRequest)) && count($searchResultSet->getSearchResults()) > 0) {
158 4
            $didASecondSearch = true;
159 4
            $documentsToAdd = $searchResultSet->getSearchResults();
160 4
            $documents = $this->addDocumentsWhenLimitNotReached($documents, $documentsToAdd, $maxDocuments);
161
        }
162
163 6
        return $this->getResultArray($searchRequest, $suggestions, $documents, $didASecondSearch);
164
    }
165
166
    /**
167
     * Retrieves the suggestions from the solr server.
168
     *
169
     * @param SuggestQuery $suggestQuery
170
     * @return array
171
     * @throws NoSolrConnectionFoundException
172
     * @throws AspectNotFoundException
173
     * @throws Exception
174
     */
175 6
    protected function getSolrSuggestions(SuggestQuery $suggestQuery): array
176
    {
177 6
        $pageId = $this->tsfe->getRequestedId();
178 6
        $languageId = Util::getLanguageUid();
179 6
        $solr = GeneralUtility::makeInstance(ConnectionManager::class)->getConnectionByPageId($pageId, $languageId);
180 6
        $search = GeneralUtility::makeInstance(Search::class, /** @scrutinizer ignore-type */ $solr);
181 6
        $response = $search->search($suggestQuery, 0, 0);
182
183 6
        $rawResponse = $response->getRawResponse();
184 6
        if ($rawResponse === null) {
185
            return [];
186
        }
187 6
        $results = json_decode($rawResponse);
188 6
        $suggestConfig = $this->typoScriptConfiguration->getObjectByPath('plugin.tx_solr.suggest.');
189 6
        $facetSuggestions = isset($suggestConfig['suggestField']) ? $results->facet_counts->facet_fields->{$suggestConfig['suggestField']} ?? [] : [];
190 6
        $facetSuggestions = ParsingUtil::getMapArrayFromFlatArray($facetSuggestions);
191
192 6
        return $facetSuggestions ?? [];
193
    }
194
195
    /**
196
     * Extracts the suggestions from solr as array.
197
     *
198
     * @param SuggestQuery $suggestQuery
199
     * @param array $solrSuggestions
200
     * @param int $maxSuggestions
201
     * @return array
202
     */
203 6
    protected function getSuggestionArray(
204
        SuggestQuery $suggestQuery,
205
        array $solrSuggestions,
206
        int $maxSuggestions
207
    ): array {
208 6
        $queryString = $suggestQuery->getQuery();
209 6
        $suggestionCount = 0;
210 6
        $suggestions = [];
211 6
        foreach ($solrSuggestions as $string => $count) {
212 6
            $suggestion = trim($queryString . ' ' . $string);
213 6
            $suggestions[$suggestion] = $count;
214 6
            $suggestionCount++;
215 6
            if ($suggestionCount === $maxSuggestions) {
216
                return $suggestions;
217
            }
218
        }
219
220 6
        return $suggestions;
221
    }
222
223
    /**
224
     * Adds documents from a collection to the result collection as soon as the limit is not reached.
225
     *
226
     * @param array $documents
227
     * @param SearchResultCollection $documentsToAdd
228
     * @param int $maxDocuments
229
     * @return array
230
     */
231 6
    protected function addDocumentsWhenLimitNotReached(array $documents, SearchResultCollection $documentsToAdd, int $maxDocuments): array
232
    {
233 6
        $additionalTopResultsFields = $this->typoScriptConfiguration->getSuggestAdditionalTopResultsFields();
234
        /** @var SearchResult $document */
235 6
        foreach ($documentsToAdd as $document) {
236 6
            $documents[] = $this->getDocumentAsArray($document, $additionalTopResultsFields);
237 6
            if (count($documents) >= $maxDocuments) {
238
                return $documents;
239
            }
240
        }
241
242 6
        return $documents;
243
    }
244
245
    /**
246
     * Creates an array representation of the result and returns it.
247
     *
248
     * @param SearchResult $document
249
     * @param array $additionalTopResultsFields
250
     * @return array
251
     */
252 6
    protected function getDocumentAsArray(SearchResult $document, array $additionalTopResultsFields = []): array
253
    {
254 6
        $fields = [
255 6
            'link' => $document->getUrl(),
256 6
            'type' => $document['type_stringS'] ? $document['type_stringS'] : $document->getType(),
257 6
            'title' => $document->getTitle(),
258 6
            'content' => $document->getContent(),
259 6
            'group' => $document->getHasGroupItem() ? $document->getGroupItem()->getGroupValue() : '',
260 6
            'previewImage' => $document['image'] ? $document['image'] : '',
261 6
        ];
262 6
        foreach ($additionalTopResultsFields as $additionalTopResultsField) {
263
            $fields[$additionalTopResultsField] = $document[$additionalTopResultsField] ? $document[$additionalTopResultsField] : '';
264
        }
265 6
        return $fields;
266
    }
267
268
    /**
269
     * Runs a search and returns the results.
270
     *
271
     * @param SearchRequest $searchRequest
272
     * @return SearchResultSet
273
     * @throws InvalidFacetPackageException
274
     */
275 6
    protected function doASearch(SearchRequest $searchRequest): SearchResultSet
276
    {
277 6
        return $this->searchService->search($searchRequest);
278
    }
279
280
    /**
281
     * Creates a result array with the required fields.
282
     *
283
     * @param SearchRequest $searchRequest
284
     * @param array $suggestions
285
     * @param array $documents
286
     * @param bool $didASecondSearch
287
     * @return array
288
     */
289 6
    protected function getResultArray(
290
        SearchRequest $searchRequest,
291
        array $suggestions,
292
        array $documents,
293
        bool $didASecondSearch
294
    ): array {
295 6
        return [
296 6
            'suggestions' => $suggestions,
297 6
            'suggestion' => $searchRequest->getRawUserQuery(),
298 6
            'documents' => $documents,
299 6
            'didSecondSearch' => $didASecondSearch,
300 6
        ];
301
    }
302
}
303