Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
76.71% covered (warning)
76.71%
56 / 73
25.00% covered (danger)
25.00%
1 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
FunctionScoreChain
76.71% covered (warning)
76.71%
56 / 73
25.00% covered (danger)
25.00%
1 / 4
43.14
0.00% covered (danger)
0.00%
0 / 1
 __construct
90.91% covered (success)
90.91%
10 / 11
0.00% covered (danger)
0.00%
0 / 1
3.01
 applyOverrides
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 buildRescoreQuery
72.73% covered (warning)
72.73%
8 / 11
0.00% covered (danger)
0.00%
0 / 1
6.73
 getImplementation
73.47% covered (warning)
73.47%
36 / 49
0.00% covered (danger)
0.00%
0 / 1
29.24
1<?php
2/**
3 * @license GPL-2.0-or-later
4 */
5
6namespace CirrusSearch\Search\Rescore;
7
8use CirrusSearch\CirrusConfigNames;
9use CirrusSearch\CirrusSearchHookRunner;
10use CirrusSearch\Profile\ArrayPathSetter;
11use CirrusSearch\Profile\SearchProfileService;
12use CirrusSearch\Search\SearchContext;
13use Elastica\Query\FunctionScore;
14
15class FunctionScoreChain {
16    /**
17     * List of allowed function_score param
18     * we keep boost and boost_mode even if they do not make sense
19     * here since we do not allow to specify the query param.
20     * The query will be MatchAll with a score to 1.
21     *
22     * @var string[]
23     */
24    private static $functionScoreParams = [
25        'boost',
26        'boost_mode',
27        'max_boost',
28        'score_mode',
29        'min_score'
30    ];
31
32    /**
33     * @var SearchContext
34     */
35    private $context;
36
37    /**
38     * @var FunctionScoreDecorator
39     */
40    private $functionScore;
41
42    /**
43     * @var array the function score chain
44     */
45    private $chain;
46
47    /**
48     * @var string the name of the chain
49     */
50    private $chainName;
51    /**
52     * @var CirrusSearchHookRunner
53     */
54    private $cirrusSearchHookRunner;
55
56    /**
57     * Builds a new function score chain.
58     *
59     * @param SearchContext $context
60     * @param string $chainName the name of the chain (must be a valid
61     *  chain in wgCirrusSearchRescoreFunctionScoreChains)
62     * @param array $overrides Parameter overrides
63     * @param CirrusSearchHookRunner $cirrusSearchHookRunner
64     */
65    public function __construct( SearchContext $context, $chainName, $overrides, CirrusSearchHookRunner $cirrusSearchHookRunner ) {
66        $this->chainName = $chainName;
67        $this->context = $context;
68        $this->functionScore = new FunctionScoreDecorator();
69        $chain = $context->getConfig()
70            ->getProfileService()
71            ->loadProfileByName( SearchProfileService::RESCORE_FUNCTION_CHAINS, $chainName );
72        $this->chain = $overrides ? $this->applyOverrides( $chain, $overrides ) : $chain;
73
74        $params = array_intersect_key( $this->chain, array_flip( self::$functionScoreParams ) );
75        foreach ( $params as $param => $value ) {
76            $this->functionScore->setParam( $param, $value );
77        }
78        $this->cirrusSearchHookRunner = $cirrusSearchHookRunner;
79    }
80
81    private function applyOverrides( array $chain, array $overrides ): array {
82        $transformer = new ArrayPathSetter( $overrides );
83        return $transformer->transform( $chain );
84    }
85
86    /**
87     * @return FunctionScore|null the rescore query or null none of functions were
88     *  needed.
89     * @throws InvalidRescoreProfileException
90     */
91    public function buildRescoreQuery() {
92        if ( !isset( $this->chain['functions'] ) ) {
93            throw new InvalidRescoreProfileException( "No functions defined in chain {$this->chainName}." );
94        }
95        foreach ( $this->chain['functions'] as $func ) {
96            $impl = $this->getImplementation( $func );
97            $impl->append( $this->functionScore );
98        }
99        // Add extensions
100        if ( !empty( $this->chain['add_extensions'] ) ) {
101            foreach ( $this->context->getExtraScoreBuilders() as $extBuilder ) {
102                $extBuilder->append( $this->functionScore );
103            }
104        }
105        if ( !$this->functionScore->isEmptyFunction() ) {
106            return $this->functionScore;
107        }
108        return null;
109    }
110
111    /**
112     * @param array $func
113     * @return BoostFunctionBuilder
114     * @throws InvalidRescoreProfileException
115     */
116    private function getImplementation( $func ) {
117        $weight = $func['weight'] ?? 1;
118        $config = $this->context->getConfig();
119        switch ( $func['type'] ) {
120            case 'boostlinks':
121                return new IncomingLinksFunctionScoreBuilder();
122            case 'recency':
123                foreach ( $this->context->getExtraScoreBuilders() as $boostFunctionBuilder ) {
124                    if ( $boostFunctionBuilder instanceof PreferRecentFunctionScoreBuilder ) {
125                        // If prefer-recent was used as a keyword we don't send the one
126                        // from the profile
127                        return new BoostedQueriesFunction( [], [] );
128                    }
129                }
130
131                $preferRecentDecayPortion = $config->get( CirrusConfigNames::PreferRecentDefaultDecayPortion );
132                $preferRecentHalfLife = 0;
133                if ( $preferRecentDecayPortion > 0 ) {
134                    $preferRecentHalfLife = $config->get( CirrusConfigNames::PreferRecentDefaultHalfLife );
135                }
136                return new PreferRecentFunctionScoreBuilder( $config, $weight,
137                    $preferRecentHalfLife, $preferRecentDecayPortion );
138            case 'templates':
139                $withDefaultBoosts = true;
140                foreach ( $this->context->getExtraScoreBuilders() as $boostFunctionBuilder ) {
141                    if ( $boostFunctionBuilder instanceof ByKeywordTemplateBoostFunction ) {
142                        $withDefaultBoosts = false;
143                        break;
144                    }
145                }
146
147                return new BoostTemplatesFunctionScoreBuilder( $config, $this->context->getNamespaces(),
148                    $this->context->getLimitSearchToLocalWiki(), $withDefaultBoosts, $weight );
149            case 'namespaces':
150                return new NamespacesFunctionScoreBuilder( $config, $this->context->getNamespaces(), $weight );
151            case 'language':
152                return new LangWeightFunctionScoreBuilder( $config, $weight );
153            case 'custom_field':
154                return new CustomFieldFunctionScoreBuilder( $config, $weight, $func['params'] );
155            case 'script':
156                return new ScriptScoreFunctionScoreBuilder( $config, $weight, $func['script'] );
157            case 'logscale_boost':
158                return new LogScaleBoostFunctionScoreBuilder( $config, $weight, $func['params'] );
159            case 'satu':
160                return new SatuFunctionScoreBuilder( $config, $weight, $func['params'] );
161            case 'log_multi':
162                return new LogMultFunctionScoreBuilder( $config, $weight, $func['params'] );
163            case 'geomean':
164                return new GeoMeanFunctionScoreBuilder( $config, $weight, $func['params'] );
165            case 'term_boost':
166                return new TermBoostScoreBuilder( $config, $weight, $func['params'] );
167            default:
168                $builder = null;
169                $this->cirrusSearchHookRunner->onCirrusSearchScoreBuilder( $func, $this->context, $builder );
170                if ( !$builder ) {
171                    throw new InvalidRescoreProfileException( "Unknown function score type {$func['type']}." );
172                }
173                if ( !( $builder instanceof BoostFunctionBuilder ) ) {
174                    throw new InvalidRescoreProfileException( "Invalid function score type {$func['type']}: expected " .
175                        BoostFunctionBuilder::class . " but was " . get_class( $builder ) );
176                }
177                return $builder;
178        }
179    }
180}