Code Coverage
 
Classes and Traits
Functions and Methods
Lines
Total
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 76
SearchRequestLog
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 10
1056
0.00% covered (danger)
0.00%
0 / 76
 __construct
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 6
 setCachedResult
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 3
 finish
0.00% covered (danger)
0.00%
0 / 1
30
0.00% covered (danger)
0.00%
0 / 10
 isCachedResponse
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 getElasticTookMs
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 4
 getLogVariables
0.00% covered (danger)
0.00%
0 / 1
20
0.00% covered (danger)
0.00%
0 / 12
 getRequests
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 4
 extractRequestVariables
0.00% covered (danger)
0.00%
0 / 1
12
0.00% covered (danger)
0.00%
0 / 7
 extractResponseVariables
0.00% covered (danger)
0.00%
0 / 1
72
0.00% covered (danger)
0.00%
0 / 16
 extractHits
0.00% covered (danger)
0.00%
0 / 1
30
0.00% covered (danger)
0.00%
0 / 13
<?php
namespace CirrusSearch;
use Title;
class SearchRequestLog extends BaseRequestLog {
    /**
     * @var bool
     */
    private $cached = false;
    /**
     * @var \Elastica\Client
     */
    private $client;
    /**
     * @var \Elastica\Request|null The elasticsearch request for this log
     */
    protected $request;
    /**
     * @var \Elastica\Request|null The elasticsearch request issued prior to
     *  this log, used to protect against accidentaly using the wrong request.
     */
    private $lastRequest;
    /**
     * @var \Elastica\Response|null The elasticsearch response for this log
     */
    protected $response;
    /**
     * @var \Elastica\Response|null The elasticsearch response issued prior to
     *  this log, used to protect against accidentaly using the wrong response.
     */
    private $lastResponse;
    /**
     * @var int[]|null (null if unknown)
     */
    private $namespaces;
    /**
     * @param \Elastica\Client $client
     * @param string $description
     * @param string $queryType
     * @param array $extra
     * @param int[]|null $namespaces list of known namespaces to query, null if unknown or inappropriate
     */
    public function __construct( \Elastica\Client $client, $description, $queryType, array $extra = [], array $namespaces = null ) {
        parent::__construct( $description, $queryType, $extra );
        $this->client = $client;
        $this->lastRequest = $client->getLastRequest();
        $this->lastResponse = $client->getLastResponse();
        $this->namespaces = $namespaces;
    }
    /**
     * @param string[] $extra
     */
    public function setCachedResult( array $extra ) {
        $this->extra += $extra;
        $this->cached = true;
    }
    public function finish() {
        if ( $this->request || $this->response ) {
            throw new \RuntimeException( 'Finishing a log more than once' );
        }
        parent::finish();
        $request = $this->client->getLastRequest();
        $this->request = $request === $this->lastRequest ? null : $request;
        $this->lastRequest = null;
        $response = $this->client->getLastResponse();
        $this->response = $response === $this->lastResponse ? null : $response;
        $this->lastResponse = null;
    }
    /**
     * @return bool
     */
    public function isCachedResponse() {
        return $this->cached;
    }
    /**
     * @return int
     */
    public function getElasticTookMs() {
        if ( !$this->response ) {
            return -1;
        }
        $data = $this->response->getData();
        return $data['took'] ?? -1;
    }
    /**
     * @return array
     */
    public function getLogVariables() {
        $vars = [
            'queryType' => $this->queryType,
            'tookMs' => $this->getTookMs(),
        ] + $this->extra;
        if ( !$this->request || !$this->response ) {
            // @todo this is probably incomplete
            return $vars;
        }
        $index = explode( '/', $this->request->getPath() );
        $vars['index'] = $index[0];
        if ( $this->namespaces !== null ) {
            $vars['namespaces'] = $this->namespaces;
        }
        return $this->extractRequestVariables( $this->request->getData() ) +
            $this->extractResponseVariables( $this->response->getData() ) +
            // $vars must come *after* extractResponseVariables, because items
            // like 'suggestion' override data provided in $this->extra
            $vars;
    }
    /**
     * @return array[]
     */
    public function getRequests() {
        $vars = $this->getLogVariables();
        if ( $this->response ) {
            $vars['hits'] = $this->extractHits( $this->response->getData() );
        }
        return [ $vars ];
    }
    /**
     * @param array $query
     * @return array
     */
    protected function extractRequestVariables( $query ) {
        if ( !is_array( $query ) ) {
            // TODO: remove references to type (T308044)
            // @todo log something? this means some request was not as expected. Often
            // happens with multi-endpoints such as \Elastica\Type::deleteIds()
            return [];
        }
        $vars = [
            'hitsOffset' => $query['from'] ?? 0,
        ];
        if ( !empty( $query['suggest'] ) ) {
            $vars['suggestionRequested'] = true;
        } else {
            $vars['suggestionRequested'] = false;
        }
        return $vars;
    }
    /**
     * @param array $responseData
     * @return array
     */
    protected function extractResponseVariables( $responseData ) {
        if ( !is_array( $responseData ) ) {
            // No known offenders, but just in case...
            return [];
        }
        $vars = [];
        if ( isset( $responseData['took'] ) ) {
            $vars['elasticTookMs'] = $responseData['took'];
        }
        if ( isset( $responseData['hits']['total'] ) ) {
            $vars['hitsTotal'] = $responseData['hits']['total'];
        }
        if ( isset( $responseData['hits']['max_score'] ) ) {
            $vars['maxScore'] = $responseData['hits']['max_score'];
        }
        if ( isset( $responseData['hits']['hits'] ) ) {
            $vars['hitsReturned'] = count( $responseData['hits']['hits'] );
        }
        if ( isset( $responseData['suggest']['suggest'][0]['options'][0]['text'] ) ) {
            $vars['suggestion'] = $responseData['suggest']['suggest'][0]['options'][0]['text'];
        }
        // in case of failures from Elastica
        if ( isset( $responseData['message'] ) ) {
            $vars['error_message'] = $responseData['message'];
        }
        return $vars;
    }
    /**
     * @param array $responseData
     * @return array[]
     */
    protected function extractHits( array $responseData ) {
        $hits = [];
        if ( isset( $responseData['hits']['hits'] ) ) {
            foreach ( $responseData['hits']['hits'] as $hit ) {
                if ( !isset( $hit['_source']['namespace'] )
                    || !isset( $hit['_source']['title'] )
                ) {
                    // This is probably a query that does not return pages like
                    // geo or namespace queries.
                    // @todo Should these get their own request logging class?
                    continue;
                }
                // duplication of work...this happens in the transformation
                // stage but we can't see that here...Perhaps we instead attach
                // this data at a later stage like CompletionSuggester?
                $title = Title::makeTitle( $hit['_source']['namespace'], $hit['_source']['title'] );
                $hits[] = [
                    'title' => (string)$title,
                    'index' => $hit['_index'] ?? "",
                    'pageId' => $hit['_id'] ?? -1,
                    'score' => $hit['_score'] ?? -1.0,
                ];
            }
        }
        return $hits;
    }
}