Passed
Push — master ( efbfe4...f2d6e9 )
by Timo
23:10
created

SearchResultSetService::__construct()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 3

Importance

Changes 0
Metric Value
dl 0
loc 7
ccs 6
cts 6
cp 1
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 5
nc 4
nop 4
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 2 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\ResultSet\Result\Parser\ResultParserRegistry;
30
use ApacheSolrForTypo3\Solr\Domain\Search\SearchRequest;
31
use ApacheSolrForTypo3\Solr\Domain\Search\SearchRequestAware;
32
use ApacheSolrForTypo3\Solr\Domain\Variants\VariantsProcessor;
33
use ApacheSolrForTypo3\Solr\Query;
34
use ApacheSolrForTypo3\Solr\Query\Modifier\Modifier;
35
use ApacheSolrForTypo3\Solr\Search;
36
use ApacheSolrForTypo3\Solr\Search\QueryAware;
37
use ApacheSolrForTypo3\Solr\Search\SearchAware;
38
use ApacheSolrForTypo3\Solr\Search\SearchComponentManager;
39
use ApacheSolrForTypo3\Solr\System\Configuration\TypoScriptConfiguration;
40
use ApacheSolrForTypo3\Solr\System\Logging\SolrLogManager;
41
use ApacheSolrForTypo3\Solr\System\Solr\SolrCommunicationException;
42
use ApacheSolrForTypo3\Solr\System\Solr\SolrIncompleteResponseException;
43
use ApacheSolrForTypo3\Solr\System\Solr\SolrInternalServerErrorException;
44
use TYPO3\CMS\Core\Utility\GeneralUtility;
45
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
46
47
/**
48
 * The SearchResultSetService is responsible to build a SearchResultSet from a SearchRequest.
49
 * It encapsulates the logic to trigger a search in order to be able to reuse it in multiple places.
50
 *
51
 * @author Timo Schmidt <[email protected]>
52
 */
