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
|
|
|
|