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