Completed
Push — master ( fb5044...a43c54 )
by Rafael
79:17 queued 75:56
created

SearchResultSetService::getParsedSearchResults()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 3
dl 0
loc 6
ccs 4
cts 4
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
crap 1
1
<?php
2
3
namespace ApacheSolrForTypo3\Solr\Domain\Search\ResultSet;
4
5
/***************************************************************
6
 *  Copyright notice
7
 *
8
 *  (c) 2015-2016 Timo Schmidt <[email protected]>
9
 *  All rights reserved
10
 *
11
 *  This script is part of the TYPO3 project. The TYPO3 project is
12
 *  free software; you can redistribute it and/or modify
13
 *  it under the terms of the GNU General Public License as published by
14
 *  the Free Software Foundation; either version 3 of the License, or
15
 *  (at your option) any later version.
16
 *
17
 *  The GNU General Public License can be found at
18
 *  http://www.gnu.org/copyleft/gpl.html.
19
 *
20
 *  This script is distributed in the hope that it will be useful,
21
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
22
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
23
 *  GNU General Public License for more details.
24
 *
25
 *  This copyright notice MUST APPEAR in all copies of the script!
26
 ***************************************************************/
27
28
use ApacheSolrForTypo3\Solr\Domain\Search\Query\ParameterBuilder\QueryFields;
29
use ApacheSolrForTypo3\Solr\Domain\Search\Query\QueryBuilder;
30
use ApacheSolrForTypo3\Solr\Domain\Search\Query\Query;
31
use ApacheSolrForTypo3\Solr\Domain\Search\Query\SearchQuery;
32
use ApacheSolrForTypo3\Solr\Domain\Search\ResultSet\Result\Parser\ResultParserRegistry;
33
use ApacheSolrForTypo3\Solr\Domain\Search\ResultSet\Result\SearchResult;
34
use ApacheSolrForTypo3\Solr\Domain\Search\ResultSet\Result\SearchResultCollection;
35
use ApacheSolrForTypo3\Solr\Domain\Search\SearchRequest;
36
use ApacheSolrForTypo3\Solr\Domain\Search\SearchRequestAware;
37
use ApacheSolrForTypo3\Solr\Domain\Variants\VariantsProcessor;
38
use ApacheSolrForTypo3\Solr\Query\Modifier\Modifier;
39
use ApacheSolrForTypo3\Solr\Search;
40
use ApacheSolrForTypo3\Solr\Search\QueryAware;
41
use ApacheSolrForTypo3\Solr\Search\SearchAware;
42
use ApacheSolrForTypo3\Solr\Search\SearchComponentManager;
43
use ApacheSolrForTypo3\Solr\System\Configuration\TypoScriptConfiguration;
44
use ApacheSolrForTypo3\Solr\System\Logging\SolrLogManager;
45
use ApacheSolrForTypo3\Solr\System\Solr\Document\Document;
46
use ApacheSolrForTypo3\Solr\System\Solr\ResponseAdapter;
47
use ApacheSolrForTypo3\Solr\System\Solr\SolrIncompleteResponseException;
48
use TYPO3\CMS\Core\Utility\GeneralUtility;
49
use ApacheSolrForTypo3\Solr\Domain\Search\ResultSet\Result\SearchResultBuilder;
50
use TYPO3\CMS\Extbase\Object\ObjectManager;
51
use TYPO3\CMS\Extbase\Object\ObjectManagerInterface;
52
53
/**
54
 * The SearchResultSetService is responsible to build a SearchResultSet from a SearchRequest.
55
 * It encapsulates the logic to trigger a search in order to be able to reuse it in multiple places.
56
 *
57
 * @author Timo Schmidt <[email protected]>
58
 */
