Passed
Push — master ( 6721bc...9819c1 )
by Timo
19:59
created

SearchResultSetService::addSearchResultsToResultSet()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 5.667

Importance

Changes 0
Metric Value
dl 0
loc 11
ccs 1
cts 3
cp 0.3333
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 6
nc 3
nop 2
crap 5.667
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 46
     * @param SolrLogManager $solrLogManager
104
     * @param SearchResultBuilder $resultBuilder
105 46
     */
106 46
    public function __construct(TypoScriptConfiguration $configuration, Search $search, SolrLogManager $solrLogManager = null, SearchResultBuilder $resultBuilder = null)
107 46
    {
108 46
        $this->search = $search;
109
        $this->typoScriptConfiguration = $configuration;
110
        $this->logger = is_null($solrLogManager) ? GeneralUtility::makeInstance(SolrLogManager::class, __CLASS__) : $solrLogManager;
111
        $this->searchResultBuilder = is_null($resultBuilder) ? GeneralUtility::makeInstance(SearchResultBuilder::class) : $resultBuilder;
112
    }
113
114 30
    /**
115
     * @param bool $useCache
116 30
     * @return bool
117 30
     */
118
    public function getIsSolrAvailable($useCache = true)
119
    {
120
        $this->isSolrAvailable = $this->search->ping($useCache);
121
        return $this->isSolrAvailable;
122
    }
123 30
124
    /**
125 30
     * @return bool
126
     */
127
    public function getHasSearched()
128
    {
129
        return $this->search->hasSearched();
130
    }
131
132
    /**
133 2
     * Retrieves the used search instance.
134
     *
135 2
     * @return Search
136
     */
137
    public function getSearch()
138
    {
139
        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
    protected function getPreparedQuery($rawQuery, $resultsPerPage)
151
    {
152
        /* @var $query Query */
153
        $query = $this->getQueryInstance($rawQuery);
154
155
        $this->applyPageSectionsRootLineFilter($query);
156
157
        if ($this->typoScriptConfiguration->getLoggingQuerySearchWords()) {
158
            $this->logger->log(
159
                SolrLogManager::INFO,
160
                'Received search query',
161
                [
162 38
                    $rawQuery
163
                ]
164
            );
165 38
        }
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
            $query->setAlternativeQuery('*:*');
173
        }
174
175
        if ($this->typoScriptConfiguration->getSearchInitializeWithQuery()) {
176
            $query->setAlternativeQuery($this->typoScriptConfiguration->getSearchInitializeWithQuery());
177
        }
178
179 38
        foreach ($this->getAdditionalFilters() as $additionalFilter) {
180
            $query->getFilters()->add($additionalFilter);
181 38
        }
182
183
        return $query;
184 31
    }
185
186
    /**
187 38
     * @param Query $query
188 3
     * @param SearchRequest $searchRequest
189
     */
190
    protected function initializeRegisteredSearchComponents(Query $query, SearchRequest $searchRequest)
191 38
    {
192 2
        $searchComponents = $this->getRegisteredSearchComponents();
193
194
        foreach ($searchComponents as $searchComponent) {
195 38
            /** @var Search\SearchComponent $searchComponent */
196
            $searchComponent->setSearchConfiguration($this->typoScriptConfiguration->getSearchConfiguration());
197
198
            if ($searchComponent instanceof QueryAware) {
199
                $searchComponent->setQuery($query);
200
            }
201
202 38
            if ($searchComponent instanceof SearchRequestAware) {
203
                $searchComponent->setSearchRequest($searchRequest);
204 38
            }
205
206 38
            $searchComponent->initializeSearchComponent();
207
        }
208 32
    }
209
210 32
    /**
211 32
     * @return string
212
     */
213
    protected function getResultSetClassName()
