Skip to main content

flusso_query/handles/
geo.rs

1//! Geographic field handles: a [`GeoPoint`] and the [`Geo`] field with the
2//! `within` query family (`within` distance / `within_box` / `within_polygon`)
3//! plus sort-by-distance.
4
5use std::marker::PhantomData;
6
7use serde_json::{Map, Value};
8
9use super::{Common, DistanceType, Sort, SortOrder, ValidationMethod, common_opts, exists_q, wrap};
10use crate::query::{AsQuery, Query, Root};
11
12/// A geographic point — latitude/longitude in degrees.
13#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]
14pub struct GeoPoint {
15    /// Latitude in degrees.
16    pub lat: f64,
17    /// Longitude in degrees.
18    pub lon: f64,
19}
20
21impl GeoPoint {
22    /// A point at `lat`/`lon` degrees.
23    pub fn new(lat: f64, lon: f64) -> Self {
24        Self { lat, lon }
25    }
26
27    /// `{ "lat": …, "lon": … }`.
28    fn to_value(self) -> Value {
29        let mut point = Map::new();
30        point.insert("lat".to_string(), Value::from(self.lat));
31        point.insert("lon".to_string(), Value::from(self.lon));
32        Value::Object(point)
33    }
34}
35
36/// A distance unit OpenSearch accepts in a `geo_distance` query or
37/// `_geo_distance` sort.
38#[derive(Debug, Clone, Copy)]
39pub enum DistanceUnit {
40    /// Kilometers (`km`).
41    Kilometers,
42    /// Meters (`m`).
43    Meters,
44    /// Centimeters (`cm`).
45    Centimeters,
46    /// Millimeters (`mm`).
47    Millimeters,
48    /// Miles (`mi`).
49    Miles,
50    /// Yards (`yd`).
51    Yards,
52    /// Feet (`ft`).
53    Feet,
54    /// Nautical miles (`nmi`).
55    NauticalMiles,
56}
57
58impl DistanceUnit {
59    pub(crate) fn as_str(self) -> &'static str {
60        match self {
61            DistanceUnit::Kilometers => "km",
62            DistanceUnit::Meters => "m",
63            DistanceUnit::Centimeters => "cm",
64            DistanceUnit::Millimeters => "mm",
65            DistanceUnit::Miles => "mi",
66            DistanceUnit::Yards => "yd",
67            DistanceUnit::Feet => "ft",
68            DistanceUnit::NauticalMiles => "nmi",
69        }
70    }
71}
72
73/// A distance with an explicit unit — e.g. `Distance::km(12.0)`. Renders to the
74/// OpenSearch distance string (`"12km"`), so a malformed radius (`"12 km"`, a
75/// typo'd unit) can't reach the query.
76#[derive(Debug, Clone, Copy)]
77pub struct Distance {
78    value: f64,
79    unit: DistanceUnit,
80}
81
82impl Distance {
83    /// `value` in `unit`.
84    pub fn new(value: f64, unit: DistanceUnit) -> Self {
85        Self { value, unit }
86    }
87
88    /// Kilometers.
89    pub fn km(value: f64) -> Self {
90        Self::new(value, DistanceUnit::Kilometers)
91    }
92
93    /// Meters.
94    pub fn meters(value: f64) -> Self {
95        Self::new(value, DistanceUnit::Meters)
96    }
97
98    /// Centimeters.
99    pub fn centimeters(value: f64) -> Self {
100        Self::new(value, DistanceUnit::Centimeters)
101    }
102
103    /// Millimeters.
104    pub fn millimeters(value: f64) -> Self {
105        Self::new(value, DistanceUnit::Millimeters)
106    }
107
108    /// Miles.
109    pub fn miles(value: f64) -> Self {
110        Self::new(value, DistanceUnit::Miles)
111    }
112
113    /// Yards.
114    pub fn yards(value: f64) -> Self {
115        Self::new(value, DistanceUnit::Yards)
116    }
117
118    /// Feet.
119    pub fn feet(value: f64) -> Self {
120        Self::new(value, DistanceUnit::Feet)
121    }
122
123    /// Nautical miles.
124    pub fn nautical_miles(value: f64) -> Self {
125        Self::new(value, DistanceUnit::NauticalMiles)
126    }
127
128    fn to_query_string(self) -> String {
129        format!("{}{}", self.value, self.unit.as_str())
130    }
131}
132
133/// A `geo_point` field — the `within` query family (distance / box / polygon),
134/// plus sort-by-distance.
135#[derive(Debug, Clone)]
136pub struct Geo<S = Root> {
137    path: String,
138    _scope: PhantomData<fn() -> S>,
139}
140
141impl<S> Geo<S> {
142    pub fn at(path: impl Into<String>) -> Self {
143        Self {
144            path: path.into(),
145            _scope: PhantomData,
146        }
147    }
148
149    /// Points within `distance` (e.g. `Distance::km(12.0)`) of `center`. Returns
150    /// a [`GeoDistanceQuery`] builder for `distance_type` / `validation_method`
151    /// plus `boost` / `name`.
152    pub fn within(&self, distance: Distance, center: GeoPoint) -> GeoDistanceQuery<S> {
153        GeoDistanceQuery {
154            path: self.path.clone(),
155            distance: distance.to_query_string(),
156            center,
157            opts: Map::new(),
158            common: Common::default(),
159            _scope: PhantomData,
160        }
161    }
162
163    /// Points inside the axis-aligned box with the given corners.
164    pub fn within_box(&self, top_left: GeoPoint, bottom_right: GeoPoint) -> Query<S> {
165        let mut corners = Map::new();
166        corners.insert("top_left".to_string(), top_left.to_value());
167        corners.insert("bottom_right".to_string(), bottom_right.to_value());
168        let mut body = Map::new();
169        body.insert(self.path.clone(), Value::Object(corners));
170        wrap_object("geo_bounding_box", body)
171    }
172
173    /// Points inside the polygon described by `points` (three or more vertices).
174    pub fn within_polygon(&self, points: impl IntoIterator<Item = GeoPoint>) -> Query<S> {
175        let vertices = points.into_iter().map(GeoPoint::to_value).collect();
176        let mut inner = Map::new();
177        inner.insert("points".to_string(), Value::Array(vertices));
178        let mut body = Map::new();
179        body.insert(self.path.clone(), Value::Object(inner));
180        wrap_object("geo_polygon", body)
181    }
182
183    /// The field has a value.
184    pub fn exists(&self) -> Query<S> {
185        exists_q(&self.path)
186    }
187
188    /// Sort by distance from `center`, nearest first. Sugar for the common
189    /// `_geo_distance` sort: ascending, OpenSearch's default unit (meters);
190    /// chain `.desc()` to flip it. For an explicit unit or order use
191    /// [`distance_sort`](Self::distance_sort).
192    pub fn distance_from(&self, center: GeoPoint) -> Sort {
193        let mut body = Map::new();
194        body.insert(self.path.clone(), center.to_value());
195        body.insert("order".to_string(), Value::String("asc".to_string()));
196        Sort::from_parts("_geo_distance".to_string(), body)
197    }
198
199    /// Sort by distance from `center`, measured in `unit`.
200    pub fn distance_sort(&self, center: GeoPoint, order: SortOrder, unit: DistanceUnit) -> Sort {
201        let mut body = Map::new();
202        body.insert(self.path.clone(), center.to_value());
203        body.insert(
204            "order".to_string(),
205            Value::String(order.as_str().to_string()),
206        );
207        body.insert("unit".to_string(), Value::String(unit.as_str().to_string()));
208        Sort::from_parts("_geo_distance".to_string(), body)
209    }
210}
211
212/// `{ "<name>": { <body> } }` as a scope-`S` query.
213fn wrap_object<S>(name: &str, body: Map<String, Value>) -> Query<S> {
214    wrap(name, body)
215}
216
217/// A `geo_distance` clause: points within a radius of a center, with the
218/// `distance_type` / `validation_method` options plus `boost` / `name`.
219#[derive(Debug, Clone)]
220pub struct GeoDistanceQuery<S = Root> {
221    path: String,
222    distance: String,
223    center: GeoPoint,
224    opts: Map<String, Value>,
225    common: Common,
226    _scope: PhantomData<fn() -> S>,
227}
228
229impl<S> GeoDistanceQuery<S> {
230    /// How distance is computed ([`DistanceType::Arc`] is the default).
231    #[must_use]
232    pub fn distance_type(mut self, distance_type: DistanceType) -> Self {
233        self.opts.insert(
234            "distance_type".to_string(),
235            Value::String(distance_type.as_str().to_string()),
236        );
237        self
238    }
239
240    /// How malformed coordinates are handled ([`ValidationMethod::Strict`] is
241    /// the default).
242    #[must_use]
243    pub fn validation_method(mut self, validation_method: ValidationMethod) -> Self {
244        self.opts.insert(
245            "validation_method".to_string(),
246            Value::String(validation_method.as_str().to_string()),
247        );
248        self
249    }
250
251    common_opts!(common);
252}
253
254impl<S> AsQuery<S> for GeoDistanceQuery<S> {
255    fn into_query(self) -> Option<Query<S>> {
256        let mut body = self.opts;
257        body.insert("distance".to_string(), Value::String(self.distance));
258        body.insert(self.path, self.center.to_value());
259        self.common.write(&mut body);
260        Some(wrap("geo_distance", body))
261    }
262}