59
class SearchResultSetService
60
{
61
62
    /**
63
     * Track, if the number of results per page has been changed by the current request
64
     *
65
     * @var bool
66
     */
67
    protected $resultsPerPageChanged = false;
68
69
    /**
70
     * @var Search
71
     */
72
    protected $search;
73
74
    /**
75
     * @var SearchResultSet
76
     */
77
    protected $lastResultSet = null;
78
79
    /**
80
     * @var boolean
81
     */
82
    protected $isSolrAvailable = false;
83
84
    /**
85
     * @var TypoScriptConfiguration
86
     */
87
    protected $typoScriptConfiguration;
88
89
    /**
90
     * @var SolrLogManager
91
     */
92
    protected $logger = null;
93
94
    /**
95
     * @var SearchResultBuilder
96
     */
97
    protected $searchResultBuilder;
98
99
    /**
100
     * @var QueryBuilder
101
     */
102
    protected $queryBuilder;
103
104
    /**
105
     * @var ObjectManagerInterface
106
     */
107
    protected $objectManager;
108
109
    /**
110
     * @param TypoScriptConfiguration $configuration
111
     * @param Search $search
112
     * @param SolrLogManager $solrLogManager
113
     * @param SearchResultBuilder $resultBuilder
114
     * @param QueryBuilder $queryBuilder
115
     */
116 47
    public function __construct(TypoScriptConfiguration $configuration, Search $search, SolrLogManager $solrLogManager = null, SearchResultBuilder $resultBuilder = null, QueryBuilder $queryBuilder = null)
117
    {
118 47
        $this->search = $search;
119 47
        $this->typoScriptConfiguration = $configuration;
120 47
        $this->logger = $solrLogManager ?? GeneralUtility::makeInstance(SolrLogManager::class, /** @scrutinizer ignore-type */ __CLASS__);
121 47
        $this->searchResultBuilder = $resultBuilder ?? GeneralUtility::makeInstance(SearchResultBuilder::class);
122 47
        $this->queryBuilder = $queryBuilder ?? GeneralUtility::makeInstance(QueryBuilder::class, /** @scrutinizer ignore-type */ $configuration, /** @scrutinizer ignore-type */ $solrLogManager);
123 47
    }
124
125
    /**
126
     * @param ObjectManagerInterface $objectManager
127
     */
128 46
    public function injectObjectManager(ObjectManagerInterface $objectManager)
129
    {
130 46
        $this->objectManager = $objectManager;
131 46
    }
132
133
    /**
134
     * @param bool $useCache
135
     * @return bool
136
     */
137
    public function getIsSolrAvailable($useCache = true)
138
    {
139
        $this->isSolrAvailable = $this->search->ping($useCache);
140
        return $this->isSolrAvailable;
141
    }
142
143
    /**
144
     * Retrieves the used search instance.
145
     *
146
     * @return Search
147
     */
148 2
    public function getSearch()
149
    {
150 2
        return $this->search;
151
    }
152
153
    /**
154
     * @param Query $query
155
     * @param SearchRequest $searchRequest
156
     */
157 40
    protected function initializeRegisteredSearchComponents(Query $query, SearchRequest $searchRequest)
158
    {
159 40
        $searchComponents = $this->getRegisteredSearchComponents();
160
161 40
        foreach ($searchComponents as $searchComponent) {
162
            /** @var Search\SearchComponent $searchComponent */
163 40
            $searchComponent->setSearchConfiguration($this->typoScriptConfiguration->getSearchConfiguration());
164
165 40
            if ($searchComponent instanceof QueryAware) {
166 40
                $searchComponent->setQuery($query);
167
            }
168
169 40
            if ($searchComponent instanceof SearchRequestAware) {
170 40
                $searchComponent->setSearchRequest($searchRequest);
171
            }
172
173 40
            $searchComponent->initializeSearchComponent();
174
        }
175 40
    }
176
177
    /**
178
     * @return string
179
     */
180 41
    protected function getResultSetClassName()
181
    {
182 41
        return isset($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr']['searchResultSetClassName ']) ?
183 41
            $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr']['searchResultSetClassName '] : SearchResultSet::class;
184
    }
185
186
    /**
187
     * Performs a search and returns a SearchResultSet.
188
     *
189
     * @param SearchRequest $searchRequest
190
     * @return SearchResultSet
191
     */
192 41
    public function search(SearchRequest $searchRequest)
193
    {
194 41
        $resultSet = $this->getInitializedSearchResultSet($searchRequest);
195 41
        $this->lastResultSet = $resultSet;
196
197 41
        $resultSet = $this->handleSearchHook('beforeSearch', $resultSet);
198 41
        if ($this->shouldReturnEmptyResultSetWithoutExecutedSearch($searchRequest)) {
199 1
            $resultSet->setHasSearched(false);
200 1
            return $resultSet;
201
        }
202
203 40
        $query = $this->queryBuilder->buildSearchQuery($searchRequest->getRawUserQuery(), (int)$searchRequest->getResultsPerPage(), $searchRequest->getAdditionalFilters());
204 40
        $this->initializeRegisteredSearchComponents($query, $searchRequest);
205 40
        $resultSet->setUsedQuery($query);
206
207
        // performing the actual search, sending the query to the Solr server
208 40
        $query = $this->modifyQuery($query, $searchRequest, $this->search);
209 40
        $response = $this->doASearch($query, $searchRequest);
210
211 39
        if ((int)$searchRequest->getResultsPerPage() === 0) {
212
            // when resultPerPage was forced to 0 we also set the numFound to 0 to hide results, e.g.
213
            // when results for the initial search should not be shown.
214
            // @extensionScannerIgnoreLine
215 2
            $response->response->numFound = 0;
216
        }
217
218 39
        $resultSet->setHasSearched(true);
219 39
        $resultSet->setResponse($response);
220
221 39
        $this->getParsedSearchResults($resultSet);
222
223 39
        $resultSet->setUsedAdditionalFilters($this->queryBuilder->getAdditionalFilters());
224
225
        /** @var $variantsProcessor VariantsProcessor */
226 39
        $variantsProcessor = GeneralUtility::makeInstance(
227 39
            VariantsProcessor::class,
228 39
            /** @scrutinizer ignore-type */ $this->typoScriptConfiguration,
229 39
            /** @scrutinizer ignore-type */ $this->searchResultBuilder
230
        );
231 39
        $variantsProcessor->process($resultSet);
232
233
        /** @var $searchResultReconstitutionProcessor ResultSetReconstitutionProcessor */
234 39
        $searchResultReconstitutionProcessor = GeneralUtility::makeInstance(ResultSetReconstitutionProcessor::class);
235 39
        $searchResultReconstitutionProcessor->process($resultSet);
236
237 39
        $resultSet = $this->getAutoCorrection($resultSet);
238
239 39
        return $this->handleSearchHook('afterSearch', $resultSet);
240
    }
241
242
    /**
243
     * Uses the configured parser and retrieves the parsed search resutls.
244
     *
245
     * @param SearchResultSet $resultSet
246
     */
247 39
    protected function getParsedSearchResults($resultSet)
248
    {
249
        /** @var ResultParserRegistry $parserRegistry */
250 39
        $parserRegistry = GeneralUtility::makeInstance(ResultParserRegistry::class, /** @scrutinizer ignore-type */ $this->typoScriptConfiguration);
251 39
        $useRawDocuments = (bool)$this->typoScriptConfiguration->getValueByPathOrDefaultValue('plugin.tx_solr.features.useRawDocuments', false);
252 39
        $parserRegistry->getParser($resultSet)->parse($resultSet, $useRawDocuments);
253 39
    }
254
255
    /**
256
     * Evaluates conditions on the request and configuration and returns true if no search should be triggered and an empty
257
     * SearchResultSet should be returned.
258
     *
259
     * @param SearchRequest $searchRequest
260
     * @return bool
261
     */
262 41
    protected function shouldReturnEmptyResultSetWithoutExecutedSearch(SearchRequest $searchRequest)
263
    {
264 41
        if ($searchRequest->getRawUserQueryIsNull() && !$this->getInitialSearchIsConfigured()) {
265
            // when no rawQuery was passed or no initialSearch is configured, we pass an empty result set
266 1
            return true;
267
        }
268
269 40
        if ($searchRequest->getRawUserQueryIsEmptyString() && !$this->typoScriptConfiguration->getSearchQueryAllowEmptyQuery()) {
270
            // the user entered an empty query string "" or "  " and empty querystring is not allowed
271
            return true;
272
        }
273
274 40
        return false;
275
    }
276
277
    /**
278
     * Initializes the SearchResultSet from the SearchRequest
279
     *
280
     * @param SearchRequest $searchRequest
281
     * @return SearchResultSet
282
     */
283 41
    protected function getInitializedSearchResultSet(SearchRequest $searchRequest):SearchResultSet
284
    {
285
        /** @var $resultSet SearchResultSet */
286 41
        $resultSetClass = $this->getResultSetClassName();
287 41
        $resultSet = $this->objectManager->get($resultSetClass);
0 ignored issues
show
Deprecated Code introduced by
The function TYPO3\CMS\Extbase\Object...ManagerInterface::get() has been deprecated: since TYPO3 10.4, will be removed in version 12.0 ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

287
        $resultSet = /** @scrutinizer ignore-deprecated */ $this->objectManager->get($resultSetClass);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
288
289 41
        $resultSet->setUsedSearchRequest($searchRequest);
290 41
        $resultSet->setUsedPage((int)$searchRequest->getPage());
291 41
        $resultSet->setUsedResultsPerPage((int)$searchRequest->getResultsPerPage());
292 41
        $resultSet->setUsedSearch($this->search);
293 41
        return $resultSet;
294
    }
295
296
    /**
297
     * Executes the search and builds a fake response for a current bug in Apache Solr 6.3
298
     *
299
     * @param Query $query
300
     * @param SearchRequest $searchRequest
301
     * @return ResponseAdapter
302
     */
303 40
    protected function doASearch($query, $searchRequest): ResponseAdapter
304
    {
305
        // the offset mulitplier is page - 1 but not less then zero
306 40
        $offsetMultiplier = max(0, $searchRequest->getPage() - 1);
307 40
        $offSet = $offsetMultiplier * (int)$searchRequest->getResultsPerPage();
308
309 40
        $response = $this->search->search($query, $offSet, null);
310 39
        if($response === null) {
311
            throw new SolrIncompleteResponseException('The response retrieved from solr was incomplete', 1505989678);
312
        }
313
314 39
        return $response;
315
    }
316
317
    /**
318
     * @param SearchResultSet $searchResultSet
319
     * @return SearchResultSet
320
     */
321 39
    protected function getAutoCorrection(SearchResultSet $searchResultSet)
322
    {
323
        // no secondary search configured
324 39
        if (!$this->typoScriptConfiguration->getSearchSpellcheckingSearchUsingSpellCheckerSuggestion()) {
325 38
            return $searchResultSet;
326
        }
327
328
        // more then zero results
329 1
        if ($searchResultSet->getAllResultCount() > 0) {
330 1
            return $searchResultSet;
331
        }
332
333
        // no corrections present
334 1
        if (!$searchResultSet->getHasSpellCheckingSuggestions()) {
335
            return $searchResultSet;
336
        }
337
338 1
        $searchResultSet = $this->peformAutoCorrection($searchResultSet);
339
340 1
        return $searchResultSet;
341
    }
342
343
    /**
344
     * @param SearchResultSet $searchResultSet
345
     * @return SearchResultSet
346
     */
347 1
    protected function peformAutoCorrection(SearchResultSet $searchResultSet)
348
    {
349 1
        $searchRequest = $searchResultSet->getUsedSearchRequest();
350 1
        $suggestions = $searchResultSet->getSpellCheckingSuggestions();
351
352 1
        $maximumRuns = $this->typoScriptConfiguration->getSearchSpellcheckingNumberOfSuggestionsToTry(1);
353 1
        $runs = 0;
354
355 1
        foreach ($suggestions as $suggestion) {
356 1
            $runs++;
357
358 1
            $correction = $suggestion->getSuggestion();
359 1
            $initialQuery = $searchRequest->getRawUserQuery();
360
361 1
            $searchRequest->setRawQueryString($correction);
362 1
            $searchResultSet = $this->search($searchRequest);
363 1
            if ($searchResultSet->getAllResultCount() > 0) {
364 1
                $searchResultSet->setIsAutoCorrected(true);
365 1
                $searchResultSet->setCorrectedQueryString($correction);
366 1
                $searchResultSet->setInitialQueryString($initialQuery);
367 1
                break;
368
            }
369
370
            if ($runs > $maximumRuns) {
371
                break;
372
            }
373
        }
374 1
        return $searchResultSet;
375
    }
376
377
    /**
378
     * Allows to modify a query before eventually handing it over to Solr.
379
     *
380
     * @param Query $query The current query before it's being handed over to Solr.
381
     * @param SearchRequest $searchRequest The searchRequest, relevant in the current context
382
     * @param Search $search The search, relevant in the current context
383
     * @throws \UnexpectedValueException
384
     * @return Query The modified query that is actually going to be given to Solr.
385
     */
386 40
    protected function modifyQuery(Query $query, SearchRequest $searchRequest, Search $search)
387
    {
388
        // hook to modify the search query
389 40
        if (is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr']['modifySearchQuery'])) {
390 40
            foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr']['modifySearchQuery'] as $classReference) {
391 40
                $queryModifier = $this->objectManager->get($classReference);
0 ignored issues
show
Deprecated Code introduced by
The function TYPO3\CMS\Extbase\Object...ManagerInterface::get() has been deprecated: since TYPO3 10.4, will be removed in version 12.0 ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

391
                $queryModifier = /** @scrutinizer ignore-deprecated */ $this->objectManager->get($classReference);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
392
393 40
                if ($queryModifier instanceof Modifier) {
394 40
                    if ($queryModifier instanceof SearchAware) {
395
                        $queryModifier->setSearch($search);
396
                    }
397
398 40
                    if ($queryModifier instanceof SearchRequestAware) {
399 36
                        $queryModifier->setSearchRequest($searchRequest);
400
                    }
401
402 40
                    $query = $queryModifier->modifyQuery($query);
403
                } else {
404
                    throw new \UnexpectedValueException(
405
                        get_class($queryModifier) . ' must implement interface ' . Modifier::class,
406
                        1310387414
407
                    );
408
                }
409
            }
410
        }
411
412 40
        return $query;
413
    }
414
415
    /**
416
     * Retrieves a single document from solr by document id.
417
     *
418
     * @param string $documentId
419
     * @return SearchResult
420
     */
421 3
    public function getDocumentById($documentId)
422
    {
423
        /* @var $query SearchQuery */
424 3
        $query = $this->queryBuilder->newSearchQuery($documentId)->useQueryFields(QueryFields::fromString('id'))->getQuery();
425 3
        $response = $this->search->search($query, 0, 1);
426 2
        $parsedData = $response->getParsedData();
427
        // @extensionScannerIgnoreLine
428 2
        $resultDocument = isset($parsedData->response->docs[0]) ? $parsedData->response->docs[0] : null;
429
430 2
        if (!$resultDocument instanceof Document) {
431
            throw new \UnexpectedValueException("Response did not contain a valid Document object");
432
        }
433
434 2
        return $this->searchResultBuilder->fromApacheSolrDocument($resultDocument);
435
    }
436
437
    /**
438
     * This method is used to call the registered hooks during the search execution.
439
     *
440
     * @param string $eventName
441
     * @param SearchResultSet $resultSet
442
     * @return SearchResultSet
443
     */
444 41
    private function handleSearchHook($eventName, SearchResultSet $resultSet)
445
    {
446 41
        if (!is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr'][$eventName])) {
447 41
            return $resultSet;
448
        }
449
450 34
        foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr'][$eventName] as $classReference) {
451 34
            $afterSearchProcessor = $this->objectManager->get($classReference);
0 ignored issues
show
Deprecated Code introduced by
The function TYPO3\CMS\Extbase\Object...ManagerInterface::get() has been deprecated: since TYPO3 10.4, will be removed in version 12.0 ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

451
            $afterSearchProcessor = /** @scrutinizer ignore-deprecated */ $this->objectManager->get($classReference);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
452 34
            if ($afterSearchProcessor instanceof SearchResultSetProcessor) {
453 34
                $afterSearchProcessor->process($resultSet);
454
            }
455
        }
456
457 34
        return $resultSet;
458
    }
