Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
96.67% covered (success)
96.67%
29 / 30
66.67% covered (warning)
66.67%
2 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
FallbackMethodTrait
96.67% covered (success)
96.67%
29 / 30
66.67% covered (warning)
66.67%
2 / 3
15
0.00% covered (danger)
0.00%
0 / 1
 resultsThreshold
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
 resultContainsFullyHighlightedMatch
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
 maybeSearchAndRewrite
94.12% covered (success)
94.12%
16 / 17
0.00% covered (danger)
0.00%
0 / 1
7.01
1<?php
2
3namespace CirrusSearch\Fallbacks;
4
5use CirrusSearch\Parser\QueryStringRegex\SearchQueryParseException;
6use CirrusSearch\Search\CirrusSearchResultSet;
7use CirrusSearch\Search\SearchQuery;
8use CirrusSearch\Search\SearchQueryBuilder;
9use CirrusSearch\Searcher;
10use Elastica\ResultSet as ElasticaResultSet;
11use HtmlArmor;
12use ISearchResultSet;
13use MediaWiki\Logger\LoggerFactory;
14
15trait FallbackMethodTrait {
16
17    /**
18     * Check the number of total hits stored in $resultSet
19     * and return true if it's greater or equals than $threshold
20     * NOTE: inter wiki results are check
21     *
22     * @param CirrusSearchResultSet $resultSet
23     * @param int $threshold (defaults to 1).
24     *
25     * @see \ISearchResultSet::getInterwikiResults()
26     * @see \ISearchResultSet::SECONDARY_RESULTS
27     * @return bool
28     */
29    public function resultsThreshold( CirrusSearchResultSet $resultSet, $threshold = 1 ) {
30        if ( $resultSet->getTotalHits() >= $threshold ) {
31            return true;
32        }
33        foreach ( $resultSet->getInterwikiResults( ISearchResultSet::SECONDARY_RESULTS ) as $resultSet ) {
34            if ( $resultSet->getTotalHits() >= $threshold ) {
35                return true;
36            }
37        }
38        return false;
39    }
40
41    /**
42     * Check if any result in the response is fully highlighted on the title field
43     * @param \Elastica\ResultSet $results
44     * @return bool true if a result has its title fully highlighted
45     */
46    public function resultContainsFullyHighlightedMatch( ElasticaResultSet $results ) {
47        foreach ( $results as $result ) {
48            $highlights = $result->getHighlights();
49            // TODO: Should we check redirects as well?
50            // If the whole string is highlighted then return true
51            $regex = '/' . Searcher::HIGHLIGHT_PRE_MARKER . '.*?' . Searcher::HIGHLIGHT_POST_MARKER . '/';
52            if ( isset( $highlights[ 'title' ] ) &&
53                 !trim( preg_replace( $regex, '', $highlights[ 'title' ][ 0 ] ) ) ) {
54                return true;
55            }
56        }
57        return false;
58    }
59
60    /**
61     * If all conditions are met execute a search query using the $suggestedQuery and returns its results.
62     * Conditions are:
63     *  - SearchQuery::isAllowRewrite() must be true on the original query
64     *  - The original query must be a simple bag of words
65     *  - FallbackRunnerContext::costlyCallAllowed() must be true
66     *  - number of displayable results must not exceed $resultsThreshold
67     *
68     * @param FallbackRunnerContext $context
69     * @param SearchQuery $originalQuery
70     * @param string $suggestedQuery
71     * @param HtmlArmor|string|null $suggestedQuerySnippet
72     * @param int $resultsThreshold
73     * @return FallbackStatus
74     * @throws \CirrusSearch\Parser\ParsedQueryClassifierException
75     * @see SearchQuery::isAllowRewrite()
76     * @see FallbackRunnerContext::costlyCallAllowed()
77     * @see FallbackMethodTrait::resultsThreshold()
78     */
79    public function maybeSearchAndRewrite(
80        FallbackRunnerContext $context,
81        SearchQuery $originalQuery,
82        string $suggestedQuery,
83        $suggestedQuerySnippet = null,
84        int $resultsThreshold = 1
85    ): FallbackStatus {
86        $previousSet = $context->getPreviousResultSet();
87        if ( !$originalQuery->isAllowRewrite()
88             || !$context->costlyCallAllowed()
89             || $this->resultsThreshold( $previousSet, $resultsThreshold )
90        ) {
91            // Only provide the suggestion, not the results of the suggestion.
92            return FallbackStatus::suggestQuery( $suggestedQuery, $suggestedQuerySnippet );
93        }
94
95        try {
96            $rewrittenQuery = SearchQueryBuilder::forRewrittenQuery( $originalQuery, $suggestedQuery,
97                    $context->getNamespacePrefixParser(), $context->getCirrusSearchHookRunner() )->build();
98        } catch ( SearchQueryParseException $e ) {
99            LoggerFactory::getInstance( 'CirrusSearch' )
100                ->warning( "Cannot parse rewritten query", [ 'exception' => $e ] );
101            // Suggest the user submits the suggested query directly
102            return FallbackStatus::suggestQuery( $suggestedQuery, $suggestedQuerySnippet );
103        }
104        $searcher = $context->makeSearcher( $rewrittenQuery );
105        $status = $searcher->search( $rewrittenQuery );
106        if ( $status->isOK() && $status->getValue() instanceof CirrusSearchResultSet ) {
107            /**
108             * @var CirrusSearchResultSet $newResults
109             */
110            $newResults = $status->getValue();
111            return FallbackStatus::replaceLocalResults( $newResults, $suggestedQuery, $suggestedQuerySnippet );
112        } else {
113            // The suggested query returned no results, there doesn't seem to be any benefit
114            // to sugesting it to the user.
115            return FallbackStatus::noSuggestion();
116        }
117    }
118}