Skip to main content

rustis/commands/
geo_commands.rs

1use crate::{
2    client::{PreparedCommand, prepare_command},
3    resp::{Response, cmd, serialize_flag},
4};
5use serde::{
6    Deserialize, Deserializer, Serialize,
7    de::{
8        self, Unexpected, Visitor,
9        value::{BytesDeserializer, SeqAccessDeserializer},
10    },
11};
12use std::{fmt, marker::PhantomData};
13
14/// A group of Redis commands related to [`Geospatial`](https://redis.io/docs/data-types/geospatial/) indices
15///
16/// # See Also
17/// [Redis Geospatial Commands](https://redis.io/commands/?group=geo)
18pub trait GeoCommands<'a>: Sized {
19    /// Adds the specified geospatial items (longitude, latitude, name) to the specified key.
20    ///
21    /// # Return
22    /// * When used without optional arguments, the number of elements added to the sorted set (excluding score updates).
23    /// * If the CH option is specified, the number of elements that were changed (added or updated).
24    ///
25    /// # See Also
26    /// [<https://redis.io/commands/geoadd/>](https://redis.io/commands/geoadd/)
27    #[must_use]
28    fn geoadd(
29        self,
30        key: impl Serialize,
31        condition: impl Into<Option<GeoAddCondition>>,
32        change: bool,
33        items: impl Serialize,
34    ) -> PreparedCommand<'a, Self, usize> {
35        prepare_command(
36            self,
37            cmd("GEOADD")
38                .key(key)
39                .arg(condition.into())
40                .arg_if(change, "CH")
41                .arg(items),
42        )
43    }
44
45    /// Return the distance between two members in the geospatial index
46    /// represented by the sorted set.
47    ///
48    /// # Return
49    /// The distance in the specified unit, or None if one or both the elements are missing.
50    ///
51    /// # See Also
52    /// [<https://redis.io/commands/geodist/>](https://redis.io/commands/geodist/)
53    #[must_use]
54    fn geodist(
55        self,
56        key: impl Serialize,
57        member1: impl Serialize,
58        member2: impl Serialize,
59        unit: GeoUnit,
60    ) -> PreparedCommand<'a, Self, Option<f64>> {
61        prepare_command(
62            self,
63            cmd("GEODIST").key(key).arg(member1).arg(member2).arg(unit),
64        )
65    }
66
67    /// Return valid [Geohash](https://en.wikipedia.org/wiki/Geohash) strings representing the position of one or more elements
68    /// in a sorted set value representing a geospatial index (where elements were added using [geoadd](GeoCommands::geoadd)).
69    ///
70    /// # Return
71    /// An array where each element is the Geohash corresponding to each member name passed as argument to the command.
72    ///
73    /// # See Also
74    /// [<https://redis.io/commands/geohash/>](https://redis.io/commands/geohash/)
75    #[must_use]
76    fn geohash<R: Response>(
77        self,
78        key: impl Serialize,
79        members: impl Serialize,
80    ) -> PreparedCommand<'a, Self, R> {
81        prepare_command(self, cmd("GEOHASH").key(key).arg(members))
82    }
83
84    /// Return the positions (longitude,latitude) of all the specified members
85    ///  of the geospatial index represented by the sorted set at key.
86    ///
87    /// # Return
88    /// n array where each element is a two elements array representing longitude and latitude
89    /// (x,y) of each member name passed as argument to the command.
90    /// Non existing elements are reported as NULL elements of the array.
91    ///
92    /// # See Also
93    /// [<https://redis.io/commands/geopos/>](https://redis.io/commands/geopos/)
94    #[must_use]
95    fn geopos(
96        self,
97        key: impl Serialize,
98        members: impl Serialize,
99    ) -> PreparedCommand<'a, Self, Vec<Option<(f64, f64)>>> {
100        prepare_command(self, cmd("GEOPOS").key(key).arg(members))
101    }
102
103    /// Return the members of a sorted set populated with geospatial information using [geoadd](GeoCommands::geoadd),
104    /// which are within the borders of the area specified by a given shape.
105    ///
106    /// # Return
107    /// An array of members + additional information depending
108    /// on which `with_xyz` options have been selected
109    ///
110    /// # See Also
111    /// [<https://redis.io/commands/geosearch/>](https://redis.io/commands/geosearch/)
112    #[must_use]
113    fn geosearch<'b, R: Response>(
114        self,
115        key: impl Serialize,
116        from: GeoSearchFrom<'b>,
117        by: GeoSearchBy,
118        options: GeoSearchOptions,
119    ) -> PreparedCommand<'a, Self, R> {
120        prepare_command(
121            self,
122            cmd("GEOSEARCH").key(key).arg(from).arg(by).arg(options),
123        )
124    }
125
126    /// This command is like [geosearch](GeoCommands::geosearch), but stores the result in destination key.
127    ///
128    /// # Return
129    /// the number of elements in the resulting set.
130    ///
131    /// # See Also
132    /// [<https://redis.io/commands/geosearchstore/>](https://redis.io/commands/geosearchstore/)
133    #[must_use]
134    fn geosearchstore<'b>(
135        self,
136        destination: impl Serialize,
137        source: impl Serialize,
138        from: GeoSearchFrom<'b>,
139        by: GeoSearchBy,
140        options: GeoSearchStoreOptions,
141    ) -> PreparedCommand<'a, Self, u32> {
142        prepare_command(
143            self,
144            cmd("GEOSEARCHSTORE")
145                .key(destination)
146                .key(source)
147                .arg(from)
148                .arg(by)
149                .arg(options),
150        )
151    }
152}
153
154/// Condition for the [`geoadd`](GeoCommands::geoadd) command
155#[derive(Serialize)]
156#[serde(rename_all = "UPPERCASE")]
157pub enum GeoAddCondition {
158    /// Don't update already existing elements. Always add new elements.
159    NX,
160    /// Only update elements that already exist. Never add elements.
161    XX,
162}
163
164/// Distance Unit
165#[derive(Serialize)]
166pub enum GeoUnit {
167    #[serde(rename = "M")]
168    Meters,
169    #[serde(rename = "KM")]
170    Kilometers,
171    #[serde(rename = "MI")]
172    Miles,
173    #[serde(rename = "FT")]
174    Feet,
175}
176
177/// The query's center point is provided by one of these mandatory options:
178#[derive(Serialize)]
179#[serde(rename_all(serialize = "UPPERCASE"))]
180pub enum GeoSearchFrom<'a> {
181    /// Use the position of the given existing `member` in the sorted set.
182    FromMember(&'a str),
183    /// Use the given `longitude` and `latitude` position.
184    FromLonLat(f64, f64),
185}
186
187impl<'a> GeoSearchFrom<'a> {
188    pub fn from_member(member: &'a str) -> Self {
189        Self::FromMember(member)
190    }
191
192    pub fn from_longitude_latitude(longitude: f64, latitude: f64) -> Self {
193        Self::FromLonLat(longitude, latitude)
194    }
195}
196
197/// The query's shape is provided by one of these mandatory options:
198#[derive(Serialize)]
199#[serde(rename_all(serialize = "UPPERCASE"))]
200pub enum GeoSearchBy {
201    /// Search inside circular area according to given `radius` in the specified `unit`.
202    ByRadius(f64, GeoUnit),
203    /// Search inside an axis-aligned rectangle, determined by `height` and `width` in the specified `unit`.
204    ByBox(f64, f64, GeoUnit),
205}
206
207impl GeoSearchBy {
208    pub fn by_radius(radius: f64, unit: GeoUnit) -> Self {
209        Self::ByRadius(radius, unit)
210    }
211
212    pub fn by_box(width: f64, height: f64, unit: GeoUnit) -> Self {
213        Self::ByBox(width, height, unit)
214    }
215}
216
217/// Matching items are returned unsorted by default.
218/// To sort them, use one of the following two options:
219#[derive(Serialize)]
220#[serde(rename_all = "UPPERCASE")]
221pub enum GeoSearchOrder {
222    /// Sort returned items from the nearest to the farthest, relative to the center point.
223    Asc,
224    /// Sort returned items from the farthest to the nearest, relative to the center point.
225    Desc,
226}
227
228/// Options for the [`geosearch`](GeoCommands::geosearch) command
229#[derive(Default, Serialize)]
230#[serde(rename_all(serialize = "UPPERCASE"))]
231pub struct GeoSearchOptions {
232    #[serde(rename = "", skip_serializing_if = "Option::is_none")]
233    order: Option<GeoSearchOrder>,
234    #[serde(skip_serializing_if = "Option::is_none")]
235    count: Option<u32>,
236    #[serde(
237        skip_serializing_if = "std::ops::Not::not",
238        serialize_with = "serialize_flag"
239    )]
240    any: bool,
241    #[serde(
242        skip_serializing_if = "std::ops::Not::not",
243        serialize_with = "serialize_flag"
244    )]
245    withcoord: bool,
246    #[serde(
247        skip_serializing_if = "std::ops::Not::not",
248        serialize_with = "serialize_flag"
249    )]
250    withdist: bool,
251    #[serde(
252        skip_serializing_if = "std::ops::Not::not",
253        serialize_with = "serialize_flag"
254    )]
255    withhash: bool,
256}
257
258impl GeoSearchOptions {
259    #[must_use]
260    pub fn order(mut self, order: GeoSearchOrder) -> Self {
261        self.order = Some(order);
262        self
263    }
264
265    #[must_use]
266    pub fn count(mut self, count: u32, any: bool) -> Self {
267        self.count = Some(count);
268        self.any = any;
269        self
270    }
271
272    #[must_use]
273    pub fn with_coord(mut self) -> Self {
274        self.withcoord = true;
275        self
276    }
277
278    #[must_use]
279    pub fn with_dist(mut self) -> Self {
280        self.withdist = true;
281        self
282    }
283
284    #[must_use]
285    pub fn with_hash(mut self) -> Self {
286        self.withhash = true;
287        self
288    }
289}
290
291/// Result of the [`geosearch`](GeoCommands::geosearch) command.
292#[derive(Debug)]
293pub struct GeoSearchResult<R: Response> {
294    /// The matched member.
295    pub member: R,
296
297    /// The distance of the matched member from the specified center.
298    pub distance: Option<f64>,
299
300    /// The geohash integer of the matched member
301    pub geo_hash: Option<i64>,
302
303    /// The coordinates (longitude, latitude) of the matched member
304    pub coordinates: Option<(f64, f64)>,
305}
306
307impl<'de, R: Response + Deserialize<'de>> Deserialize<'de> for GeoSearchResult<R> {
308    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
309    where
310        D: Deserializer<'de>,
311    {
312        pub enum GeoSearchResultField {
313            Distance(f64),
314            GeoHash(i64),
315            Coordinates((f64, f64)),
316        }
317
318        impl<'de> Deserialize<'de> for GeoSearchResultField {
319            #[inline]
320            fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
321            where
322                D: Deserializer<'de>,
323            {
324                struct GeoSearchResultFieldVisitor;
325
326                impl<'de> Visitor<'de> for GeoSearchResultFieldVisitor {
327                    type Value = GeoSearchResultField;
328
329                    fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
330                        formatter.write_str("GeoSearchResultField")
331                    }
332
333                    fn visit_borrowed_bytes<E>(self, v: &'de [u8]) -> Result<Self::Value, E>
334                    where
335                        E: de::Error,
336                    {
337                        let Ok(distance) = std::str::from_utf8(v) else {
338                            return Err(de::Error::invalid_value(
339                                Unexpected::Bytes(v),
340                                &"A valid f64 encoded in an UTF8 string",
341                            ));
342                        };
343
344                        let Ok(distance) = distance.parse::<f64>() else {
345                            return Err(de::Error::invalid_value(
346                                Unexpected::Bytes(v),
347                                &"A valid f64 encoded in an UTF8 string",
348                            ));
349                        };
350
351                        Ok(GeoSearchResultField::Distance(distance))
352                    }
353
354                    fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>
355                    where
356                        E: de::Error,
357                    {
358                        Ok(GeoSearchResultField::GeoHash(v))
359                    }
360
361                    fn visit_seq<A>(self, seq: A) -> Result<Self::Value, A::Error>
362                    where
363                        A: de::SeqAccess<'de>,
364                    {
365                        let coordinates =
366                            <(f64, f64)>::deserialize(SeqAccessDeserializer::new(seq))?;
367                        Ok(GeoSearchResultField::Coordinates(coordinates))
368                    }
369                }
370
371                deserializer.deserialize_any(GeoSearchResultFieldVisitor)
372            }
373        }
374
375        pub struct GeoSearchResultVisitor<R: Response> {
376            phantom: PhantomData<R>,
377        }
378
379        impl<'de, R: Response + Deserialize<'de>> Visitor<'de> for GeoSearchResultVisitor<R> {
380            type Value = GeoSearchResult<R>;
381
382            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
383                formatter.write_str("GeoSearchResult<M>")
384            }
385
386            fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
387            where
388                A: de::SeqAccess<'de>,
389            {
390                let Some(member) = seq.next_element::<R>().map_err(de::Error::custom)? else {
391                    return Err(de::Error::invalid_length(0, &"more elements in sequence"));
392                };
393
394                let mut distance: Option<f64> = None;
395                let mut geo_hash: Option<i64> = None;
396                let mut coordinates: Option<(f64, f64)> = None;
397
398                while let Some(field) = seq.next_element::<GeoSearchResultField>()? {
399                    match field {
400                        GeoSearchResultField::Distance(d) => distance = Some(d),
401                        GeoSearchResultField::GeoHash(gh) => geo_hash = Some(gh),
402                        GeoSearchResultField::Coordinates(c) => coordinates = Some(c),
403                    }
404                }
405
406                Ok(GeoSearchResult {
407                    member,
408                    distance,
409                    geo_hash,
410                    coordinates,
411                })
412            }
413
414            fn visit_borrowed_bytes<E>(self, v: &[u8]) -> Result<Self::Value, E>
415            where
416                E: de::Error,
417            {
418                let member = R::deserialize(BytesDeserializer::new(v))?;
419
420                Ok(GeoSearchResult {
421                    member,
422                    distance: None,
423                    geo_hash: None,
424                    coordinates: None,
425                })
426            }
427        }
428
429        deserializer.deserialize_any(GeoSearchResultVisitor::<R> {
430            phantom: PhantomData,
431        })
432    }
433}
434
435/// Options for the [`geosearchstore`](GeoCommands::geosearchstore) command
436#[derive(Default, Serialize)]
437#[serde(rename_all(serialize = "UPPERCASE"))]
438pub struct GeoSearchStoreOptions {
439    #[serde(rename = "", skip_serializing_if = "Option::is_none")]
440    order: Option<GeoSearchOrder>,
441    #[serde(skip_serializing_if = "Option::is_none")]
442    count: Option<u32>,
443    #[serde(
444        skip_serializing_if = "std::ops::Not::not",
445        serialize_with = "serialize_flag"
446    )]
447    any: bool,
448    #[serde(
449        skip_serializing_if = "std::ops::Not::not",
450        serialize_with = "serialize_flag"
451    )]
452    storedist: bool,
453}
454
455impl GeoSearchStoreOptions {
456    #[must_use]
457    pub fn order(mut self, order: GeoSearchOrder) -> Self {
458        self.order = Some(order);
459        self
460    }
461
462    #[must_use]
463    pub fn count(mut self, count: u32, any: bool) -> Self {
464        self.count = Some(count);
465        self.any = any;
466        self
467    }
468
469    #[must_use]
470    pub fn store_dist(mut self, store_dist: bool) -> Self {
471        self.storedist = store_dist;
472        self
473    }
474}