Completed
Push — master ( ee9c7d...9819c1 )
by Rafael
14:45
created

SearchResultSetService::addSearchResultsToResultSet()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 3

Importance

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