Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
LogScaleBoostFunctionScoreBuilder
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 4
72
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
20
 findCenterFactor
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 append
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 getScript
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace CirrusSearch\Search\Rescore;
4
5use CirrusSearch\SearchConfig;
6use Elastica\Query\FunctionScore;
7
8/**
9 * Normalize values in the [0,1] range
10 * Allows to set:
11 * - a scale
12 * - midpoint
13 * It will generate a log scale factor where :
14 * - f(0) = 0
15 * - f(scale) = 1
16 * - f(midpoint) = 0.5
17 *
18 * Based on log10( a . x + 1 ) / log10( a . M + 1 )
19 * a: a factor used to adjust the midpoint
20 * M: the max value used to scale
21 */
22class LogScaleBoostFunctionScoreBuilder extends FunctionScoreBuilder {
23    /** @var string */
24    private $field;
25    /** @var float */
26    private $midpoint;
27    /** @var float */
28    private $scale;
29
30    /**
31     * @param SearchConfig $config
32     * @param float $weight
33     * @param array $profile
34     * @throws InvalidRescoreProfileException
35     */
36    public function __construct( SearchConfig $config, $weight, $profile ) {
37        parent::__construct( $config, $weight );
38
39        if ( isset( $profile['midpoint'] ) ) {
40            $this->midpoint = $this->getOverriddenFactor( $profile['midpoint'] );
41        } else {
42            throw new InvalidRescoreProfileException( 'midpoint is mandatory' );
43        }
44
45        if ( isset( $profile['scale'] ) ) {
46            $this->scale = $this->getOverriddenFactor( $profile['scale'] );
47        } else {
48            throw new InvalidRescoreProfileException( 'scale is mandatory' );
49        }
50
51        if ( isset( $profile['field'] ) ) {
52            $this->field = $profile['field'];
53        } else {
54            throw new InvalidRescoreProfileException( 'field is mandatory' );
55        }
56    }
57
58    /**
59     * find the factor to adjust the scale center,
60     * it's like finding the log base to have f(N) = 0.5
61     *
62     * @param float $M
63     * @param float $N
64     * @return float
65     * @throws InvalidRescoreProfileException
66     */
67    private function findCenterFactor( $M, $N ) {
68        // Neutral point is found by resolving
69        // log10( x . N + 1 ) / log10( x . M + 1 ) = 0.5
70        // it's equivalent to resolving:
71        // N²x² + (2N - M)x + 1 = 0
72        // so we we use the quadratic formula:
73        // (-(2N-M) + sqrt((2N-M)²-4N²)) / 2N²
74        if ( 4 * $N >= $M ) {
75            throw new InvalidRescoreProfileException( 'The midpoint point cannot be higher than scale/4' );
76        }
77
78        return ( -( 2 * $N - $M ) + sqrt( ( 2 * $N - $M ) * ( 2 * $N - $M ) - 4 * $N * $N ) ) /
79               ( 2 * $N * $N );
80    }
81
82    public function append( FunctionScore $functionScore ) {
83        $formula = $this->getScript();
84
85        $functionScore->addScriptScoreFunction( new \Elastica\Script\Script( $formula, null,
86            'expression' ), null, $this->weight );
87    }
88
89    /**
90     * @return string
91     */
92    public function getScript() {
93        $midFactor = $this->findCenterFactor( $this->scale, $this->midpoint );
94        $formula = "log10($midFactor * min(doc['{$this->field}'].value,{$this->scale}) + 1)";
95        $formula .= "/log10($midFactor * {$this->scale} + 1)";
96
97        return $formula;
98    }
99}