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, MinimumShouldMatch, Operator, 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, Sub>(fields: impl IntoIterator<Item = Text<S, Sub>>) -> 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<Sub>(mut self, fields: impl IntoIterator<Item = Text<S, Sub>>) -> Self {
91        self.opts
92            .insert("fields".to_string(), Value::Array(field_specs(fields)));
93        self
94    }
95
96    /// Default boolean operator between terms ([`Operator::Or`] default).
97    #[must_use]
98    pub fn default_operator(mut self, operator: Operator) -> Self {
99        self.opts.insert(
100            "default_operator".to_string(),
101            Value::String(operator.as_str().to_string()),
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<Sub>(mut self, fields: impl IntoIterator<Item = Text<S, Sub>>) -> Self {
164        self.opts
165            .insert("fields".to_string(), Value::Array(field_specs(fields)));
166        self
167    }
168
169    /// Default boolean operator between terms ([`Operator::Or`] default).
170    #[must_use]
171    pub fn default_operator(mut self, operator: Operator) -> Self {
172        self.opts.insert(
173            "default_operator".to_string(),
174            Value::String(operator.as_str().to_string()),
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. `2`, `MinimumShouldMatch::percent(75)`).
196    #[must_use]
197    pub fn minimum_should_match(mut self, value: impl Into<MinimumShouldMatch>) -> Self {
198        self.opts
199            .insert("minimum_should_match".to_string(), value.into().to_value());
200        self
201    }
202
203    common_opts!(common);
204}
205
206impl<S> AsQuery<S> for SimpleQueryStringQuery<S> {
207    fn into_query(self) -> Option<Query<S>> {
208        let mut body = self.opts;
209        body.insert("query".to_string(), Value::String(self.query));
210        self.common.write(&mut body);
211        Some(wrap("simple_query_string", body))
212    }
213}
214
215/// Term-centric full-text across several fields, treating them as one combined
216/// field (`combined_fields`).
217pub fn combined_fields<S, Sub>(
218    query: impl Into<String>,
219    fields: impl IntoIterator<Item = Text<S, Sub>>,
220) -> CombinedFieldsQuery<S> {
221    CombinedFieldsQuery {
222        query: query.into(),
223        fields: field_specs(fields),
224        opts: Map::new(),
225        common: Common::default(),
226        _scope: PhantomData,
227    }
228}
229
230/// A `combined_fields` clause.
231#[derive(Debug, Clone)]
232pub struct CombinedFieldsQuery<S = Root> {
233    query: String,
234    fields: Vec<Value>,
235    opts: Map<String, Value>,
236    common: Common,
237    _scope: PhantomData<fn() -> S>,
238}
239
240impl<S> CombinedFieldsQuery<S> {
241    /// Combine analyzed terms with [`Operator::And`] or [`Operator::Or`].
242    #[must_use]
243    pub fn operator(mut self, operator: Operator) -> Self {
244        self.opts.insert(
245            "operator".to_string(),
246            Value::String(operator.as_str().to_string()),
247        );
248        self
249    }
250
251    /// How many terms must match (e.g. `2`, `MinimumShouldMatch::percent(75)`).
252    #[must_use]
253    pub fn minimum_should_match(mut self, value: impl Into<MinimumShouldMatch>) -> Self {
254        self.opts
255            .insert("minimum_should_match".to_string(), value.into().to_value());
256        self
257    }
258
259    common_opts!(common);
260}
261
262impl<S> AsQuery<S> for CombinedFieldsQuery<S> {
263    fn into_query(self) -> Option<Query<S>> {
264        let mut body = self.opts;
265        body.insert("query".to_string(), Value::String(self.query));
266        body.insert("fields".to_string(), Value::Array(self.fields));
267        self.common.write(&mut body);
268        Some(wrap("combined_fields", body))
269    }
270}
271
272/// Keep documents for which a painless `source` returns true (a non-scoring
273/// filter).
274pub fn script<S>(source: impl Into<String>) -> ScriptQuery<S> {
275    ScriptQuery {
276        source: source.into(),
277        params: None,
278        lang: None,
279        common: Common::default(),
280        _scope: PhantomData,
281    }
282}
283
284/// A `script` clause.
285#[derive(Debug, Clone)]
286pub struct ScriptQuery<S = Root> {
287    source: String,
288    params: Option<Value>,
289    lang: Option<String>,
290    common: Common,
291    _scope: PhantomData<fn() -> S>,
292}
293
294impl<S> ScriptQuery<S> {
295    /// Bound parameters available to the script as `params`.
296    #[must_use]
297    pub fn params(mut self, params: Value) -> Self {
298        self.params = Some(params);
299        self
300    }
301
302    /// Scripting language (default `"painless"`).
303    #[must_use]
304    pub fn lang(mut self, lang: impl Into<String>) -> Self {
305        self.lang = Some(lang.into());
306        self
307    }
308
309    common_opts!(common);
310}
311
312fn script_object(source: String, params: Option<Value>, lang: Option<String>) -> Value {
313    let mut script = Map::new();
314    script.insert("source".to_string(), Value::String(source));
315    if let Some(params) = params {
316        script.insert("params".to_string(), params);
317    }
318    if let Some(lang) = lang {
319        script.insert("lang".to_string(), Value::String(lang));
320    }
321    Value::Object(script)
322}
323
324impl<S> AsQuery<S> for ScriptQuery<S> {
325    fn into_query(self) -> Option<Query<S>> {
326        let mut body = Map::new();
327        body.insert(
328            "script".to_string(),
329            script_object(self.source, self.params, self.lang),
330        );
331        self.common.write(&mut body);
332        Some(wrap("script", body))
333    }
334}
335
336/// Recompute `query`'s score with a painless `source` (`script_score`).
337pub fn script_score<S>(query: impl AsQuery<S>, source: impl Into<String>) -> ScriptScoreQuery<S> {
338    ScriptScoreQuery {
339        query: query
340            .into_query()
341            .map_or_else(super::match_all_value, |q| q.to_value()),
342        source: source.into(),
343        params: None,
344        min_score: None,
345        common: Common::default(),
346        _scope: PhantomData,
347    }
348}
349
350/// A `script_score` clause.
351#[derive(Debug, Clone)]
352pub struct ScriptScoreQuery<S = Root> {
353    query: Value,
354    source: String,
355    params: Option<Value>,
356    min_score: Option<f32>,
357    common: Common,
358    _scope: PhantomData<fn() -> S>,
359}
360
361impl<S> ScriptScoreQuery<S> {
362    /// Bound parameters available to the script as `params`.
363    #[must_use]
364    pub fn params(mut self, params: Value) -> Self {
365        self.params = Some(params);
366        self
367    }
368
369    /// Exclude hits whose recomputed score is below this.
370    #[must_use]
371    pub fn min_score(mut self, min_score: f32) -> Self {
372        self.min_score = Some(min_score);
373        self
374    }
375
376    common_opts!(common);
377}
378
379impl<S> AsQuery<S> for ScriptScoreQuery<S> {
380    fn into_query(self) -> Option<Query<S>> {
381        let mut body = Map::new();
382        body.insert("query".to_string(), self.query);
383        body.insert(
384            "script".to_string(),
385            script_object(self.source, self.params, None),
386        );
387        if let Some(min_score) = self.min_score {
388            body.insert("min_score".to_string(), Value::from(min_score));
389        }
390        self.common.write(&mut body);
391        Some(wrap("script_score", body))
392    }
393}
394
395/// Boost by proximity of a `field` (date or geo) to `origin`, decaying over
396/// `pivot` (`distance_feature`).
397pub fn distance_feature<S>(
398    field: impl Into<String>,
399    origin: impl Into<Value>,
400    pivot: impl Into<String>,
401) -> DistanceFeatureQuery<S> {
402    DistanceFeatureQuery {
403        field: field.into(),
404        origin: origin.into(),
405        pivot: pivot.into(),
406        common: Common::default(),
407        _scope: PhantomData,
408    }
409}
410
411/// A `distance_feature` clause.
412#[derive(Debug, Clone)]
413pub struct DistanceFeatureQuery<S = Root> {
414    field: String,
415    origin: Value,
416    pivot: String,
417    common: Common,
418    _scope: PhantomData<fn() -> S>,
419}
420
421impl<S> DistanceFeatureQuery<S> {
422    common_opts!(common);
423}
424
425impl<S> AsQuery<S> for DistanceFeatureQuery<S> {
426    fn into_query(self) -> Option<Query<S>> {
427        let mut body = Map::new();
428        body.insert("field".to_string(), Value::String(self.field));
429        body.insert("origin".to_string(), self.origin);
430        body.insert("pivot".to_string(), Value::String(self.pivot));
431        self.common.write(&mut body);
432        Some(wrap("distance_feature", body))
433    }
434}
435
436/// Boost by a `rank_feature` / `rank_features` field's stored value. The
437/// default saturation function applies unless one is chosen.
438pub fn rank_feature<S>(field: impl Into<String>) -> RankFeatureQuery<S> {
439    RankFeatureQuery {
440        field: field.into(),
441        function: None,
442        common: Common::default(),
443        _scope: PhantomData,
444    }
445}
446
447/// A `rank_feature` clause.
448#[derive(Debug, Clone)]
449pub struct RankFeatureQuery<S = Root> {
450    field: String,
451    function: Option<(&'static str, Value)>,
452    common: Common,
453    _scope: PhantomData<fn() -> S>,
454}
455
456impl<S> RankFeatureQuery<S> {
457    /// The `saturation` function with an explicit `pivot`.
458    #[must_use]
459    pub fn saturation(mut self, pivot: f32) -> Self {
460        let mut body = Map::new();
461        body.insert("pivot".to_string(), Value::from(pivot));
462        self.function = Some(("saturation", Value::Object(body)));
463        self
464    }
465
466    /// The `log` function with a `scaling_factor`.
467    #[must_use]
468    pub fn log(mut self, scaling_factor: f32) -> Self {
469        let mut body = Map::new();
470        body.insert("scaling_factor".to_string(), Value::from(scaling_factor));
471        self.function = Some(("log", Value::Object(body)));
472        self
473    }
474
475    /// The `sigmoid` function with `pivot` and `exponent`.
476    #[must_use]
477    pub fn sigmoid(mut self, pivot: f32, exponent: f32) -> Self {
478        let mut body = Map::new();
479        body.insert("pivot".to_string(), Value::from(pivot));
480        body.insert("exponent".to_string(), Value::from(exponent));
481        self.function = Some(("sigmoid", Value::Object(body)));
482        self
483    }
484
485    common_opts!(common);
486}
487
488impl<S> AsQuery<S> for RankFeatureQuery<S> {
489    fn into_query(self) -> Option<Query<S>> {
490        let mut body = Map::new();
491        body.insert("field".to_string(), Value::String(self.field));
492        if let Some((name, function)) = self.function {
493            body.insert(name.to_string(), function);
494        }
495        self.common.write(&mut body);
496        Some(wrap("rank_feature", body))
497    }
498}
499
500/// Find documents similar to `like` text(s) across `fields` (`more_like_this`).
501pub fn more_like_this<S, Sub>(
502    fields: impl IntoIterator<Item = Text<S, Sub>>,
503    like: impl IntoIterator<Item = impl Into<String>>,
504) -> MoreLikeThisQuery<S> {
505    MoreLikeThisQuery {
506        fields: field_specs(fields),
507        like: string_array(like),
508        opts: Map::new(),
509        common: Common::default(),
510        _scope: PhantomData,
511    }
512}
513
514/// A `more_like_this` clause.
515#[derive(Debug, Clone)]
516pub struct MoreLikeThisQuery<S = Root> {
517    fields: Vec<Value>,
518    like: Vec<Value>,
519    opts: Map<String, Value>,
520    common: Common,
521    _scope: PhantomData<fn() -> S>,
522}
523
524impl<S> MoreLikeThisQuery<S> {
525    /// Ignore source terms occurring fewer than this many times.
526    #[must_use]
527    pub fn min_term_freq(mut self, min_term_freq: u32) -> Self {
528        self.opts
529            .insert("min_term_freq".to_string(), Value::from(min_term_freq));
530        self
531    }
532
533    /// Cap on the terms selected from the source text.
534    #[must_use]
535    pub fn max_query_terms(mut self, max_query_terms: u32) -> Self {
536        self.opts
537            .insert("max_query_terms".to_string(), Value::from(max_query_terms));
538        self
539    }
540
541    /// How many selected terms must match (e.g. `MinimumShouldMatch::percent(30)`).
542    #[must_use]
543    pub fn minimum_should_match(mut self, value: impl Into<MinimumShouldMatch>) -> Self {
544        self.opts
545            .insert("minimum_should_match".to_string(), value.into().to_value());
546        self
547    }
548
549    common_opts!(common);
550}
551
552impl<S> AsQuery<S> for MoreLikeThisQuery<S> {
553    fn into_query(self) -> Option<Query<S>> {
554        let mut body = self.opts;
555        body.insert("fields".to_string(), Value::Array(self.fields));
556        body.insert("like".to_string(), Value::Array(self.like));
557        self.common.write(&mut body);
558        Some(wrap("more_like_this", body))
559    }
560}