Passed
Push — master ( 4e40a2...cc3f84 )
by Timo
24:09
created

SearchResultSetService::search()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 47
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 27
CRAP Score 3

Importance

Changes 0
Metric Value
eloc 26
dl 0
loc 47
ccs 27
cts 27
cp 1
rs 9.504
c 0
b 0
f 0
cc 3
nc 3
nop 1
crap 3
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\Query;
30
use ApacheSolrForTypo3\Solr\Domain\Search\Query\QueryBuilder;
31
use ApacheSolrForTypo3\Solr\Domain\Search\ResultSet\Result\Parser\ResultParserRegistry;
32
use ApacheSolrForTypo3\Solr\Domain\Search\ResultSet\Result\SearchResult;
33
use ApacheSolrForTypo3\Solr\Domain\Search\ResultSet\Result\SearchResultCollection;
34
use ApacheSolrForTypo3\Solr\Domain\Search\SearchRequest;
35
use ApacheSolrForTypo3\Solr\Domain\Search\SearchRequestAware;
36
use ApacheSolrForTypo3\Solr\Domain\Variants\VariantsProcessor;
37
use ApacheSolrForTypo3\Solr\Query\Modifier\Modifier;
38
use ApacheSolrForTypo3\Solr\Search;
39
use ApacheSolrForTypo3\Solr\Search\QueryAware;
40
use ApacheSolrForTypo3\Solr\Search\SearchAware;
41
use ApacheSolrForTypo3\Solr\Search\SearchComponentManager;
42
use ApacheSolrForTypo3\Solr\System\Configuration\TypoScriptConfiguration;
43
use ApacheSolrForTypo3\Solr\System\Logging\SolrLogManager;
44
use ApacheSolrForTypo3\Solr\System\Solr\SolrIncompleteResponseException;
45
use TYPO3\CMS\Core\Utility\GeneralUtility;
46
use ApacheSolrForTypo3\Solr\Domain\Search\ResultSet\Result\SearchResultBuilder;
47
48
/**
49
 * The SearchResultSetService is responsible to build a SearchResultSet from a SearchRequest.
50
 * It encapsulates the logic to trigger a search in order to be able to reuse it in multiple places.
51
 *
52
 * @author Timo Schmidt <[email protected]>
53
 */