53
class SearchResultSetService
54
{
55
    /**
56
     * Additional filters, which will be added to the query, as well as to
57
     * suggest queries.
58
     *
59
     * @var array
60
     */
61
    protected $additionalFilters = [];
62
63
    /**
64
     * Track, if the number of results per page has been changed by the current request
65
     *
66
     * @var bool
67
     */
68
    protected $resultsPerPageChanged = false;
69
70
    /**
71
     * @var Search
72
     */
73
    protected $search;
74
75
    /**
76
     * @var SearchResultSet
77
     */
78
    protected $lastResultSet = null;
79
80
    /**
81
     * @var boolean
82
     */
83
    protected $isSolrAvailable = false;
84
85
    /**
86
     * @var TypoScriptConfiguration
87
     */
88
    protected $typoScriptConfiguration;
89
90
    /**
91
     * @var SolrLogManager;
92
     */
93
    protected $logger = null;
94
95
    /**
96
     * @var SearchResultBuilder
97
     */
98
    protected $searchResultBuilder;
99
100
    /**
101
     * @param TypoScriptConfiguration $configuration
102
     * @param Search $search
103
     * @param SolrLogManager $solrLogManager
104
     * @param SearchResultBuilder $resultBuilder
105
     */
106 47
    public function __construct(TypoScriptConfiguration $configuration, Search $search, SolrLogManager $solrLogManager = null, SearchResultBuilder $resultBuilder = null)
107
    {
108 47
        $this->search = $search;
109 47
        $this->typoScriptConfiguration = $configuration;
110 47
        $this->logger = is_null($solrLogManager) ? GeneralUtility::makeInstance(SolrLogManager::class, __CLASS__) : $solrLogManager;
111 47
        $this->searchResultBuilder = is_null($resultBuilder) ? GeneralUtility::makeInstance(SearchResultBuilder::class) : $resultBuilder;
112 47
    }
113
114
    /**
115
     * @param bool $useCache
116
     * @return bool
117
     */
118
    public function getIsSolrAvailable($useCache = true)
119
    {
120
        $this->isSolrAvailable = $this->search->ping($useCache);
121
        return $this->isSolrAvailable;
122
    }
123
124
    /**
125
     * @return bool
126
     */
127 30
    public function getHasSearched()
128
    {
129 30
        return $this->search->hasSearched();
130
    }
131
132
    /**
133
     * Retrieves the used search instance.
134
     *
135
     * @return Search
136
     */
137 2
    public function getSearch()
138
    {
139 2
        return $this->search;
140
    }
141
142
    /**
143
     * Initializes the Query object and SearchComponents and returns
144
     * the initialized query object, when a search should be executed.
145
     *
146
     * @param string|null $rawQuery
147
     * @param int $resultsPerPage
148
     * @return Query
149
     */
150 38
    protected function getPreparedQuery($rawQuery, $resultsPerPage)
151
    {
152
        /* @var $query Query */
153 38
        $query = $this->getQueryInstance($rawQuery);
154
155 38
        $this->applyPageSectionsRootLineFilter($query);
156
157 38
        if ($this->typoScriptConfiguration->getLoggingQuerySearchWords()) {
158
            $this->logger->log(
159
                SolrLogManager::INFO,
160
                'Received search query',
161
                [
162
                    $rawQuery
163
                ]
164
            );
165
        }
166
167 38
        $query->setResultsPerPage($resultsPerPage);
168
169 38
        if ($this->typoScriptConfiguration->getSearchInitializeWithEmptyQuery() || $this->typoScriptConfiguration->getSearchQueryAllowEmptyQuery()) {
170
            // empty main query, but using a "return everything"
171
            // alternative query in q.alt
172 32
            $query->setAlternativeQuery('*:*');
173
        }
174
175 38
        if ($this->typoScriptConfiguration->getSearchInitializeWithQuery()) {
176 3
            $query->setAlternativeQuery($this->typoScriptConfiguration->getSearchInitializeWithQuery());
177
        }
178
179 38
        foreach ($this->getAdditionalFilters() as $additionalFilter) {
180 2
            $query->getFilters()->add($additionalFilter);
181
        }
182
183 38
        return $query;
184
    }
185
186
    /**
187
     * @param Query $query
188
     * @param SearchRequest $searchRequest
189
     */
190 38
    protected function initializeRegisteredSearchComponents(Query $query, SearchRequest $searchRequest)
191
    {
192 38
        $searchComponents = $this->getRegisteredSearchComponents();
193
194 38
        foreach ($searchComponents as $searchComponent) {
195
            /** @var Search\SearchComponent $searchComponent */
196 33
            $searchComponent->setSearchConfiguration($this->typoScriptConfiguration->getSearchConfiguration());
197
198 33
            if ($searchComponent instanceof QueryAware) {
199 33
                $searchComponent->setQuery($query);
200
            }
201
202 33
            if ($searchComponent instanceof SearchRequestAware) {
203 32
                $searchComponent->setSearchRequest($searchRequest);
204
            }
205
206 33
            $searchComponent->initializeSearchComponent();
207
        }
208 38
    }
209
210
    /**
211
     * @return string
212
     */
213 40
    protected function getResultSetClassName()
214
    {
215 40
        return isset($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr']['searchResultSetClassName ']) ?
216 40
            $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr']['searchResultSetClassName '] : SearchResultSet::class;
217
    }
218
219
    /**
220
     * Initializes additional filters configured through TypoScript and
221
     * Flexforms for use in regular queries and suggest queries.
222
     *
223
     * @param Query $query
224
     * @return void
225
     */
226 38
    protected function applyPageSectionsRootLineFilter(Query $query)
227
    {
228 38
        $searchQueryFilters = $this->typoScriptConfiguration->getSearchQueryFilterConfiguration();
229 38
        if (count($searchQueryFilters) <= 0) {
230 36
            return;
231
        }
232
233
        // special filter to limit search to specific page tree branches
234 2
        if (array_key_exists('__pageSections', $searchQueryFilters)) {
235
            $query->setRootlineFilter($searchQueryFilters['__pageSections']);
236
            $this->typoScriptConfiguration->removeSearchQueryFilterForPageSections();
237
        }
238 2
    }
239
240
    /**
241
     * Retrieves the configuration filters from the TypoScript configuration, except the __pageSections filter.
242
     *
243
     * @return array
244
     */
245 43
    public function getAdditionalFilters()
246
    {
247
        // when we've build the additionalFilter once, we could return them
248 43
        if (count($this->additionalFilters) > 0) {
249 2
            return $this->additionalFilters;
250
        }
251
252 43
        $searchQueryFilters = $this->typoScriptConfiguration->getSearchQueryFilterConfiguration();
253 43
        if (count($searchQueryFilters) <= 0) {
254 41
            return [];
255
        }
256
257 2
        $cObj = GeneralUtility::makeInstance(ContentObjectRenderer::class);
258
259
        // all other regular filters
260 2
        foreach ($searchQueryFilters as $filterKey => $filter) {
261
            // the __pageSections filter should not be handled as additional filter
262 2
            if ($filterKey === '__pageSections') {
263
                continue;
264
            }
265
266 2
            $filterIsArray = is_array($searchQueryFilters[$filterKey]);
267 2
            if ($filterIsArray) {
268
                continue;
269
            }
270
271 2
            $hasSubConfiguration = is_array($searchQueryFilters[$filterKey . '.']);
272 2
            if ($hasSubConfiguration) {
273
                $filter = $cObj->stdWrap($searchQueryFilters[$filterKey], $searchQueryFilters[$filterKey . '.']);
274
            }
275
276 2
            $this->additionalFilters[$filterKey] = $filter;
277
        }
278
279 2
        return $this->additionalFilters;
280
    }
281
282
    /**
283
     * Performs a search and returns a SearchResultSet.
284
     *
285
     * @param SearchRequest $searchRequest
286
     * @return SearchResultSet
287
     */
288 40
    public function search(SearchRequest $searchRequest)
289
    {
290
        /** @var $resultSet SearchResultSet */
291 40
        $resultSetClass = $this->getResultSetClassName();
292 40
        $resultSet = GeneralUtility::makeInstance($resultSetClass);
293 40
        $resultSet->setUsedSearchRequest($searchRequest);
294 40
        $this->lastResultSet = $resultSet;
295
296 40
        $resultSet = $this->handleSearchHook('beforeSearch', $resultSet);
297
298 40
        if ($searchRequest->getRawUserQueryIsNull() && !$this->getInitialSearchIsConfigured()) {
299
            // when no rawQuery was passed or no initialSearch is configured, we pass an empty result set
300 2
            return $resultSet;
301
        }
302
303 38
        if ($searchRequest->getRawUserQueryIsEmptyString() && !$this->typoScriptConfiguration->getSearchQueryAllowEmptyQuery()) {
304
            // the user entered an empty query string "" or "  " and empty querystring is not allowed
305
            return $resultSet;
306
        }
307
308 38
        $rawQuery = $searchRequest->getRawUserQuery();
309 38
        $resultsPerPage = (int)$searchRequest->getResultsPerPage();
310 38
        $query = $this->getPreparedQuery($rawQuery, $resultsPerPage);
311 38
        $this->initializeRegisteredSearchComponents($query, $searchRequest);
312 38
        $resultSet->setUsedQuery($query);
313
314
        // the offset mulitplier is page - 1 but not less then zero
315 38
        $offsetMultiplier = max(0, $searchRequest->getPage() - 1);
316 38
        $offSet = $offsetMultiplier * $resultsPerPage;
317
318
        // performing the actual search, sending the query to the Solr server
319 38
        $query = $this->modifyQuery($query, $searchRequest, $this->search);
320 38
        $response = $this->doASearch($query, $offSet);
321
322 37
        if ($resultsPerPage === 0) {
323
            // when resultPerPage was forced to 0 we also set the numFound to 0 to hide results, e.g.
324
            // when results for the initial search should not be shown.
325 2
            $response->response->numFound = 0;
0 ignored issues
show
Bug introduced by
The property response does not seem to exist. Did you mean _response?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
326
        }
327
328 37
        $resultSet->setUsedSearch($this->search);
329 37
        $resultSet->setResponse($response);
330
331
            /** @var ResultParserRegistry $parserRegistry */
332 37
        $parserRegistry = GeneralUtility::makeInstance(ResultParserRegistry::class, $this->typoScriptConfiguration);
333 37
        $useRawDocuments = (bool)$this->typoScriptConfiguration->getValueByPathOrDefaultValue('plugin.tx_solr.features.useRawDocuments', false);
334 37
        $searchResults = $parserRegistry->getParser($resultSet)->parse($resultSet, $useRawDocuments);
335 37
        $resultSet->setSearchResults($searchResults);
336
337 37
        $resultSet->setUsedPage((int)$searchRequest->getPage());
338 37
        $resultSet->setUsedResultsPerPage($resultsPerPage);
339 37
        $resultSet->setUsedAdditionalFilters($this->getAdditionalFilters());
340
341
        /** @var $variantsProcessor VariantsProcessor */
342 37
        $variantsProcessor = GeneralUtility::makeInstance(VariantsProcessor::class, $this->typoScriptConfiguration, $this->searchResultBuilder);
343 37
        $variantsProcessor->process($resultSet);
344
345
        /** @var $searchResultReconstitutionProcessor ResultSetReconstitutionProcessor */
346 37
        $searchResultReconstitutionProcessor = GeneralUtility::makeInstance(ResultSetReconstitutionProcessor::class);
347 37
        $searchResultReconstitutionProcessor->process($resultSet);
348
349 37
        $resultSet = $this->getAutoCorrection($resultSet);
350
351 37
        return $this->handleSearchHook('afterSearch', $resultSet);
352
    }
353
354
    /**
355
     * Executes the search and builds a fake response for a current bug in Apache Solr 6.3
356
     *
357
     * @param Query $query
358
     * @param int $offSet
359
     * @return \Apache_Solr_Response
360
     */
361 38
    protected function doASearch($query, $offSet)
362
    {
363 38
        $response = $this->search->search($query, $offSet, null);
364 37
        if($response === null) {
365
            throw new SolrIncompleteResponseException('The response retrieved from solr was incomplete', 1505989678);
366
        }
367
368 37
        return $response;
369
    }
370
371
    /**
372
     * @param SearchResultSet $searchResultSet
373
     * @return SearchResultSet
374
     */
375 37
    protected function getAutoCorrection(SearchResultSet $searchResultSet)
376
    {
377
        // no secondary search configured
378 37
        if (!$this->typoScriptConfiguration->getSearchSpellcheckingSearchUsingSpellCheckerSuggestion()) {
379 36
            return $searchResultSet;
380
        }
381
382
        // more then zero results
383 1
        if ($searchResultSet->getAllResultCount() > 0) {
384 1
            return $searchResultSet;
385
        }
386
387
        // no corrections present
388 1
        if (!$searchResultSet->getHasSpellCheckingSuggestions()) {
389
            return $searchResultSet;
390
        }
391
392 1
        $searchResultSet = $this->peformAutoCorrection($searchResultSet);
393
394 1
        return $searchResultSet;
395
    }
396
397
    /**
398
     * @param SearchResultSet $searchResultSet
399
     * @return SearchResultSet
400
     */
401 1
    protected function peformAutoCorrection(SearchResultSet $searchResultSet)
402
    {
403 1
        $searchRequest = $searchResultSet->getUsedSearchRequest();
404 1
        $suggestions = $searchResultSet->getSpellCheckingSuggestions();
405
406 1
        $maximumRuns = $this->typoScriptConfiguration->getSearchSpellcheckingNumberOfSuggestionsToTry(1);
407 1
        $runs = 0;
408
409 1
        foreach ($suggestions as $suggestion) {
410 1
            $runs++;
411
412 1
            $correction = $suggestion->getSuggestion();
413 1
            $initialQuery = $searchRequest->getRawUserQuery();
414
415 1
            $searchRequest->setRawQueryString($correction);
416 1
            $searchResultSet = $this->search($searchRequest);
417 1
            if ($searchResultSet->getAllResultCount() > 0) {
418 1
                $searchResultSet->setIsAutoCorrected(true);
419 1
                $searchResultSet->setCorrectedQueryString($correction);
420 1
                $searchResultSet->setInitialQueryString($initialQuery);
421 1
                break;
422
            }
423
424
            if ($runs > $maximumRuns) {
425
                break;
426
            }
427
        }
428 1
        return $searchResultSet;
429
    }
430
431
    /**
432
     * Allows to modify a query before eventually handing it over to Solr.
433
     *
434
     * @param Query $query The current query before it's being handed over to Solr.
435
     * @param SearchRequest $searchRequest The searchRequest, relevant in the current context
436
     * @param Search $search The search, relevant in the current context
437
     * @throws \UnexpectedValueException
438
     * @return Query The modified query that is actually going to be given to Solr.
439
     */
440 38
    protected function modifyQuery(Query $query, SearchRequest $searchRequest, Search $search)
441
    {
442
        // hook to modify the search query
443 38
        if (is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr']['modifySearchQuery'])) {
444 32
            foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr']['modifySearchQuery'] as $classReference) {
445 32
                $queryModifier = GeneralUtility::getUserObj($classReference);
446
447 32
                if ($queryModifier instanceof Modifier) {
448 32
                    if ($queryModifier instanceof SearchAware) {
449
                        $queryModifier->setSearch($search);
450
                    }
451
452 32
                    if ($queryModifier instanceof SearchRequestAware) {
453 32
                        $queryModifier->setSearchRequest($searchRequest);
454
                    }
455
456 32
                    $query = $queryModifier->modifyQuery($query);
457
                } else {
458
                    throw new \UnexpectedValueException(
459
                        get_class($queryModifier) . ' must implement interface ' . Modifier::class,
460 32
                        1310387414
461
                    );
462
                }
463
            }
464
        }
465
466 38
        return $query;
467
    }
468
469
    /**
470
     * Retrieves a single document from solr by document id.
471
     *
472
     * @param string $documentId
473
     * @return SearchResult
474
     */
475 3
    public function getDocumentById($documentId)
476
    {
477
        /* @var $query Query */
478 3
        $query = GeneralUtility::makeInstance(Query::class, $documentId);
479 3
        $query->setQueryFields(QueryFields::fromString('id'));
480 3
        $response = $this->search->search($query, 0, 1);
481 2
        $parsedData = $response->getParsedData();
482 2
        $resultDocument = isset($parsedData->response->docs[0]) ? $parsedData->response->docs[0] : null;
483
484 2
        return $this->searchResultBuilder->fromApacheSolrDocument($resultDocument);
485
    }
486
487
    /**
488
     * This method is used to call the registered hooks during the search execution.
489
     *
490
     * @param string $eventName
491
     * @param SearchResultSet $resultSet
492
     * @return SearchResultSet
493
     */
494 40
    private function handleSearchHook($eventName, SearchResultSet $resultSet)
495
    {
496 40
        if (!is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr'][$eventName])) {
497 40
            return $resultSet;
498
        }
499
500 28
        foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr'][$eventName] as $classReference) {
501 28
            $afterSearchProcessor = GeneralUtility::getUserObj($classReference);
502 28
            if ($afterSearchProcessor instanceof SearchResultSetProcessor) {
503 28
                $afterSearchProcessor->process($resultSet);
504
            }
505
        }
506
507 28
        return $resultSet;
508
    }
