Skip to content

Commit 5e53f12

Browse files
prvykTalZaccai
andauthored
Implement GEOSEARCH BYRADIUS. (#1129)
Implement also ASC, DESC orderings, ANY, WITHHASH and FROMLONLAT. Implement GEORADIUS_RO and GEORADIUSBYMEMBER_RO. Implement GEORADIUS and GEORADIUSBYMEMBER. Fix GEOSEARCHSTORE without STOREDIST and other bugs. Add tests. Co-authored-by: prvyk <[email protected]> Co-authored-by: Tal Zaccai <[email protected]>
1 parent 97c5f00 commit 5e53f12

File tree

21 files changed

+2067
-329
lines changed

21 files changed

+2067
-329
lines changed

libs/resources/RespCommandsDocs.json

Lines changed: 522 additions & 0 deletions
Large diffs are not rendered by default.

libs/resources/RespCommandsInfo.json

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1532,6 +1532,162 @@
15321532
}
15331533
]
15341534
},
1535+
{
1536+
"Command": "GEORADIUS",
1537+
"Name": "GEORADIUS",
1538+
"Arity": -6,
1539+
"Flags": "DenyOom, MovableKeys, Write",
1540+
"FirstKey": 1,
1541+
"LastKey": 1,
1542+
"Step": 1,
1543+
"AclCategories": "Geo, Slow, Write",
1544+
"KeySpecifications": [
1545+
{
1546+
"BeginSearch": {
1547+
"TypeDiscriminator": "BeginSearchIndex",
1548+
"Index": 1
1549+
},
1550+
"FindKeys": {
1551+
"TypeDiscriminator": "FindKeysRange",
1552+
"LastKey": 0,
1553+
"KeyStep": 1,
1554+
"Limit": 0
1555+
},
1556+
"Flags": "RO, Access"
1557+
},
1558+
{
1559+
"BeginSearch": {
1560+
"TypeDiscriminator": "BeginSearchKeyword",
1561+
"Keyword": "STORE",
1562+
"StartFrom": 6
1563+
},
1564+
"FindKeys": {
1565+
"TypeDiscriminator": "FindKeysRange",
1566+
"LastKey": 0,
1567+
"KeyStep": 1,
1568+
"Limit": 0
1569+
},
1570+
"Flags": "OW, Update"
1571+
},
1572+
{
1573+
"BeginSearch": {
1574+
"TypeDiscriminator": "BeginSearchKeyword",
1575+
"Keyword": "STOREDIST",
1576+
"StartFrom": 6
1577+
},
1578+
"FindKeys": {
1579+
"TypeDiscriminator": "FindKeysRange",
1580+
"LastKey": 0,
1581+
"KeyStep": 1,
1582+
"Limit": 0
1583+
},
1584+
"Flags": "OW, Update"
1585+
}
1586+
]
1587+
},
1588+
{
1589+
"Command": "GEORADIUS_RO",
1590+
"Name": "GEORADIUS_RO",
1591+
"Arity": -6,
1592+
"Flags": "ReadOnly",
1593+
"FirstKey": 1,
1594+
"LastKey": 1,
1595+
"Step": 1,
1596+
"AclCategories": "Geo, Read, Slow",
1597+
"KeySpecifications": [
1598+
{
1599+
"BeginSearch": {
1600+
"TypeDiscriminator": "BeginSearchIndex",
1601+
"Index": 1
1602+
},
1603+
"FindKeys": {
1604+
"TypeDiscriminator": "FindKeysRange",
1605+
"LastKey": 0,
1606+
"KeyStep": 1,
1607+
"Limit": 0
1608+
},
1609+
"Flags": "RO, Access"
1610+
}
1611+
]
1612+
},
1613+
{
1614+
"Command": "GEORADIUSBYMEMBER",
1615+
"Name": "GEORADIUSBYMEMBER",
1616+
"Arity": -5,
1617+
"Flags": "DenyOom, MovableKeys, Write",
1618+
"FirstKey": 1,
1619+
"LastKey": 1,
1620+
"Step": 1,
1621+
"AclCategories": "Geo, Slow, Write",
1622+
"KeySpecifications": [
1623+
{
1624+
"BeginSearch": {
1625+
"TypeDiscriminator": "BeginSearchIndex",
1626+
"Index": 1
1627+
},
1628+
"FindKeys": {
1629+
"TypeDiscriminator": "FindKeysRange",
1630+
"LastKey": 0,
1631+
"KeyStep": 1,
1632+
"Limit": 0
1633+
},
1634+
"Flags": "RO, Access"
1635+
},
1636+
{
1637+
"BeginSearch": {
1638+
"TypeDiscriminator": "BeginSearchKeyword",
1639+
"Keyword": "STORE",
1640+
"StartFrom": 5
1641+
},
1642+
"FindKeys": {
1643+
"TypeDiscriminator": "FindKeysRange",
1644+
"LastKey": 0,
1645+
"KeyStep": 1,
1646+
"Limit": 0
1647+
},
1648+
"Flags": "OW, Update"
1649+
},
1650+
{
1651+
"BeginSearch": {
1652+
"TypeDiscriminator": "BeginSearchKeyword",
1653+
"Keyword": "STOREDIST",
1654+
"StartFrom": 5
1655+
},
1656+
"FindKeys": {
1657+
"TypeDiscriminator": "FindKeysRange",
1658+
"LastKey": 0,
1659+
"KeyStep": 1,
1660+
"Limit": 0
1661+
},
1662+
"Flags": "OW, Update"
1663+
}
1664+
]
1665+
},
1666+
{
1667+
"Command": "GEORADIUSBYMEMBER_RO",
1668+
"Name": "GEORADIUSBYMEMBER_RO",
1669+
"Arity": -5,
1670+
"Flags": "ReadOnly",
1671+
"FirstKey": 1,
1672+
"LastKey": 1,
1673+
"Step": 1,
1674+
"AclCategories": "Geo, Read, Slow",
1675+
"KeySpecifications": [
1676+
{
1677+
"BeginSearch": {
1678+
"TypeDiscriminator": "BeginSearchIndex",
1679+
"Index": 1
1680+
},
1681+
"FindKeys": {
1682+
"TypeDiscriminator": "FindKeysRange",
1683+
"LastKey": 0,
1684+
"KeyStep": 1,
1685+
"Limit": 0
1686+
},
1687+
"Flags": "RO, Access"
1688+
}
1689+
]
1690+
},
15351691
{
15361692
"Command": "GEOSEARCH",
15371693
"Name": "GEOSEARCH",

libs/server/API/GarnetApiObjectCommands.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -182,9 +182,14 @@ public GarnetStatus GeoCommands(byte[] key, ref ObjectInput input, ref GarnetObj
182182
=> storageSession.GeoCommands(key, ref input, ref outputFooter, ref objectContext);
183183

184184
/// <inheritdoc />
185-
public GarnetStatus GeoSearchStore(ArgSlice key, ArgSlice destinationKey, ref ObjectInput input, ref SpanByteAndMemory output)
186-
=> storageSession.GeoSearchStore(key, destinationKey, ref input, ref output, ref objectContext);
185+
public GarnetStatus GeoSearchReadOnly(ArgSlice key, ref GeoSearchOptions opts,
186+
ref ObjectInput input, ref SpanByteAndMemory output)
187+
=> storageSession.GeoSearchReadOnly(key, ref opts, ref input, ref output, ref objectContext);
187188

189+
/// <inheritdoc />
190+
public GarnetStatus GeoSearchStore(ArgSlice key, ArgSlice destinationKey, ref GeoSearchOptions opts,
191+
ref ObjectInput input, ref SpanByteAndMemory output)
192+
=> storageSession.GeoSearchStore(key, destinationKey, ref opts, ref input, ref output, ref objectContext);
188193
#endregion
189194

190195
#region List Methods

libs/server/API/GarnetWatchApi.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,14 @@ public GarnetStatus GeoCommands(byte[] key, ref ObjectInput input, ref GarnetObj
216216
return garnetApi.GeoCommands(key, ref input, ref outputFooter);
217217
}
218218

219+
/// <inheritdoc />
220+
public GarnetStatus GeoSearchReadOnly(ArgSlice key, ref GeoSearchOptions opts,
221+
ref ObjectInput input, ref SpanByteAndMemory output)
222+
{
223+
garnetApi.WATCH(key, StoreType.Object);
224+
return garnetApi.GeoSearchReadOnly(key, ref opts, ref input, ref output);
225+
}
226+
219227
/// <inheritdoc />
220228
public GarnetStatus SortedSetScan(ArgSlice key, long cursor, string match, int count, out ArgSlice[] items)
221229
{

libs/server/API/IGarnetApi.cs

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -525,10 +525,13 @@ public interface IGarnetApi : IGarnetReadApi, IGarnetAdvancedApi
525525
/// Geospatial search and store in destination key.
526526
/// </summary>
527527
/// <param name="key"></param>
528+
/// <param name="destinationKey"></param>
529+
/// <param name="opts"></param>
528530
/// <param name="input"></param>
529531
/// <param name="output"></param>
530532
/// <returns></returns>
531-
GarnetStatus GeoSearchStore(ArgSlice key, ArgSlice destinationKey, ref ObjectInput input, ref SpanByteAndMemory output);
533+
GarnetStatus GeoSearchStore(ArgSlice key, ArgSlice destinationKey, ref GeoSearchOptions opts,
534+
ref ObjectInput input, ref SpanByteAndMemory output);
532535

533536
/// <summary>
534537
/// Intersects multiple sorted sets and stores the result in the destination key.
@@ -1414,14 +1417,28 @@ public interface IGarnetReadApi
14141417
/// GEOHASH: Returns valid Geohash strings representing the position of one or more elements in a geospatial data of the sorted set.
14151418
/// GEODIST: Returns the distance between two members in the geospatial index represented by the sorted set.
14161419
/// GEOPOS: Returns the positions (longitude,latitude) of all the specified members in the sorted set.
1417-
/// GEOSEARCH: Returns the members of a sorted set populated with geospatial data, which are within the borders of the area specified by a given shape.
14181420
/// </summary>
14191421
/// <param name="key"></param>
14201422
/// <param name="input"></param>
14211423
/// <param name="outputFooter"></param>
14221424
/// <returns></returns>
14231425
GarnetStatus GeoCommands(byte[] key, ref ObjectInput input, ref GarnetObjectStoreOutput outputFooter);
14241426

1427+
/// <summary>
1428+
/// GEORADIUS (read variant): Return the members of a sorted set populated with geospatial data, which are inside the circular area delimited by center and radius.
1429+
/// GEORADIUS_RO: Return the members of a sorted set populated with geospatial data, which are inside the circular area delimited by center and radius.
1430+
/// GEORADIUSBYMEMBER (read variant): Return the members of a sorted set populated with geospatial data, which are inside the circular area delimited by center (derived from member) and radius.
1431+
/// GEORADIUSBYMEMBER_RO: Return the members of a sorted set populated with geospatial data, which are inside the circular area delimited by center (derived from member) and radius.
1432+
/// GEOSEARCH: Returns the members of a sorted set populated with geospatial data, which are within the borders of the area specified by a given shape.
1433+
/// </summary>
1434+
/// <param name="key"></param>
1435+
/// <param name="opts"></param>
1436+
/// <param name="input"></param>
1437+
/// <param name="output"></param>
1438+
/// <returns></returns>
1439+
GarnetStatus GeoSearchReadOnly(ArgSlice key, ref GeoSearchOptions opts,
1440+
ref ObjectInput input, ref SpanByteAndMemory output);
1441+
14251442
#endregion
14261443

14271444
#region List Methods

libs/server/Objects/SortedSet/SortedSetObject.cs

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ public enum SortedSetOperation : byte
3232
GEODIST,
3333
GEOPOS,
3434
GEOSEARCH,
35-
GEOSEARCHSTORE,
3635
ZREVRANK,
3736
ZREMRANGEBYLEX,
3837
ZREMRANGEBYRANK,
@@ -297,10 +296,6 @@ public override unsafe bool Operate(ref ObjectInput input, ref GarnetObjectStore
297296
case SortedSetOperation.GEOPOS:
298297
GeoPosition(ref input, ref output.SpanByteAndMemory);
299298
break;
300-
case SortedSetOperation.GEOSEARCH:
301-
case SortedSetOperation.GEOSEARCHSTORE:
302-
GeoSearch(ref input, ref output.SpanByteAndMemory);
303-
break;
304299
case SortedSetOperation.ZRANGE:
305300
SortedSetRange(ref input, ref output.SpanByteAndMemory);
306301
break;

libs/server/Objects/SortedSetGeo/GeoHash.cs

Lines changed: 55 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@
66
using System.Runtime.InteropServices;
77
using System.Runtime.Intrinsics.X86;
88

9-
using Garnet.common;
10-
119
namespace Garnet.server
1210
{
1311
/// <summary>
@@ -16,12 +14,26 @@ namespace Garnet.server
1614
public static class GeoHash
1715
{
1816
// Constraints from WGS 84 / Pseudo-Mercator (EPSG:3857)
19-
private const double LongitudeMin = -180.0;
20-
private const double LongitudeMax = 180.0;
17+
18+
/// <summary>
19+
/// Minimum allowed longitude.
20+
/// </summary>
21+
public const double LongitudeMin = -180.0;
22+
/// <summary>
23+
/// Maximum allowed longitude.
24+
/// </summary>
25+
public const double LongitudeMax = 180.0;
2126

2227
// TODO: These are "wrong" in a sense that according to EPSG:3857 latitude should be from -85.05112878 to 85.05112878
23-
private const double LatitudeMin = -90.0;
24-
private const double LatitudeMax = 90.0;
28+
29+
/// <summary>
30+
/// Minimum allowed latitude.
31+
/// </summary>
32+
public const double LatitudeMin = -90.0;
33+
/// <summary>
34+
/// Maximum allowed latitude.
35+
/// </summary>
36+
public const double LatitudeMax = 90.0;
2537

2638
/// <summary>
2739
/// The number of bits used for the precision of the geohash.
@@ -263,6 +275,26 @@ public static double Distance(double sourceLat, double sourceLon, double targetL
263275
return 2 * Math.Asin(Math.Sqrt(latHaversine + (tmp * lonHaversine))) * EarthRadiusInMeters;
264276
}
265277

278+
/// <summary>
279+
/// Find if a point is within radius of the given center point.
280+
/// <paramref name="radius">Radius</paramref>
281+
/// <paramref name="latCenterPoint">Center point latitude</paramref>
282+
/// <paramref name="lonCenterPoint">Center point longitude</paramref>
283+
/// <paramref name="lat">Point latitude</paramref>
284+
/// <paramref name="lon">Point longitude</paramref>
285+
/// <paramref name="distance">Distance</paramref>
286+
/// </summary>
287+
public static bool IsPointWithinRadius(double radius, double latCenterPoint, double lonCenterPoint, double lat, double lon, ref double distance)
288+
{
289+
distance = Distance(latCenterPoint, lonCenterPoint, lat, lon);
290+
if (distance >= radius)
291+
{
292+
return false;
293+
}
294+
295+
return true;
296+
}
297+
266298
/// <summary>
267299
/// Find if a point is in the axis-aligned rectangle.
268300
/// when the distance between the searched point and the center point is less than or equal to
@@ -297,44 +329,32 @@ public static (double LatError, double LonError) GetGeoErrorByPrecision()
297329
return (latError, longError);
298330
}
299331

300-
public static double ConvertValueToMeters(double value, ReadOnlySpan<byte> units)
332+
/// <summary>
333+
/// Helper to convert kilometers, feet, or miles to meters.
334+
/// </summary>
335+
public static double ConvertValueToMeters(double value, GeoDistanceUnitType unit)
301336
{
302-
if (units.EqualsUpperCaseSpanIgnoringCase("KM"u8))
303-
{
304-
return value / 0.001;
305-
}
306-
else if (units.EqualsUpperCaseSpanIgnoringCase("FT"u8))
307-
{
308-
return value / 3.28084;
309-
}
310-
else if (units.EqualsUpperCaseSpanIgnoringCase("MI"u8))
337+
return unit switch
311338
{
312-
return value / 0.000621371;
313-
}
314-
315-
return value;
339+
GeoDistanceUnitType.KM => value / 0.001,
340+
GeoDistanceUnitType.FT => value / 3.28084,
341+
GeoDistanceUnitType.MI => value / 0.000621371,
342+
_ => value
343+
};
316344
}
317345

318-
319346
/// <summary>
320347
/// Helper to convert meters to kilometers, feet, or miles
321348
/// </summary>
322-
public static double ConvertMetersToUnits(double value, ReadOnlySpan<byte> units)
349+
public static double ConvertMetersToUnits(double value, GeoDistanceUnitType unit)
323350
{
324-
if (units.EqualsUpperCaseSpanIgnoringCase("KM"u8))
325-
{
326-
return value * 0.001;
327-
}
328-
else if (units.EqualsUpperCaseSpanIgnoringCase("FT"u8))
351+
return unit switch
329352
{
330-
return value * 3.28084;
331-
}
332-
else if (units.EqualsUpperCaseSpanIgnoringCase("MI"u8))
333-
{
334-
return value * 0.000621371;
335-
}
336-
337-
return value;
353+
GeoDistanceUnitType.KM => value * 0.001,
354+
GeoDistanceUnitType.FT => value * 3.28084,
355+
GeoDistanceUnitType.MI => value * 0.000621371,
356+
_ => value
357+
};
338358
}
339359
}
340360
}

0 commit comments

Comments
 (0)