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 *
22 */
23class LogScaleBoostFunctionScoreBuilder extends FunctionScoreBuilder {
24    /** @var string */
25    private $field;
26    /** @var float */
27    private $midpoint;
28    /** @var float */
29    private $scale;
30
31    /**
32     * @param SearchConfig $config
33     * @param float $weight
34     * @param array $profile
35     * @throws InvalidRescoreProfileException
36     */
37    public function __construct( SearchConfig $config, $weight, $profile ) {
38        parent::__construct( $config, $weight );
39
40        if ( isset( $profile['midpoint'] ) ) {
41            $this->midpoint = $this->getOverriddenFactor( $profile['midpoint'] );
42        } else {
43            throw new InvalidRescoreProfileException( 'midpoint is mandatory' );
44        }
45
46        if ( isset( $profile['scale'] ) ) {
47            $this->scale = $this->getOverriddenFactor( $profile['scale'] );
48        } else {
49            throw new InvalidRescoreProfileException( 'scale is mandatory' );
50        }
51
52        if ( isset( $profile['field'] ) ) {
53            $this->field = $profile['field'];
54        } else {
55            throw new InvalidRescoreProfileException( 'field is mandatory' );
56        }
57    }
58
59    /**
60     * find the factor to adjust the scale center,
61     * it's like finding the log base to have f(N) = 0.5
62     *
63     * @param float $M
64     * @param float $N
65     * @return float
66     * @throws InvalidRescoreProfileException
67     */
68    private function findCenterFactor( $M, $N ) {
69        // Neutral point is found by resolving
70        // log10( x . N + 1 ) / log10( x . M + 1 ) = 0.5
71        // it's equivalent to resolving:
72        // N²x² + (2N - M)x + 1 = 0
73        // so we we use the quadratic formula:
74        // (-(2N-M) + sqrt((2N-M)²-4N²)) / 2N²
75        if ( 4 * $N >= $M ) {
76            throw new InvalidRescoreProfileException( 'The midpoint point cannot be higher than scale/4' );
77        }
78
79        return ( -( 2 * $N - $M ) + sqrt( ( 2 * $N - $M ) * ( 2 * $N - $M ) - 4 * $N * $N ) ) /
80               ( 2 * $N * $N );
81    }
82
83    public function append( FunctionScore $functionScore ) {
84        $formula = $this->getScript();
85
86        $functionScore->addScriptScoreFunction( new \Elastica\Script\Script( $formula, null,
87            'expression' ), null, $this->weight );
88    }
89
90    /**
91     * @return string
92     */
93    public function getScript() {
94        $midFactor = $this->findCenterFactor( $this->scale, $this->midpoint );
95        $formula = "log10($midFactor * min(doc['{$this->field}'].value,{$this->scale}) + 1)";
96        $formula .= "/log10($midFactor * {$this->scale} + 1)";
97
98        return $formula;
99    }
100}