Skip to main content

flusso_query/handles/
sort.rs

1//! Sort keys: [`SortOrder`], [`SortMode`], and the [`Sort`] builder produced by
2//! `.asc()` / `.desc()` on a sortable handle (or `Geo::distance_sort`,
3//! [`Sort::score`], [`Sort::script`]).
4//!
5//! A [`Sort`] carries the key it sorts on (a field path, `_score`,
6//! `_geo_distance`, or `_script`) plus its options (`missing`, `mode`,
7//! `unmapped_type`, …); `.missing_first()` / `.mode(..)` chain onto it, and it
8//! renders to one entry in the `sort` array. The typed handle is always the
9//! entry point — there is no public string-path sort.
10
11use serde_json::{Map, Value};
12
13use super::{Geo, GeoPoint, NumericType, ScriptSortType};
14use crate::query::AsQuery;
15use crate::{FlussoDocument, nested_boundaries};
16
17/// Sort direction.
18#[derive(Debug, Clone, Copy)]
19pub enum SortOrder {
20    /// Ascending.
21    Asc,
22    /// Descending.
23    Desc,
24}
25
26impl SortOrder {
27    pub(crate) fn as_str(self) -> &'static str {
28        match self {
29            SortOrder::Asc => "asc",
30            SortOrder::Desc => "desc",
31        }
32    }
33}
34
35/// How a multi-valued field collapses to one sort value.
36#[derive(Debug, Clone, Copy)]
37pub enum SortMode {
38    /// Smallest value.
39    Min,
40    /// Largest value.
41    Max,
42    /// Arithmetic mean (numeric fields).
43    Avg,
44    /// Sum (numeric fields).
45    Sum,
46    /// Median (numeric fields).
47    Median,
48}
49
50impl SortMode {
51    fn as_str(self) -> &'static str {
52        match self {
53            SortMode::Min => "min",
54            SortMode::Max => "max",
55            SortMode::Avg => "avg",
56            SortMode::Sum => "sum",
57            SortMode::Median => "median",
58        }
59    }
60}
61
62/// A single sort key. Produced by `.asc()` / `.desc()` on a sortable handle, by
63/// [`Sort::score`] / [`Sort::script`], or by `Geo::distance_sort`; chain the
64/// option setters (`missing_first`, `mode`, `unmapped_type`, …) onto it.
65#[derive(Debug, Clone)]
66pub struct Sort {
67    key: String,
68    body: Map<String, Value>,
69}
70
71impl Sort {
72    /// A field/order sort: `{ "<field>": { "order": "asc"|"desc" } }`.
73    pub(crate) fn new(field: &str, order: SortOrder) -> Self {
74        let mut body = Map::new();
75        body.insert(
76            "order".to_string(),
77            Value::String(order.as_str().to_string()),
78        );
79        Self {
80            key: field.to_string(),
81            body,
82        }
83    }
84
85    /// Sort by relevance `_score` (descending by default).
86    #[must_use]
87    pub fn score() -> Self {
88        let mut sort = Self {
89            key: "_score".to_string(),
90            body: Map::new(),
91        };
92        sort.body
93            .insert("order".to_string(), Value::String("desc".to_string()));
94        sort
95    }
96
97    /// Sort by a computed script value. `script_type` is the emitted value type
98    /// ([`ScriptSortType::Number`] / [`ScriptSortType::String`]); `source` is
99    /// the painless expression.
100    #[must_use]
101    pub fn script(
102        script_type: ScriptSortType,
103        source: impl Into<String>,
104        order: SortOrder,
105    ) -> Self {
106        let mut script = Map::new();
107        script.insert("source".to_string(), Value::String(source.into()));
108        let mut body = Map::new();
109        body.insert(
110            "type".to_string(),
111            Value::String(script_type.as_str().to_string()),
112        );
113        body.insert("script".to_string(), Value::Object(script));
114        body.insert(
115            "order".to_string(),
116            Value::String(order.as_str().to_string()),
117        );
118        Self {
119            key: "_script".to_string(),
120            body,
121        }
122    }
123
124    /// A pre-built sort clause (e.g. `_geo_distance`).
125    pub(crate) fn from_parts(key: String, body: Map<String, Value>) -> Self {
126        Self { key, body }
127    }
128
129    /// A field sort that is **nesting-aware**: it reads the scope `S`'s path and,
130    /// when the field sits inside one or more `nested` arrays, wraps the sort in
131    /// the matching `nested` chain (and defaults `mode` from the direction —
132    /// `asc → min`, `desc → max`). A root or flattened-object field (empty path)
133    /// renders a plain sort. Backs every [`Sortable`] handle.
134    pub(crate) fn field<S: FlussoDocument>(path: &str, order: SortOrder) -> Self {
135        let mut sort = Sort::new(path, order);
136        let boundaries = nested_boundaries(S::PATH);
137        if let Some(nested) = nested_clause(&boundaries) {
138            sort.body.insert("nested".to_string(), nested);
139            sort.body.insert(
140                "mode".to_string(),
141                Value::String(default_mode(order).to_string()),
142            );
143        }
144        sort
145    }
146
147    /// Sort ascending.
148    #[must_use]
149    pub fn asc(mut self) -> Self {
150        self.body
151            .insert("order".to_string(), Value::String("asc".to_string()));
152        self
153    }
154
155    /// Sort descending.
156    #[must_use]
157    pub fn desc(mut self) -> Self {
158        self.body
159            .insert("order".to_string(), Value::String("desc".to_string()));
160        self
161    }
162
163    /// Place documents missing this field first.
164    #[must_use]
165    pub fn missing_first(mut self) -> Self {
166        self.body
167            .insert("missing".to_string(), Value::String("_first".to_string()));
168        self
169    }
170
171    /// Place documents missing this field last.
172    #[must_use]
173    pub fn missing_last(mut self) -> Self {
174        self.body
175            .insert("missing".to_string(), Value::String("_last".to_string()));
176        self
177    }
178
179    /// Substitute a literal value for documents missing this field.
180    #[must_use]
181    pub fn missing(mut self, value: impl Into<Value>) -> Self {
182        self.body.insert("missing".to_string(), value.into());
183        self
184    }
185
186    /// How a multi-valued field reduces to one sort value.
187    #[must_use]
188    pub fn mode(mut self, mode: SortMode) -> Self {
189        self.body
190            .insert("mode".to_string(), Value::String(mode.as_str().to_string()));
191        self
192    }
193
194    /// Type to assume when the field is unmapped on some shard (instead of
195    /// failing the search), e.g. `"long"`.
196    #[must_use]
197    pub fn unmapped_type(mut self, unmapped_type: impl Into<String>) -> Self {
198        self.body.insert(
199            "unmapped_type".to_string(),
200            Value::String(unmapped_type.into()),
201        );
202        self
203    }
204
205    /// Numeric type to sort as ([`NumericType`]), for cross-index type coercion.
206    #[must_use]
207    pub fn numeric_type(mut self, numeric_type: NumericType) -> Self {
208        self.body.insert(
209            "numeric_type".to_string(),
210            Value::String(numeric_type.as_str().to_string()),
211        );
212        self
213    }
214
215    /// Date `format` for a `date` field sort.
216    #[must_use]
217    pub fn format(mut self, format: impl Into<String>) -> Self {
218        self.body
219            .insert("format".to_string(), Value::String(format.into()));
220        self
221    }
222
223    /// Sort by a field inside a `nested` array scoped to `path`, considering
224    /// only elements matching `filter`. An escape hatch for the rare
225    /// filter-scoped nested sort; ordinary nested sorts come from a
226    /// [`Sortable`] handle, which derives the (possibly multi-level) `nested`
227    /// chain from the field's scope automatically.
228    #[must_use]
229    pub fn nested_filtered<S>(mut self, path: impl Into<String>, filter: impl AsQuery<S>) -> Self {
230        let mut nested = Map::new();
231        nested.insert("path".to_string(), Value::String(path.into()));
232        if let Some(query) = filter.into_query() {
233            nested.insert("filter".to_string(), query.to_value());
234        }
235        self.body
236            .insert("nested".to_string(), Value::Object(nested));
237        self
238    }
239
240    pub(crate) fn to_value(&self) -> Value {
241        let mut outer = Map::new();
242        outer.insert(self.key.clone(), Value::Object(self.body.clone()));
243        Value::Object(outer)
244    }
245
246    /// The key this sort orders on (a field path, `_score`, `_geo_distance`, or
247    /// `_script`) — what [`SortBuilder`] dedups on.
248    pub(crate) fn key(&self) -> &str {
249        &self.key
250    }
251
252    /// Drop the `nested` chain (and its companion `mode`) for use inside
253    /// `inner_hits`: there the sort already runs within the nested document, so
254    /// the field path is relative and no wrapper applies. A plain or `_score`
255    /// sort is unchanged.
256    pub(crate) fn without_nested_context(mut self) -> Self {
257        if self.body.remove("nested").is_some() {
258            self.body.remove("mode");
259        }
260        self
261    }
262}
263
264/// Wrap a plain sort in the `nested` chain for `boundaries` (cumulative dotted
265/// paths, outermost first). `None` when there is no nesting.
266fn nested_clause(boundaries: &[String]) -> Option<Value> {
267    let (path, rest) = boundaries.split_first()?;
268    let mut clause = Map::new();
269    clause.insert("path".to_string(), Value::String(path.clone()));
270    if let Some(inner) = nested_clause(rest) {
271        clause.insert("nested".to_string(), inner);
272    }
273    Some(Value::Object(clause))
274}
275
276/// The `mode` a nested sort defaults to for `order`: the smallest element value
277/// when ascending, the largest when descending (so the chosen element is the one
278/// that sorts the parent extremally).
279fn default_mode(order: SortOrder) -> &'static str {
280    match order {
281        SortOrder::Asc => "min",
282        SortOrder::Desc => "max",
283    }
284}
285
286/// A handle that can produce a field [`Sort`]. The compile-time gate for
287/// [`SortBuilder::by`] / [`tiebreak`](SortBuilder::tiebreak): implemented for the
288/// orderable leaf handles (`Keyword`, `Text`, `Number<K>`, `Date`, `Bool`) and
289/// **not** for `Geo` / `Object` / map handles, so `by(geo_handle, …)` fails to
290/// compile (geo sorts go through [`SortBuilder::near`] / [`raw`](SortBuilder::raw)).
291///
292/// `.asc()` / `.desc()` are nesting-aware: a field inside one or more `nested`
293/// arrays renders the matching `nested` chain automatically, from the handle's
294/// scope.
295pub trait Sortable {
296    /// Sort ascending.
297    fn asc(&self) -> Sort;
298    /// Sort descending.
299    fn desc(&self) -> Sort;
300}
301
302/// Where to place documents missing the sorted field (a field-sort `missing`).
303#[derive(Debug, Clone)]
304pub enum Missing {
305    /// Missing values sort first (`_first`).
306    First,
307    /// Missing values sort last (`_last`).
308    Last,
309    /// Missing values take this substitute value.
310    Value(Value),
311}
312
313/// A field sort minus the field — a direction plus the field-sort modifiers,
314/// ready to attach to whatever handle [`SortBuilder::by`] is given.
315///
316/// This is what a consumer converts its own request enum into, once
317/// (`impl From<MyDir> for OrderBy`). It carries full parity with [`Sort`]'s
318/// field-sort options; everything but the direction defaults to unset.
319#[derive(Debug, Clone)]
320pub struct OrderBy {
321    order: SortOrder,
322    missing: Option<Missing>,
323    mode: Option<SortMode>,
324    numeric_type: Option<NumericType>,
325    unmapped_type: Option<String>,
326    format: Option<String>,
327}
328
329impl OrderBy {
330    /// Ascending, no modifiers.
331    #[must_use]
332    pub fn asc() -> Self {
333        Self::new(SortOrder::Asc)
334    }
335
336    /// Descending, no modifiers.
337    #[must_use]
338    pub fn desc() -> Self {
339        Self::new(SortOrder::Desc)
340    }
341
342    fn new(order: SortOrder) -> Self {
343        Self {
344            order,
345            missing: None,
346            mode: None,
347            numeric_type: None,
348            unmapped_type: None,
349            format: None,
350        }
351    }
352
353    /// Place documents missing this field first.
354    #[must_use]
355    pub fn missing_first(mut self) -> Self {
356        self.missing = Some(Missing::First);
357        self
358    }
359
360    /// Place documents missing this field last.
361    #[must_use]
362    pub fn missing_last(mut self) -> Self {
363        self.missing = Some(Missing::Last);
364        self
365    }
366
367    /// Substitute a literal value for documents missing this field.
368    #[must_use]
369    pub fn missing(mut self, value: impl Into<Value>) -> Self {
370        self.missing = Some(Missing::Value(value.into()));
371        self
372    }
373
374    /// How a multi-valued field reduces to one sort value.
375    #[must_use]
376    pub fn mode(mut self, mode: SortMode) -> Self {
377        self.mode = Some(mode);
378        self
379    }
380
381    /// Numeric type to sort as, for cross-index type coercion.
382    #[must_use]
383    pub fn numeric_type(mut self, numeric_type: NumericType) -> Self {
384        self.numeric_type = Some(numeric_type);
385        self
386    }
387
388    /// Type to assume when the field is unmapped on some shard.
389    #[must_use]
390    pub fn unmapped_type(mut self, unmapped_type: impl Into<String>) -> Self {
391        self.unmapped_type = Some(unmapped_type.into());
392        self
393    }
394
395    /// Date `format` for a `date` field sort.
396    #[must_use]
397    pub fn format(mut self, format: impl Into<String>) -> Self {
398        self.format = Some(format.into());
399        self
400    }
401
402    /// Build the field [`Sort`] for `handle`, in this order with these modifiers.
403    fn into_sort<H: Sortable>(self, handle: &H) -> Sort {
404        let mut sort = match self.order {
405            SortOrder::Asc => handle.asc(),
406            SortOrder::Desc => handle.desc(),
407        };
408        sort = match self.missing {
409            Some(Missing::First) => sort.missing_first(),
410            Some(Missing::Last) => sort.missing_last(),
411            Some(Missing::Value(value)) => sort.missing(value),
412            None => sort,
413        };
414        if let Some(mode) = self.mode {
415            sort = sort.mode(mode);
416        }
417        if let Some(numeric_type) = self.numeric_type {
418            sort = sort.numeric_type(numeric_type);
419        }
420        if let Some(unmapped_type) = self.unmapped_type {
421            sort = sort.unmapped_type(unmapped_type);
422        }
423        if let Some(format) = self.format {
424            sort = sort.format(format);
425        }
426        sort
427    }
428}
429
430impl From<SortOrder> for OrderBy {
431    fn from(order: SortOrder) -> Self {
432        Self::new(order)
433    }
434}
435
436/// The optionality carrier for [`SortBuilder::by`]: an absent order skips the
437/// field. A local newtype because coherence forbids `impl From<…> for Option<_>`.
438///
439/// `SortOrder`, `OrderBy`, and — via the umbrella impl — `Option<T: Into<OrderBy>>`
440/// all flow in, so a consumer's `Option<MyDir>` self-skips on `None` after one
441/// `impl From<MyDir> for OrderBy`.
442#[derive(Debug, Clone)]
443pub struct MaybeOrderBy(Option<OrderBy>);
444
445impl From<OrderBy> for MaybeOrderBy {
446    fn from(order: OrderBy) -> Self {
447        Self(Some(order))
448    }
449}
450
451impl From<SortOrder> for MaybeOrderBy {
452    fn from(order: SortOrder) -> Self {
453        Self(Some(order.into()))
454    }
455}
456
457impl<T: Into<OrderBy>> From<Option<T>> for MaybeOrderBy {
458    fn from(order: Option<T>) -> Self {
459        Self(order.map(Into::into))
460    }
461}
462
463/// Builds the `sort` array, one fluent verb per concern — each absorbing its own
464/// optionality so a request maps straight through with no per-field `if let`.
465///
466/// `by`/`near`/`tiebreak`/`or_default` **dedup** by sort key (first wins), so a
467/// field added twice — or an explicit sort that a tiebreak/default would repeat —
468/// appears once; `raw` is exempt. `or_default` only contributes when the builder
469/// would otherwise be empty.
470///
471/// ```
472/// use flusso_query::{SortBuilder, SortOrder, OrderBy};
473/// # use flusso_query::{Keyword, Number, kind, Root};
474/// # fn keyword(p: &str) -> Keyword<Root> { Keyword::at(p) }
475/// # fn count() -> Number<kind::Long, Root> { Number::at("orderCount") }
476/// let sorts = SortBuilder::new()
477///     .score_if(true)
478///     .by(count(), SortOrder::Desc)
479///     .by(keyword("city"), None::<OrderBy>)   // skipped
480///     .tiebreak(keyword("id"))
481///     .build();
482/// assert_eq!(sorts.len(), 3);                 // _score, orderCount, id
483/// ```
484#[derive(Debug, Default)]
485pub struct SortBuilder {
486    sorts: Vec<Sort>,
487    fallback: Option<Sort>,
488}
489
490impl SortBuilder {
491    /// An empty builder.
492    #[must_use]
493    pub fn new() -> Self {
494        Self::default()
495    }
496
497    /// Push `sort` unless its key is already present (first wins).
498    fn push_unique(&mut self, sort: Sort) {
499        if !self
500            .sorts
501            .iter()
502            .any(|existing| existing.key() == sort.key())
503        {
504            self.sorts.push(sort);
505        }
506    }
507
508    /// Sort by a field. `dir` accepts a [`SortOrder`], an [`OrderBy`], or an
509    /// `Option` of either (a `None` skips the field) — so a request's
510    /// `Option<dir>` flows straight in. Nesting-aware: a field inside `nested`
511    /// arrays renders the right `nested` chain from its scope.
512    #[must_use]
513    pub fn by<H: Sortable>(mut self, handle: H, dir: impl Into<MaybeOrderBy>) -> Self {
514        if let Some(order) = dir.into().0 {
515            self.push_unique(order.into_sort(&handle));
516        }
517        self
518    }
519
520    /// Sort by distance from `center` (`_geo_distance`, nearest first). A `None`
521    /// center skips it. Pass a unit / script geo sort through [`raw`](Self::raw).
522    #[must_use]
523    pub fn near<S>(mut self, handle: Geo<S>, center: impl Into<Option<GeoPoint>>) -> Self {
524        if let Some(center) = center.into() {
525            self.push_unique(handle.distance_from(center));
526        }
527        self
528    }
529
530    /// Sort by relevance `_score` (descending).
531    #[must_use]
532    pub fn score(mut self) -> Self {
533        self.push_unique(Sort::score());
534        self
535    }
536
537    /// Sort by `_score` only when `cond` holds (e.g. a free-text query is present).
538    #[must_use]
539    pub fn score_if(self, cond: bool) -> Self {
540        if cond { self.score() } else { self }
541    }
542
543    /// Append a pre-built [`Sort`] verbatim — the escape hatch for sorts the
544    /// typed verbs don't cover (`_script`, a geo sort with options). A `None`
545    /// adds nothing. **Not** deduped.
546    #[must_use]
547    pub fn raw(mut self, sort: impl Into<Option<Sort>>) -> Self {
548        if let Some(sort) = sort.into() {
549            self.sorts.push(sort);
550        }
551        self
552    }
553
554    /// A stable final sort key (ascending) — append a unique field so equal
555    /// leading keys still page deterministically.
556    #[must_use]
557    pub fn tiebreak<H: Sortable>(mut self, handle: H) -> Self {
558        self.push_unique(handle.asc());
559        self
560    }
561
562    /// A fallback used only if nothing else lands in the builder.
563    #[must_use]
564    pub fn or_default(mut self, sort: impl Into<Sort>) -> Self {
565        if self.fallback.is_none() {
566            self.fallback = Some(sort.into());
567        }
568        self
569    }
570
571    /// Finish: the `sort` array (the fallback, if set, when otherwise empty).
572    #[must_use]
573    pub fn build(mut self) -> Vec<Sort> {
574        if self.sorts.is_empty()
575            && let Some(fallback) = self.fallback
576        {
577            self.sorts.push(fallback);
578        }
579        self.sorts
580    }
581}
582
583impl IntoIterator for SortBuilder {
584    type Item = Sort;
585    type IntoIter = std::vec::IntoIter<Sort>;
586
587    fn into_iter(self) -> Self::IntoIter {
588        self.build().into_iter()
589    }
590}