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 | } |