Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 54
0.00% covered (danger)
0.00%
0 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
GeoMeanFunctionScoreBuilder
0.00% covered (danger)
0.00%
0 / 54
0.00% covered (danger)
0.00%
0 / 3
380
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 32
0.00% covered (danger)
0.00%
0 / 1
182
 getScript
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
20
 append
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2
3namespace CirrusSearch\Search\Rescore;
4
5use CirrusSearch\SearchConfig;
6use Elastica\Query\FunctionScore;
7
8/**
9 * Utility function to compute a weighted geometric mean.
10 * According to https://en.wikipedia.org/wiki/Weighted_geometric_mean
11 * this is equivalent to exp ( w1*ln(value1)+w2*ln(value2) / (w1 + w2) ) ^ impact
12 * impact is applied as a power factor because this function is applied in a
13 * multiplication.
14 * Members can use only LogScaleBoostFunctionScoreBuilder or SatuFunctionScoreBuilder
15 * these are the only functions that normalize the value in the [0,1] range.
16 */
17class GeoMeanFunctionScoreBuilder extends FunctionScoreBuilder {
18    /** @var float */
19    private $impact;
20    /** @var array[] */
21    private $scriptFunctions = [];
22    /** @var float */
23    private $epsilon = 0.0000001;
24
25    /**
26     * @param SearchConfig $config
27     * @param float $weight
28     * @param array $profile
29     * @throws InvalidRescoreProfileException
30     */
31    public function __construct( SearchConfig $config, $weight, $profile ) {
32        parent::__construct( $config, $weight );
33
34        if ( isset( $profile['impact'] ) ) {
35            $this->impact = $this->getOverriddenFactor( $profile['impact'] );
36            if ( $this->impact <= 0 ) {
37                throw new InvalidRescoreProfileException( 'Param impact must be > 0' );
38            }
39        } else {
40            throw new InvalidRescoreProfileException( 'Param impact is mandatory' );
41        }
42
43        if ( isset( $profile['epsilon'] ) ) {
44            $this->epsilon = $this->getOverriddenFactor( $profile['epsilon'] );
45        }
46
47        if ( !isset( $profile['members'] ) || !is_array( $profile['members'] ) ) {
48            throw new InvalidRescoreProfileException( 'members must be an array of arrays' );
49        }
50        foreach ( $profile['members'] as $member ) {
51            if ( !is_array( $member ) ) {
52                throw new InvalidRescoreProfileException( "members must be an array of arrays" );
53            }
54            if ( !isset( $member['weight'] ) ) {
55                $weight = 1;
56            } else {
57                $weight = $this->getOverriddenFactor( $member['weight'] );
58            }
59            $function = [ 'weight' => $weight ];
60            switch ( $member['type'] ) {
61                case 'satu':
62                    $function['script'] =
63                        new SatuFunctionScoreBuilder( $this->config, 1,
64                            $member['params'] );
65                    break;
66                case 'logscale_boost':
67                    $function['script'] =
68                        new LogScaleBoostFunctionScoreBuilder( $this->config, 1,
69                            $member['params'] );
70                    break;
71                default:
72                    throw new InvalidRescoreProfileException( "Unsupported function in {$member['type']}." );
73            }
74            $this->scriptFunctions[] = $function;
75        }
76        if ( count( $this->scriptFunctions ) < 2 ) {
77            throw new InvalidRescoreProfileException( "At least 2 members are needed to compute a geometric mean." );
78        }
79    }
80
81    /**
82     * Build a weighted geometric mean using a logarithmic arithmetic mean.
83     * exp(w1*ln(value1)+w2*ln(value2) / (w1+w2))
84     * NOTE: We need to use an epsilon value in case value is 0.
85     *
86     * @return string|null the script
87     */
88    public function getScript() {
89        $formula = "pow(";
90        $formula .= "exp((";
91        $first = true;
92        $sumWeight = 0;
93        foreach ( $this->scriptFunctions as $func ) {
94            if ( $first ) {
95                $first = false;
96            } else {
97                $formula .= " + ";
98            }
99            $sumWeight += $func['weight'];
100            $formula .= "{$func['weight']}*ln(max(";
101
102            $formula .= $func['script']->getScript();
103
104            $formula .= "{$this->epsilon}))";
105        }
106        if ( $sumWeight == 0 ) {
107            return null;
108        }
109        $formula .= ")";
110        $formula .= "$sumWeight )";
111        $formula .= "{$this->impact})"; // pow(
112
113        return $formula;
114    }
115
116    public function append( FunctionScore $functionScore ) {
117        $formula = $this->getScript();
118        if ( $formula != null ) {
119            $functionScore->addScriptScoreFunction( new \Elastica\Script\Script( $formula, null,
120                'expression' ), null, $this->weight );
121        }
122    }
123}