Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 54 |
|
0.00% |
0 / 3 |
CRAP | |
0.00% |
0 / 1 |
GeoMeanFunctionScoreBuilder | |
0.00% |
0 / 54 |
|
0.00% |
0 / 3 |
380 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 32 |
|
0.00% |
0 / 1 |
182 | |||
getScript | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
20 | |||
append | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 |
1 | <?php |
2 | |
3 | namespace CirrusSearch\Search\Rescore; |
4 | |
5 | use CirrusSearch\SearchConfig; |
6 | use 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 | */ |
17 | class 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 | } |