Skip to main content

flusso_query/handles/
compound.rs

1//! Compound / scoring queries that wrap other clauses rather than a field:
2//! [`constant_score`], [`dis_max`], [`boosting`], and [`function_score`]. Each
3//! returns a builder implementing [`AsQuery`], so it composes
4//! exactly like a leaf query.
5
6use std::marker::PhantomData;
7
8use serde_json::{Map, Value};
9
10use super::{Common, common_opts, wrap};
11use crate::query::{AsQuery, Query, Root};
12
13fn clause_value<S>(query: impl AsQuery<S>) -> Value {
14    query
15        .into_query()
16        .map_or_else(super::match_all_value, |q| q.to_value())
17}
18
19/// Wrap a `filter` so every match scores the same fixed `boost` (default 1.0).
20pub fn constant_score<S>(filter: impl AsQuery<S>) -> ConstantScoreQuery<S> {
21    ConstantScoreQuery {
22        filter: clause_value(filter),
23        common: Common::default(),
24        _scope: PhantomData,
25    }
26}
27
28/// A `constant_score` clause. Set the fixed score via [`boost`](Self::boost).
29#[derive(Debug, Clone)]
30pub struct ConstantScoreQuery<S = Root> {
31    filter: Value,
32    common: Common,
33    _scope: PhantomData<fn() -> S>,
34}
35
36impl<S> ConstantScoreQuery<S> {
37    common_opts!(common);
38}
39
40impl<S> AsQuery<S> for ConstantScoreQuery<S> {
41    fn into_query(self) -> Option<Query<S>> {
42        let mut body = Map::new();
43        body.insert("filter".to_string(), self.filter);
44        self.common.write(&mut body);
45        Some(wrap("constant_score", body))
46    }
47}
48
49/// Score by the single best-matching clause, optionally crediting the others
50/// via [`tie_breaker`](DisMaxQuery::tie_breaker).
51pub fn dis_max<S>(queries: impl IntoIterator<Item = impl AsQuery<S>>) -> DisMaxQuery<S> {
52    DisMaxQuery {
53        queries: queries.into_iter().map(clause_value).collect(),
54        tie_breaker: None,
55        common: Common::default(),
56        _scope: PhantomData,
57    }
58}
59
60/// A `dis_max` clause.
61#[derive(Debug, Clone)]
62pub struct DisMaxQuery<S = Root> {
63    queries: Vec<Value>,
64    tie_breaker: Option<f32>,
65    common: Common,
66    _scope: PhantomData<fn() -> S>,
67}
68
69impl<S> DisMaxQuery<S> {
70    /// How much the non-winning clauses contribute (0.0–1.0).
71    #[must_use]
72    pub fn tie_breaker(mut self, tie_breaker: f32) -> Self {
73        self.tie_breaker = Some(tie_breaker);
74        self
75    }
76
77    common_opts!(common);
78}
79
80impl<S> AsQuery<S> for DisMaxQuery<S> {
81    fn into_query(self) -> Option<Query<S>> {
82        let mut body = Map::new();
83        body.insert("queries".to_string(), Value::Array(self.queries));
84        if let Some(tie_breaker) = self.tie_breaker {
85            body.insert("tie_breaker".to_string(), Value::from(tie_breaker));
86        }
87        self.common.write(&mut body);
88        Some(wrap("dis_max", body))
89    }
90}
91
92/// Keep documents matching `positive`, but demote (don't exclude) those that
93/// also match `negative` by `negative_boost` (0.0–1.0).
94pub fn boosting<S>(
95    positive: impl AsQuery<S>,
96    negative: impl AsQuery<S>,
97    negative_boost: f32,
98) -> BoostingQuery<S> {
99    BoostingQuery {
100        positive: clause_value(positive),
101        negative: clause_value(negative),
102        negative_boost,
103        common: Common::default(),
104        _scope: PhantomData,
105    }
106}
107
108/// A `boosting` clause.
109#[derive(Debug, Clone)]
110pub struct BoostingQuery<S = Root> {
111    positive: Value,
112    negative: Value,
113    negative_boost: f32,
114    common: Common,
115    _scope: PhantomData<fn() -> S>,
116}
117
118impl<S> BoostingQuery<S> {
119    common_opts!(common);
120}
121
122impl<S> AsQuery<S> for BoostingQuery<S> {
123    fn into_query(self) -> Option<Query<S>> {
124        let mut body = Map::new();
125        body.insert("positive".to_string(), self.positive);
126        body.insert("negative".to_string(), self.negative);
127        body.insert(
128            "negative_boost".to_string(),
129            Value::from(self.negative_boost),
130        );
131        self.common.write(&mut body);
132        Some(wrap("boosting", body))
133    }
134}
135
136/// Recompute relevance for `query` via one or more scoring functions.
137pub fn function_score<S>(query: impl AsQuery<S>) -> FunctionScoreQuery<S> {
138    FunctionScoreQuery {
139        query: clause_value(query),
140        functions: Vec::new(),
141        opts: Map::new(),
142        common: Common::default(),
143        _scope: PhantomData,
144    }
145}
146
147/// A `function_score` clause. Add functions with [`weight`](Self::weight) /
148/// [`function`](Self::function) and tune combination with `score_mode` /
149/// `boost_mode` / `max_boost` / `min_score`.
150#[derive(Debug, Clone)]
151pub struct FunctionScoreQuery<S = Root> {
152    query: Value,
153    functions: Vec<Value>,
154    opts: Map<String, Value>,
155    common: Common,
156    _scope: PhantomData<fn() -> S>,
157}
158
159impl<S> FunctionScoreQuery<S> {
160    /// Add a constant `weight` function applied to every match.
161    #[must_use]
162    pub fn weight(mut self, weight: f32) -> Self {
163        let mut function = Map::new();
164        function.insert("weight".to_string(), Value::from(weight));
165        self.functions.push(Value::Object(function));
166        self
167    }
168
169    /// Add a constant `weight` function applied only to matches of `filter`.
170    #[must_use]
171    pub fn weight_when(mut self, weight: f32, filter: impl AsQuery<S>) -> Self {
172        let mut function = Map::new();
173        function.insert("weight".to_string(), Value::from(weight));
174        function.insert("filter".to_string(), clause_value(filter));
175        self.functions.push(Value::Object(function));
176        self
177    }
178
179    /// Add a raw function entry (e.g. a `field_value_factor`, `gauss`, or
180    /// `script_score` object) — the escape hatch for the long tail of function
181    /// types, still composed into the typed clause.
182    #[must_use]
183    pub fn function(mut self, function: Value) -> Self {
184        self.functions.push(function);
185        self
186    }
187
188    /// How the functions combine (`"multiply"` default / `"sum"` / `"avg"` /
189    /// `"first"` / `"max"` / `"min"`).
190    #[must_use]
191    pub fn score_mode(mut self, score_mode: impl Into<String>) -> Self {
192        self.opts
193            .insert("score_mode".to_string(), Value::String(score_mode.into()));
194        self
195    }
196
197    /// How the function score combines with the query score (`"multiply"`
198    /// default / `"replace"` / `"sum"` / `"avg"` / `"max"` / `"min"`).
199    #[must_use]
200    pub fn boost_mode(mut self, boost_mode: impl Into<String>) -> Self {
201        self.opts
202            .insert("boost_mode".to_string(), Value::String(boost_mode.into()));
203        self
204    }
205
206    /// Cap on the combined function score.
207    #[must_use]
208    pub fn max_boost(mut self, max_boost: f32) -> Self {
209        self.opts
210            .insert("max_boost".to_string(), Value::from(max_boost));
211        self
212    }
213
214    /// Drop hits scoring below this threshold.
215    #[must_use]
216    pub fn min_score(mut self, min_score: f32) -> Self {
217        self.opts
218            .insert("min_score".to_string(), Value::from(min_score));
219        self
220    }
221
222    common_opts!(common);
223}
224
225impl<S> AsQuery<S> for FunctionScoreQuery<S> {
226    fn into_query(self) -> Option<Query<S>> {
227        let mut body = self.opts;
228        body.insert("query".to_string(), self.query);
229        if !self.functions.is_empty() {
230            body.insert("functions".to_string(), Value::Array(self.functions));
231        }
232        self.common.write(&mut body);
233        Some(wrap("function_score", body))
234    }
235}