SearchResultSetService::handleSearchHook()   A
last analyzed

Complexity

Conditions 4
Paths 4

Size

Total Lines 14
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 4

Importance

Changes 0
Metric Value
eloc 7
dl 0
loc 14
rs 10
c 0
b 0
f 0
ccs 5
cts 5
cp 1
cc 4
nc 4
nop 2
crap 4
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 56
    /**
110
     * @param TypoScriptConfiguration $configuration
111 56
     * @param Search $search
112 56
     * @param SolrLogManager $solrLogManager
113 56
     * @param SearchResultBuilder $resultBuilder
114 56
     * @param QueryBuilder $queryBuilder
115 56
     */
116 56
    public function __construct(TypoScriptConfiguration $configuration, Search $search, SolrLogManager $solrLogManager = null, SearchResultBuilder $resultBuilder = null, QueryBuilder $queryBuilder = null)
117
    {
118
        $this->search = $search;
119
        $this->typoScriptConfiguration = $configuration;
120
        $this->logger = $solrLogManager ?? GeneralUtility::makeInstance(SolrLogManager::class, /** @scrutinizer ignore-type */ __CLASS__);
121
        $this->searchResultBuilder = $resultBuilder ?? GeneralUtility::makeInstance(SearchResultBuilder::class);
122
        $this->queryBuilder = $queryBuilder ?? GeneralUtility::makeInstance(QueryBuilder::class, /** @scrutinizer ignore-type */ $configuration, /** @scrutinizer ignore-type */ $solrLogManager);
123
    }
124
125
    /**
126
     * @param ObjectManagerInterface $objectManager
127
     */
128
    public function injectObjectManager(ObjectManagerInterface $objectManager)
129
    {
130
        $this->objectManager = $objectManager;
131
    }
132
133 2
    /**
134
     * @param bool $useCache
135 2
     * @return bool
136
     */
137
    public function getIsSolrAvailable($useCache = true)
138
    {
139
        $this->isSolrAvailable = $this->search->ping($useCache);
140
        return $this->isSolrAvailable;
141
    }
142 45
143
    /**
144 45
     * Retrieves the used search instance.
145
     *
146 45
     * @return Search
147
     */
148 40
    public function getSearch()
149
    {
150 40
        return $this->search;
151 40
    }
152
153
    /**
154 40
     * @param Query $query
155 39
     * @param SearchRequest $searchRequest
156
     */
157
    protected function initializeRegisteredSearchComponents(Query $query, SearchRequest $searchRequest)
158 40
    {
159
        $searchComponents = $this->getRegisteredSearchComponents();
160 45
161
        foreach ($searchComponents as $searchComponent) {
162
            /** @var Search\SearchComponent $searchComponent */
163
            $searchComponent->setSearchConfiguration($this->typoScriptConfiguration->getSearchConfiguration());
164
165 49
            if ($searchComponent instanceof QueryAware) {
166
                $searchComponent->setQuery($query);
167 49
            }
168 49
169
            if ($searchComponent instanceof SearchRequestAware) {
170
                $searchComponent->setSearchRequest($searchRequest);
171
            }
172
173
            $searchComponent->initializeSearchComponent();
174
        }
175
    }
176
177 49
    /**
178
     * @return string
179 49
     */
180 49
    protected function getResultSetClassName()
