Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 80 |
|
0.00% |
0 / 10 |
CRAP | |
0.00% |
0 / 1 |
SearchRequestLog | |
0.00% |
0 / 80 |
|
0.00% |
0 / 10 |
1122 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
setCachedResult | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
finish | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
30 | |||
isCachedResponse | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getElasticTookMs | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
getLogVariables | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
20 | |||
getRequests | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
extractRequestVariables | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
12 | |||
extractResponseVariables | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
90 | |||
extractHits | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
30 |
1 | <?php |
2 | |
3 | namespace CirrusSearch; |
4 | |
5 | use MediaWiki\Title\Title; |
6 | |
7 | class SearchRequestLog extends BaseRequestLog { |
8 | /** |
9 | * @var bool |
10 | */ |
11 | private $cached = false; |
12 | |
13 | /** |
14 | * @var \Elastica\Client |
15 | */ |
16 | private $client; |
17 | |
18 | /** |
19 | * @var \Elastica\Request|null The elasticsearch request for this log |
20 | */ |
21 | protected $request; |
22 | |
23 | /** |
24 | * @var \Elastica\Request|null The elasticsearch request issued prior to |
25 | * this log, used to protect against accidentaly using the wrong request. |
26 | */ |
27 | private $lastRequest; |
28 | |
29 | /** |
30 | * @var \Elastica\Response|null The elasticsearch response for this log |
31 | */ |
32 | protected $response; |
33 | |
34 | /** |
35 | * @var \Elastica\Response|null The elasticsearch response issued prior to |
36 | * this log, used to protect against accidentaly using the wrong response. |
37 | */ |
38 | private $lastResponse; |
39 | |
40 | /** |
41 | * @var int[]|null (null if unknown) |
42 | */ |
43 | private $namespaces; |
44 | |
45 | /** |
46 | * @param \Elastica\Client $client |
47 | * @param string $description |
48 | * @param string $queryType |
49 | * @param array $extra |
50 | * @param int[]|null $namespaces list of known namespaces to query, null if unknown or inappropriate |
51 | */ |
52 | public function __construct( \Elastica\Client $client, $description, $queryType, array $extra = [], ?array $namespaces = null ) { |
53 | parent::__construct( $description, $queryType, $extra ); |
54 | $this->client = $client; |
55 | $this->lastRequest = $client->getLastRequest(); |
56 | $this->lastResponse = $client->getLastResponse(); |
57 | $this->namespaces = $namespaces; |
58 | } |
59 | |
60 | /** |
61 | * @param string[] $extra |
62 | */ |
63 | public function setCachedResult( array $extra ) { |
64 | $this->extra += $extra; |
65 | $this->cached = true; |
66 | } |
67 | |
68 | public function finish() { |
69 | if ( $this->request || $this->response ) { |
70 | throw new \RuntimeException( 'Finishing a log more than once' ); |
71 | } |
72 | parent::finish(); |
73 | $request = $this->client->getLastRequest(); |
74 | $this->request = $request === $this->lastRequest ? null : $request; |
75 | $this->lastRequest = null; |
76 | $response = $this->client->getLastResponse(); |
77 | $this->response = $response === $this->lastResponse ? null : $response; |
78 | $this->lastResponse = null; |
79 | } |
80 | |
81 | /** |
82 | * @return bool |
83 | */ |
84 | public function isCachedResponse() { |
85 | return $this->cached; |
86 | } |
87 | |
88 | /** |
89 | * @return int |
90 | */ |
91 | public function getElasticTookMs() { |
92 | if ( !$this->response ) { |
93 | return -1; |
94 | } |
95 | $data = $this->response->getData(); |
96 | |
97 | return $data['took'] ?? -1; |
98 | } |
99 | |
100 | /** |
101 | * @return array |
102 | */ |
103 | public function getLogVariables() { |
104 | $vars = [ |
105 | 'queryType' => $this->queryType, |
106 | 'tookMs' => $this->getTookMs(), |
107 | ] + $this->extra; |
108 | |
109 | if ( !$this->request || !$this->response ) { |
110 | // @todo this is probably incomplete |
111 | return $vars; |
112 | } |
113 | |
114 | $index = explode( '/', $this->request->getPath() ); |
115 | $vars['index'] = $index[0]; |
116 | if ( $this->namespaces !== null ) { |
117 | $vars['namespaces'] = $this->namespaces; |
118 | } |
119 | |
120 | return $this->extractRequestVariables( $this->request->getData() ) + |
121 | $this->extractResponseVariables( $this->response->getData() ) + |
122 | // $vars must come *after* extractResponseVariables, because items |
123 | // like 'suggestion' override data provided in $this->extra |
124 | $vars; |
125 | } |
126 | |
127 | /** |
128 | * @return array[] |
129 | */ |
130 | public function getRequests() { |
131 | $vars = $this->getLogVariables(); |
132 | if ( $this->response ) { |
133 | $vars['hits'] = $this->extractHits( $this->response->getData() ); |
134 | } |
135 | |
136 | return [ $vars ]; |
137 | } |
138 | |
139 | /** |
140 | * @param array $query |
141 | * @return array |
142 | */ |
143 | protected function extractRequestVariables( $query ) { |
144 | if ( !is_array( $query ) ) { |
145 | // @todo log something and verify that this can still happen? |
146 | return []; |
147 | } |
148 | |
149 | $vars = [ |
150 | 'hitsOffset' => $query['from'] ?? 0, |
151 | ]; |
152 | if ( !empty( $query['suggest'] ) ) { |
153 | $vars['suggestionRequested'] = true; |
154 | } else { |
155 | $vars['suggestionRequested'] = false; |
156 | } |
157 | |
158 | return $vars; |
159 | } |
160 | |
161 | /** |
162 | * @param array $responseData |
163 | * @return array |
164 | */ |
165 | protected function extractResponseVariables( $responseData ) { |
166 | if ( !is_array( $responseData ) ) { |
167 | // No known offenders, but just in case... |
168 | return []; |
169 | } |
170 | $vars = []; |
171 | if ( isset( $responseData['took'] ) ) { |
172 | $vars['elasticTookMs'] = $responseData['took']; |
173 | } |
174 | $hitsTotal = $responseData['hits']['total'] ?? null; |
175 | if ( $hitsTotal !== null ) { |
176 | // BC for ES6 |
177 | if ( is_int( $hitsTotal ) ) { |
178 | $vars['hitsTotal'] = $hitsTotal; |
179 | } else { |
180 | Util::setIfDefined( $hitsTotal, 'value', $vars, 'hitsTotal' ); |
181 | } |
182 | } |
183 | if ( isset( $responseData['hits']['max_score'] ) ) { |
184 | $vars['maxScore'] = $responseData['hits']['max_score']; |
185 | } |
186 | if ( isset( $responseData['hits']['hits'] ) ) { |
187 | $vars['hitsReturned'] = count( $responseData['hits']['hits'] ); |
188 | } |
189 | if ( isset( $responseData['suggest']['suggest'][0]['options'][0]['text'] ) ) { |
190 | $vars['suggestion'] = $responseData['suggest']['suggest'][0]['options'][0]['text']; |
191 | } |
192 | |
193 | // in case of failures from Elastica |
194 | if ( isset( $responseData['message'] ) ) { |
195 | $vars['error_message'] = $responseData['message']; |
196 | } |
197 | |
198 | return $vars; |
199 | } |
200 | |
201 | /** |
202 | * @param array $responseData |
203 | * @return array[] |
204 | */ |
205 | protected function extractHits( array $responseData ) { |
206 | $hits = []; |
207 | if ( isset( $responseData['hits']['hits'] ) ) { |
208 | foreach ( $responseData['hits']['hits'] as $hit ) { |
209 | if ( !isset( $hit['_source']['namespace'] ) |
210 | || !isset( $hit['_source']['title'] ) |
211 | ) { |
212 | // This is probably a query that does not return pages like |
213 | // geo or namespace queries. |
214 | // @todo Should these get their own request logging class? |
215 | continue; |
216 | } |
217 | // duplication of work...this happens in the transformation |
218 | // stage but we can't see that here...Perhaps we instead attach |
219 | // this data at a later stage like CompletionSuggester? |
220 | $title = Title::makeTitle( $hit['_source']['namespace'], $hit['_source']['title'] ); |
221 | $hits[] = [ |
222 | 'title' => (string)$title, |
223 | 'index' => $hit['_index'] ?? "", |
224 | 'pageId' => $hit['_id'] ?? -1, |
225 | 'score' => $hit['_score'] ?? -1.0, |
226 | ]; |
227 | } |
228 | } |
229 | |
230 | return $hits; |
231 | } |
232 | } |