rustis/commands/
geo_commands.rs

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