Skip to content

Commit d81ec2c

Browse files
GEOSEARCH & GEOSEARCHSTORE (#2089)
This PR implements [`GEOSEARCH`](https://redis.io/commands/geosearch/) and [`GEOSEARCHSTORE`](https://redis.io/commands/geosearchstore/) for #2055 To abstract the box/circle sub-options from GEOSEARCH I added a new abstract class `GeoSearchShape` who's children are responsible for maintaining the bounding shape, its unit of measurement, the number of arguments required for the sub-option, and of course the sub-option name. Rather than casting/extracting the arguments, I have it use an IEnumerable state-machine with yield/return. Wasn't sure which was the better option, the IEnumerable seemed cleaner, open to whichever you want. I changed the `GEORADIUS` pattern of having a `GeoSearch(key, member, args. . .)` and a `GeoSearch(key, lon, lat, args. . .)`, and instead have `GeoSearchByMember` and `GeoSearchByCoordinates`. If I'm honest, it was because my IDE was complaining about breaking compatibility rules by having more than 1 override with optional parameters, not sure if you have a strong feeling on this. Co-authored-by: Nick Craver <[email protected]>
1 parent cb571bd commit d81ec2c

File tree

16 files changed

+930
-23
lines changed

16 files changed

+930
-23
lines changed

src/StackExchange.Redis/Enums/GeoUnit.cs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
namespace StackExchange.Redis
1+
using System;
2+
3+
namespace StackExchange.Redis
24
{
35
/// <summary>
46
/// Units associated with Geo Commands.
@@ -22,4 +24,16 @@ public enum GeoUnit
2224
/// </summary>
2325
Feet,
2426
}
27+
28+
internal static class GeoUnitExtensions
29+
{
30+
internal static RedisValue ToLiteral(this GeoUnit unit) => unit switch
31+
{
32+
GeoUnit.Feet => RedisLiterals.ft,
33+
GeoUnit.Kilometers => RedisLiterals.km,
34+
GeoUnit.Meters => RedisLiterals.m,
35+
GeoUnit.Miles => RedisLiterals.mi,
36+
_ => throw new ArgumentOutOfRangeException(nameof(unit))
37+
};
38+
}
2539
}

src/StackExchange.Redis/Enums/Order.cs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
namespace StackExchange.Redis
1+
using System;
2+
3+
namespace StackExchange.Redis
24
{
35
/// <summary>
46
/// The direction in which to sequence elements.
@@ -14,4 +16,14 @@ public enum Order
1416
/// </summary>
1517
Descending,
1618
}
19+
20+
internal static class OrderExtensions
21+
{
22+
internal static RedisValue ToLiteral(this Order order) => order switch
23+
{
24+
Order.Ascending => RedisLiterals.ASC,
25+
Order.Descending => RedisLiterals.DESC,
26+
_ => throw new ArgumentOutOfRangeException(nameof(order))
27+
};
28+
}
1729
}

src/StackExchange.Redis/Enums/RedisCommand.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ internal enum RedisCommand
4747
GEOPOS,
4848
GEORADIUS,
4949
GEORADIUSBYMEMBER,
50+
GEOSEARCH,
51+
GEOSEARCHSTORE,
5052

5153
GET,
5254
GETBIT,

src/StackExchange.Redis/GeoEntry.cs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Collections.Generic;
23

34
namespace StackExchange.Redis
45
{
@@ -27,7 +28,26 @@ public enum GeoRadiusOptions
2728
/// <summary>
2829
/// Populates the commonly used values from the entry (the integer hash is not returned as it is not commonly useful).
2930
/// </summary>
30-
Default = WithCoordinates | GeoRadiusOptions.WithDistance
31+
Default = WithCoordinates | WithDistance
32+
}
33+
34+
internal static class GeoRadiusOptionsExtensions
35+
{
36+
internal static void AddArgs(this GeoRadiusOptions options, List<RedisValue> values)
37+
{
38+
if ((options & GeoRadiusOptions.WithCoordinates) != 0)
39+
{
40+
values.Add(RedisLiterals.WITHCOORD);
41+
}
42+
if ((options & GeoRadiusOptions.WithDistance) != 0)
43+
{
44+
values.Add(RedisLiterals.WITHDIST);
45+
}
46+
if ((options & GeoRadiusOptions.WithGeoHash) != 0)
47+
{
48+
values.Add(RedisLiterals.WITHHASH);
49+
}
50+
}
3151
}
3252

