Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 80
0.00% covered (danger)
0.00%
0 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
SearchRequestLog
0.00% covered (danger)
0.00%
0 / 80
0.00% covered (danger)
0.00%
0 / 10
1122
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 setCachedResult
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 finish
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
30
 isCachedResponse
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getElasticTookMs
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 getLogVariables
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
20
 getRequests
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 extractRequestVariables
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
 extractResponseVariables
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
90
 extractHits
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
30
1<?php
2
3namespace CirrusSearch;
4
5use MediaWiki\Title\Title;
6
7class 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}