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