3353
/// <summary>
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
using System.Collections.Generic;
2+
3+
namespace StackExchange.Redis;
4+
5+
/// <summary>
6+
/// A Shape that you can use for a GeoSearch
7+
/// </summary>
8+
public abstract class GeoSearchShape
9+
{
10+
/// <summary>
11+
/// The unit to use for creating the shape.
12+
/// </summary>
13+
protected GeoUnit Unit { get; }
14+
15+
/// <summary>
16+
/// The number of shape arguments.
17+
/// </summary>
18+
internal abstract int ArgCount { get; }
19+
20+
/// <summary>
21+
/// constructs a <see cref="GeoSearchShape"/>
22+
/// </summary>
23+
/// <param name="unit"></param>
24+
public GeoSearchShape(GeoUnit unit)
25+
{
26+
Unit = unit;
27+
}
28+
29+
internal abstract void AddArgs(List<RedisValue> args);
30+
}
31+
32+
/// <summary>
33+
/// A circle drawn on a map bounding
34+
/// </summary>
35+
public class GeoSearchCircle : GeoSearchShape
36+
{
37+
private readonly double _radius;
38+
39+
/// <summary>
40+
/// Creates a <see cref="GeoSearchCircle"/> Shape.
41+
/// </summary>
42+
/// <param name="radius">The radius of the circle.</param>
43+
/// <param name="unit">The distance unit the circle will use, defaults to Meters.</param>
44+
public GeoSearchCircle(double radius, GeoUnit unit = GeoUnit.Meters) : base (unit)
45+
{
46+
_radius = radius;
47+
}
48+
49+
internal override int ArgCount => 3;
50+
51+
/// <summary>
52+
/// Gets the <exception cref="RedisValue"/>s for this shape
53+
/// </summary>
54+
/// <returns></returns>
55+
internal override void AddArgs(List<RedisValue> args)
56+
{
57+
args.Add(RedisLiterals.BYRADIUS);
58+
args.Add(_radius);
59+
args.Add(Unit.ToLiteral());
60+
}
61+
}
62+
63+
/// <summary>
64+
/// A box drawn on a map
65+
/// </summary>
66+
public class GeoSearchBox : GeoSearchShape
67+
{
68+
private readonly double _height;
69+
70+
private readonly double _width;
71+
72+
/// <summary>
73+
/// Initializes a GeoBox.
74+
/// </summary>
75+
/// <param name="height">The height of the box.</param>
76+
/// <param name="width">The width of the box.</param>
77+
/// <param name="unit">The distance unit the box will use, defaults to Meters.</param>
78+
public GeoSearchBox(double height, double width, GeoUnit unit = GeoUnit.Meters) : base(unit)
79+
{
80+
_height = height;
81+
_width = width;
82+
}
83+
84+
internal override int ArgCount => 4;
85+
86+
internal override void AddArgs(List<RedisValue> args)
87+
{
88+
args.Add(RedisLiterals.BYBOX);
89+
args.Add(_width);
90+
args.Add(_height);
91+
args.Add(Unit.ToLiteral());
92+
}
93+
}

src/StackExchange.Redis/Interfaces/IDatabase.cs

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,74 @@ public interface IDatabase : IRedis, IDatabaseAsync
193193
/// <remarks>https://redis.io/commands/georadius</remarks>
194194
GeoRadiusResult[] GeoRadius(RedisKey key, double longitude, double latitude, double radius, GeoUnit unit = GeoUnit.Meters, int count = -1, Order? order = null, GeoRadiusOptions options = GeoRadiusOptions.Default, CommandFlags flags = CommandFlags.None);
195195