214 32
    {
215 31
        return isset($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr']['searchResultSetClassName ']) ?
216
            $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr']['searchResultSetClassName '] : SearchResultSet::class;
217
    }
218 32
219
    /**
220 38
     * 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
    protected function applyPageSectionsRootLineFilter(Query $query)
227
    {
228
        $searchQueryFilters = $this->typoScriptConfiguration->getSearchQueryFilterConfiguration();
229
        if (count($searchQueryFilters) <= 0) {
230
            return;
231 38
        }
232
233 38
        // special filter to limit search to specific page tree branches
234 38
        if (array_key_exists('__pageSections', $searchQueryFilters)) {
235 38
            $query->setRootlineFilter($searchQueryFilters['__pageSections']);
236 4
            $this->typoScriptConfiguration->removeSearchQueryFilterForPageSections();
237 4
        }
238
    }
239
240 38
    /**
241 38
     * Retrieves the configuration filters from the TypoScript configuration, except the __pageSections filter.
242
     *
243 38
     * @return array
244 38
     */
245 3
    public function getAdditionalFilters()
246
    {
247
        // when we've build the additionalFilter once, we could return them
248 38
        if (count($this->additionalFilters) > 0) {
249
            return $this->additionalFilters;
250
        }
251
252 2
        $searchQueryFilters = $this->typoScriptConfiguration->getSearchQueryFilterConfiguration();
253
        if (count($searchQueryFilters) <= 0) {
254
            return [];
255 38
        }
256
257
        $cObj = GeneralUtility::makeInstance(ContentObjectRenderer::class);
258
259
        // all other regular filters
260
        foreach ($searchQueryFilters as $filterKey => $filter) {
261
            // the __pageSections filter should not be handled as additional filter
262
            if ($filterKey === '__pageSections') {
263
                continue;
264 40
            }
265
266 40
            $filterIsArray = is_array($searchQueryFilters[$filterKey]);
267 40
            if ($filterIsArray) {
268
                continue;
269 40
            }
270 40
271
            $hasSubConfiguration = is_array($searchQueryFilters[$filterKey . '.']);
272
            if ($hasSubConfiguration) {
273
                $filter = $cObj->stdWrap($searchQueryFilters[$filterKey], $searchQueryFilters[$filterKey . '.']);
274
            }
275
276
            $this->additionalFilters[$filterKey] = $filter;
277
        }
278
279 40
        return $this->additionalFilters;
280
    }
281 40
282 1
    /**
283
     * Performs a search and returns a SearchResultSet.
284 1
     *
285 1
     * @param SearchRequest $searchRequest
286 1
     * @return SearchResultSet
287 1
     */
288
    public function search(SearchRequest $searchRequest)
289
    {
290
        /** @var $resultSet SearchResultSet */
291 40
        $resultSetClass = $this->getResultSetClassName();
292
        $resultSet = GeneralUtility::makeInstance($resultSetClass);
293
        $resultSet->setUsedSearchRequest($searchRequest);
294
        $this->lastResultSet = $resultSet;
295
296
        $resultSet = $this->handleSearchHook('beforeSearch', $resultSet);
297
298
        if ($searchRequest->getRawUserQueryIsNull() && !$this->getInitialSearchIsConfigured()) {
299 40
            // when no rawQuery was passed or no initialSearch is configured, we pass an empty result set
300
            return $resultSet;
301 40
        }
302 5
303
        if ($searchRequest->getRawUserQueryIsEmptyString() && !$this->typoScriptConfiguration->getSearchQueryAllowEmptyQuery()) {
304
            // the user entered an empty query string "" or "  " and empty querystring is not allowed
305 35
            return $resultSet;
306 32
        }
307
308
        $rawQuery = $searchRequest->getRawUserQuery();
309 3
        $resultsPerPage = (int)$searchRequest->getResultsPerPage();
310 3
        $query = $this->getPreparedQuery($rawQuery, $resultsPerPage);
311
        $this->initializeRegisteredSearchComponents($query, $searchRequest);
312 2
        $resultSet->setUsedQuery($query);
313 2
314
        // the offset mulitplier is page - 1 but not less then zero
315
        $offsetMultiplier = max(0, $searchRequest->getPage() - 1);
316 2
        $offSet = $offsetMultiplier * $resultsPerPage;
317
318
        // performing the actual search, sending the query to the Solr server
319
        $query = $this->modifyQuery($query, $searchRequest, $this->search);
320 2
        $response = $this->doASearch($query, $offSet);
321 2
322
        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 2
        }
327 2
328 2
        $resultSet->setUsedSearch($this->search);
329
        $resultSet->setResponse($response);
330 2
331 2
            /** @var ResultParserRegistry $parserRegistry */
332 2
        $parserRegistry = GeneralUtility::makeInstance(ResultParserRegistry::class, $this->typoScriptConfiguration);
333
        $useRawDocuments = (bool)$this->typoScriptConfiguration->getValueByPathOrDefaultValue('plugin.tx_solr.features.useRawDocuments', false);
334 2
        $searchResults = $parserRegistry->getParser($resultSet)->parse($resultSet, $useRawDocuments);
335
        $resultSet->setSearchResults($searchResults);
336
337 3
        $resultSet->setUsedPage((int)$searchRequest->getPage());
338
        $resultSet->setUsedResultsPerPage($resultsPerPage);
339
        $resultSet->setUsedAdditionalFilters($this->getAdditionalFilters());
340
341
        /** @var $variantsProcessor VariantsProcessor */
342
        $variantsProcessor = GeneralUtility::makeInstance(VariantsProcessor::class, $this->typoScriptConfiguration, $this->searchResultBuilder);
343
        $variantsProcessor->process($resultSet);
344
345
        /** @var $searchResultReconstitutionProcessor ResultSetReconstitutionProcessor */
346
        $searchResultReconstitutionProcessor = GeneralUtility::makeInstance(ResultSetReconstitutionProcessor::class);
347
        $searchResultReconstitutionProcessor->process($resultSet);
348
349
        $resultSet = $this->getAutoCorrection($resultSet);
350
351 40
        return $this->handleSearchHook('afterSearch', $resultSet);
352
    }