181
    {
182 49
        return isset($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr']['searchResultSetClassName ']) ?
183 49
            $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr']['searchResultSetClassName '] : SearchResultSet::class;
184 4
    }
185 4
186
    /**
187
     * Performs a search and returns a SearchResultSet.
188 45
     *
189 45
     * @param SearchRequest $searchRequest
190 45
     * @return SearchResultSet
191
     */
192
    public function search(SearchRequest $searchRequest)
193 45
    {
194 45
        $resultSet = $this->getInitializedSearchResultSet($searchRequest);
195
        $this->lastResultSet = $resultSet;
196 44
197
        $resultSet = $this->handleSearchHook('beforeSearch', $resultSet);
198
        if ($this->shouldReturnEmptyResultSetWithoutExecutedSearch($searchRequest)) {
199 2
            $resultSet->setHasSearched(false);
200
            return $resultSet;
201
        }
202 44
203 44
        $query = $this->queryBuilder->buildSearchQuery($searchRequest->getRawUserQuery(), (int)$searchRequest->getResultsPerPage(), $searchRequest->getAdditionalFilters());
204
        $this->initializeRegisteredSearchComponents($query, $searchRequest);
205 44
        $resultSet->setUsedQuery($query);
206
207 44
        // performing the actual search, sending the query to the Solr server
208
        $query = $this->modifyQuery($query, $searchRequest, $this->search);
209
        $response = $this->doASearch($query, $searchRequest);
210 44
211 44
        if ((int)$searchRequest->getResultsPerPage() === 0) {
212 44
            // when resultPerPage was forced to 0 we also set the numFound to 0 to hide results, e.g.
213 44
            // when results for the initial search should not be shown.
214
            // @extensionScannerIgnoreLine
215 44
            $response->response->numFound = 0;
216
        }
217
218 44
        $resultSet->setHasSearched(true);
219 44
        $resultSet->setResponse($response);
220
221 44
        $this->getParsedSearchResults($resultSet);
222
223 44
        $resultSet->setUsedAdditionalFilters($this->queryBuilder->getAdditionalFilters());
224
225
        /** @var $variantsProcessor VariantsProcessor */
226
        $variantsProcessor = GeneralUtility::makeInstance(
227
            VariantsProcessor::class,
228
            /** @scrutinizer ignore-type */ $this->typoScriptConfiguration,
229
            /** @scrutinizer ignore-type */ $this->searchResultBuilder
230
        );
231 44
        $variantsProcessor->process($resultSet);
232
233
        /** @var $searchResultReconstitutionProcessor ResultSetReconstitutionProcessor */
234 44
        $searchResultReconstitutionProcessor = GeneralUtility::makeInstance(ResultSetReconstitutionProcessor::class);
235 44
        $searchResultReconstitutionProcessor->process($resultSet);
236 44
237 44
        $resultSet = $this->getAutoCorrection($resultSet);
238
239
        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 49
     */
247
    protected function getParsedSearchResults($resultSet)
248 49
    {
249
        /** @var ResultParserRegistry $parserRegistry */
250 3
        $parserRegistry = GeneralUtility::makeInstance(ResultParserRegistry::class, /** @scrutinizer ignore-type */ $this->typoScriptConfiguration);
251
        $useRawDocuments = (bool)$this->typoScriptConfiguration->getValueByPathOrDefaultValue('plugin.tx_solr.features.useRawDocuments', false);
252
        $parserRegistry->getParser($resultSet)->parse($resultSet, $useRawDocuments);
253 46
    }
254
255 1
    /**
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 45
     *
259
     * @param SearchRequest $searchRequest
260
     * @return bool
261
     */
262
    protected function shouldReturnEmptyResultSetWithoutExecutedSearch(SearchRequest $searchRequest)
263
    {
264
        if ($searchRequest->getRawUserQueryIsNull() && !$this->getInitialSearchIsConfigured()) {
265
            // when no rawQuery was passed or no initialSearch is configured, we pass an empty result set
266
            return true;
267 49
        }
268
269
        if ($searchRequest->getRawUserQueryIsEmptyString() && !$this->typoScriptConfiguration->getSearchQueryAllowEmptyQuery()) {
270 49
            // the user entered an empty query string "" or "  " and empty querystring is not allowed
271 49
            return true;
272
        }
273 49
274 49
        return false;
275 49
    }
276 49
277 49
    /**
278
     * Initializes the SearchResultSet from the SearchRequest
279
     *
280
     * @param SearchRequest $searchRequest
281
     * @return SearchResultSet
282
     */
283
    protected function getInitializedSearchResultSet(SearchRequest $searchRequest):SearchResultSet
284
    {
285 38
        /** @var $resultSet SearchResultSet */
286
        $resultSetClass = $this->getResultSetClassName();
287 38
        $resultSet = $this->objectManager->get($resultSetClass);
288
289
        $resultSet->setUsedSearchRequest($searchRequest);
290
        $resultSet->setUsedPage((int)$searchRequest->getPage());
291
        $resultSet->setUsedResultsPerPage((int)$searchRequest->getResultsPerPage());
292
        $resultSet->setUsedSearch($this->search);
293
        return $resultSet;
294
    }
295
296
    /**
297 45
     * Executes the search and builds a fake response for a current bug in Apache Solr 6.3
298
     *
299
     * @param Query $query
300 45
     * @param SearchRequest $searchRequest
301 45
     * @return ResponseAdapter
302
     */
303 45
    protected function doASearch($query, $searchRequest): ResponseAdapter
304 44
    {
305
        // the offset mulitplier is page - 1 but not less then zero
306
        $offsetMultiplier = max(0, $searchRequest->getPage() - 1);
307
        $offSet = $offsetMultiplier * (int)$searchRequest->getResultsPerPage();
308 44
309
        $response = $this->search->search($query, $offSet, null);
310
        if($response === null) {
311
            throw new SolrIncompleteResponseException('The response retrieved from solr was incomplete', 1505989678);
312
        }
313
314
        return $response;
315 44
    }
316
317
    /**
318 44
     * @param SearchResultSet $searchResultSet
319 43
     * @return SearchResultSet
320
     */
321
    protected function getAutoCorrection(SearchResultSet $searchResultSet)
322
    {
323 1
        // no secondary search configured
324 1
        if (!$this->typoScriptConfiguration->getSearchSpellcheckingSearchUsingSpellCheckerSuggestion()) {
325
            return $searchResultSet;
326
        }
327
328 1
        // more then zero results
329
        if ($searchResultSet->getAllResultCount() > 0) {
330
            return $searchResultSet;
331
        }
332 1
333
        // no corrections present
334 1
        if (!$searchResultSet->getHasSpellCheckingSuggestions()) {
335
            return $searchResultSet;
336
        }
337
338
        $searchResultSet = $this->peformAutoCorrection($searchResultSet);
339
340
        return $searchResultSet;
341 1
    }
342
343 1
    /**
344 1
     * @param SearchResultSet $searchResultSet
345
     * @return SearchResultSet
346 1
     */
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 1
358 1
            $correction = $suggestion->getSuggestion();
359 1
            $initialQuery = $searchRequest->getRawUserQuery();
360 1
361 1
            $searchRequest->setRawQueryString($correction);
362
            $searchResultSet = $this->search($searchRequest);
363
            if ($searchResultSet->getAllResultCount() > 0) {
364
                $searchResultSet->setIsAutoCorrected(true);
365
                $searchResultSet->setCorrectedQueryString($correction);
366
                $searchResultSet->setInitialQueryString($initialQuery);
367
                break;
368 1
            }
369
370
            if ($runs > $maximumRuns) {
371
                break;
372
            }
373
        }
374
        return $searchResultSet;
375
    }