196+
/// <summary>
197+
/// Return the members of the geo-encoded sorted set stored at <paramref name="key"/> bounded by the provided
198+
/// <paramref name="shape"/>, centered at the provided set <paramref name="member"/>.
199+
/// </summary>
200+
/// <param name="key">The key of the set.</param>
201+
/// <param name="member">The set member to use as the center of the shape.</param>
202+
/// <param name="shape">The shape to use to bound the geo search.</param>
203+
/// <param name="count">The maximum number of results to pull back.</param>
204+
/// <param name="demandClosest">Whether or not to terminate the search after finding <paramref name="count"/> results. Must be true of count is -1.</param>
205+
/// <param name="order">The order to sort by (defaults to unordered).</param>
206+
/// <param name="options">The search options to use</param>
207+
/// <param name="flags">The flags for this operation.</param>
208+
/// <returns>The results found within the shape, if any.</returns>
209+
/// <remarks>https://redis.io/commands/geosearch</remarks>
210+
GeoRadiusResult[] GeoSearch(RedisKey key, RedisValue member, GeoSearchShape shape, int count = -1, bool demandClosest = true, Order? order = null, GeoRadiusOptions options = GeoRadiusOptions.Default, CommandFlags flags = CommandFlags.None);
211+
212+
/// <summary>
213+
/// Return the members of the geo-encoded sorted set stored at <paramref name="key"/> bounded by the provided
214+
/// <paramref name="shape"/>, centered at the point provided by the <paramref name="longitude"/> and <paramref name="latitude"/>.
215+
/// </summary>
216+
/// <param name="key">The key of the set.</param>
217+
/// <param name="longitude">The longitude of the center point.</param>
218+
/// <param name="latitude">The latitude of the center point.</param>
219+
/// <param name="shape">The shape to use to bound the geo search.</param>
220+
/// <param name="count">The maximum number of results to pull back.</param>
221+
/// <param name="demandClosest">Whether or not to terminate the search after finding <paramref name="count"/> results. Must be true of count is -1.</param>
222+
/// <param name="order">The order to sort by (defaults to unordered).</param>
223+
/// <param name="options">The search options to use</param>
224+
/// <param name="flags">The flags for this operation.</param>
225+
/// <returns>The results found within the shape, if any.</returns>
226+
/// /// <remarks>https://redis.io/commands/geosearch</remarks>
227+
GeoRadiusResult[] GeoSearch(RedisKey key, double longitude, double latitude, GeoSearchShape shape, int count = -1, bool demandClosest = true, Order? order = null, GeoRadiusOptions options = GeoRadiusOptions.Default, CommandFlags flags = CommandFlags.None);
228+
229+
/// <summary>
230+
/// Stores the members of the geo-encoded sorted set stored at <paramref name="sourceKey"/> bounded by the provided
231+
/// <paramref name="shape"/>, centered at the provided set <paramref name="member"/>.
232+
/// </summary>
233+
/// <param name="sourceKey">The key of the set.</param>
234+
/// <param name="destinationKey">The key to store the result at.</param>
235+
/// <param name="member">The set member to use as the center of the shape.</param>
236+
/// <param name="shape">The shape to use to bound the geo search.</param>
237+
/// <param name="count">The maximum number of results to pull back.</param>
238+
/// <param name="demandClosest">Whether or not to terminate the search after finding <paramref name="count"/> results. Must be true of count is -1.</param>
239+
/// <param name="order">The order to sort by (defaults to unordered).</param>
240+
/// <param name="storeDistances">If set to true, the resulting set will be a regular sorted-set containing only distances, rather than a geo-encoded sorted-set.</param>
241+
/// <param name="flags">The flags for this operation.</param>
242+
/// <returns>The size of the set stored at <paramref name="destinationKey"/>.</returns>
243+
/// <remarks>https://redis.io/commands/geosearchstore</remarks>
244+
long GeoSearchAndStore(RedisKey sourceKey, RedisKey destinationKey, RedisValue member, GeoSearchShape shape, int count = -1, bool demandClosest = true, Order? order = null, bool storeDistances = false, CommandFlags flags = CommandFlags.None);
245+
246+
/// <summary>
247+
/// Stores the members of the geo-encoded sorted set stored at <paramref name="sourceKey"/> bounded by the provided
248+
/// <paramref name="shape"/>, centered at the point provided by the <paramref name="longitude"/> and <paramref name="latitude"/>.
249+
/// </summary>
250+
/// <param name="sourceKey">The key of the set.</param>
251+
/// <param name="destinationKey">The key to store the result at.</param>
252+
/// <param name="longitude">The longitude of the center point.</param>
253+
/// <param name="latitude">The latitude of the center point.</param>
254+
/// <param name="shape">The shape to use to bound the geo search.</param>
255+
/// <param name="count">The maximum number of results to pull back.</param>
256+
/// <param name="demandClosest">Whether or not to terminate the search after finding <paramref name="count"/> results. Must be true of count is -1.</param>
257+
/// <param name="order">The order to sort by (defaults to unordered).</param>
258+
/// <param name="storeDistances">If set to true, the resulting set will be a regular sorted-set containing only distances, rather than a geo-encoded sorted-set.</param>
259+
/// <param name="flags">The flags for this operation.</param>
260+
/// <returns>The size of the set stored at <paramref name="destinationKey"/>.</returns>
261+
/// <remarks>https://redis.io/commands/geosearchstore</remarks>
262+
long GeoSearchAndStore(RedisKey sourceKey, RedisKey destinationKey, double longitude, double latitude, GeoSearchShape shape, int count = -1, bool demandClosest = true, Order? order = null, bool storeDistances = false, CommandFlags flags = CommandFlags.None);
263+
196264
/// <summary>
197265
/// Decrements the number stored at field in the hash stored at key by decrement.
198266
/// If key does not exist, a new key holding a hash is created.

