Skip to main content

flusso_query/handles/
nested.rs

1//! Handles for `nested` arrays of objects: [`Nested`] (lifting element queries
2//! into the enclosing scope) and [`NestedProjection`] (shaping the returned
3//! array via `inner_hits`).
4
5use std::marker::PhantomData;
6
7use serde_json::{Map, Value};
8
9use super::{Common, Sort, common_opts, exists_q, match_all_value};
10use crate::query::{AsQuery, Query, Root};
11
12/// `{ "nested": { "path": "<path>", "query": <query> } }`.
13fn nested_value(path: &str, query: Value) -> Value {
14    let mut body = Map::new();
15    body.insert("path".to_string(), Value::String(path.to_string()));
16    body.insert("query".to_string(), query);
17    let mut outer = Map::new();
18    outer.insert("nested".to_string(), Value::Object(body));
19    Value::Object(outer)
20}
21
22/// `{ "bool": { "<clause>": [ … ] } }`.
23fn bool_value(clause: &str, items: Vec<Value>) -> Value {
24    let mut body = Map::new();
25    body.insert(clause.to_string(), Value::Array(items));
26    let mut outer = Map::new();
27    outer.insert("bool".to_string(), Value::Object(body));
28    Value::Object(outer)
29}
30
31/// A `nested` array of objects. `E` is the **enclosing** scope (where queries
32/// over this array land — `Root` at the top level, the parent element type when
33/// deeper); `C` is the **child** scope (the element type). Lifting a child query
34/// (`Query<C>`) through `any`/`all` produces a `Query<E>`.
35#[derive(Debug, Clone)]
36pub struct Nested<E = Root, C = serde_json::Value> {
37    path: String,
38    _marker: PhantomData<fn() -> (E, C)>,
39}
40
41impl<E, C> Nested<E, C> {
42    pub fn at(path: impl Into<String>) -> Self {
43        Self {
44            path: path.into(),
45            _marker: PhantomData,
46        }
47    }
48
49    /// Parents with **at least one** element matching `query`. Returns a
50    /// [`NestedQuery`] builder for `score_mode` / `ignore_unmapped` plus
51    /// `boost` / `name`.
52    pub fn any(&self, query: impl AsQuery<C>) -> NestedQuery<E> {
53        let inner = query
54            .into_query()
55            .map_or_else(match_all_value, |q| q.to_value());
56        NestedQuery {
57            path: self.path.clone(),
58            query: inner,
59            opts: Map::new(),
60            common: Common::default(),
61            _marker: PhantomData,
62        }
63    }
64
65    /// Parents where **every** element matches `query` ("no element fails it").
66    pub fn all(&self, query: impl AsQuery<C>) -> Query<E> {
67        let inner = query
68            .into_query()
69            .map_or_else(match_all_value, |q| q.to_value());
70        let fails = bool_value("must_not", vec![inner]);
71        let nested = nested_value(&self.path, fails);
72        Query::leaf(bool_value("must_not", vec![nested]))
73    }
74
75    /// The nested array has at least one element.
76    pub fn exists(&self) -> Query<E> {
77        exists_q(&self.path)
78    }
79
80    /// Shape the **returned** array: keep elements matching `query` (with the
81    /// builder's sort/size). Pass to [`crate::Search::filter_nested`].
82    pub fn matching(&self, query: impl AsQuery<C>) -> NestedProjection {
83        NestedProjection {
84            path: self.path.clone(),
85            query: query.into_query().map(|q| q.to_value()),
86            sort: Vec::new(),
87            size: None,
88            from: None,
89        }
90    }
91
92    /// Like [`matching`](Self::matching) with no predicate — every element.
93    pub fn project(&self) -> NestedProjection {
94        NestedProjection {
95            path: self.path.clone(),
96            query: None,
97            sort: Vec::new(),
98            size: None,
99            from: None,
100        }
101    }
102}
103
104/// A `nested` clause (parents with a matching element), with the `score_mode` /
105/// `ignore_unmapped` options plus `boost` / `name`. `E` is the enclosing scope.
106#[derive(Debug, Clone)]
107pub struct NestedQuery<E = Root> {
108    path: String,
109    query: Value,
110    opts: Map<String, Value>,
111    common: Common,
112    _marker: PhantomData<fn() -> E>,
113}
114
115impl<E> NestedQuery<E> {
116    /// How matching elements' scores combine into the parent score:
117    /// `"avg"` (default) / `"sum"` / `"min"` / `"max"` / `"none"`.
118    #[must_use]
119    pub fn score_mode(mut self, score_mode: impl Into<String>) -> Self {
120        self.opts
121            .insert("score_mode".to_string(), Value::String(score_mode.into()));
122        self
123    }
124
125    /// Treat an unmapped `path` as matching nothing instead of erroring.
126    #[must_use]
127    pub fn ignore_unmapped(mut self, ignore_unmapped: bool) -> Self {
128        self.opts
129            .insert("ignore_unmapped".to_string(), Value::Bool(ignore_unmapped));
130        self
131    }
132
133    common_opts!(common);
134}
135
136impl<E> AsQuery<E> for NestedQuery<E> {
137    fn into_query(self) -> Option<Query<E>> {
138        let mut body = self.opts;
139        body.insert("path".to_string(), Value::String(self.path));
140        body.insert("query".to_string(), self.query);
141        self.common.write(&mut body);
142        let mut outer = Map::new();
143        outer.insert("nested".to_string(), Value::Object(body));
144        Some(Query::leaf(Value::Object(outer)))
145    }
146}
147
148/// A request to shape one nested array in the results (via `inner_hits`).
149#[derive(Debug, Clone)]
150pub struct NestedProjection {
151    path: String,
152    query: Option<Value>,
153    sort: Vec<Sort>,
154    size: Option<u64>,
155    from: Option<u64>,
156}
157
158impl NestedProjection {
159    /// Order the returned elements.
160    #[must_use]
161    pub fn sort(mut self, sort: Sort) -> Self {
162        self.sort.push(sort);
163        self
164    }
165
166    /// Cap how many elements are returned per parent.
167    #[must_use]
168    pub fn size(mut self, size: u64) -> Self {
169        self.size = Some(size);
170        self
171    }
172
173    /// Offset within each parent's matching elements.
174    #[must_use]
175    pub fn from(mut self, from: u64) -> Self {
176        self.from = Some(from);
177        self
178    }
179
180    pub(crate) fn path(&self) -> &str {
181        &self.path
182    }
183
184    /// The `{ "nested": { path, query, inner_hits } }` clause (inner_hits named
185    /// after the path, for retrieval).
186    pub(crate) fn to_value(&self) -> Value {
187        let query = self.query.clone().unwrap_or_else(match_all_value);
188        let mut inner_hits = Map::new();
189        inner_hits.insert("name".to_string(), Value::String(self.path.clone()));
190        if let Some(size) = self.size {
191            inner_hits.insert("size".to_string(), Value::from(size));
192        }
193        if let Some(from) = self.from {
194            inner_hits.insert("from".to_string(), Value::from(from));
195        }
196        if !self.sort.is_empty() {
197            let keys = self.sort.iter().map(Sort::to_value).collect();
198            inner_hits.insert("sort".to_string(), Value::Array(keys));
199        }
200        let mut nested = Map::new();
201        nested.insert("path".to_string(), Value::String(self.path.clone()));
202        nested.insert("query".to_string(), query);
203        nested.insert("inner_hits".to_string(), Value::Object(inner_hits));
204        let mut outer = Map::new();
205        outer.insert("nested".to_string(), Value::Object(nested));
206        Value::Object(outer)
207    }
208}