Passed
Push — master ( 14b490...3d7c7d )
by Timo
23:43
created

SearchResultSetService::getDocumentById()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 3.0175

Importance

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