src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,74 @@ public interface IDatabaseAsync : IRedisAsync
180180
/// <remarks>https://redis.io/commands/georadius</remarks>
181181
Task<GeoRadiusResult[]> GeoRadiusAsync(RedisKey key, double longitude, double latitude, double radius, GeoUnit unit = GeoUnit.Meters, int count = -1, Order? order = null, GeoRadiusOptions options = GeoRadiusOptions.Default, CommandFlags flags = CommandFlags.None);
182182

183+
/// <summary>
184+
/// Return the members of the geo-encoded sorted set stored at <paramref name="key"/> bounded by the provided
185+
/// <paramref name="shape"/>, centered at the provided set <paramref name="member"/>.
186+
/// </summary>
187+
/// <param name="key">The key of the set.</param>
188+
/// <param name="member">The set member to use as the center of the shape.</param>
189+
/// <param name="shape">The shape to use to bound the geo search.</param>
190+
/// <param name="count">The maximum number of results to pull back.</param>
191+
/// <param name="demandClosest">Whether or not to terminate the search after finding <paramref name="count"/> results. Must be true of count is -1.</param>
192+
/// <param name="order">The order to sort by (defaults to unordered).</param>
193+
/// <param name="options">The search options to use.</param>
194+
/// <param name="flags">The flags for this operation.</param>
195+
/// <returns>The results found within the shape, if any.</returns>
196+
/// <remarks>https://redis.io/commands/geosearch</remarks>
197+
Task<GeoRadiusResult[]> GeoSearchAsync(RedisKey key, RedisValue member, GeoSearchShape shape, int count = -1, bool demandClosest = true, Order? order = null, GeoRadiusOptions options = GeoRadiusOptions.Default, CommandFlags flags = CommandFlags.None);
198+
199+
/// <summary>
200+
/// Return the members of the geo-encoded sorted set stored at <paramref name="key"/> bounded by the provided
201+
/// <paramref name="shape"/>, centered at the point provided by the <paramref name="longitude"/> and <paramref name="latitude"/>.
202+
/// </summary>
203+
/// <param name="key">The key of the set.</param>
204+
/// <param name="longitude">The longitude of the center point.</param>
205+
/// <param name="latitude">The latitude of the center point.</param>
206+
/// <param name="shape">The shape to use to bound the geo search.</param>
207+
/// <param name="count">The maximum number of results to pull back.</param>
208+
/// <param name="demandClosest">Whether or not to terminate the search after finding <paramref name="count"/> results. Must be true of count is -1.</param>
209+
/// <param name="order">The order to sort by (defaults to unordered).</param>
210+
/// <param name="options">The search options to use.</param>
211+
/// <param name="flags">The flags for this operation.</param>
212+
/// <returns>The results found within the shape, if any.</returns>
213+
/// /// <remarks>https://redis.io/commands/geosearch</remarks>
214+
Task<GeoRadiusResult[]> GeoSearchAsync(RedisKey key, double longitude, double latitude, GeoSearchShape shape, int count = -1, bool demandClosest = true, Order? order = null, GeoRadiusOptions options = GeoRadiusOptions.Default, CommandFlags flags = CommandFlags.None);
215+
216+
/// <summary>
217+
/// Stores the members of the geo-encoded sorted set stored at <paramref name="sourceKey"/> bounded by the provided
218+
/// <paramref name="shape"/>, centered at the provided set <paramref name="member"/>.
219+
/// </summary>
220+
/// <param name="sourceKey">The key of the set.</param>
221+
/// <param name="destinationKey">The key to store the result at.</param>
222+
/// <param name="member">The set member to use as the center of the shape.</param>
223+
/// <param name="shape">The shape to use to bound the geo search.</param>
224+
/// <param name="count">The maximum number of results to pull back.</param>
225+
/// <param name="demandClosest">Whether or not to terminate the search after finding <paramref name="count"/> results. Must be true of count is -1.</param>
226+
/// <param name="order">The order to sort by (defaults to unordered).</param>
227+
/// <param name="storeDistances">If set to true, the resulting set will be a regular sorted-set containing only distances, rather than a geo-encoded sorted-set.</param>
228+
/// <param name="flags">The flags for this operation.</param>
229+
/// <returns>The size of the set stored at <paramref name="destinationKey"/>.</returns>
230+
/// <remarks>https://redis.io/commands/geosearchstore</remarks>
231+
Task<long> GeoSearchAndStoreAsync(RedisKey sourceKey, RedisKey destinationKey, RedisValue member, GeoSearchShape shape, int count = -1, bool demandClosest = true, Order? order = null, bool storeDistances = false, CommandFlags flags = CommandFlags.None);
232+
233+
/// <summary>
234+
/// Stores the members of the geo-encoded sorted set stored at <paramref name="sourceKey"/> bounded by the provided
235+
/// <paramref name="shape"/>, centered at the point provided by the <paramref name="longitude"/> and <paramref name="latitude"/>.
236+
/// </summary>
237+
/// <param name="sourceKey">The key of the set.</param>
238+
/// <param name="destinationKey">The key to store the result at.</param>
239+
/// <param name="longitude">The longitude of the center point.</param>
240+
/// <param name="latitude">The latitude of the center point.</param>
241+
/// <param name="shape">The shape to use to bound the geo search.</param>
242+
/// <param name="count">The maximum number of results to pull back.</param>
243+
/// <param name="demandClosest">Whether or not to terminate the search after finding <paramref name="count"/> results. Must be true of count is -1.</param>
244+
/// <param name="order">The order to sort by (defaults to unordered).</param>
245+
/// <param name="storeDistances">If set to true, the resulting set will be a regular sorted-set containing only distances, rather than a geo-encoded sorted-set.</param>
246+
/// <param name="flags">The flags for this operation.</param>
247+
/// <returns>The size of the set stored at <paramref name="destinationKey"/>.</returns>
248+
/// <remarks>https://redis.io/commands/geosearchstore</remarks>
249+
Task<long> GeoSearchAndStoreAsync(RedisKey sourceKey, RedisKey destinationKey, double longitude, double latitude, GeoSearchShape shape, int count = -1, bool demandClosest = true, Order? order = null, bool storeDistances = false, CommandFlags flags = CommandFlags.None);
250+
183251
/// <summary>
184252
/// Decrements the number stored at field in the hash stored at key by decrement.
185253
/// If key does not exist, a new key holding a hash is created.

0 commit comments

Comments
 (0)