353
354 40
    /**
355 1
     * 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
     * @throws SolrCommunicationException
360 1
     * @return \Apache_Solr_Response
361
     */
362
    protected function doASearch($query, $offSet)
363
    {
364 1
        try {
365 1
            $response = $this->search->search($query, $offSet, null);
366 1
        } catch (SolrInternalServerErrorException $e) {
367 1
            // when variants are enable and the index is empty, we get a parse exception, because of a
368 1
            // Apache Solr Bug.
369
            // see: https://github.com/TYPO3-Solr/ext-solr/issues/668
370 1
            // @todo this try/catch block can be removed after upgrading to Apache Solr 6.4
371
            if (!$this->typoScriptConfiguration->getSearchVariants()) {
372
                throw $e;
373 40
            }
374 5
375
            $response = $e->getSolrResponse();
376
377 35
            $parsedData = new \stdClass();
378 29
            $parsedData->response = new \stdClass();
379 29
            $parsedData->response->docs = [];
380
            $parsedData->spellcheck = [];
381
            $parsedData->debug = [];
382 35
            $parsedData->responseHeader = [];
383 35
            $parsedData->facet_counts = [];
384
            $parsedData->facets = [];
385
            $response->setParsedData($parsedData);
386
387
        }
388
389
        if($response === null) {
390
            throw new SolrIncompleteResponseException('The response retrieved from solr was incomplete', 1505989678);
391
        }
392
393 29
        return $response;
394
    }
395 29
396 29
    /**
397 29
     * @param SearchResultSet $searchResultSet
398
     * @return SearchResultSet
399
     */
400
    protected function getAutoCorrection(SearchResultSet $searchResultSet)
401 29
    {
402
        // no secondary search configured
403
        if (!$this->typoScriptConfiguration->getSearchSpellcheckingSearchUsingSpellCheckerSuggestion()) {
404
            return $searchResultSet;
405
        }
406
407 29
        // more then zero results
408
        if ($searchResultSet->getAllResultCount() > 0) {
409 29
            return $searchResultSet;
410 29
        }
411
412
        // no corrections present
413
        if (!$searchResultSet->getHasSpellCheckingSuggestions()) {
414
            return $searchResultSet;
415
        }
416 40
417
        $searchResultSet = $this->peformAutoCorrection($searchResultSet);
418 40
419 40
        return $searchResultSet;
420
    }
421
422
    /**
423
     * @param SearchResultSet $searchResultSet
424
     * @return SearchResultSet
425
     */
426
    protected function peformAutoCorrection(SearchResultSet $searchResultSet)
427
    {
428 38
        $searchRequest = $searchResultSet->getUsedSearchRequest();
429
        $suggestions = $searchResultSet->getSpellCheckingSuggestions();
430 38
431
        $maximumRuns = $this->typoScriptConfiguration->getSearchSpellcheckingNumberOfSuggestionsToTry(1);
432
        $runs = 0;
433
434
        foreach ($suggestions as $suggestion) {
435
            $runs++;
436
437
            $correction = $suggestion->getSuggestion();
438
            $initialQuery = $searchRequest->getRawUserQuery();
439
440 38
            $searchRequest->setRawQueryString($correction);
441
            $searchResultSet = $this->search($searchRequest);
442 38
            if ($searchResultSet->getAllResultCount() > 0) {
443 38
                $searchResultSet->setIsAutoCorrected(true);
444 36
                $searchResultSet->setCorrectedQueryString($correction);
445
                $searchResultSet->setInitialQueryString($initialQuery);
446
                break;
447
            }
448 2
449
            if ($runs > $maximumRuns) {
450
                break;
451
            }
452 2
        }
453
        return $searchResultSet;
454
    }
455
456
    /**
457
     * Allows to modify a query before eventually handing it over to Solr.
458
     *
459 43
     * @param Query $query The current query before it's being handed over to Solr.
460
     * @param SearchRequest $searchRequest The searchRequest, relevant in the current context
461
     * @param Search $search The search, relevant in the current context
462 43
     * @throws \UnexpectedValueException
463 2
     * @return Query The modified query that is actually going to be given to Solr.
464
     */
