Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
88.66% covered (warning)
88.66%
86 / 97
37.50% covered (danger)
37.50%
3 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
IndexLookupFallbackMethod
88.66% covered (warning)
88.66%
86 / 97
37.50% covered (danger)
37.50%
3 / 8
32.40
0.00% covered (danger)
0.00%
0 / 1
 getMetrics
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getSearchRequest
95.83% covered (success)
95.83%
23 / 24
0.00% covered (danger)
0.00%
0 / 1
5
 extractParam
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
5
 __construct
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
1
 build
95.24% covered (success)
95.24%
20 / 21
0.00% covered (danger)
0.00%
0 / 1
5
 successApproximation
76.92% covered (warning)
76.92%
10 / 13
0.00% covered (danger)
0.00%
0 / 1
6.44
 extractMethodResponse
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 rewrite
66.67% covered (warning)
66.67%
10 / 15
0.00% covered (danger)
0.00%
0 / 1
7.33
1<?php
2
3namespace CirrusSearch\Fallbacks;
4
5use CirrusSearch\InterwikiResolver;
6use CirrusSearch\Parser\AST\Visitor\QueryFixer;
7use CirrusSearch\Parser\BasicQueryClassifier;
8use CirrusSearch\Profile\ArrayPathSetter;
9use CirrusSearch\Profile\SearchProfileException;
10use CirrusSearch\Profile\SearchProfileService;
11use CirrusSearch\Search\SearchMetricsProvider;
12use CirrusSearch\Search\SearchQuery;
13use Elastica\Client;
14use Elastica\Query;
15use Elastica\Search;
16
17class IndexLookupFallbackMethod implements FallbackMethod, ElasticSearchRequestFallbackMethod, SearchMetricsProvider {
18    use FallbackMethodTrait;
19
20    /**
21     * @var string One of 'enabled' or 'metrics' indicating how
22     * the fallback operates. 'metrics' is for collecting data without
23     * using the result.
24     */
25    private string $mode;
26
27    /**
28     * @var SearchQuery
29     */
30    private $query;
31
32    /**
33     * @var string
34     */
35    private $index;
36
37    /**
38     * @var array
39     */
40    private $queryTemplate;
41
42    /**
43     * @var string[]
44     */
45    private $queryParams;
46
47    /**
48     * @var string
49     */
50    private $suggestionField;
51
52    /**
53     * @var array
54     */
55    private $profileParams;
56
57    /**
58     * @var QueryFixer
59     */
60    private $queryFixer;
61
62    /**
63     * @var array
64     */
65    private $searchMetrics = [];
66
67    /**
68     * @var string[] Stored fields to request from elasticsearch
69     */
70    private $storedFields;
71
72    public function getMetrics(): array {
73        return $this->searchMetrics;
74    }
75
76    /**
77     * @param Client $client
78     * @return Search|null null if no additional request is to be executed for this method.
79     * @see FallbackRunnerContext::getMethodResponse()
80     */
81    public function getSearchRequest( Client $client ) {
82        $fixablePart = $this->queryFixer->getFixablePart();
83        if ( $fixablePart === null ) {
84            return null;
85        }
86        $queryParams = array_map(
87            function ( $v ) {
88                switch ( $v ) {
89                    case 'query':
90                        return $this->queryFixer->getFixablePart();
91                    case 'wiki':
92                        return $this->query->getSearchConfig()->getWikiId();
93                    default:
94                        return $this->extractParam( $v );
95                }
96            },
97            $this->queryParams
98        );
99        $arrayPathSetter = new ArrayPathSetter( $queryParams );
100        $query = $arrayPathSetter->transform( $this->queryTemplate );
101        $query = new Query( [ 'query' => $query ] );
102        $query->setFrom( 0 )
103            ->setSize( 1 )
104            ->setSource( false )
105            ->setStoredFields( $this->storedFields );
106        $search = new Search( $client );
107        $search->setQuery( $query )
108            ->addIndex( $client->getIndex( $this->index ) );
109        return $search;
110    }
111
112    /**
113     * @param string $keyAndValue
114     * @return mixed
115     */
116    private function extractParam( $keyAndValue ) {
117        $ar = explode( ':', $keyAndValue, 2 );
118        if ( count( $ar ) != 2 ) {
119            throw new SearchProfileException( "Invalid profile parameter [$keyAndValue]" );
120        }
121        [ $key, $value ] = $ar;
122        switch ( $key ) {
123            case 'params':
124                $paramValue = $this->profileParams[$value] ?? null;
125                if ( $paramValue == null ) {
126                    throw new SearchProfileException( "Missing profile parameter [$value]" );
127                }
128                return $paramValue;
129            default:
130                throw new SearchProfileException( "Unsupported profile parameter type [$key]" );
131        }
132    }
133
134    /**
135     * @param string $mode Either 'enabled' or 'metrics'.
136     * @param SearchQuery $query
137     * @param string $index
138     * @param array $queryTemplate
139     * @param string $suggestionField
140     * @param string[] $queryParams
141     * @param string[] $metricFields Additional stored fields to request and
142     *  report with metrics.
143     * @param array $profileParams
144     */
145    public function __construct(
146        string $mode,
147        SearchQuery $query,
148        $index,
149        $queryTemplate,
150        $suggestionField,
151        array $queryParams,
152        array $metricFields,
153        array $profileParams
154    ) {
155        $this->mode = $mode;
156        $this->query = $query;
157        $this->index = $index;
158        $this->queryTemplate = $queryTemplate;
159        $this->suggestionField = $suggestionField;
160        $this->queryParams = $queryParams;
161        $this->profileParams = $profileParams;
162        $this->queryFixer = QueryFixer::build( $this->query->getParsedQuery() );
163        $this->storedFields = $metricFields;
164        $this->storedFields[] = $suggestionField;
165    }
166
167    /**
168     * @param SearchQuery $query
169     * @param array $params
170     * @param InterwikiResolver|null $interwikiResolver
171     * @return FallbackMethod|null the method instance or null if unavailable
172     */
173    public static function build( SearchQuery $query, array $params, ?InterwikiResolver $interwikiResolver = null ) {
174        if ( !$query->isWithDYMSuggestion() ) {
175            return null;
176        }
177        // TODO: Should this be tested at an upper level?
178        if ( $query->getOffset() !== 0 ) {
179            return null;
180        }
181        if ( !$query->getParsedQuery()->isQueryOfClass( BasicQueryClassifier::SIMPLE_BAG_OF_WORDS ) ) {
182            // This method does not highlight the problem and thus is not able to use QueryFixer to fix parts of the query.
183            // Ignore complex queries to avoid destructing them.
184            return null;
185        }
186        if ( !isset( $params['profile'] ) ) {
187            throw new SearchProfileException( "Missing mandatory field profile" );
188        }
189
190        $profileParams = $params['profile_params'] ?? [];
191
192        $profile = $query->getSearchConfig()->getProfileService()
193            ->loadProfileByName( SearchProfileService::INDEX_LOOKUP_FALLBACK, $params['profile'] );
194        '@phan-var array $profile';
195
196        return new self(
197            $params['mode'] ?? 'enabled',
198            $query,
199            $profile['index'],
200            $profile['query'],
201            $profile['suggestion_field'],
202            $profile['params'],
203            $profile['metric_fields'],
204            $profileParams
205        );
206    }
207
208    /**
209     * @param FallbackRunnerContext $context
210     * @return float
211     */
212    public function successApproximation( FallbackRunnerContext $context ) {
213        if ( $this->mode !== 'enabled' ) {
214            return 0.0;
215        }
216        $rset = $this->extractMethodResponse( $context );
217        if ( $rset === null || $rset->getResults() === [] ) {
218            return 0.0;
219        }
220        $fields = $rset->getResults()[0]->getFields();
221        $suggestion = $fields[$this->suggestionField][0] ?? null;
222        if ( $suggestion === null ) {
223            return 0.0;
224        }
225        // Metrics fields are everything except the suggestion field
226        unset( $fields[$this->suggestionField] );
227        if ( $fields ) {
228            $this->searchMetrics += $fields;
229        }
230        return 0.5;
231    }
232
233    /**
234     * @param FallbackRunnerContext $context
235     * @return \Elastica\ResultSet|null null if there are no response or no results
236     */
237    private function extractMethodResponse( FallbackRunnerContext $context ) {
238        if ( !$context->hasMethodResponse() ) {
239            return null;
240        }
241
242        return $context->getMethodResponse();
243    }
244
245    /**
246     * Rewrite the results,
247     * A costly call is allowed here, if nothing is to be done $previousSet
248     * must be returned.
249     *
250     * @param FallbackRunnerContext $context
251     * @return FallbackStatus
252     */
253    public function rewrite( FallbackRunnerContext $context ): FallbackStatus {
254        if ( $this->mode !== 'enabled' ) {
255            // we are only collecting metrics
256            return FallbackStatus::noSuggestion();
257        }
258        $previousSet = $context->getPreviousResultSet();
259        if ( !$context->costlyCallAllowed() ) {
260            // a method rewrote the query before us.
261            return FallbackStatus::noSuggestion();
262        }
263        if ( $previousSet->getSuggestionQuery() !== null ) {
264            // a method suggested something before us
265            return FallbackStatus::noSuggestion();
266        }
267        $resultSet = $context->getMethodResponse();
268        if ( !$resultSet->getResults() ) {
269            return FallbackStatus::noSuggestion();
270        }
271        $res = $resultSet->getResults()[0];
272        $suggestedQuery = $res->getFields()[$this->suggestionField][0] ?? null;
273        if ( $suggestedQuery === null ) {
274            return FallbackStatus::noSuggestion();
275        }
276        // Maybe rewrite
277        return $this->maybeSearchAndRewrite( $context, $this->query, $suggestedQuery );
278    }
279}