54
class SearchResultSetService
55
{
56
57
    /**
58
     * Track, if the number of results per page has been changed by the current request
59
     *
60
     * @var bool
61
     */
62
    protected $resultsPerPageChanged = false;
63
64
    /**
65
     * @var Search
66
     */
67
    protected $search;
68
69
    /**
70
     * @var SearchResultSet
71
     */
72
    protected $lastResultSet = null;
73
74
    /**
75
     * @var boolean
76
     */
77
    protected $isSolrAvailable = false;
78
79
    /**
80
     * @var TypoScriptConfiguration
81
     */
82
    protected $typoScriptConfiguration;
83
84
    /**
85
     * @var SolrLogManager;
86
     */
87
    protected $logger = null;
88
89
    /**
90
     * @var SearchResultBuilder
91
     */
92
    protected $searchResultBuilder;
93
94
    /**
95
     * @var QueryBuilder
96
     */
97
    protected $queryBuilder;
98
99
    /**
100
     * @param TypoScriptConfiguration $configuration
101
     * @param Search $search
102
     * @param SolrLogManager $solrLogManager
103
     * @param SearchResultBuilder $resultBuilder
104
     * @param QueryBuilder $queryBuilder
105
     */
106 54
    public function __construct(TypoScriptConfiguration $configuration, Search $search, SolrLogManager $solrLogManager = null, SearchResultBuilder $resultBuilder = null, QueryBuilder $queryBuilder = null)
107
    {
108 54
        $this->search = $search;
109 54
        $this->typoScriptConfiguration = $configuration;
110 54
        $this->logger = $solrLogManager ?? GeneralUtility::makeInstance(SolrLogManager::class, /** @scrutinizer ignore-type */ __CLASS__);
111 54
        $this->searchResultBuilder = $resultBuilder ?? GeneralUtility::makeInstance(SearchResultBuilder::class);
112 54
        $this->queryBuilder = $queryBuilder ?? GeneralUtility::makeInstance(QueryBuilder::class, /** @scrutinizer ignore-type */ $configuration, /** @scrutinizer ignore-type */ $solrLogManager);
113 54
    }
114
115
    /**
116
     * @param bool $useCache
117
     * @return bool
118
     */
119
    public function getIsSolrAvailable($useCache = true)
120
    {
121
        $this->isSolrAvailable = $this->search->ping($useCache);
122
        return $this->isSolrAvailable;
123
    }
124
125
    /**
126
     * Retrieves the used search instance.
127
     *
128
     * @return Search
129
     */
130 2
    public function getSearch()
131
    {
132 2
        return $this->search;
133
    }
134
135
    /**
136
     * @param Query $query
137
     * @param SearchRequest $searchRequest
138
     */
139 43
    protected function initializeRegisteredSearchComponents(Query $query, SearchRequest $searchRequest)
140
    {
141 43
        $searchComponents = $this->getRegisteredSearchComponents();
142
143 43
        foreach ($searchComponents as $searchComponent) {
144
            /** @var Search\SearchComponent $searchComponent */
145 38
            $searchComponent->setSearchConfiguration($this->typoScriptConfiguration->getSearchConfiguration());
146
147 38
            if ($searchComponent instanceof QueryAware) {
148 38
                $searchComponent->setQuery($query);
149
            }
150
151 38
            if ($searchComponent instanceof SearchRequestAware) {
152 37
                $searchComponent->setSearchRequest($searchRequest);
153
            }
154
155 38
            $searchComponent->initializeSearchComponent();
156
        }
157 43
    }
158
159
    /**
160
     * @return string
161
     */
162 47
    protected function getResultSetClassName()
163
    {
164 47
        return isset($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr']['searchResultSetClassName ']) ?
165 47
            $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr']['searchResultSetClassName '] : SearchResultSet::class;
166
    }
167
168
    /**
169
     * Performs a search and returns a SearchResultSet.
170
     *
171
     * @param SearchRequest $searchRequest
172
     * @return SearchResultSet
173
     */
174 47
    public function search(SearchRequest $searchRequest)
175
    {
176 47
        $resultSet = $this->getInitializedSearchResultSet($searchRequest);
177 47
        $this->lastResultSet = $resultSet;
178
179 47
        $resultSet = $this->handleSearchHook('beforeSearch', $resultSet);
180 47
        if ($this->shouldReturnEmptyResultSetWithoutExecutedSearch($searchRequest)) {
181 4
            $resultSet->setHasSearched(false);
182 4
            return $resultSet;
183
        }
184
185 43
        $query = $this->queryBuilder->buildSearchQuery($searchRequest->getRawUserQuery(), (int)$searchRequest->getResultsPerPage(), $searchRequest->getAdditionalFilters());
186 43
        $this->initializeRegisteredSearchComponents($query, $searchRequest);
187 43
        $resultSet->setUsedQuery($query);
188
189
        // performing the actual search, sending the query to the Solr server
190 43
        $query = $this->modifyQuery($query, $searchRequest, $this->search);
191 43
        $response = $this->doASearch($query, $searchRequest);
192
193 42
        if ((int)$searchRequest->getResultsPerPage() === 0) {
194
            // when resultPerPage was forced to 0 we also set the numFound to 0 to hide results, e.g.
195
            // when results for the initial search should not be shown.
196 2
            $response->response->numFound = 0;
197
        }
198
199 42
        $resultSet->setHasSearched(true);
200 42
        $resultSet->setResponse($response);
201
202 42
        $this->getParsedSearchResults($resultSet);
203
204 42
        $resultSet->setUsedAdditionalFilters($this->queryBuilder->getAdditionalFilters());
205
206
        /** @var $variantsProcessor VariantsProcessor */
207 42
        $variantsProcessor = GeneralUtility::makeInstance(
208 42
            VariantsProcessor::class,
209 42
            /** @scrutinizer ignore-type */ $this->typoScriptConfiguration,
210 42
            /** @scrutinizer ignore-type */ $this->searchResultBuilder
211
        );
212 42
        $variantsProcessor->process($resultSet);
213
214
        /** @var $searchResultReconstitutionProcessor ResultSetReconstitutionProcessor */
215 42
        $searchResultReconstitutionProcessor = GeneralUtility::makeInstance(ResultSetReconstitutionProcessor::class);
216 42
        $searchResultReconstitutionProcessor->process($resultSet);
217
218 42
        $resultSet = $this->getAutoCorrection($resultSet);
219
220 42
        return $this->handleSearchHook('afterSearch', $resultSet);
221
    }
222
223
    /**
224
     * Uses the configured parser and retrieves the parsed search resutls.
225
     *
226
     * @param SearchResultSet $resultSet
227
     */
228 42
    protected function getParsedSearchResults($resultSet)
229
    {
230
        /** @var ResultParserRegistry $parserRegistry */
231 42
        $parserRegistry = GeneralUtility::makeInstance(ResultParserRegistry::class, /** @scrutinizer ignore-type */ $this->typoScriptConfiguration);
232 42
        $useRawDocuments = (bool)$this->typoScriptConfiguration->getValueByPathOrDefaultValue('plugin.tx_solr.features.useRawDocuments', false);
233 42
        $parserRegistry->getParser($resultSet)->parse($resultSet, $useRawDocuments);
234 42
    }
235
236
    /**
237
     * Evaluates conditions on the request and configuration and returns true if no search should be triggered and an empty
238
     * SearchResultSet should be returned.
239
     *
240
     * @param SearchRequest $searchRequest
241
     * @return bool
242
     */
243 47
    protected function shouldReturnEmptyResultSetWithoutExecutedSearch(SearchRequest $searchRequest)
244
    {
245 47
        if ($searchRequest->getRawUserQueryIsNull() && !$this->getInitialSearchIsConfigured()) {
246
            // when no rawQuery was passed or no initialSearch is configured, we pass an empty result set
247 3
            return true;
248
        }
249
250 44
        if ($searchRequest->getRawUserQueryIsEmptyString() && !$this->typoScriptConfiguration->getSearchQueryAllowEmptyQuery()) {
251
            // the user entered an empty query string "" or "  " and empty querystring is not allowed
252 1
            return true;
253
        }
254
255 43
        return false;
256
    }
257
258
    /**
259
     * Initializes the SearchResultSet from the SearchRequest
260
     *
261
     * @param SearchRequest $searchRequest
262
     * @return SearchResultSet
263
     */
264 47
    protected function getInitializedSearchResultSet(SearchRequest $searchRequest):SearchResultSet
265
    {
266
        /** @var $resultSet SearchResultSet */
267 47
        $resultSetClass = $this->getResultSetClassName();
268 47
        $resultSet = GeneralUtility::makeInstance($resultSetClass);
269
270 47
        $resultSet->setUsedSearchRequest($searchRequest);
271 47
        $resultSet->setUsedPage((int)$searchRequest->getPage());
272 47
        $resultSet->setUsedResultsPerPage((int)$searchRequest->getResultsPerPage());
273 47
        $resultSet->setUsedSearch($this->search);
274 47
        return $resultSet;
275
    }
276
277
    /**
278
     * Retrieves the configuration filters from the TypoScript configuration, except the __pageSections filter.
279
     *
280
     * @return array
281
     */
282 37
    public function getAdditionalFilters()
283
    {
284 37
        return $this->queryBuilder->getAdditionalFilters();
285
    }
286
287
    /**
288
     * Executes the search and builds a fake response for a current bug in Apache Solr 6.3
289
     *
290
     * @param Query $query
291
     * @param SearchRequest $searchRequest
292
     * @return \Apache_Solr_Response
293
     */
294 43
    protected function doASearch($query, $searchRequest)
295
    {
296
        // the offset mulitplier is page - 1 but not less then zero
297 43
        $offsetMultiplier = max(0, $searchRequest->getPage() - 1);
298 43
        $offSet = $offsetMultiplier * (int)$searchRequest->getResultsPerPage();
299
300 43
        $response = $this->search->search($query, $offSet, null);
301 42
        if($response === null) {
302
            throw new SolrIncompleteResponseException('The response retrieved from solr was incomplete', 1505989678);
303
        }
304
305 42
        return $response;
306
    }
307
308
    /**
309
     * @param SearchResultSet $searchResultSet
310
     * @return SearchResultSet
311
     */
312 42
    protected function getAutoCorrection(SearchResultSet $searchResultSet)
313
    {
314
        // no secondary search configured
315 42
        if (!$this->typoScriptConfiguration->getSearchSpellcheckingSearchUsingSpellCheckerSuggestion()) {
316 41
            return $searchResultSet;
317
        }
318
319
        // more then zero results
320 1
        if ($searchResultSet->getAllResultCount() > 0) {
321 1
            return $searchResultSet;
322
        }
323
324
        // no corrections present
325 1
        if (!$searchResultSet->getHasSpellCheckingSuggestions()) {
326
            return $searchResultSet;
327
        }
328
329 1
        $searchResultSet = $this->peformAutoCorrection($searchResultSet);
330
331 1
        return $searchResultSet;
332
    }
333
334
    /**
335
     * @param SearchResultSet $searchResultSet
336
     * @return SearchResultSet
337
     */
338 1
    protected function peformAutoCorrection(SearchResultSet $searchResultSet)
339
    {
340 1
        $searchRequest = $searchResultSet->getUsedSearchRequest();
341 1
        $suggestions = $searchResultSet->getSpellCheckingSuggestions();
342
343 1
        $maximumRuns = $this->typoScriptConfiguration->getSearchSpellcheckingNumberOfSuggestionsToTry(1);
344 1
        $runs = 0;
345
346 1
        foreach ($suggestions as $suggestion) {
347 1
            $runs++;
348
349 1
            $correction = $suggestion->getSuggestion();
350 1
            $initialQuery = $searchRequest->getRawUserQuery();
351
352 1
            $searchRequest->setRawQueryString($correction);
353 1
            $searchResultSet = $this->search($searchRequest);
354 1
            if ($searchResultSet->getAllResultCount() > 0) {
355 1
                $searchResultSet->setIsAutoCorrected(true);
356 1
                $searchResultSet->setCorrectedQueryString($correction);
357 1
                $searchResultSet->setInitialQueryString($initialQuery);
358 1
                break;
359
            }
360
361
            if ($runs > $maximumRuns) {
362
                break;
363
            }
364
        }
365 1
        return $searchResultSet;
366
    }
367
368
    /**
369
     * Allows to modify a query before eventually handing it over to Solr.
370
     *
371
     * @param Query $query The current query before it's being handed over to Solr.
372
     * @param SearchRequest $searchRequest The searchRequest, relevant in the current context
373
     * @param Search $search The search, relevant in the current context
374
     * @throws \UnexpectedValueException
375
     * @return Query The modified query that is actually going to be given to Solr.
376
     */
377 43
    protected function modifyQuery(Query $query, SearchRequest $searchRequest, Search $search)
378
    {
379
        // hook to modify the search query
380 43
        if (is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr']['modifySearchQuery'])) {
381 37
            foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr']['modifySearchQuery'] as $classReference) {
382 37
                $queryModifier = GeneralUtility::makeInstance($classReference);
383
384 37
                if ($queryModifier instanceof Modifier) {
385 37
                    if ($queryModifier instanceof SearchAware) {
386
                        $queryModifier->setSearch($search);
387
                    }
388
389 37
                    if ($queryModifier instanceof SearchRequestAware) {
390 33
                        $queryModifier->setSearchRequest($searchRequest);
391
                    }
392
393 37
                    $query = $queryModifier->modifyQuery($query);
394
                } else {
395
                    throw new \UnexpectedValueException(
396
                        get_class($queryModifier) . ' must implement interface ' . Modifier::class,
397 37
                        1310387414
398
                    );
399
                }
400
            }
401
        }
402
403 43
        return $query;
404
    }
405
406
    /**
407
     * Retrieves a single document from solr by document id.
408
     *
409
     * @param string $documentId
410
     * @return SearchResult
411
     */
412 3
    public function getDocumentById($documentId)
413
    {
414
        /* @var $query Query */
415 3
        $query = GeneralUtility::makeInstance(Query::class, /** @scrutinizer ignore-type */ $documentId);
416 3
        $query->setQueryFields(QueryFields::fromString('id'));
417 3
        $response = $this->search->search($query, 0, 1);
418 2
        $parsedData = $response->getParsedData();
419 2
        $resultDocument = isset($parsedData->response->docs[0]) ? $parsedData->response->docs[0] : null;
420
421 2
        return $this->searchResultBuilder->fromApacheSolrDocument($resultDocument);
0 ignored issues
show
Bug introduced by
It seems like $resultDocument can also be of type null; however, parameter $originalDocument of ApacheSolrForTypo3\Solr\...romApacheSolrDocument() does only seem to accept Apache_Solr_Document, maybe add an additional type check? ( Ignorable by Annotation )

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

421
        return $this->searchResultBuilder->fromApacheSolrDocument(/** @scrutinizer ignore-type */ $resultDocument);
Loading history...
422
    }
423
424
    /**
425
     * This method is used to call the registered hooks during the search execution.
426
     *
427
     * @param string $eventName
428
     * @param SearchResultSet $resultSet
429
     * @return SearchResultSet
430
     */
431 47
    private function handleSearchHook($eventName, SearchResultSet $resultSet)
432
    {
433 47
        if (!is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr'][$eventName])) {
434 47
            return $resultSet;
435
        }
436
437 32
        foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr'][$eventName] as $classReference) {
438 32
            $afterSearchProcessor = GeneralUtility::makeInstance($classReference);
439 32
            if ($afterSearchProcessor instanceof SearchResultSetProcessor) {
440 32
                $afterSearchProcessor->process($resultSet);
441
            }
442
        }
443
444 32
        return $resultSet;
445
    }