465
    protected function modifyQuery(Query $query, SearchRequest $searchRequest, Search $search)
466 43
    {
467 43
        // hook to modify the search query
468 41
        if (is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr']['modifySearchQuery'])) {
469
            foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr']['modifySearchQuery'] as $classReference) {
470
                $queryModifier = GeneralUtility::getUserObj($classReference);
471 2
472
                if ($queryModifier instanceof Modifier) {
473
                    if ($queryModifier instanceof SearchAware) {
474 2
                        $queryModifier->setSearch($search);
475
                    }
476 2
477
                    if ($queryModifier instanceof SearchRequestAware) {
478
                        $queryModifier->setSearchRequest($searchRequest);
479
                    }
480 2
481 2
                    $query = $queryModifier->modifyQuery($query);
482
                } else {
483
                    throw new \UnexpectedValueException(
484
                        get_class($queryModifier) . ' must implement interface ' . Modifier::class,
485 2
                        1310387414
486 2
                    );
487
                }
488
            }
489
        }
490 2
491
        return $query;
492
    }
493 2
494
    /**
495
     * Retrieves a single document from solr by document id.
496
     *
497
     * @param string $documentId
498
     * @return SearchResult
499
     */
500
    public function getDocumentById($documentId)
501
    {
502 40
        /* @var $query Query */
503
        $query = GeneralUtility::makeInstance(Query::class, $documentId);
504
        $query->setQueryFields(QueryFields::fromString('id'));
505 40
        $response = $this->search->search($query, 0, 1);
506 40
        $parsedData = $response->getParsedData();
507 40
        $resultDocument = isset($parsedData->response->docs[0]) ? $parsedData->response->docs[0] : null;
508 40
509
        return $this->searchResultBuilder->fromApacheSolrDocument($resultDocument);
510 40
    }
511
512 40
    /**
513
     * This method is used to call the registered hooks during the search execution.
514 2
     *
515
     * @param string $eventName
516
     * @param SearchResultSet $resultSet
517 38
     * @return SearchResultSet
518
     */
519
    private function handleSearchHook($eventName, SearchResultSet $resultSet)
520
    {
521
        if (!is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr'][$eventName])) {
522 38
            return $resultSet;
523 38
        }
524 38
525 38
        foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr'][$eventName] as $classReference) {
526 38
            $afterSearchProcessor = GeneralUtility::getUserObj($classReference);
527
            if ($afterSearchProcessor instanceof SearchResultSetProcessor) {
528 38
                $afterSearchProcessor->process($resultSet);
529
            }
530 38
        }
531 4
532
        return $resultSet;
533
    }
534 38
535
    /**
536
     * @return SearchResultSet
537 38
     */
538 38
    public function getLastResultSet()
539 38
    {
540
        return $this->lastResultSet;
541 38
    }
542
543
    /**
544 2
     * This method returns true when the last search was executed with an empty query
545
     * string or whitespaces only. When no search was triggered it will return false.
546
     *
547 38
     * @return bool
548
     */
549 38
    public function getLastSearchWasExecutedWithEmptyQueryString()
550
    {
551 38
        $wasEmptyQueryString = false;
552 38
        if ($this->lastResultSet != null) {
553 38
            $wasEmptyQueryString = $this->lastResultSet->getUsedSearchRequest()->getRawUserQueryIsEmptyString();
554 38
        }
555 38
556
        return $wasEmptyQueryString;
557
    }
558 38
559 38
    /**
560
     * @return bool
561 38
     */
562
    protected function getInitialSearchIsConfigured()
563 38
    {
564
        return $this->typoScriptConfiguration->getSearchInitializeWithEmptyQuery() || $this->typoScriptConfiguration->getSearchShowResultsOfInitialEmptyQuery() || $this->typoScriptConfiguration->getSearchInitializeWithQuery() || $this->typoScriptConfiguration->getSearchShowResultsOfInitialQuery();
565
    }
566
567
    /**
568
     * @return mixed
569
     */
570 38
    protected function getRegisteredSearchComponents()
571
    {
572
        return GeneralUtility::makeInstance(SearchComponentManager::class)->getSearchComponents();
573 38
    }
574 37
575
    /**
576
     * @param string $rawQuery
577
     * @return Query|object
578 1
     */
579 1
    protected function getQueryInstance($rawQuery)
580
    {
581
        $query = GeneralUtility::makeInstance(Query::class, $rawQuery, $this->typoScriptConfiguration);
582
        return $query;
583 1
    }
584
}
585