459
460
    /**
461
     * @return SearchResultSet
462
     */
463 34
    public function getLastResultSet()
464
    {
465 34
        return $this->lastResultSet;
466
    }
467
468
    /**
469
     * This method returns true when the last search was executed with an empty query
470
     * string or whitespaces only. When no search was triggered it will return false.
471
     *
472
     * @return bool
473
     */
474
    public function getLastSearchWasExecutedWithEmptyQueryString()
475
    {
476
        $wasEmptyQueryString = false;
477
        if ($this->lastResultSet != null) {
478
            $wasEmptyQueryString = $this->lastResultSet->getUsedSearchRequest()->getRawUserQueryIsEmptyString();
479
        }
480
481
        return $wasEmptyQueryString;
482
    }
483
484
    /**
485
     * @return bool
486
     */
487 5
    protected function getInitialSearchIsConfigured()
488
    {
489 5
        return $this->typoScriptConfiguration->getSearchInitializeWithEmptyQuery() || $this->typoScriptConfiguration->getSearchShowResultsOfInitialEmptyQuery() || $this->typoScriptConfiguration->getSearchInitializeWithQuery() || $this->typoScriptConfiguration->getSearchShowResultsOfInitialQuery();
490
    }
491
492
    /**
493
     * @return mixed
494
     */
495 40
    protected function getRegisteredSearchComponents()
496
    {
497 40
        return GeneralUtility::makeInstance(SearchComponentManager::class)->getSearchComponents();
498
    }
499
}
500