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