Skip to main content

flusso_query/handles/
extra.rs

1//! Standalone query types that aren't tied to one field handle: document-id
2//! lookup ([`ids`]), the user-facing full-text strings ([`query_string`],
3//! [`simple_query_string`], [`combined_fields`]), and the advanced-relevance
4//! queries ([`script`], [`script_score`], [`distance_feature`], [`rank_feature`],
5//! [`more_like_this`]). Each returns a builder implementing [`AsQuery`].
6
7use std::marker::PhantomData;
8
9use serde_json::{Map, Value};
10
11use super::{Common, Text, common_opts, wrap};
12use crate::query::{AsQuery, Query, Root};
13
14fn string_array(values: impl IntoIterator<Item = impl Into<String>>) -> Vec<Value> {
15    values
16        .into_iter()
17        .map(|v| Value::String(v.into()))
18        .collect()
19}
20
21fn field_specs<S>(fields: impl IntoIterator<Item = Text<S>>) -> Vec<Value> {
22    fields
23        .into_iter()
24        .map(|f| Value::String(f.field_spec()))
25        .collect()
26}
27
28/// Match documents by a list of `_id` values — typed get-many-by-id.
29pub fn ids<S>(values: impl IntoIterator<Item = impl Into<String>>) -> IdsQuery<S> {
30    IdsQuery {
31        values: string_array(values),
32        common: Common::default(),
33        _scope: PhantomData,
34    }
35}
36
37/// An `ids` clause.
38#[derive(Debug, Clone)]
39pub struct IdsQuery<S = Root> {
40    values: Vec<Value>,
41    common: Common,
42    _scope: PhantomData<fn() -> S>,
43}
44
45impl<S> IdsQuery<S> {
46    common_opts!(common);
47}
48
49impl<S> AsQuery<S> for IdsQuery<S> {
50    fn into_query(self) -> Option<Query<S>> {
51        let mut body = Map::new();
52        body.insert("values".to_string(), Value::Array(self.values));
53        self.common.write(&mut body);
54        Some(wrap("ids", body))
55    }
56}
57
58/// Full Lucene query-string syntax (power users; can error on malformed input).
59/// Target fields with [`default_field`](QueryStringQuery::default_field) or
60/// [`fields`](QueryStringQuery::fields).
61pub fn query_string<S>(query: impl Into<String>) -> QueryStringQuery<S> {
62    QueryStringQuery {
63        query: query.into(),
64        opts: Map::new(),
65        common: Common::default(),
66        _scope: PhantomData,
67    }
68}
69
70/// A `query_string` clause.
71#[derive(Debug, Clone)]
72pub struct QueryStringQuery<S = Root> {
73    query: String,
74    opts: Map<String, Value>,
75    common: Common,
76    _scope: PhantomData<fn() -> S>,
77}
78
79impl<S> QueryStringQuery<S> {
80    /// The single field searched when the query text names none.
81    #[must_use]
82    pub fn default_field(mut self, field: impl Into<String>) -> Self {
83        self.opts
84            .insert("default_field".to_string(), Value::String(field.into()));
85        self
86    }
87
88    /// The fields searched (each may carry a `^weight` via [`Text::boosted`]).
89    #[must_use]
90    pub fn fields(mut self, fields: impl IntoIterator<Item = Text<S>>) -> Self {
91        self.opts
92            .insert("fields".to_string(), Value::Array(field_specs(fields)));
93        self
94    }
95
96    /// Default boolean operator between terms (`"OR"` default / `"AND"`).
97    #[must_use]
98    pub fn default_operator(mut self, operator: impl Into<String>) -> Self {
99        self.opts.insert(
100            "default_operator".to_string(),
101            Value::String(operator.into()),
102        );
103        self
104    }
105
106    /// Override the analyzer applied to the query text.
107    #[must_use]
108    pub fn analyzer(mut self, analyzer: impl Into<String>) -> Self {
109        self.opts
110            .insert("analyzer".to_string(), Value::String(analyzer.into()));
111        self
112    }
113
114    /// Ignore format errors (e.g. text against a numeric field).
115    #[must_use]
116    pub fn lenient(mut self, lenient: bool) -> Self {
117        self.opts
118            .insert("lenient".to_string(), Value::Bool(lenient));
119        self
120    }
121
122    /// Set any other `query_string` parameter verbatim.
123    #[must_use]
124    pub fn param(mut self, key: impl Into<String>, value: impl Into<Value>) -> Self {
125        self.opts.insert(key.into(), value.into());
126        self
127    }
128
129    common_opts!(common);
130}
131
132impl<S> AsQuery<S> for QueryStringQuery<S> {
133    fn into_query(self) -> Option<Query<S>> {
134        let mut body = self.opts;
135        body.insert("query".to_string(), Value::String(self.query));
136        self.common.write(&mut body);
137        Some(wrap("query_string", body))
138    }
139}
140
141/// Lenient search-bar syntax over chosen fields — never errors on bad input.
142pub fn simple_query_string<S>(query: impl Into<String>) -> SimpleQueryStringQuery<S> {
143    SimpleQueryStringQuery {
144        query: query.into(),
145        opts: Map::new(),
146        common: Common::default(),
147        _scope: PhantomData,
148    }
149}
150
151/// A `simple_query_string` clause.
152#[derive(Debug, Clone)]
153pub struct SimpleQueryStringQuery<S = Root> {
154    query: String,
155    opts: Map<String, Value>,
156    common: Common,
157    _scope: PhantomData<fn() -> S>,
158}
159
160impl<S> SimpleQueryStringQuery<S> {
161    /// The fields searched (each may carry a `^weight` via [`Text::boosted`]).
162    #[must_use]
163    pub fn fields(mut self, fields: impl IntoIterator<Item = Text<S>>) -> Self {
164        self.opts
165            .insert("fields".to_string(), Value::Array(field_specs(fields)));
166        self
167    }
168
169    /// Default boolean operator between terms (`"OR"` default / `"AND"`).
170    #[must_use]
171    pub fn default_operator(mut self, operator: impl Into<String>) -> Self {
172        self.opts.insert(
173            "default_operator".to_string(),
174            Value::String(operator.into()),
175        );
176        self
177    }
178
179    /// Override the analyzer applied to the query text.
180    #[must_use]
181    pub fn analyzer(mut self, analyzer: impl Into<String>) -> Self {
182        self.opts
183            .insert("analyzer".to_string(), Value::String(analyzer.into()));
184        self
185    }
186
187    /// Enabled syntax features (e.g. `"AND|OR|PREFIX"`).
188    #[must_use]
189    pub fn flags(mut self, flags: impl Into<String>) -> Self {
190        self.opts
191            .insert("flags".to_string(), Value::String(flags.into()));
192        self
193    }
194
195    /// How many terms must match (e.g. `"75%"`, `"2"`).
196    #[must_use]
197    pub fn minimum_should_match(mut self, value: impl Into<String>) -> Self {
198        self.opts.insert(
199            "minimum_should_match".to_string(),
200            Value::String(value.into()),
201        );
202        self
203    }
204
205    common_opts!(common);
206}
207
208impl<S> AsQuery<S> for SimpleQueryStringQuery<S> {
209    fn into_query(self) -> Option<Query<S>> {
210        let mut body = self.opts;
211        body.insert("query".to_string(), Value::String(self.query));
212        self.common.write(&mut body);
213        Some(wrap("simple_query_string", body))
214    }
215}
216
217/// Term-centric full-text across several fields, treating them as one combined
218/// field (`combined_fields`).
219pub fn combined_fields<S>(
220    query: impl Into<String>,
221    fields: impl IntoIterator<Item = Text<S>>,
222) -> CombinedFieldsQuery<S> {
223    CombinedFieldsQuery {
224        query: query.into(),
225        fields: field_specs(fields),
226        opts: Map::new(),
227        common: Common::default(),
228        _scope: PhantomData,
229    }
230}
231
232/// A `combined_fields` clause.
233#[derive(Debug, Clone)]
234pub struct CombinedFieldsQuery<S = Root> {
235    query: String,
236    fields: Vec<Value>,
237    opts: Map<String, Value>,
238    common: Common,
239    _scope: PhantomData<fn() -> S>,
240}
241
242impl<S> CombinedFieldsQuery<S> {
243    /// Combine analyzed terms with `"AND"` or `"OR"`.
244    #[must_use]
245    pub fn operator(mut self, operator: impl Into<String>) -> Self {
246        self.opts
247            .insert("operator".to_string(), Value::String(operator.into()));
248        self
249    }
250
251    /// How many terms must match (e.g. `"75%"`, `"2"`).
252    #[must_use]
253    pub fn minimum_should_match(mut self, value: impl Into<String>) -> Self {
254        self.opts.insert(
255            "minimum_should_match".to_string(),
256            Value::String(value.into()),
257        );
258        self
259    }
260
261    common_opts!(common);
262}
263
264impl<S> AsQuery<S> for CombinedFieldsQuery<S> {
265    fn into_query(self) -> Option<Query<S>> {
266        let mut body = self.opts;
267        body.insert("query".to_string(), Value::String(self.query));
268        body.insert("fields".to_string(), Value::Array(self.fields));
269        self.common.write(&mut body);
270        Some(wrap("combined_fields", body))
271    }
272}
273
274/// Keep documents for which a painless `source` returns true (a non-scoring
275/// filter).
276pub fn script<S>(source: impl Into<String>) -> ScriptQuery<S> {
277    ScriptQuery {
278        source: source.into(),
279        params: None,
280        lang: None,
281        common: Common::default(),
282        _scope: PhantomData,
283    }
284}
285
286/// A `script` clause.
287#[derive(Debug, Clone)]
288pub struct ScriptQuery<S = Root> {
289    source: String,
290    params: Option<Value>,
291    lang: Option<String>,
292    common: Common,
293    _scope: PhantomData<fn() -> S>,
294}
295
296impl<S> ScriptQuery<S> {
297    /// Bound parameters available to the script as `params`.
298    #[must_use]
299    pub fn params(mut self, params: Value) -> Self {
300        self.params = Some(params);
301        self
302    }
303
304    /// Scripting language (default `"painless"`).
305    #[must_use]
306    pub fn lang(mut self, lang: impl Into<String>) -> Self {
307        self.lang = Some(lang.into());
308        self
309    }
310
311    common_opts!(common);
312}
313
314fn script_object(source: String, params: Option<Value>, lang: Option<String>) -> Value {
315    let mut script = Map::new();
316    script.insert("source".to_string(), Value::String(source));
317    if let Some(params) = params {
318        script.insert("params".to_string(), params);
319    }
320    if let Some(lang) = lang {
321        script.insert("lang".to_string(), Value::String(lang));
322    }
323    Value::Object(script)
324}
325
326impl<S> AsQuery<S> for ScriptQuery<S> {
327    fn into_query(self) -> Option<Query<S>> {
328        let mut body = Map::new();
329        body.insert(
330            "script".to_string(),
331            script_object(self.source, self.params, self.lang),
332        );
333        self.common.write(&mut body);
334        Some(wrap("script", body))
335    }
336}
337
338/// Recompute `query`'s score with a painless `source` (`script_score`).
339pub fn script_score<S>(query: impl AsQuery<S>, source: impl Into<String>) -> ScriptScoreQuery<S> {
340    ScriptScoreQuery {
341        query: query
342            .into_query()
343            .map_or_else(super::match_all_value, |q| q.to_value()),
344        source: source.into(),
345        params: None,
346        min_score: None,
347        common: Common::default(),
348        _scope: PhantomData,
349    }
350}
351
352/// A `script_score` clause.
353#[derive(Debug, Clone)]
354pub struct ScriptScoreQuery<S = Root> {
355    query: Value,
356    source: String,
357    params: Option<Value>,
358    min_score: Option<f32>,
359    common: Common,
360    _scope: PhantomData<fn() -> S>,
361}
362
363impl<S> ScriptScoreQuery<S> {
364    /// Bound parameters available to the script as `params`.
365    #[must_use]
366    pub fn params(mut self, params: Value) -> Self {
367        self.params = Some(params);
368        self
369    }
370
371    /// Exclude hits whose recomputed score is below this.
372    #[must_use]
373    pub fn min_score(mut self, min_score: f32) -> Self {
374        self.min_score = Some(min_score);
375        self
376    }
377
378    common_opts!(common);
379}
380
381impl<S> AsQuery<S> for ScriptScoreQuery<S> {
382    fn into_query(self) -> Option<Query<S>> {
383        let mut body = Map::new();
384        body.insert("query".to_string(), self.query);
385        body.insert(
386            "script".to_string(),
387            script_object(self.source, self.params, None),
388        );
389        if let Some(min_score) = self.min_score {
390            body.insert("min_score".to_string(), Value::from(min_score));
391        }
392        self.common.write(&mut body);
393        Some(wrap("script_score", body))
394    }
395}
396
397/// Boost by proximity of a `field` (date or geo) to `origin`, decaying over
398/// `pivot` (`distance_feature`).
399pub fn distance_feature<S>(
400    field: impl Into<String>,
401    origin: impl Into<Value>,
402    pivot: impl Into<String>,
403) -> DistanceFeatureQuery<S> {
404    DistanceFeatureQuery {
405        field: field.into(),
406        origin: origin.into(),
407        pivot: pivot.into(),
408        common: Common::default(),
409        _scope: PhantomData,
410    }
411}
412
413/// A `distance_feature` clause.
414#[derive(Debug, Clone)]
415pub struct DistanceFeatureQuery<S = Root> {
416    field: String,
417    origin: Value,
418    pivot: String,
419    common: Common,
420    _scope: PhantomData<fn() -> S>,
421}
422
423impl<S> DistanceFeatureQuery<S> {
424    common_opts!(common);
425}
426
427impl<S> AsQuery<S> for DistanceFeatureQuery<S> {
428    fn into_query(self) -> Option<Query<S>> {
429        let mut body = Map::new();
430        body.insert("field".to_string(), Value::String(self.field));
431        body.insert("origin".to_string(), self.origin);
432        body.insert("pivot".to_string(), Value::String(self.pivot));
433        self.common.write(&mut body);
434        Some(wrap("distance_feature", body))
435    }
436}
437
438/// Boost by a `rank_feature` / `rank_features` field's stored value. The
439/// default saturation function applies unless one is chosen.
440pub fn rank_feature<S>(field: impl Into<String>) -> RankFeatureQuery<S> {
441    RankFeatureQuery {
442        field: field.into(),
443        function: None,
444        common: Common::default(),
445        _scope: PhantomData,
446    }
447}
448
449/// A `rank_feature` clause.
450#[derive(Debug, Clone)]
451pub struct RankFeatureQuery<S = Root> {
452    field: String,
453    function: Option<(&'static str, Value)>,
454    common: Common,
455    _scope: PhantomData<fn() -> S>,
456}
457
458impl<S> RankFeatureQuery<S> {
459    /// The `saturation` function with an explicit `pivot`.
460    #[must_use]
461    pub fn saturation(mut self, pivot: f32) -> Self {
462        let mut body = Map::new();
463        body.insert("pivot".to_string(), Value::from(pivot));
464        self.function = Some(("saturation", Value::Object(body)));
465        self
466    }
467
468    /// The `log` function with a `scaling_factor`.
469    #[must_use]
470    pub fn log(mut self, scaling_factor: f32) -> Self {
471        let mut body = Map::new();
472        body.insert("scaling_factor".to_string(), Value::from(scaling_factor));
473        self.function = Some(("log", Value::Object(body)));
474        self
475    }
476
477    /// The `sigmoid` function with `pivot` and `exponent`.
478    #[must_use]
479    pub fn sigmoid(mut self, pivot: f32, exponent: f32) -> Self {
480        let mut body = Map::new();
481        body.insert("pivot".to_string(), Value::from(pivot));
482        body.insert("exponent".to_string(), Value::from(exponent));
483        self.function = Some(("sigmoid", Value::Object(body)));
484        self
485    }
486
487    common_opts!(common);
488}
489
490impl<S> AsQuery<S> for RankFeatureQuery<S> {
491    fn into_query(self) -> Option<Query<S>> {
492        let mut body = Map::new();
493        body.insert("field".to_string(), Value::String(self.field));
494        if let Some((name, function)) = self.function {
495            body.insert(name.to_string(), function);
496        }
497        self.common.write(&mut body);
498        Some(wrap("rank_feature", body))
499    }
500}
501
502/// Find documents similar to `like` text(s) across `fields` (`more_like_this`).
503pub fn more_like_this<S>(
504    fields: impl IntoIterator<Item = Text<S>>,
505    like: impl IntoIterator<Item = impl Into<String>>,
506) -> MoreLikeThisQuery<S> {
507    MoreLikeThisQuery {
508        fields: field_specs(fields),
509        like: string_array(like),
510        opts: Map::new(),
511        common: Common::default(),
512        _scope: PhantomData,
513    }
514}
515
516/// A `more_like_this` clause.
517#[derive(Debug, Clone)]
518pub struct MoreLikeThisQuery<S = Root> {
519    fields: Vec<Value>,
520    like: Vec<Value>,
521    opts: Map<String, Value>,
522    common: Common,
523    _scope: PhantomData<fn() -> S>,
524}
525
526impl<S> MoreLikeThisQuery<S> {
527    /// Ignore source terms occurring fewer than this many times.
528    #[must_use]
529    pub fn min_term_freq(mut self, min_term_freq: u32) -> Self {
530        self.opts
531            .insert("min_term_freq".to_string(), Value::from(min_term_freq));
532        self
533    }
534
535    /// Cap on the terms selected from the source text.
536    #[must_use]
537    pub fn max_query_terms(mut self, max_query_terms: u32) -> Self {
538        self.opts
539            .insert("max_query_terms".to_string(), Value::from(max_query_terms));
540        self
541    }
542
543    /// How many selected terms must match (e.g. `"30%"`).
544    #[must_use]
545    pub fn minimum_should_match(mut self, value: impl Into<String>) -> Self {
546        self.opts.insert(
547            "minimum_should_match".to_string(),
548            Value::String(value.into()),
549        );
550        self
551    }
552
553    common_opts!(common);
554}
555
556impl<S> AsQuery<S> for MoreLikeThisQuery<S> {
557    fn into_query(self) -> Option<Query<S>> {
558        let mut body = self.opts;
559        body.insert("fields".to_string(), Value::Array(self.fields));
560        body.insert("like".to_string(), Value::Array(self.like));
561        self.common.write(&mut body);
562        Some(wrap("more_like_this", body))
563    }
564}