Code Coverage
 
Classes and Traits
Functions and Methods
Lines
Total
0.00% covered (danger)
0.00%
0 / 1
50.00% covered (danger)
50.00%
4 / 8
CRAP
90.70% covered (success)
90.70%
78 / 86
IndexLookupFallbackMethod
0.00% covered (danger)
0.00%
0 / 1
50.00% covered (danger)
50.00%
4 / 8
26.54
90.70% covered (success)
90.70%
78 / 86
 getMetrics
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 getSearchRequest
100.00% covered (success)
100.00%
1 / 1
4
100.00% covered (success)
100.00%
22 / 22
 extractParam
100.00% covered (success)
100.00%
1 / 1
4
100.00% covered (success)
100.00%
10 / 10
 __construct
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
10 / 10
 build
0.00% covered (danger)
0.00%
0 / 1
4.00
93.75% covered (success)
93.75%
15 / 16
 successApproximation
0.00% covered (danger)
0.00%
0 / 1
5.15
81.82% covered (warning)
81.82%
9 / 11
 extractMethodResponse
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
3 / 3
 rewrite
0.00% covered (danger)
0.00%
0 / 1
5.73
69.23% covered (warning)
69.23%
9 / 13
<?php
namespace CirrusSearch\Fallbacks;
use CirrusSearch\InterwikiResolver;
use CirrusSearch\Parser\AST\Visitor\QueryFixer;
use CirrusSearch\Profile\ArrayPathSetter;
use CirrusSearch\Profile\SearchProfileException;
use CirrusSearch\Profile\SearchProfileService;
use CirrusSearch\Search\SearchMetricsProvider;
use CirrusSearch\Search\SearchQuery;
use Elastica\Client;
use Elastica\Query;
use Elastica\Search;
class IndexLookupFallbackMethod implements FallbackMethod, ElasticSearchRequestFallbackMethod, SearchMetricsProvider {
    use FallbackMethodTrait;
    /**
     * @var SearchQuery
     */
    private $query;
    /**
     * @var string
     */
    private $index;
    /**
     * @var array
     */
    private $queryTemplate;
    /**
     * @var string[]
     */
    private $queryParams;
    /**
     * @var string
     */
    private $suggestionField;
    /**
     * @var array
     */
    private $profileParams;
    /**
     * @var QueryFixer
     */
    private $queryFixer;
    /**
     * @var array
     */
    private $searchMetrics = [];
    /**
     * @var string[] Stored fields to request from elasticsearch
     */
    private $storedFields;
    public function getMetrics(): array {
        return $this->searchMetrics;
    }
    /**
     * @param Client $client
     * @return Search|null null if no additional request is to be executed for this method.
     * @see FallbackRunnerContext::getMethodResponse()
     */
    public function getSearchRequest( Client $client ) {
        $fixablePart = $this->queryFixer->getFixablePart();
        if ( $fixablePart === null ) {
            return null;
        }
        $queryParams = array_map(
            function ( $v ) {
                switch ( $v ) {
                    case 'query':
                        return $this->queryFixer->getFixablePart();
                    case 'wiki':
                        return $this->query->getSearchConfig()->getWikiId();
                    default:
                        return $this->extractParam( $v );
                }
            },
            $this->queryParams
        );
        $arrayPathSetter = new ArrayPathSetter( $queryParams );
        $query = $arrayPathSetter->transform( $this->queryTemplate );
        $query = new Query( [ 'query' => $query ] );
        $query->setFrom( 0 )
            ->setSize( 1 )
            ->setSource( false )
            ->setStoredFields( $this->storedFields );
        $search = new Search( $client );
        $search->setQuery( $query )
            ->addIndex( $this->index );
        return $search;
    }
    /**
     * @param string $keyAndValue
     * @return mixed
     */
    private function extractParam( $keyAndValue ) {
        $ar = explode( ':', $keyAndValue, 2 );
        if ( count( $ar ) != 2 ) {
            throw new SearchProfileException( "Invalid profile parameter [$keyAndValue]" );
        }
        list( $key, $value ) = $ar;
        switch ( $key ) {
            case 'params':
                $paramValue = $this->profileParams[$value] ?? null;
                if ( $paramValue == null ) {
                    throw new SearchProfileException( "Missing profile parameter [$value]" );
                }
                return $paramValue;
            default:
                throw new SearchProfileException( "Unsupported profile parameter type [$key]" );
        }
    }
    /**
     * @param SearchQuery $query
     * @param string $index
     * @param array $queryTemplate
     * @param string $suggestionField
     * @param string[] $queryParams
     * @param string[] $metricFields Additional stored fields to request and
     *  report with metrics.
     * @param array $profileParams
     */
    public function __construct(
        SearchQuery $query,
        $index,
        $queryTemplate,
        $suggestionField,
        array $queryParams,
        array $metricFields,
        array $profileParams
    ) {
        $this->query = $query;
        $this->index = $index;
        $this->queryTemplate = $queryTemplate;
        $this->suggestionField = $suggestionField;
        $this->queryParams = $queryParams;
        $this->profileParams = $profileParams;
        $this->queryFixer = QueryFixer::build( $this->query->getParsedQuery() );
        $this->storedFields = $metricFields;
        $this->storedFields[] = $suggestionField;
    }
    /**
     * @param SearchQuery $query
     * @param array $params
     * @param InterwikiResolver|null $interwikiResolver
     * @return FallbackMethod|null the method instance or null if unavailable
     */
    public static function build( SearchQuery $query, array $params, InterwikiResolver $interwikiResolver = null ) {
        if ( !$query->isWithDYMSuggestion() ) {
            return null;
        }
        // TODO: Should this be tested at an upper level?
        if ( $query->getOffset() !== 0 ) {
            return null;
        }
        if ( !isset( $params['profile'] ) ) {
            throw new SearchProfileException( "Missing mandatory field profile" );
        }
        $profileParams = $params['profile_params'] ?? [];
        $profile = $query->getSearchConfig()->getProfileService()
            ->loadProfileByName( SearchProfileService::INDEX_LOOKUP_FALLBACK, $params['profile'] );
        '@phan-var array $profile';
        return new self(
            $query,
            $profile['index'],
            $profile['query'],
            $profile['suggestion_field'],
            $profile['params'],
            $profile['metric_fields'],
            $profileParams
        );
    }
    /**
     * @param FallbackRunnerContext $context
     * @return float
     */
    public function successApproximation( FallbackRunnerContext $context ) {
        $rset = $this->extractMethodResponse( $context );
        if ( $rset === null || $rset->getResults() === [] ) {
            return 0.0;
        }
        $fields = $rset->getResults()[0]->getFields();
        $suggestion = $fields[$this->suggestionField][0] ?? null;
        if ( $suggestion === null ) {
            return 0.0;
        }
        // Metrics fields are everything except the suggestion field
        unset( $fields[$this->suggestionField] );
        if ( $fields ) {
            $this->searchMetrics += $fields;
        }
        return 0.5;
    }
    /**
     * @param FallbackRunnerContext $context
     * @return \Elastica\ResultSet|null null if there are no response or no results
     */
    private function extractMethodResponse( FallbackRunnerContext $context ) {
        if ( !$context->hasMethodResponse() ) {
            return null;
        }
        return $context->getMethodResponse();
    }
    /**
     * Rewrite the results,
     * A costly call is allowed here, if nothing is to be done $previousSet
     * must be returned.
     *
     * @param FallbackRunnerContext $context
     * @return FallbackStatus
     */
    public function rewrite( FallbackRunnerContext $context ): FallbackStatus {
        $previousSet = $context->getPreviousResultSet();
        if ( !$context->costlyCallAllowed() ) {
            // a method rewrote the query before us.
            return FallbackStatus::noSuggestion();
        }
        if ( $previousSet->getSuggestionQuery() !== null ) {
            // a method suggested something before us
            return FallbackStatus::noSuggestion();
        }
        $resultSet = $context->getMethodResponse();
        if ( empty( $resultSet->getResults() ) ) {
            return FallbackStatus::noSuggestion();
        }
        $res = $resultSet->getResults()[0];
        $suggestedQuery = $res->getFields()[$this->suggestionField][0] ?? null;
        if ( $suggestedQuery === null ) {
            return FallbackStatus::noSuggestion();
        }
        // Maybe rewrite
        return $this->maybeSearchAndRewrite( $context, $this->query, $suggestedQuery );
    }
}