376
377
    /**
378
     * Allows to modify a query before eventually handing it over to Solr.
379
     *
380 45
     * @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 45
     * @throws \UnexpectedValueException
384 39
     * @return Query The modified query that is actually going to be given to Solr.
385 39
     */
386
    protected function modifyQuery(Query $query, SearchRequest $searchRequest, Search $search)
387 39
    {
388 39
        // hook to modify the search query
389
        if (is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr']['modifySearchQuery'])) {
390
            foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr']['modifySearchQuery'] as $classReference) {
391
                $queryModifier = $this->objectManager->get($classReference);
392 39
393 35
                if ($queryModifier instanceof Modifier) {
394
                    if ($queryModifier instanceof SearchAware) {
395
                        $queryModifier->setSearch($search);
396 39
                    }
397
398
                    if ($queryModifier instanceof SearchRequestAware) {
399
                        $queryModifier->setSearchRequest($searchRequest);
400 39
                    }
401
402
                    $query = $queryModifier->modifyQuery($query);
403
                } else {
404
                    throw new \UnexpectedValueException(
405
                        get_class($queryModifier) . ' must implement interface ' . Modifier::class,
406 45
                        1310387414
407
                    );
408
                }
409
            }
410
        }
411
412
        return $query;
413
    }
414
415 3
    /**
416
     * Retrieves a single document from solr by document id.
417
     *
418 3
     * @param string $documentId
419 3
     * @return SearchResult
420 2
     */
421 2
    public function getDocumentById($documentId)
422
    {
423 2
        /* @var $query SearchQuery */
424
        $query = $this->queryBuilder->newSearchQuery($documentId)->useQueryFields(QueryFields::fromString('id'))->getQuery();
425
        $response = $this->search->search($query, 0, 1);
426
        $parsedData = $response->getParsedData();
427 2
        // @extensionScannerIgnoreLine
428
        $resultDocument = isset($parsedData->response->docs[0]) ? $parsedData->response->docs[0] : null;
429
430
        if (!$resultDocument instanceof Document) {
431
            throw new \UnexpectedValueException("Response did not contain a valid Document object");
432
        }
433
434
        return $this->searchResultBuilder->fromApacheSolrDocument($resultDocument);
435
    }
436
437 49
    /**
438
     * This method is used to call the registered hooks during the search execution.
439 49
     *
440 49
     * @param string $eventName
441
     * @param SearchResultSet $resultSet
442
     * @return SearchResultSet
443 34
     */
444 34
    private function handleSearchHook($eventName, SearchResultSet $resultSet)
445 34
    {
446 34
        if (!is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr'][$eventName])) {
447
            return $resultSet;
448
        }
449
450 34
        foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr'][$eventName] as $classReference) {
451
            $afterSearchProcessor = $this->objectManager->get($classReference);
452
            if ($afterSearchProcessor instanceof SearchResultSetProcessor) {
453
                $afterSearchProcessor->process($resultSet);
454
            }
455
        }
456 34
457
        return $resultSet;
458 34
    }
459
460
    /**
461
     * @return SearchResultSet
462
     */
463
    public function getLastResultSet()
464
    {
465
        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 7
481
        return $wasEmptyQueryString;
482 7
    }
483
484
    /**
485
     * @return bool
486
     */
487
    protected function getInitialSearchIsConfigured()
488 39
    {
489
        return $this->typoScriptConfiguration->getSearchInitializeWithEmptyQuery() || $this->typoScriptConfiguration->getSearchShowResultsOfInitialEmptyQuery() || $this->typoScriptConfiguration->getSearchInitializeWithQuery() || $this->typoScriptConfiguration->getSearchShowResultsOfInitialQuery();
490 39
    }
491
492
    /**
493
     * @return mixed
494
     */
495
    protected function getRegisteredSearchComponents()
496
    {
497
        return GeneralUtility::makeInstance(SearchComponentManager::class)->getSearchComponents();
498
    }
499
}
500