446
447
    /**
448
     * @return SearchResultSet
449
     */
450 33
    public function getLastResultSet()
451
    {
452 33
        return $this->lastResultSet;
453
    }
454
455
    /**
456
     * This method returns true when the last search was executed with an empty query
457
     * string or whitespaces only. When no search was triggered it will return false.
458
     *
459
     * @return bool
460
     */
461
    public function getLastSearchWasExecutedWithEmptyQueryString()
462
    {
463
        $wasEmptyQueryString = false;
464
        if ($this->lastResultSet != null) {
465
            $wasEmptyQueryString = $this->lastResultSet->getUsedSearchRequest()->getRawUserQueryIsEmptyString();
466
        }
467
468
        return $wasEmptyQueryString;
469
    }
470
471
    /**
472
     * @return bool
473
     */
474 7
    protected function getInitialSearchIsConfigured()
475
    {
476 7
        return $this->typoScriptConfiguration->getSearchInitializeWithEmptyQuery() || $this->typoScriptConfiguration->getSearchShowResultsOfInitialEmptyQuery() || $this->typoScriptConfiguration->getSearchInitializeWithQuery() || $this->typoScriptConfiguration->getSearchShowResultsOfInitialQuery();
477
    }
478
479
    /**
480
     * @return mixed
481
     */
482 37
    protected function getRegisteredSearchComponents()
483
    {
484 37
        return GeneralUtility::makeInstance(SearchComponentManager::class)->getSearchComponents();
485
    }
486
}
487