Completed
Push — master ( 9819c1...f2d6e9 )
by Rafael
06:19
created

SearchResultSetService::doASearch()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
dl 0
loc 9
ccs 4
cts 4
cp 1
rs 9.6666
c 1
b 0
f 0
cc 2
eloc 5
nc 2
nop 2
crap 2
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
     * @return \Apache_Solr_Response
360
     */
361 37
    protected function doASearch($query, $offSet)
362
    {
363
        $response = $this->search->search($query, $offSet, null);
364 2
        if($response === null) {
365
            throw new SolrIncompleteResponseException('The response retrieved from solr was incomplete', 1505989678);
366
        }
367 37
368 37
        return $response;
369
    }
370 37
371 37
    /**
372 37
     * @param SearchResultSet $searchResultSet
373 37
     * @return SearchResultSet
374 37
     */
375
    protected function getAutoCorrection(SearchResultSet $searchResultSet)
376
    {
377 37
        // no secondary search configured
378 37
        if (!$this->typoScriptConfiguration->getSearchSpellcheckingSearchUsingSpellCheckerSuggestion()) {
379
            return $searchResultSet;
380
        }
381 37
382 37
        // more then zero results
383
        if ($searchResultSet->getAllResultCount() > 0) {
384 37
            return $searchResultSet;
385
        }
386 37
387
        // no corrections present
388
        if (!$searchResultSet->getHasSpellCheckingSuggestions()) {
389
            return $searchResultSet;
390
        }
391
392
        $searchResultSet = $this->peformAutoCorrection($searchResultSet);
393
394
        return $searchResultSet;
395
    }
396
397 38
    /**
398
     * @param SearchResultSet $searchResultSet
399
     * @return SearchResultSet
400 38
     */
401 2
    protected function peformAutoCorrection(SearchResultSet $searchResultSet)
402
    {
403
        $searchRequest = $searchResultSet->getUsedSearchRequest();
404
        $suggestions = $searchResultSet->getSpellCheckingSuggestions();
405
406 1
        $maximumRuns = $this->typoScriptConfiguration->getSearchSpellcheckingNumberOfSuggestionsToTry(1);
407
        $runs = 0;
408
409
        foreach ($suggestions as $suggestion) {
410 1
            $runs++;
411
412 1
            $correction = $suggestion->getSuggestion();
413 1
            $initialQuery = $searchRequest->getRawUserQuery();
414 1
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
                break;
422
            }
423
424 37
            if ($runs > $maximumRuns) {
425
                break;
426
            }
427
        }
428 37
        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 37
     * @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 37
     * @return Query The modified query that is actually going to be given to Solr.
439 36
     */
440
    protected function modifyQuery(Query $query, SearchRequest $searchRequest, Search $search)
441
    {
442
        // hook to modify the search query
443 1
        if (is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr']['modifySearchQuery'])) {
444 1
            foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr']['modifySearchQuery'] as $classReference) {
445
                $queryModifier = GeneralUtility::getUserObj($classReference);
446
447
                if ($queryModifier instanceof Modifier) {
448 1
                    if ($queryModifier instanceof SearchAware) {
449
                        $queryModifier->setSearch($search);
450
                    }
451
452 1
                    if ($queryModifier instanceof SearchRequestAware) {
453
                        $queryModifier->setSearchRequest($searchRequest);
454 1
                    }
455
456
                    $query = $queryModifier->modifyQuery($query);
457
                } else {
458
                    throw new \UnexpectedValueException(
459
                        get_class($queryModifier) . ' must implement interface ' . Modifier::class,
460
                        1310387414
461 1
                    );
462
                }
463 1
            }
464 1
        }
465
466 1
        return $query;
467 1
    }
468
469 1
    /**
470 1
     * Retrieves a single document from solr by document id.
471
     *
472 1
     * @param string $documentId
473 1
     * @return SearchResult
474
     */
475 1
    public function getDocumentById($documentId)
476 1
    {
477 1
        /* @var $query Query */
478 1
        $query = GeneralUtility::makeInstance(Query::class, $documentId);
479 1
        $query->setQueryFields(QueryFields::fromString('id'));
480 1
        $response = $this->search->search($query, 0, 1);
481 1
        $parsedData = $response->getParsedData();
482
        $resultDocument = isset($parsedData->response->docs[0]) ? $parsedData->response->docs[0] : null;
483
484
        return $this->searchResultBuilder->fromApacheSolrDocument($resultDocument);
485
    }
486
487
    /**
488 1
     * 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
    private function handleSearchHook($eventName, SearchResultSet $resultSet)
495
    {
496
        if (!is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr'][$eventName])) {
497
            return $resultSet;
498
        }
499
500 38
        foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr'][$eventName] as $classReference) {
501
            $afterSearchProcessor = GeneralUtility::getUserObj($classReference);
502
            if ($afterSearchProcessor instanceof SearchResultSetProcessor) {
503 38
                $afterSearchProcessor->process($resultSet);
504 32
            }
505 32
        }
506
507 32
        return $resultSet;
508 32
    }
509
510
    /**
511
     * @return SearchResultSet
512 32
     */
513 32
    public function getLastResultSet()
514
    {
515
        return $this->lastResultSet;
516 32
    }
517
518
    /**
519
     * This method returns true when the last search was executed with an empty query
520 32
     * string or whitespaces only. When no search was triggered it will return false.
521
     *
522
     * @return bool
523
     */
524
    public function getLastSearchWasExecutedWithEmptyQueryString()
525
    {
526 38
        $wasEmptyQueryString = false;
527
        if ($this->lastResultSet != null) {
528
            $wasEmptyQueryString = $this->lastResultSet->getUsedSearchRequest()->getRawUserQueryIsEmptyString();
529
        }
530
531
        return $wasEmptyQueryString;
532
    }
533
534
    /**
535 3
     * @return bool
536
     */
537
    protected function getInitialSearchIsConfigured()
538 3
    {
539 3
        return $this->typoScriptConfiguration->getSearchInitializeWithEmptyQuery() || $this->typoScriptConfiguration->getSearchShowResultsOfInitialEmptyQuery() || $this->typoScriptConfiguration->getSearchInitializeWithQuery() || $this->typoScriptConfiguration->getSearchShowResultsOfInitialQuery();
540
    }
541 3
542 2
    /**
543
     * @return mixed
544 2
     */
545 2
    protected function getRegisteredSearchComponents()
546 2
    {
547
        return GeneralUtility::makeInstance(SearchComponentManager::class)->getSearchComponents();
548
    }
549
550
    /**
551
     * @param string $rawQuery
552
     * @return Query|object
553
     */
554
    protected function getQueryInstance($rawQuery)
555
    {
556 40
        $query = GeneralUtility::makeInstance(Query::class, $rawQuery, $this->typoScriptConfiguration);
557
        return $query;
558 40
    }
559
}
560