1<?php
2
3namespace Elastica\Query;
4
5use Elastica\Script\AbstractScript;
6
7/**
8 * Class FunctionScore.
9 *
10 * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-function-score-query.html
11 */
12class FunctionScore extends AbstractQuery
13{
14    public const BOOST_MODE_MULTIPLY = 'multiply';
15    public const BOOST_MODE_REPLACE = 'replace';
16    public const BOOST_MODE_SUM = 'sum';
17    public const BOOST_MODE_AVERAGE = 'avg';
18    public const BOOST_MODE_MAX = 'max';
19    public const BOOST_MODE_MIN = 'min';
20
21    public const SCORE_MODE_MULTIPLY = 'multiply';
22    public const SCORE_MODE_SUM = 'sum';
23    public const SCORE_MODE_AVERAGE = 'avg';
24    public const SCORE_MODE_FIRST = 'first';
25    public const SCORE_MODE_MAX = 'max';
26    public const SCORE_MODE_MIN = 'min';
27
28    public const DECAY_GAUSS = 'gauss';
29    public const DECAY_EXPONENTIAL = 'exp';
30    public const DECAY_LINEAR = 'linear';
31
32    public const FIELD_VALUE_FACTOR_MODIFIER_NONE = 'none';
33    public const FIELD_VALUE_FACTOR_MODIFIER_LOG = 'log';
34    public const FIELD_VALUE_FACTOR_MODIFIER_LOG1P = 'log1p';
35    public const FIELD_VALUE_FACTOR_MODIFIER_LOG2P = 'log2p';
36    public const FIELD_VALUE_FACTOR_MODIFIER_LN = 'ln';
37    public const FIELD_VALUE_FACTOR_MODIFIER_LN1P = 'ln1p';
38    public const FIELD_VALUE_FACTOR_MODIFIER_LN2P = 'ln2p';
39    public const FIELD_VALUE_FACTOR_MODIFIER_SQUARE = 'square';
40    public const FIELD_VALUE_FACTOR_MODIFIER_SQRT = 'sqrt';
41    public const FIELD_VALUE_FACTOR_MODIFIER_RECIPROCAL = 'reciprocal';
42
43    public const MULTI_VALUE_MODE_MIN = 'min';
44    public const MULTI_VALUE_MODE_MAX = 'max';
45    public const MULTI_VALUE_MODE_AVG = 'avg';
46    public const MULTI_VALUE_MODE_SUM = 'sum';
47
48    public const RANDOM_SCORE_FIELD_ID = '_id';
49    public const RANDOM_SCORE_FIELD_SEQ_NO = '_seq_no';
50
51    protected $_functions = [];
52
53    /**
54     * Set the child query for this function_score query.
55     *
56     * @return $this
57     */
58    public function setQuery(AbstractQuery $query): self
59    {
60        return $this->setParam('query', $query);
61    }
62
63    /**
64     * Add a function to the function_score query.
65     *
66     * @param string                     $functionType   valid values are DECAY_* constants and script_score
67     * @param AbstractScript|array|float $functionParams the body of the function. See documentation for proper syntax.
68     * @param AbstractQuery|null         $filter         filter to apply to the function
69     * @param float|null                 $weight         function weight
70     *
71     * @return $this
72     */
73    public function addFunction(
74        string $functionType,
75        $functionParams,
76        ?AbstractQuery $filter = null,
77        ?float $weight = null
78    ): self {
79        $function = [
80            $functionType => $functionParams,
81        ];
82
83        if (null !== $filter) {
84            $function['filter'] = $filter;
85        }
86
87        if (null !== $weight) {
88            $function['weight'] = $weight;
89        }
90
91        $this->_functions[] = $function;
92
93        return $this;
94    }
95
96    /**
97     * Add a script_score function to the query.
98     *
99     * @param AbstractScript     $script a Script object
100     * @param AbstractQuery|null $filter an optional filter to apply to the function
101     * @param float|null         $weight the weight of the function
102     *
103     * @return $this
104     */
105    public function addScriptScoreFunction(AbstractScript $script, ?AbstractQuery $filter = null, ?float $weight = null)
106    {
107        return $this->addFunction('script_score', $script, $filter, $weight);
108    }
109
110    /**
111     * Add a decay function to the query.
112     *
113     * TODO: Change "$origin" and "$scale" parameter types to allow "float|int|string".
114     *
115     * @param string             $function       see DECAY_* constants for valid options
116     * @param string             $field          the document field on which to perform the decay function
117     * @param string             $origin         the origin value for this decay function
118     * @param string             $scale          a scale to define the rate of decay for this function
119     * @param string|null        $offset         If defined, this function will only be computed for documents with a distance from the origin greater than this value
120     * @param float|null         $decay          optionally defines how documents are scored at the distance given by the $scale parameter
121     * @param float|null         $weight         optional factor by which to multiply the score at the value provided by the $scale parameter
122     * @param AbstractQuery|null $filter         a filter associated with this function
123     * @param string|null        $multiValueMode see MULTI_VALUE_MODE_* constants for valid options
124     *
125     * @return $this
126     */
127    public function addDecayFunction(
128        string $function,
129        string $field,
130        string $origin,
131        string $scale,
132        ?string $offset = null,
133        ?float $decay = null,
134        ?float $weight = null,
135        ?AbstractQuery $filter = null,
136        ?string $multiValueMode = null
137    ) {
138        $functionParams = [
139            $field => [
140                'origin' => $origin,
141                'scale' => $scale,
142            ],
143        ];
144        if (null !== $offset) {
145            $functionParams[$field]['offset'] = $offset;
146        }
147        if (null !== $decay) {
148            $functionParams[$field]['decay'] = $decay;
149        }
150
151        if (null !== $multiValueMode) {
152            $functionParams['multi_value_mode'] = $multiValueMode;
153        }
154
155        return $this->addFunction($function, $functionParams, $filter, $weight);
156    }
157
158    /**
159     * @return $this
160     */
161    public function addFieldValueFactorFunction(
162        string $field,
163        ?float $factor = null,
164        ?string $modifier = null,
165        ?float $missing = null,
166        ?float $weight = null,
167        ?AbstractQuery $filter = null
168    ): self {
169        $functionParams = [
170            'field' => $field,
171        ];
172
173        if (null !== $factor) {
174            $functionParams['factor'] = $factor;
175        }
176
177        if (null !== $modifier) {
178            $functionParams['modifier'] = $modifier;
179        }
180
181        if (null !== $missing) {
182            $functionParams['missing'] = $missing;
183        }
184
185        return $this->addFunction('field_value_factor', $functionParams, $filter, $weight);
186    }
187
188    /**
189     * @param float              $weight the weight of the function
190     * @param AbstractQuery|null $filter a filter associated with this function
191     *
192     * @return $this
193     */
194    public function addWeightFunction(float $weight, ?AbstractQuery $filter = null): self
195    {
196        return $this->addFunction('weight', $weight, $filter);
197    }
198
199    /**
200     * Add a random_score function to the query.
201     *
202     * @param int                $seed   the seed value
203     * @param AbstractQuery|null $filter a filter associated with this function
204     * @param float|null         $weight an optional boost value associated with this function
205     * @param string|null        $field  the field to be used for random number generation
206     *
207     * @return $this
208     */
209    public function addRandomScoreFunction(
210        int $seed,
211        ?AbstractQuery $filter = null,
212        ?float $weight = null,
213        ?string $field = null
214    ): self {
215        $functionParams = [
216            'seed' => $seed,
217        ];
218
219        if (null !== $field) {
220            $functionParams['field'] = $field;
221        }
222
223        return $this->addFunction('random_score', $functionParams, $filter, $weight);
224    }
225
226    /**
227     * Set an overall boost value for this query.
228     *
229     * @return $this
230     */
231    public function setBoost(float $boost): self
232    {
233        return $this->setParam('boost', $boost);
234    }
235
236    /**
237     * Restrict the combined boost of the function_score query and its child query.
238     *
239     * @return $this
240     */
241    public function setMaxBoost(float $maxBoost): self
242    {
243        return $this->setParam('max_boost', $maxBoost);
244    }
245
246    /**
247     * The boost mode determines how the score of this query is combined with that of the child query.
248     *
249     * @param string $mode see BOOST_MODE_* constants for valid options. Default is multiply.
250     *
251     * @return $this
252     */
253    public function setBoostMode(string $mode = self::BOOST_MODE_MULTIPLY): self
254    {
255        return $this->setParam('boost_mode', $mode);
256    }
257
258    /**
259     * If set, this query will return results in random order.
260     *
261     * @param int|null $seed set a seed value to return results in the same random order for consistent pagination
262     *
263     * @return $this
264     */
265    public function setRandomScore(?int $seed = null): self
266    {
267        $seedParam = new \stdClass();
268        if (null !== $seed) {
269            $seedParam->seed = $seed;
270        }
271
272        return $this->setParam('random_score', $seedParam);
273    }
274
275    /**
276     * Set the score method.
277     *
278     * @param string $mode see SCORE_MODE_* constants for valid options. Default is multiply.
279     *
280     * @return $this
281     */
282    public function setScoreMode(string $mode = self::SCORE_MODE_MULTIPLY): self
283    {
284        return $this->setParam('score_mode', $mode);
285    }
286
287    /**
288     * Set min_score option.
289     *
290     * @return $this
291     */
292    public function setMinScore(float $minScore): self
293    {
294        return $this->setParam('min_score', $minScore);
295    }
296
297    /**
298     * {@inheritdoc}
299     */
300    public function toArray(): array
301    {
302        if (0 < \count($this->_functions)) {
303            $this->setParam('functions', $this->_functions);
304        }
305
306        return parent::toArray();
307    }
308}
309