509
510
    /**
511
     * @return SearchResultSet
512
     */
513 29
    public function getLastResultSet()
514
    {
515 29
        return $this->lastResultSet;
516
    }
517
518
    /**
519
     * This method returns true when the last search was executed with an empty query
520
     * string or whitespaces only. When no search was triggered it will return false.
521
     *
522
     * @return bool
523
     */
524
    public function getLastSearchWasExecutedWithEmptyQueryString()
525
    {
526
        $wasEmptyQueryString = false;
527
        if ($this->lastResultSet != null) {
528
            $wasEmptyQueryString = $this->lastResultSet->getUsedSearchRequest()->getRawUserQueryIsEmptyString();
529
        }
530
531
        return $wasEmptyQueryString;
532
    }
533
534
    /**
535
     * @return bool
536
     */
537 6
    protected function getInitialSearchIsConfigured()
538
    {
539 6
        return $this->typoScriptConfiguration->getSearchInitializeWithEmptyQuery() || $this->typoScriptConfiguration->getSearchShowResultsOfInitialEmptyQuery() || $this->typoScriptConfiguration->getSearchInitializeWithQuery() || $this->typoScriptConfiguration->getSearchShowResultsOfInitialQuery();
540
    }
541
542
    /**
543
     * @return mixed
544
     */
545 32
    protected function getRegisteredSearchComponents()
546
    {
547 32
        return GeneralUtility::makeInstance(SearchComponentManager::class)->getSearchComponents();
548
    }
549
550
    /**
551
     * @param string $rawQuery
552
     * @return Query|object
553
     */
554 32
    protected function getQueryInstance($rawQuery)
555
    {
556 32
        $query = GeneralUtility::makeInstance(Query::class, $rawQuery, $this->typoScriptConfiguration);
557 32
        return $query;
558
    }
559
}
560