diff --git a/libs/resources/RespCommandsDocs.json b/libs/resources/RespCommandsDocs.json index f42b5bcccc..ce86e2ad85 100644 --- a/libs/resources/RespCommandsDocs.json +++ b/libs/resources/RespCommandsDocs.json @@ -2448,6 +2448,528 @@ } ] }, + { + "Command": "GEORADIUS", + "Name": "GEORADIUS", + "Summary": "Query a sorted set representing a geospatial index to fetch members matching a given maximum distance from a point", + "Group": "Geo", + "Complexity": "O(N\u002Blog(M)) where N is the number of elements inside the bounding box of the circular area delimited by center and radius and M is the number of items inside the index.", + "DocFlags": "Deprecated", + "ReplacedBy": "\u0060GEOSEARCH\u0060 and \u0060GEOSEARCHSTORE\u0060 with the \u0060BYRADIUS\u0060 argument", + "Arguments": [ + { + "TypeDiscriminator": "RespCommandKeyArgument", + "Name": "KEY", + "Type": "Key", + "KeySpecIndex": 0 + }, + { + "TypeDiscriminator": "RespCommandBasicArgument", + "Name": "LONGITUDE", + "Type": "Double" + }, + { + "TypeDiscriminator": "RespCommandBasicArgument", + "Name": "LATITUDE", + "Type": "Double" + }, + { + "TypeDiscriminator": "RespCommandBasicArgument", + "Name": "RADIUS", + "Type": "Double" + }, + { + "TypeDiscriminator": "RespCommandContainerArgument", + "Name": "UNIT", + "Type": "OneOf", + "Arguments": [ + { + "TypeDiscriminator": "RespCommandBasicArgument", + "Name": "M", + "Type": "PureToken", + "Token": "M" + }, + { + "TypeDiscriminator": "RespCommandBasicArgument", + "Name": "KM", + "Type": "PureToken", + "Token": "KM" + }, + { + "TypeDiscriminator": "RespCommandBasicArgument", + "Name": "FT", + "Type": "PureToken", + "Token": "FT" + }, + { + "TypeDiscriminator": "RespCommandBasicArgument", + "Name": "MI", + "Type": "PureToken", + "Token": "MI" + } + ] + }, + { + "TypeDiscriminator": "RespCommandBasicArgument", + "Name": "WITHCOORD", + "Type": "PureToken", + "Token": "WITHCOORD", + "ArgumentFlags": "Optional" + }, + { + "TypeDiscriminator": "RespCommandBasicArgument", + "Name": "WITHDIST", + "Type": "PureToken", + "Token": "WITHDIST", + "ArgumentFlags": "Optional" + }, + { + "TypeDiscriminator": "RespCommandBasicArgument", + "Name": "WITHHASH", + "Type": "PureToken", + "Token": "WITHHASH", + "ArgumentFlags": "Optional" + }, + { + "TypeDiscriminator": "RespCommandContainerArgument", + "Name": "COUNT", + "Type": "Block", + "ArgumentFlags": "Optional", + "Arguments": [ + { + "TypeDiscriminator": "RespCommandBasicArgument", + "Name": "COUNT", + "Type": "Integer", + "Token": "COUNT" + }, + { + "TypeDiscriminator": "RespCommandBasicArgument", + "Name": "ANY", + "Type": "PureToken", + "Token": "ANY", + "ArgumentFlags": "Optional" + } + ] + }, + { + "TypeDiscriminator": "RespCommandContainerArgument", + "Name": "ORDER", + "Type": "OneOf", + "ArgumentFlags": "Optional", + "Arguments": [ + { + "TypeDiscriminator": "RespCommandBasicArgument", + "Name": "ASC", + "Type": "PureToken", + "Token": "ASC" + }, + { + "TypeDiscriminator": "RespCommandBasicArgument", + "Name": "DESC", + "Type": "PureToken", + "Token": "DESC" + } + ] + }, + { + "TypeDiscriminator": "RespCommandKeyArgument", + "Name": "KEY", + "Type": "Key", + "Token": "STORE", + "ArgumentFlags": "Optional", + "KeySpecIndex": 1 + }, + { + "TypeDiscriminator": "RespCommandKeyArgument", + "Name": "KEY", + "Type": "Key", + "Token": "STOREDIST", + "ArgumentFlags": "Optional", + "KeySpecIndex": 2 + } + ] + }, + { + "Command": "GEORADIUS_RO", + "Name": "GEORADIUS_RO", + "Summary": "A read-only variant for GEORADIUS", + "Group": "Geo", + "Complexity": "O(N\u002Blog(M)) where N is the number of elements inside the bounding box of the circular area delimited by center and radius and M is the number of items inside the index.", + "DocFlags": "Deprecated", + "ReplacedBy": "\u0060GEOSEARCH\u0060 with the \u0060BYRADIUS\u0060 argument", + "Arguments": [ + { + "TypeDiscriminator": "RespCommandKeyArgument", + "Name": "KEY", + "Type": "Key", + "KeySpecIndex": 0 + }, + { + "TypeDiscriminator": "RespCommandBasicArgument", + "Name": "LONGITUDE", + "Type": "Double" + }, + { + "TypeDiscriminator": "RespCommandBasicArgument", + "Name": "LATITUDE", + "Type": "Double" + }, + { + "TypeDiscriminator": "RespCommandBasicArgument", + "Name": "RADIUS", + "Type": "Double" + }, + { + "TypeDiscriminator": "RespCommandContainerArgument", + "Name": "UNIT", + "Type": "OneOf", + "Arguments": [ + { + "TypeDiscriminator": "RespCommandBasicArgument", + "Name": "M", + "Type": "PureToken", + "Token": "M" + }, + { + "TypeDiscriminator": "RespCommandBasicArgument", + "Name": "KM", + "Type": "PureToken", + "Token": "KM" + }, + { + "TypeDiscriminator": "RespCommandBasicArgument", + "Name": "FT", + "Type": "PureToken", + "Token": "FT" + }, + { + "TypeDiscriminator": "RespCommandBasicArgument", + "Name": "MI", + "Type": "PureToken", + "Token": "MI" + } + ] + }, + { + "TypeDiscriminator": "RespCommandBasicArgument", + "Name": "WITHCOORD", + "Type": "PureToken", + "Token": "WITHCOORD", + "ArgumentFlags": "Optional" + }, + { + "TypeDiscriminator": "RespCommandBasicArgument", + "Name": "WITHDIST", + "Type": "PureToken", + "Token": "WITHDIST", + "ArgumentFlags": "Optional" + }, + { + "TypeDiscriminator": "RespCommandBasicArgument", + "Name": "WITHHASH", + "Type": "PureToken", + "Token": "WITHHASH", + "ArgumentFlags": "Optional" + }, + { + "TypeDiscriminator": "RespCommandContainerArgument", + "Name": "COUNT", + "Type": "Block", + "ArgumentFlags": "Optional", + "Arguments": [ + { + "TypeDiscriminator": "RespCommandBasicArgument", + "Name": "COUNT", + "Type": "Integer", + "Token": "COUNT" + }, + { + "TypeDiscriminator": "RespCommandBasicArgument", + "Name": "ANY", + "Type": "PureToken", + "Token": "ANY", + "ArgumentFlags": "Optional" + } + ] + }, + { + "TypeDiscriminator": "RespCommandContainerArgument", + "Name": "ORDER", + "Type": "OneOf", + "ArgumentFlags": "Optional", + "Arguments": [ + { + "TypeDiscriminator": "RespCommandBasicArgument", + "Name": "ASC", + "Type": "PureToken", + "Token": "ASC" + }, + { + "TypeDiscriminator": "RespCommandBasicArgument", + "Name": "DESC", + "Type": "PureToken", + "Token": "DESC" + } + ] + } + ] + }, + { + "Command": "GEORADIUSBYMEMBER", + "Name": "GEORADIUSBYMEMBER", + "Summary": "Query a sorted set representing a geospatial index to fetch members matching a given maximum distance from a member", + "Group": "Geo", + "Complexity": "O(N\u002Blog(M)) where N is the number of elements inside the bounding box of the circular area delimited by center and radius and M is the number of items inside the index.", + "DocFlags": "Deprecated", + "ReplacedBy": "\u0060GEOSEARCH\u0060 and \u0060GEOSEARCHSTORE\u0060 with the \u0060BYRADIUS\u0060 and \u0060FROMMEMBER\u0060 arguments", + "Arguments": [ + { + "TypeDiscriminator": "RespCommandKeyArgument", + "Name": "KEY", + "Type": "Key", + "KeySpecIndex": 0 + }, + { + "TypeDiscriminator": "RespCommandBasicArgument", + "Name": "MEMBER", + "Type": "String" + }, + { + "TypeDiscriminator": "RespCommandBasicArgument", + "Name": "RADIUS", + "Type": "Double" + }, + { + "TypeDiscriminator": "RespCommandContainerArgument", + "Name": "UNIT", + "Type": "OneOf", + "Arguments": [ + { + "TypeDiscriminator": "RespCommandBasicArgument", + "Name": "M", + "Type": "PureToken", + "Token": "M" + }, + { + "TypeDiscriminator": "RespCommandBasicArgument", + "Name": "KM", + "Type": "PureToken", + "Token": "KM" + }, + { + "TypeDiscriminator": "RespCommandBasicArgument", + "Name": "FT", + "Type": "PureToken", + "Token": "FT" + }, + { + "TypeDiscriminator": "RespCommandBasicArgument", + "Name": "MI", + "Type": "PureToken", + "Token": "MI" + } + ] + }, + { + "TypeDiscriminator": "RespCommandBasicArgument", + "Name": "WITHCOORD", + "Type": "PureToken", + "Token": "WITHCOORD", + "ArgumentFlags": "Optional" + }, + { + "TypeDiscriminator": "RespCommandBasicArgument", + "Name": "WITHDIST", + "Type": "PureToken", + "Token": "WITHDIST", + "ArgumentFlags": "Optional" + }, + { + "TypeDiscriminator": "RespCommandBasicArgument", + "Name": "WITHHASH", + "Type": "PureToken", + "Token": "WITHHASH", + "ArgumentFlags": "Optional" + }, + { + "TypeDiscriminator": "RespCommandContainerArgument", + "Name": "COUNT", + "Type": "Block", + "ArgumentFlags": "Optional", + "Arguments": [ + { + "TypeDiscriminator": "RespCommandBasicArgument", + "Name": "COUNT", + "Type": "Integer", + "Token": "COUNT" + }, + { + "TypeDiscriminator": "RespCommandBasicArgument", + "Name": "ANY", + "Type": "PureToken", + "Token": "ANY", + "ArgumentFlags": "Optional" + } + ] + }, + { + "TypeDiscriminator": "RespCommandContainerArgument", + "Name": "ORDER", + "Type": "OneOf", + "ArgumentFlags": "Optional", + "Arguments": [ + { + "TypeDiscriminator": "RespCommandBasicArgument", + "Name": "ASC", + "Type": "PureToken", + "Token": "ASC" + }, + { + "TypeDiscriminator": "RespCommandBasicArgument", + "Name": "DESC", + "Type": "PureToken", + "Token": "DESC" + } + ] + }, + { + "TypeDiscriminator": "RespCommandKeyArgument", + "Name": "KEY", + "Type": "Key", + "Token": "STORE", + "ArgumentFlags": "Optional", + "KeySpecIndex": 1 + }, + { + "TypeDiscriminator": "RespCommandKeyArgument", + "Name": "KEY", + "Type": "Key", + "Token": "STOREDIST", + "ArgumentFlags": "Optional", + "KeySpecIndex": 2 + } + ] + }, + { + "Command": "GEORADIUSBYMEMBER_RO", + "Name": "GEORADIUSBYMEMBER_RO", + "Summary": "A read-only variant for GEORADIUSBYMEMBER", + "Group": "Geo", + "Complexity": "O(N\u002Blog(M)) where N is the number of elements inside the bounding box of the circular area delimited by center and radius and M is the number of items inside the index.", + "DocFlags": "Deprecated", + "ReplacedBy": "\u0060GEOSEARCH\u0060 with the \u0060BYRADIUS\u0060 and \u0060FROMMEMBER\u0060 arguments", + "Arguments": [ + { + "TypeDiscriminator": "RespCommandKeyArgument", + "Name": "KEY", + "Type": "Key", + "KeySpecIndex": 0 + }, + { + "TypeDiscriminator": "RespCommandBasicArgument", + "Name": "MEMBER", + "Type": "String" + }, + { + "TypeDiscriminator": "RespCommandBasicArgument", + "Name": "RADIUS", + "Type": "Double" + }, + { + "TypeDiscriminator": "RespCommandContainerArgument", + "Name": "UNIT", + "Type": "OneOf", + "Arguments": [ + { + "TypeDiscriminator": "RespCommandBasicArgument", + "Name": "M", + "Type": "PureToken", + "Token": "M" + }, + { + "TypeDiscriminator": "RespCommandBasicArgument", + "Name": "KM", + "Type": "PureToken", + "Token": "KM" + }, + { + "TypeDiscriminator": "RespCommandBasicArgument", + "Name": "FT", + "Type": "PureToken", + "Token": "FT" + }, + { + "TypeDiscriminator": "RespCommandBasicArgument", + "Name": "MI", + "Type": "PureToken", + "Token": "MI" + } + ] + }, + { + "TypeDiscriminator": "RespCommandBasicArgument", + "Name": "WITHCOORD", + "Type": "PureToken", + "Token": "WITHCOORD", + "ArgumentFlags": "Optional" + }, + { + "TypeDiscriminator": "RespCommandBasicArgument", + "Name": "WITHDIST", + "Type": "PureToken", + "Token": "WITHDIST", + "ArgumentFlags": "Optional" + }, + { + "TypeDiscriminator": "RespCommandBasicArgument", + "Name": "WITHHASH", + "Type": "PureToken", + "Token": "WITHHASH", + "ArgumentFlags": "Optional" + }, + { + "TypeDiscriminator": "RespCommandContainerArgument", + "Name": "COUNT", + "Type": "Block", + "ArgumentFlags": "Optional", + "Arguments": [ + { + "TypeDiscriminator": "RespCommandBasicArgument", + "Name": "COUNT", + "Type": "Integer", + "Token": "COUNT" + }, + { + "TypeDiscriminator": "RespCommandBasicArgument", + "Name": "ANY", + "Type": "PureToken", + "Token": "ANY", + "ArgumentFlags": "Optional" + } + ] + }, + { + "TypeDiscriminator": "RespCommandContainerArgument", + "Name": "ORDER", + "Type": "OneOf", + "ArgumentFlags": "Optional", + "Arguments": [ + { + "TypeDiscriminator": "RespCommandBasicArgument", + "Name": "ASC", + "Type": "PureToken", + "Token": "ASC" + }, + { + "TypeDiscriminator": "RespCommandBasicArgument", + "Name": "DESC", + "Type": "PureToken", + "Token": "DESC" + } + ] + } + ] + }, { "Command": "GEOSEARCH", "Name": "GEOSEARCH", diff --git a/libs/resources/RespCommandsInfo.json b/libs/resources/RespCommandsInfo.json index dd1499f05d..ec77f63be7 100644 --- a/libs/resources/RespCommandsInfo.json +++ b/libs/resources/RespCommandsInfo.json @@ -1528,6 +1528,162 @@ } ] }, + { + "Command": "GEORADIUS", + "Name": "GEORADIUS", + "Arity": -6, + "Flags": "DenyOom, MovableKeys, Write", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Geo, Slow, Write", + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Flags": "RO, Access" + }, + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchKeyword", + "Keyword": "STORE", + "StartFrom": 6 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Flags": "OW, Update" + }, + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchKeyword", + "Keyword": "STOREDIST", + "StartFrom": 6 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Flags": "OW, Update" + } + ] + }, + { + "Command": "GEORADIUS_RO", + "Name": "GEORADIUS_RO", + "Arity": -6, + "Flags": "ReadOnly", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Geo, Read, Slow", + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Flags": "RO, Access" + } + ] + }, + { + "Command": "GEORADIUSBYMEMBER", + "Name": "GEORADIUSBYMEMBER", + "Arity": -5, + "Flags": "DenyOom, MovableKeys, Write", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Geo, Slow, Write", + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Flags": "RO, Access" + }, + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchKeyword", + "Keyword": "STORE", + "StartFrom": 5 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Flags": "OW, Update" + }, + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchKeyword", + "Keyword": "STOREDIST", + "StartFrom": 5 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Flags": "OW, Update" + } + ] + }, + { + "Command": "GEORADIUSBYMEMBER_RO", + "Name": "GEORADIUSBYMEMBER_RO", + "Arity": -5, + "Flags": "ReadOnly", + "FirstKey": 1, + "LastKey": 1, + "Step": 1, + "AclCategories": "Geo, Read, Slow", + "KeySpecifications": [ + { + "BeginSearch": { + "TypeDiscriminator": "BeginSearchIndex", + "Index": 1 + }, + "FindKeys": { + "TypeDiscriminator": "FindKeysRange", + "LastKey": 0, + "KeyStep": 1, + "Limit": 0 + }, + "Flags": "RO, Access" + } + ] + }, { "Command": "GEOSEARCH", "Name": "GEOSEARCH", diff --git a/libs/server/Objects/SortedSet/SortedSetObject.cs b/libs/server/Objects/SortedSet/SortedSetObject.cs index aa0e520f46..cd80ecc41d 100644 --- a/libs/server/Objects/SortedSet/SortedSetObject.cs +++ b/libs/server/Objects/SortedSet/SortedSetObject.cs @@ -32,7 +32,6 @@ public enum SortedSetOperation : byte GEODIST, GEOPOS, GEOSEARCH, - GEOSEARCHSTORE, ZREVRANK, ZREMRANGEBYLEX, ZREMRANGEBYRANK, @@ -77,6 +76,30 @@ public enum SortedSetRangeOpts : byte WithScores = 1 << 4 } + /// + /// Options for specifying command type in sorted set geo operations. + /// + [Flags] + public enum SortedSetGeoOpts : byte + { + /// + /// No options specified. + /// + None = 0, + /// + /// Operation can store to database. + /// + Store = 1, + /// + /// Operate by radius. + /// + ByRadius = 1 << 1, + /// + /// Get lonlat from member. + /// + ByMember = 1 << 2 + } + [Flags] public enum SortedSetAddOption { @@ -298,7 +321,6 @@ public override unsafe bool Operate(ref ObjectInput input, ref GarnetObjectStore GeoPosition(ref input, ref output.SpanByteAndMemory); break; case SortedSetOperation.GEOSEARCH: - case SortedSetOperation.GEOSEARCHSTORE: GeoSearch(ref input, ref output.SpanByteAndMemory); break; case SortedSetOperation.ZRANGE: diff --git a/libs/server/Objects/SortedSetGeo/GeoHash.cs b/libs/server/Objects/SortedSetGeo/GeoHash.cs index 2cdedb5c96..cfa4a6b3f0 100644 --- a/libs/server/Objects/SortedSetGeo/GeoHash.cs +++ b/libs/server/Objects/SortedSetGeo/GeoHash.cs @@ -263,6 +263,20 @@ public static double Distance(double sourceLat, double sourceLon, double targetL return 2 * Math.Asin(Math.Sqrt(latHaversine + (tmp * lonHaversine))) * EarthRadiusInMeters; } + /// + /// Find if a point is in the circle. + /// + public static bool GetDistanceWhenInCircle(double radius, double latCenterPoint, double lonCenterPoint, double lat2, double lon2, ref double distance) + { + distance = Distance(latCenterPoint, lonCenterPoint, lat2, lon2); + if (distance > radius) + { + return false; + } + + return true; + } + /// /// Find if a point is in the axis-aligned rectangle. /// when the distance between the searched point and the center point is less than or equal to diff --git a/libs/server/Objects/SortedSetGeo/SortedSetGeoObjectImpl.cs b/libs/server/Objects/SortedSetGeo/SortedSetGeoObjectImpl.cs index 6d7ee6a77a..bddcda2890 100644 --- a/libs/server/Objects/SortedSetGeo/SortedSetGeoObjectImpl.cs +++ b/libs/server/Objects/SortedSetGeo/SortedSetGeoObjectImpl.cs @@ -5,6 +5,7 @@ using System.Buffers; using System.Collections.Generic; using System.Diagnostics; +using System.Linq; using System.Text; using Garnet.common; using Tsavorite.core; @@ -23,26 +24,104 @@ private struct GeoSearchData { public Byte[] Member; public double Distance; + public long GeoHash; public string GeoHashCode; public (double Latitude, double Longitude) Coordinates; } + /// + /// Type of GeoSearch + /// + internal enum GeoSearchType + { + /// + /// No defined order. + /// + Undefined, + + /// + /// Search inside circular area + /// + ByRadius, + + /// + /// Search inside an axis-aligned rectangle + /// + ByBox, + } + + /// + /// The direction in which to sequence elements. + /// + internal enum GeoOrder + { + /// + /// No defined order. + /// + None, + + /// + /// Order from low values to high values. + /// + Ascending, + + /// + /// Order from high values to low values. + /// + Descending, + } + + internal enum GeoOriginType + { + /// + /// Not defined. + /// + Undefined, + + /// + /// From explicit lon lat coordinates. + /// + FromLonLat, + + /// + /// From member key + /// + FromMember + } + /// /// Small struct to store options for GEOSEARCH command /// - private struct GeoSearchOptions + private ref struct GeoSearchOptions { - public bool FromMember { get; set; } - public bool FromLonLat { get; set; } - public bool ByRadius { get; set; } - public bool ByBox { get; set; } - public bool SortDescending { get; set; } - public bool WithCount { get; set; } - public int WithCountValue { get; set; } - public bool WithCountAny { get; set; } - public bool WithCoord { get; set; } - public bool WithDist { get; set; } - public bool WithHash { get; set; } + internal GeoSearchType searchType; + internal ReadOnlySpan unit; + internal double radius; + internal double boxWidth; + internal double boxHeight + { + get + { + return radius; + } + set + { + radius = value; + } + } + + internal int countValue; + + internal GeoOriginType origin; + + internal byte[] fromMember; + internal double lon, lat; + + internal bool withCoord; + internal bool withHash; + internal bool withCountAny; + internal bool withDist; + internal GeoOrder sort; } private void GeoAdd(ref ObjectInput input, ref SpanByteAndMemory output) @@ -284,6 +363,10 @@ private void GeoPosition(ref ObjectInput input, ref SpanByteAndMemory output) private void GeoSearch(ref ObjectInput input, ref SpanByteAndMemory output) { + var cmtOpt = (SortedSetGeoOpts)input.arg2; + var byRadiusCmd = (cmtOpt & SortedSetGeoOpts.ByRadius) != 0; + var readOnlyCmd = (cmtOpt & SortedSetGeoOpts.Store) == 0; + var isMemory = false; MemoryHandle ptrHandle = default; var ptr = output.SpanByte.ToPointer(); @@ -294,122 +377,266 @@ private void GeoSearch(ref ObjectInput input, ref SpanByteAndMemory output) ObjectOutputHeader _output = default; try { - var opts = new GeoSearchOptions(); - byte[] fromMember = null; - var byBoxUnits = "M"u8; - double width = 0, height = 0; - var countValue = 0; + var opts = new GeoSearchOptions() + { + unit = "M"u8 + }; ReadOnlySpan errorMessage = default; var argNumError = false; - var currTokenIdx = 0; + if (byRadiusCmd) + { + // Read coordinates, note we already checked the number of arguments earlier. + if ((cmtOpt & SortedSetGeoOpts.ByMember) != 0) + { + // From Member + opts.fromMember = input.parseState.GetArgSliceByRef(currTokenIdx++).SpanByte.ToByteArray(); + opts.origin = GeoOriginType.FromMember; + } + else + { + if (!input.parseState.TryGetDouble(currTokenIdx++, out var lon) || + !input.parseState.TryGetDouble(currTokenIdx++, out var lat) || + (Math.Abs(lon) > 180) || (Math.Abs(lat) > 90)) + { + errorMessage = CmdStrings.RESP_ERR_NOT_VALID_FLOAT; + + while (!RespWriteUtils.TryWriteError(errorMessage, ref curr, end)) + ObjectUtils.ReallocateOutput(ref output, ref isMemory, ref ptr, ref ptrHandle, ref curr, ref end); + return; + } + + opts.origin = GeoOriginType.FromLonLat; + opts.lon = lon; + opts.lat = lat; + } + + // Radius + if (!input.parseState.TryGetDouble(currTokenIdx++, out var Radius) || (Radius < 0)) + { + errorMessage = CmdStrings.RESP_ERR_NOT_VALID_FLOAT; + + while (!RespWriteUtils.TryWriteError(errorMessage, ref curr, end)) + ObjectUtils.ReallocateOutput(ref output, ref isMemory, ref ptr, ref ptrHandle, ref curr, ref end); + return; + } + + opts.searchType = GeoSearchType.ByRadius; + opts.radius = Radius; + opts.unit = input.parseState.GetArgSliceByRef(currTokenIdx++).ReadOnlySpan; + } + // Read the options while (currTokenIdx < input.parseState.Count) { // Read token var tokenBytes = input.parseState.GetArgSliceByRef(currTokenIdx++).ReadOnlySpan; - if (tokenBytes.EqualsUpperCaseSpanIgnoringCase("FROMMEMBER"u8)) + if (!byRadiusCmd) { - if (input.parseState.Count - currTokenIdx == 0) + if (tokenBytes.EqualsUpperCaseSpanIgnoringCase("FROMMEMBER"u8)) { - argNumError = true; - break; - } + if (opts.origin != GeoOriginType.Undefined) + { + errorMessage = CmdStrings.RESP_SYNTAX_ERROR; + break; + } - fromMember = input.parseState.GetArgSliceByRef(currTokenIdx++).SpanByte.ToByteArray(); - opts.FromMember = true; - } - else if (tokenBytes.EqualsUpperCaseSpanIgnoringCase("FROMLONLAT"u8)) - { - if (input.parseState.Count - currTokenIdx < 2) - { - argNumError = true; - break; + if (input.parseState.Count - currTokenIdx == 0) + { + argNumError = true; + break; + } + + opts.fromMember = input.parseState.GetArgSliceByRef(currTokenIdx++).SpanByte.ToByteArray(); + opts.origin = GeoOriginType.FromMember; + continue; } - // Read coordinates - if (!input.parseState.TryGetDouble(currTokenIdx++, out _) || - !input.parseState.TryGetDouble(currTokenIdx++, out _)) + if (tokenBytes.EqualsUpperCaseSpanIgnoringCase("FROMLONLAT"u8)) { - errorMessage = CmdStrings.RESP_ERR_NOT_VALID_FLOAT; - break; + if (opts.origin != GeoOriginType.Undefined) + { + errorMessage = CmdStrings.RESP_SYNTAX_ERROR; + break; + } + + if (input.parseState.Count - currTokenIdx < 2) + { + argNumError = true; + break; + } + + // Read coordinates + if (!input.parseState.TryGetDouble(currTokenIdx++, out var lon) || + !input.parseState.TryGetDouble(currTokenIdx++, out var lat) || + (Math.Abs(lon) > 180) || (Math.Abs(lat) > 90)) + { + errorMessage = CmdStrings.RESP_ERR_NOT_VALID_FLOAT; + break; + } + + opts.origin = GeoOriginType.FromLonLat; + opts.lon = lon; + opts.lat = lat; + continue; } - opts.FromLonLat = true; - } - else if (tokenBytes.EqualsUpperCaseSpanIgnoringCase("BYRADIUS"u8)) - { - if (input.parseState.Count - currTokenIdx < 2) + if (tokenBytes.EqualsUpperCaseSpanIgnoringCase("BYRADIUS"u8)) { - argNumError = true; - break; + if (opts.searchType != GeoSearchType.Undefined) + { + errorMessage = CmdStrings.RESP_SYNTAX_ERROR; + break; + } + + if (input.parseState.Count - currTokenIdx < 2) + { + argNumError = true; + break; + } + + // Read radius and units + if (!input.parseState.TryGetDouble(currTokenIdx++, out opts.radius)) + { + errorMessage = CmdStrings.RESP_ERR_NOT_VALID_FLOAT; + break; + } + + if (opts.radius < 0) + { + errorMessage = CmdStrings.RESP_ERR_GENERIC_VALUE_IS_OUT_OF_RANGE; + break; + } + + opts.searchType = GeoSearchType.ByRadius; + opts.unit = input.parseState.GetArgSliceByRef(currTokenIdx++).ReadOnlySpan; + continue; } - // Read radius and units - if (!input.parseState.TryGetDouble(currTokenIdx++, out _)) + if (tokenBytes.EqualsUpperCaseSpanIgnoringCase("BYBOX"u8)) { - errorMessage = CmdStrings.RESP_ERR_NOT_VALID_FLOAT; - break; + if (opts.searchType != GeoSearchType.Undefined) + { + errorMessage = CmdStrings.RESP_SYNTAX_ERROR; + break; + } + + if (input.parseState.Count - currTokenIdx < 3) + { + argNumError = true; + break; + } + + // Read width, height + if (!input.parseState.TryGetDouble(currTokenIdx++, out opts.boxWidth) || + !input.parseState.TryGetDouble(currTokenIdx++, out var height)) + { + errorMessage = CmdStrings.RESP_ERR_NOT_VALID_FLOAT; + break; + } + opts.boxHeight = height; + + if (opts.boxWidth < 0 || opts.boxHeight < 0) + { + errorMessage = CmdStrings.RESP_ERR_GENERIC_VALUE_IS_OUT_OF_RANGE; + break; + } + + // Read units + opts.searchType = GeoSearchType.ByBox; + opts.unit = input.parseState.GetArgSliceByRef(currTokenIdx++).ReadOnlySpan; + continue; } + } - opts.ByRadius = true; + if (tokenBytes.EqualsUpperCaseSpanIgnoringCase("ASC"u8)) + { + opts.sort = GeoOrder.Ascending; + continue; } - else if (tokenBytes.EqualsUpperCaseSpanIgnoringCase("BYBOX"u8)) + + if (tokenBytes.EqualsUpperCaseSpanIgnoringCase("DESC"u8)) { - if (input.parseState.Count - currTokenIdx < 3) + opts.sort = GeoOrder.Descending; + continue; + } + + if (tokenBytes.EqualsUpperCaseSpanIgnoringCase(CmdStrings.COUNT)) + { + if (input.parseState.Count - currTokenIdx == 0) { argNumError = true; break; } - // Read width, height - if (!input.parseState.TryGetDouble(currTokenIdx++, out width) || - !input.parseState.TryGetDouble(currTokenIdx++, out height)) + if (!input.parseState.TryGetInt(currTokenIdx++, out var countValue)) { - errorMessage = CmdStrings.RESP_ERR_NOT_VALID_FLOAT; + errorMessage = CmdStrings.RESP_ERR_GENERIC_VALUE_IS_NOT_INTEGER; break; } - // Read units - byBoxUnits = input.parseState.GetArgSliceByRef(currTokenIdx++).ReadOnlySpan; + if (countValue <= 0) + { + errorMessage = CmdStrings.RESP_ERR_GENERIC_VALUE_IS_OUT_OF_RANGE; + break; + } - opts.ByBox = true; + opts.countValue = countValue; + continue; } - else if (tokenBytes.EqualsUpperCaseSpanIgnoringCase("ASC"u8)) opts.SortDescending = false; - else if (tokenBytes.EqualsUpperCaseSpanIgnoringCase("DESC"u8)) opts.SortDescending = true; - else if (tokenBytes.EqualsUpperCaseSpanIgnoringCase("COUNT"u8)) + + if (tokenBytes.EqualsUpperCaseSpanIgnoringCase("ANY"u8)) { - if (input.parseState.Count - currTokenIdx == 0) + opts.withCountAny = true; + continue; + } + + if (!readOnlyCmd) + { + if (byRadiusCmd && tokenBytes.EqualsUpperCaseSpanIgnoringCase(CmdStrings.STORE)) { - argNumError = true; - break; + if (byRadiusCmd) + currTokenIdx++; + continue; } - - if (!input.parseState.TryGetInt(currTokenIdx++, out countValue)) + if (tokenBytes.EqualsUpperCaseSpanIgnoringCase(CmdStrings.STOREDIST)) { - errorMessage = CmdStrings.RESP_ERR_GENERIC_VALUE_IS_NOT_INTEGER; - break; + if (byRadiusCmd) + currTokenIdx++; + opts.withDist = true; + continue; } - - opts.WithCount = true; } - else if (input.header.SortedSetOp == SortedSetOperation.GEOSEARCH && tokenBytes.EqualsUpperCaseSpanIgnoringCase("WITHCOORD"u8)) opts.WithCoord = true; - else if ((input.header.SortedSetOp == SortedSetOperation.GEOSEARCH && tokenBytes.EqualsUpperCaseSpanIgnoringCase("WITHDIST"u8)) || - (input.header.SortedSetOp == SortedSetOperation.GEOSEARCHSTORE && tokenBytes.EqualsUpperCaseSpanIgnoringCase("STOREDIST"u8))) opts.WithDist = true; - else if (input.header.SortedSetOp == SortedSetOperation.GEOSEARCH && tokenBytes.EqualsUpperCaseSpanIgnoringCase("WITHHASH"u8)) opts.WithHash = true; - else if (tokenBytes.EqualsUpperCaseSpanIgnoringCase("ANY"u8)) opts.WithCountAny = true; else { - errorMessage = CmdStrings.RESP_SYNTAX_ERROR; - break; + if (tokenBytes.EqualsUpperCaseSpanIgnoringCase(CmdStrings.WITHCOORD)) + { + opts.withCoord = true; + continue; + } + + if (tokenBytes.EqualsUpperCaseSpanIgnoringCase(CmdStrings.WITHDIST)) + { + opts.withDist = true; + continue; + } + + if (tokenBytes.EqualsUpperCaseSpanIgnoringCase(CmdStrings.WITHHASH)) + { + opts.withHash = true; + continue; + } } + + errorMessage = CmdStrings.RESP_SYNTAX_ERROR; + break; } // Check that we have the mandatory options - if (errorMessage == default && !((opts.FromMember || opts.FromLonLat) && (opts.ByRadius || opts.ByBox))) + if (errorMessage == default && ((opts.origin == 0) || (opts.searchType == 0))) argNumError = true; // Check if we have a wrong number of arguments @@ -427,120 +654,170 @@ private void GeoSearch(ref ObjectInput input, ref SpanByteAndMemory output) return; } - // Not supported options in Garnet: WITHHASH - if (opts.WithHash) + // On storing to ZSET, we need to use either dist or hash as score. + if (!readOnlyCmd && !opts.withDist && !opts.withHash) { - while (!RespWriteUtils.TryWriteError(CmdStrings.RESP_ERR_GENERIC_UNK_CMD, ref curr, end)) - ObjectUtils.ReallocateOutput(ref output, ref isMemory, ref ptr, ref ptrHandle, ref curr, ref end); - return; + opts.withHash = true; } - // Get the results + #region act + // FROMLONLAT + bool hasLonLat = opts.origin == GeoOriginType.FromLonLat; // FROMMEMBER - if (opts.FromMember && sortedSetDict.TryGetValue(fromMember, out var centerPointScore)) + if (opts.origin == GeoOriginType.FromMember && sortedSetDict.TryGetValue(opts.fromMember, out var centerPointScore)) { - var (lat, lon) = server.GeoHash.GetCoordinatesFromLong((long)centerPointScore); + (opts.lat, opts.lon) = server.GeoHash.GetCoordinatesFromLong((long)centerPointScore); + hasLonLat = true; + } - if (opts.ByRadius) + // Get the results + if (hasLonLat) + { + var len = opts.withCountAny ? opts.countValue : sortedSet.Count; + var responseData = new List(len); + foreach (var point in sortedSet) { - // Not supported in Garnet: ByRadius - while (!RespWriteUtils.TryWriteError(CmdStrings.RESP_ERR_GENERIC_UNK_CMD, ref curr, end)) + var coorInItem = server.GeoHash.GetCoordinatesFromLong((long)point.Score); + double distance = 0; + + if (opts.searchType == GeoSearchType.ByBox) + { + if (!server.GeoHash.GetDistanceWhenInRectangle( + server.GeoHash.ConvertValueToMeters(opts.boxWidth, opts.unit), + server.GeoHash.ConvertValueToMeters(opts.boxHeight, opts.unit), + opts.lat, opts.lon, coorInItem.Latitude, coorInItem.Longitude, ref distance)) + { + continue; + } + } + else /* byRadius == true */ + { + if (!server.GeoHash.GetDistanceWhenInCircle( + server.GeoHash.ConvertValueToMeters(opts.radius, opts.unit), + opts.lat, opts.lon, coorInItem.Latitude, coorInItem.Longitude, ref distance)) + { + continue; + } + } + + // The item is inside the shape + responseData.Add(new GeoSearchData() + { + Member = point.Element, + Distance = distance, + GeoHash = (long)point.Score, + GeoHashCode = server.GeoHash.GetGeoHashCode((long)point.Score), + Coordinates = server.GeoHash.GetCoordinatesFromLong((long)point.Score) + }); + + if (opts.withCountAny && (responseData.Count == opts.countValue)) + break; + } + + if (responseData.Count == 0) + { + while (!RespWriteUtils.TryWriteEmptyArray(ref curr, end)) ObjectUtils.ReallocateOutput(ref output, ref isMemory, ref ptr, ref ptrHandle, ref curr, ref end); } else { - var responseData = new List(); - foreach (var point in sortedSet) + var innerArrayLength = 1; + if (opts.withDist) { - var coorInItem = server.GeoHash.GetCoordinatesFromLong((long)point.Item1); - double distance = 0; - if (opts.ByBox) - { - if (server.GeoHash.GetDistanceWhenInRectangle(server.GeoHash.ConvertValueToMeters(width, byBoxUnits), server.GeoHash.ConvertValueToMeters(height, byBoxUnits), lat, lon, coorInItem.Item1, coorInItem.Item2, ref distance)) - { - // The item is inside the shape - responseData.Add(new GeoSearchData() - { - Member = point.Item2, - Distance = distance, - GeoHashCode = server.GeoHash.GetGeoHashCode((long)point.Item1), - Coordinates = server.GeoHash.GetCoordinatesFromLong((long)point.Item1) - }); - - if (opts.WithCount && responseData.Count == countValue) - break; - } - } + innerArrayLength++; + } + if (opts.withHash) + { + innerArrayLength++; + } + if (opts.withCoord) + { + innerArrayLength++; + } + + var q = responseData.AsQueryable(); + switch (opts.sort) + { + case GeoOrder.Descending: + q = q.OrderByDescending(i => i.Distance); + break; + case GeoOrder.Ascending: + q = q.OrderBy(i => i.Distance); + break; + case GeoOrder.None: + if (opts.countValue > 0) + q = q.OrderBy(i => i.Distance); + break; } - if (responseData.Count == 0) + // Write results + if (opts.countValue > 0) { - while (!RespWriteUtils.TryWriteInt32(0, ref curr, end)) + q = q.Take(opts.countValue); + while (!RespWriteUtils.TryWriteArrayLength(opts.countValue, ref curr, end)) ObjectUtils.ReallocateOutput(ref output, ref isMemory, ref ptr, ref ptrHandle, ref curr, ref end); } else { - var innerArrayLength = 1; - if (opts.WithDist) - { - innerArrayLength++; - } - if (opts.WithHash) - { - innerArrayLength++; - } - if (opts.WithCoord) + while (!RespWriteUtils.TryWriteArrayLength(responseData.Count, ref curr, end)) + ObjectUtils.ReallocateOutput(ref output, ref isMemory, ref ptr, ref ptrHandle, ref curr, ref end); + } + + foreach (var item in q) + { + if (innerArrayLength > 1) { - innerArrayLength++; + while (!RespWriteUtils.TryWriteArrayLength(innerArrayLength, ref curr, end)) + ObjectUtils.ReallocateOutput(ref output, ref isMemory, ref ptr, ref ptrHandle, ref curr, ref end); } - // Write results - while (!RespWriteUtils.TryWriteArrayLength(responseData.Count, ref curr, end)) + while (!RespWriteUtils.TryWriteBulkString(item.Member, ref curr, end)) ObjectUtils.ReallocateOutput(ref output, ref isMemory, ref ptr, ref ptrHandle, ref curr, ref end); - foreach (var item in responseData) + if (opts.withDist) { - if (innerArrayLength > 1) + var distanceValue = opts.searchType switch { - while (!RespWriteUtils.TryWriteArrayLength(innerArrayLength, ref curr, end)) - ObjectUtils.ReallocateOutput(ref output, ref isMemory, ref ptr, ref ptrHandle, ref curr, ref end); - } + GeoSearchType.ByBox => server.GeoHash.ConvertMetersToUnits(item.Distance, opts.unit), + + // byRadius + _ => server.GeoHash.ConvertMetersToUnits(item.Distance, opts.unit), + }; - while (!RespWriteUtils.TryWriteBulkString(item.Member, ref curr, end)) + while (!RespWriteUtils.TryWriteDoubleBulkString(distanceValue, ref curr, end)) ObjectUtils.ReallocateOutput(ref output, ref isMemory, ref ptr, ref ptrHandle, ref curr, ref end); + } - if (opts.WithDist) + if (opts.withHash) + { + if (readOnlyCmd) { - var distanceValue = (byBoxUnits.Length == 1 && (byBoxUnits[0] == (int)'M' || byBoxUnits[0] == (int)'m')) ? item.Distance - : server.GeoHash.ConvertMetersToUnits(item.Distance, byBoxUnits); - - while (!RespWriteUtils.TryWriteDoubleBulkString(distanceValue, ref curr, end)) + while (!RespWriteUtils.TryWriteInt64(item.GeoHash, ref curr, end)) ObjectUtils.ReallocateOutput(ref output, ref isMemory, ref ptr, ref ptrHandle, ref curr, ref end); } - - if (opts.WithCoord) + else { - // Write array of 2 values - while (!RespWriteUtils.TryWriteArrayLength(2, ref curr, end)) + while (!RespWriteUtils.TryWriteArrayItem(item.GeoHash, ref curr, end)) ObjectUtils.ReallocateOutput(ref output, ref isMemory, ref ptr, ref ptrHandle, ref curr, ref end); + } + } - while (!RespWriteUtils.TryWriteDoubleBulkString(item.Coordinates.Longitude, ref curr, end)) - ObjectUtils.ReallocateOutput(ref output, ref isMemory, ref ptr, ref ptrHandle, ref curr, ref end); + if (opts.withCoord) + { + // Write array of 2 values + while (!RespWriteUtils.TryWriteArrayLength(2, ref curr, end)) + ObjectUtils.ReallocateOutput(ref output, ref isMemory, ref ptr, ref ptrHandle, ref curr, ref end); - while (!RespWriteUtils.TryWriteDoubleBulkString(item.Coordinates.Latitude, ref curr, end)) - ObjectUtils.ReallocateOutput(ref output, ref isMemory, ref ptr, ref ptrHandle, ref curr, ref end); - } + while (!RespWriteUtils.TryWriteDoubleBulkString(item.Coordinates.Longitude, ref curr, end)) + ObjectUtils.ReallocateOutput(ref output, ref isMemory, ref ptr, ref ptrHandle, ref curr, ref end); + + while (!RespWriteUtils.TryWriteDoubleBulkString(item.Coordinates.Latitude, ref curr, end)) + ObjectUtils.ReallocateOutput(ref output, ref isMemory, ref ptr, ref ptrHandle, ref curr, ref end); } } } } - - // Not supported options in Garnet: FROMLONLAT BYBOX BYRADIUS - if (opts.FromLonLat) - { - while (!RespWriteUtils.TryWriteError(CmdStrings.RESP_ERR_GENERIC_UNK_CMD, ref curr, end)) - ObjectUtils.ReallocateOutput(ref output, ref isMemory, ref ptr, ref ptrHandle, ref curr, ref end); - } + #endregion } finally { diff --git a/libs/server/Resp/CmdStrings.cs b/libs/server/Resp/CmdStrings.cs index d5b135bce0..e95b1b7285 100644 --- a/libs/server/Resp/CmdStrings.cs +++ b/libs/server/Resp/CmdStrings.cs @@ -23,6 +23,8 @@ static partial class CmdStrings public static ReadOnlySpan get => "get"u8; public static ReadOnlySpan SET => "SET"u8; public static ReadOnlySpan set => "set"u8; + public static ReadOnlySpan GEORADIUSBYMEMBER_RO => "GEORADIUSBYMEMBER_RO"u8; + public static ReadOnlySpan REWRITE => "REWRITE"u8; public static ReadOnlySpan rewrite => "rewrite"u8; public static ReadOnlySpan CONFIG => "CONFIG"u8; @@ -120,7 +122,9 @@ static partial class CmdStrings public static ReadOnlySpan CHANNELS => "CHANNELS"u8; public static ReadOnlySpan NUMPAT => "NUMPAT"u8; public static ReadOnlySpan NUMSUB => "NUMSUB"u8; + public static ReadOnlySpan STORE => "STORE"u8; public static ReadOnlySpan STOREDIST => "STOREDIST"u8; + public static ReadOnlySpan WITHCOORD => "WITHCOORD"u8; public static ReadOnlySpan WITHDIST => "WITHDIST"u8; public static ReadOnlySpan WITHHASH => "WITHHASH"u8; public static ReadOnlySpan LIB_NAME => "LIB-NAME"u8; diff --git a/libs/server/Resp/Objects/SortedSetGeoCommands.cs b/libs/server/Resp/Objects/SortedSetGeoCommands.cs index 9c4c5de1f6..af3788b4bf 100644 --- a/libs/server/Resp/Objects/SortedSetGeoCommands.cs +++ b/libs/server/Resp/Objects/SortedSetGeoCommands.cs @@ -61,7 +61,7 @@ private unsafe bool GeoAdd(ref TGarnetApi storageApi) /// /// /// - private unsafe bool GeoCommands(RespCommand command, ref TGarnetApi storageApi) + private unsafe bool GeoCommands(RespCommand command, ref TGarnetApi storageApi, bool @readonly = true) where TGarnetApi : IGarnetApi { var paramsRequiredInCommand = 0; @@ -79,7 +79,15 @@ private unsafe bool GeoCommands(RespCommand command, ref TGarnetApi paramsRequiredInCommand = 1; break; case RespCommand.GEOSEARCH: - paramsRequiredInCommand = 3; + paramsRequiredInCommand = 6; + break; + case RespCommand.GEORADIUS: + case RespCommand.GEORADIUS_RO: + paramsRequiredInCommand = 5; + break; + case RespCommand.GEORADIUSBYMEMBER: + case RespCommand.GEORADIUSBYMEMBER_RO: + paramsRequiredInCommand = 4; break; } @@ -92,20 +100,49 @@ private unsafe bool GeoCommands(RespCommand command, ref TGarnetApi var sbKey = parseState.GetArgSliceByRef(0).SpanByte; var keyBytes = sbKey.ToByteArray(); - var op = - command switch - { - RespCommand.GEOHASH => SortedSetOperation.GEOHASH, - RespCommand.GEODIST => SortedSetOperation.GEODIST, - RespCommand.GEOPOS => SortedSetOperation.GEOPOS, - RespCommand.GEOSEARCH => SortedSetOperation.GEOSEARCH, - _ => throw new Exception($"Unexpected {nameof(SortedSetOperation)}: {command}") - }; + SortedSetOperation op; + SortedSetGeoOpts opts = 0; + switch (command) + { + case RespCommand.GEOHASH: + op = SortedSetOperation.GEOHASH; + break; + case RespCommand.GEODIST: + op = SortedSetOperation.GEODIST; + break; + case RespCommand.GEOPOS: + op = SortedSetOperation.GEOPOS; + break; + case RespCommand.GEORADIUS: + op = SortedSetOperation.GEOSEARCH; + opts = SortedSetGeoOpts.ByRadius | SortedSetGeoOpts.Store; + break; + case RespCommand.GEORADIUS_RO: + op = SortedSetOperation.GEOSEARCH; + opts = SortedSetGeoOpts.ByRadius; + break; + case RespCommand.GEORADIUSBYMEMBER: + op = SortedSetOperation.GEOSEARCH; + opts = SortedSetGeoOpts.ByRadius | SortedSetGeoOpts.Store | SortedSetGeoOpts.ByMember; + break; + case RespCommand.GEORADIUSBYMEMBER_RO: + op = SortedSetOperation.GEOSEARCH; + opts = SortedSetGeoOpts.ByRadius | SortedSetGeoOpts.ByMember; + break; + case RespCommand.GEOSEARCH: + op = SortedSetOperation.GEOSEARCH; + break; + default: + throw new Exception($"Unexpected {nameof(SortedSetOperation)}: {command}"); + } + + if (@readonly) + opts &= ~SortedSetGeoOpts.Store; // Prepare input var header = new RespInputHeader(GarnetObjectType.SortedSet) { SortedSetOp = op }; - var input = new ObjectInput(header, ref parseState, startIdx: 1); + var input = new ObjectInput(header, ref parseState, startIdx: 1, arg2: (int)opts); var outputFooter = new GarnetObjectStoreOutput { SpanByteAndMemory = new SpanByteAndMemory(dcurr, (int)(dend - dcurr)) }; @@ -123,6 +160,10 @@ private unsafe bool GeoCommands(RespCommand command, ref TGarnetApi while (!RespWriteUtils.TryWriteDirect(CmdStrings.RESP_ERRNOTFOUND, ref dcurr, dend)) SendAndReset(); break; + case SortedSetOperation.GEOSEARCH: + while (!RespWriteUtils.TryWriteEmptyArray(ref dcurr, dend)) + SendAndReset(); + break; default: var inputCount = parseState.Count - 1; while (!RespWriteUtils.TryWriteArrayLength(inputCount, ref dcurr, dend)) @@ -134,8 +175,8 @@ private unsafe bool GeoCommands(RespCommand command, ref TGarnetApi } break; } - break; + case GarnetStatus.WRONGTYPE: while (!RespWriteUtils.TryWriteError(CmdStrings.RESP_ERR_WRONG_TYPE, ref dcurr, dend)) SendAndReset(); @@ -149,24 +190,81 @@ private unsafe bool GeoCommands(RespCommand command, ref TGarnetApi /// GEOSEARCHSTORE: Store the the members of a sorted set populated with geospatial data, which are within the borders of the area specified by a given shape. /// /// + /// /// /// - private unsafe bool GeoSearchStore(ref TGarnetApi storageApi) + private unsafe bool GeoSearchStore(RespCommand cmd, ref TGarnetApi storageApi) where TGarnetApi : IGarnetApi { - if (parseState.Count < 4) + int GetDestIdx() + { + var idx = 1; + while (true) + { + if (idx >= parseState.Count - 1) + break; + + var argSpan = parseState.GetArgSliceByRef(idx++).ReadOnlySpan; + + if (argSpan.EqualsUpperCaseSpanIgnoringCase(CmdStrings.STORE) || + argSpan.EqualsUpperCaseSpanIgnoringCase(CmdStrings.STOREDIST)) + { + return idx; + } + } + + return -1; + } + + SortedSetGeoOpts opt; + int destIdx, sourceIdx; + + switch (cmd) { - return AbortWithWrongNumberOfArguments(nameof(RespCommand.GEOSEARCHSTORE)); + case RespCommand.GEORADIUS: + // GERADIUS src lon lat 0 km + if (parseState.Count < 5) + { + return AbortWithWrongNumberOfArguments(nameof(RespCommand.GEORADIUS)); + } + opt = SortedSetGeoOpts.Store | SortedSetGeoOpts.ByRadius; + sourceIdx = 0; + destIdx = GetDestIdx(); + break; + case RespCommand.GEORADIUSBYMEMBER: + // GERADIUSBYMEMBER src member 0 km + if (parseState.Count < 4) + { + return AbortWithWrongNumberOfArguments(nameof(RespCommand.GEORADIUSBYMEMBER)); + } + opt = SortedSetGeoOpts.Store | SortedSetGeoOpts.ByRadius | SortedSetGeoOpts.ByMember; + sourceIdx = 0; + destIdx = GetDestIdx(); + break; + case RespCommand.GEOSEARCHSTORE: + default: + // GEOSEARCHSTORE dst src FROMEMBER key BYRADIUS 0 m + if (parseState.Count < 7) + { + return AbortWithWrongNumberOfArguments(nameof(RespCommand.GEOSEARCHSTORE)); + } + opt = SortedSetGeoOpts.Store; + destIdx = 0; + sourceIdx = 1; + break; } - var destinationKey = parseState.GetArgSliceByRef(0); - var sourceKey = parseState.GetArgSliceByRef(1); + if (destIdx == -1) + return GeoCommands(cmd, ref storageApi, true); + + var destinationKey = parseState.GetArgSliceByRef(destIdx); + var sourceKey = parseState.GetArgSliceByRef(sourceIdx); var input = new ObjectInput(new RespInputHeader { type = GarnetObjectType.SortedSet, - SortedSetOp = SortedSetOperation.GEOSEARCHSTORE - }, ref parseState, startIdx: 2); + SortedSetOp = SortedSetOperation.GEOSEARCH, + }, ref parseState, startIdx: sourceIdx + 1, arg2: (int)opt); var output = new SpanByteAndMemory(dcurr, (int)(dend - dcurr)); var status = storageApi.GeoSearchStore(sourceKey, destinationKey, ref input, ref output); diff --git a/libs/server/Resp/Parser/RespCommand.cs b/libs/server/Resp/Parser/RespCommand.cs index 1c74fd8ac1..f8bb9f8266 100644 --- a/libs/server/Resp/Parser/RespCommand.cs +++ b/libs/server/Resp/Parser/RespCommand.cs @@ -31,6 +31,8 @@ public enum RespCommand : ushort GEODIST, GEOHASH, GEOPOS, + GEORADIUS_RO, + GEORADIUSBYMEMBER_RO, GEOSEARCH, GET, GETBIT, @@ -116,6 +118,8 @@ public enum RespCommand : ushort FLUSHALL, FLUSHDB, GEOADD, + GEORADIUS, + GEORADIUSBYMEMBER, GEOSEARCHSTORE, GETDEL, GETEX, @@ -1473,6 +1477,10 @@ private RespCommand FastParseArrayCommand(ref int count, ref ReadOnlySpan { return RespCommand.HEXPIREAT; } + else if (*(ulong*)(ptr + 4) == MemoryMarshal.Read("GEORADIU"u8) && *(uint*)(ptr + 11) == MemoryMarshal.Read("US\r\n"u8)) + { + return RespCommand.GEORADIUS; + } break; case 10: if (*(ulong*)(ptr + 4) == MemoryMarshal.Read("SSUBSCRI"u8) && *(uint*)(ptr + 11) == MemoryMarshal.Read("BE\r\n"u8)) @@ -1617,6 +1625,10 @@ private RespCommand FastParseArrayCommand(ref int count, ref ReadOnlySpan { return RespCommand.HPEXPIRETIME; } + else if (*(ulong*)(ptr + 3) == MemoryMarshal.Read("\r\nGEORAD"u8) && *(ulong*)(ptr + 11) == MemoryMarshal.Read("IUS_RO\r\n"u8)) + { + return RespCommand.GEORADIUS_RO; + } break; case 13: @@ -1662,6 +1674,12 @@ private RespCommand FastParseArrayCommand(ref int count, ref ReadOnlySpan return RespCommand.ZREVRANGEBYSCORE; } break; + case 17: + if (*(ulong*)(ptr + 4) == MemoryMarshal.Read("\nGEORADI"u8) && *(ulong*)(ptr + 12) == MemoryMarshal.Read("USBYMEMB"u8) && *(uint*)(ptr + 20) == MemoryMarshal.Read("ER\r\n"u8)) + { + return RespCommand.GEORADIUSBYMEMBER; + } + break; } // Reset optimistically changed state, if no matching command was found @@ -1755,6 +1773,10 @@ private RespCommand SlowParseCommand(ref int count, ref ReadOnlySpan speci { return RespCommand.ECHO; } + else if (command.SequenceEqual(CmdStrings.GEORADIUSBYMEMBER_RO)) + { + return RespCommand.GEORADIUSBYMEMBER_RO; + } else if (command.SequenceEqual(CmdStrings.REPLICAOF)) { return RespCommand.REPLICAOF; diff --git a/libs/server/Resp/RespServerSession.cs b/libs/server/Resp/RespServerSession.cs index b401f75282..b0b2996fb7 100644 --- a/libs/server/Resp/RespServerSession.cs +++ b/libs/server/Resp/RespServerSession.cs @@ -726,8 +726,12 @@ private bool ProcessArrayCommands(RespCommand cmd, ref TGarnetApi st RespCommand.GEOHASH => GeoCommands(cmd, ref storageApi), RespCommand.GEODIST => GeoCommands(cmd, ref storageApi), RespCommand.GEOPOS => GeoCommands(cmd, ref storageApi), + RespCommand.GEORADIUS => GeoSearchStore(cmd, ref storageApi), + RespCommand.GEORADIUS_RO => GeoCommands(cmd, ref storageApi), + RespCommand.GEORADIUSBYMEMBER => GeoSearchStore(cmd, ref storageApi), + RespCommand.GEORADIUSBYMEMBER_RO => GeoCommands(cmd, ref storageApi), RespCommand.GEOSEARCH => GeoCommands(cmd, ref storageApi), - RespCommand.GEOSEARCHSTORE => GeoSearchStore(ref storageApi), + RespCommand.GEOSEARCHSTORE => GeoSearchStore(cmd, ref storageApi), //HLL Commands RespCommand.PFADD => HyperLogLogAdd(ref storageApi), RespCommand.PFMERGE => HyperLogLogMerge(ref storageApi), diff --git a/playground/CommandInfoUpdater/SupportedCommand.cs b/playground/CommandInfoUpdater/SupportedCommand.cs index 2d59d8be80..4d937bdd71 100644 --- a/playground/CommandInfoUpdater/SupportedCommand.cs +++ b/playground/CommandInfoUpdater/SupportedCommand.cs @@ -140,6 +140,10 @@ public class SupportedCommand new("GEODIST", RespCommand.GEODIST), new("GEOHASH", RespCommand.GEOHASH), new("GEOPOS", RespCommand.GEOPOS), + new("GEORADIUS", RespCommand.GEORADIUS), + new("GEORADIUS_RO", RespCommand.GEORADIUS_RO), + new("GEORADIUSBYMEMBER", RespCommand.GEORADIUSBYMEMBER), + new("GEORADIUSBYMEMBER_RO", RespCommand.GEORADIUSBYMEMBER_RO), new("GEOSEARCH", RespCommand.GEOSEARCH), new("GEOSEARCHSTORE", RespCommand.GEOSEARCHSTORE), new("GET", RespCommand.GET), diff --git a/test/Garnet.test/Resp/ACL/RespCommandTests.cs b/test/Garnet.test/Resp/ACL/RespCommandTests.cs index d721f9103b..4dd66adb9b 100644 --- a/test/Garnet.test/Resp/ACL/RespCommandTests.cs +++ b/test/Garnet.test/Resp/ACL/RespCommandTests.cs @@ -6285,6 +6285,66 @@ static async Task DoGeoPosMultiAsync(GarnetClient client) } } + [Test] + public async Task GeoRadiusACLsAsync() + { + await CheckCommandsAsync( + "GEORADIUS", + [DoGeoRadiusAsync], + skipPermitted: true + ); + + static async Task DoGeoRadiusAsync(GarnetClient client) + { + await client.ExecuteForStringResultAsync("GEORADIUS", ["foo", "0", "85", "10", "km"]); + } + } + + [Test] + public async Task GeoRadiusROACLsAsync() + { + await CheckCommandsAsync( + "GEORADIUS_RO", + [DoGeoRadiusROAsync], + skipPermitted: true + ); + + static async Task DoGeoRadiusROAsync(GarnetClient client) + { + await client.ExecuteForStringResultAsync("GEORADIUS_RO", ["foo", "0", "85", "10", "km"]); + } + } + + [Test] + public async Task GeoRadiusByMemberACLsAsync() + { + await CheckCommandsAsync( + "GEORADIUSBYMEMBER", + [DoGeoRadiusByMemberAsync], + skipPermitted: true + ); + + static async Task DoGeoRadiusByMemberAsync(GarnetClient client) + { + await client.ExecuteForStringResultAsync("GEORADIUSBYMEMBER", ["foo", "bar", "10", "km"]); + } + } + + [Test] + public async Task GeoRadiusByMemberROACLsAsync() + { + await CheckCommandsAsync( + "GEORADIUSBYMEMBER_RO", + [DoGeoRadiusByMemberROAsync], + skipPermitted: true + ); + + static async Task DoGeoRadiusByMemberROAsync(GarnetClient client) + { + await client.ExecuteForStringResultAsync("GEORADIUSBYMEMBER_RO", ["foo", "bar", "10", "km"]); + } + } + [Test] public async Task GeoSearchACLsAsync() { diff --git a/test/Garnet.test/RespSortedSetGeoTests.cs b/test/Garnet.test/RespSortedSetGeoTests.cs index b7c6d1cc88..9d2e5d8303 100644 --- a/test/Garnet.test/RespSortedSetGeoTests.cs +++ b/test/Garnet.test/RespSortedSetGeoTests.cs @@ -14,6 +14,7 @@ namespace Garnet.test { [TestFixture] + [Category("GEOTESTS")] public class RespSortedSetGeoTests { GarnetServer server; @@ -227,17 +228,6 @@ public void CanUseGeoPos() ClassicAssert.AreEqual(expectedResponse, actualValue); } - [Test] - public void CanValidateUnknownWithNotSupportedOptions() - { - using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); - var db = redis.GetDatabase(0); - var key = new RedisKey("Sicily"); - db.GeoAdd(key, 13.361389, 38.115556, new RedisValue("Palermo"), CommandFlags.None); - var box = new GeoSearchBox(500, 500, GeoUnit.Kilometers); - Assert.Throws(() => db.GeoSearch(key, 73.9262, 40.8296, box, count: 2)); - } - [Test] public void CheckGeoSortedSetOperationsOnWrongTypeObjectSE() { @@ -270,6 +260,8 @@ public void CheckGeoSortedSetOperationsOnWrongTypeObjectSE() RespTestsUtils.CheckCommandOnWrongTypeObjectSE(() => db.GeoDistance(keys[0], values[0][1], values[0][1])); // GEOPOS RespTestsUtils.CheckCommandOnWrongTypeObjectSE(() => db.GeoPosition(keys[0], values[0])); + // GEORADIUS + RespTestsUtils.CheckCommandOnWrongTypeObjectSE(() => db.GeoRadius(keys[0], values[0][1], 800, GeoUnit.Kilometers)); // GEOSEARCH RespTestsUtils.CheckCommandOnWrongTypeObjectSE(() => db.GeoSearch(keys[0], values[0][1], new GeoSearchBox(800, 800, GeoUnit.Kilometers))); } @@ -282,7 +274,7 @@ public void CanUseGeoSearch() var entries = new GeoEntry[cities.GetLength(0)]; var key = new RedisKey("cities"); var destinationKey = new RedisKey("newCities"); - for (int j = 0; j < cities.GetLength(0); j++) + for (var j = 0; j < cities.GetLength(0); j++) { entries[j] = new GeoEntry( double.Parse(cities[j, 0], CultureInfo.InvariantCulture), @@ -291,7 +283,8 @@ public void CanUseGeoSearch() } var response = db.GeoAdd(key, entries, CommandFlags.None); - var res = db.GeoSearch(key, new RedisValue("Washington"), new GeoSearchBox(800, 800, GeoUnit.Kilometers), options: GeoRadiusOptions.None); + var res = db.GeoSearch(key, new RedisValue("Washington"), new GeoSearchBox(800, 800, GeoUnit.Kilometers), + order: Order.Ascending, options: GeoRadiusOptions.None); ClassicAssert.AreEqual(3, res.Length); ClassicAssert.AreEqual("Washington", (string)res[0].Member); ClassicAssert.AreEqual(res[0].Distance, null); @@ -303,7 +296,9 @@ public void CanUseGeoSearch() ClassicAssert.AreEqual(res[2].Distance, null); ClassicAssert.AreEqual(res[2].Position, null); - res = db.GeoSearch(key, new RedisValue("Washington"), new GeoSearchBox(800, 800, GeoUnit.Kilometers), options: GeoRadiusOptions.WithDistance); + res = db.GeoSearch(key, new RedisValue("Washington"), new GeoSearchBox(800, 800, GeoUnit.Kilometers), + order: Order.Ascending, + options: GeoRadiusOptions.WithDistance); ClassicAssert.AreEqual(3, res.Length); ClassicAssert.AreEqual("Washington", (string)res[0].Member); Assert.That(res[0].Distance, Is.EqualTo(0).Within(1.0 / Math.Pow(10, 6))); @@ -315,7 +310,9 @@ public void CanUseGeoSearch() Assert.That(res[2].Distance, Is.EqualTo(327.676458633557).Within(1.0 / Math.Pow(10, 6))); ClassicAssert.AreEqual(res[2].Position, null); - res = db.GeoSearch(key, new RedisValue("Washington"), new GeoSearchBox(800, 800, GeoUnit.Kilometers), options: GeoRadiusOptions.WithCoordinates); + res = db.GeoSearch(key, new RedisValue("Washington"), new GeoSearchBox(800, 800, GeoUnit.Kilometers), + order: Order.Ascending, + options: GeoRadiusOptions.WithCoordinates); ClassicAssert.AreEqual(3, res.Length); ClassicAssert.AreEqual("Washington", (string)res[0].Member); ClassicAssert.AreEqual(res[0].Distance, null); @@ -330,7 +327,9 @@ public void CanUseGeoSearch() Assert.That(res[2].Position.Value.Longitude, Is.EqualTo(-74.00594205).Within(1.0 / Math.Pow(10, 6))); Assert.That(res[2].Position.Value.Latitude, Is.EqualTo(40.71278259).Within(1.0 / Math.Pow(10, 6))); - res = db.GeoSearch(key, new RedisValue("Washington"), new GeoSearchBox(800, 800, GeoUnit.Kilometers), options: GeoRadiusOptions.WithDistance | GeoRadiusOptions.WithCoordinates); + res = db.GeoSearch(key, new RedisValue("Washington"), new GeoSearchBox(800, 800, GeoUnit.Kilometers), + order: Order.Ascending, + options: GeoRadiusOptions.WithDistance | GeoRadiusOptions.WithCoordinates); ClassicAssert.AreEqual(3, res.Length); ClassicAssert.AreEqual("Washington", (string)res[0].Member); Assert.That(res[0].Distance, Is.EqualTo(0).Within(1.0 / Math.Pow(10, 6))); @@ -344,6 +343,31 @@ public void CanUseGeoSearch() Assert.That(res[2].Distance, Is.EqualTo(327.676458633557).Within(1.0 / Math.Pow(10, 6))); Assert.That(res[2].Position.Value.Longitude, Is.EqualTo(-74.00594205).Within(1.0 / Math.Pow(10, 6))); Assert.That(res[2].Position.Value.Latitude, Is.EqualTo(40.71278259).Within(1.0 / Math.Pow(10, 6))); + + res = db.GeoSearch(key, new RedisValue("Washington"), new GeoSearchCircle(530, GeoUnit.Kilometers), + order: Order.Descending, + options: GeoRadiusOptions.WithDistance | GeoRadiusOptions.WithCoordinates); + ClassicAssert.AreEqual(4, res.Length); + ClassicAssert.AreEqual("Columbus", (string)res[0].Member); + Assert.That(res[0].Distance, Is.EqualTo(525.298997019908).Within(1.0 / Math.Pow(10, 6))); + Assert.That(res[0].Position.Value.Longitude, Is.EqualTo(-82.99879419999999).Within(1.0 / Math.Pow(10, 6))); + Assert.That(res[0].Position.Value.Latitude, Is.EqualTo(39.9611766).Within(1.0 / Math.Pow(10, 6))); + ClassicAssert.AreEqual("New York", (string)res[1].Member); + Assert.That(res[1].Distance, Is.EqualTo(327.676458633557).Within(1.0 / Math.Pow(10, 6))); + Assert.That(res[1].Position.Value.Longitude, Is.EqualTo(-74.00594205).Within(1.0 / Math.Pow(10, 6))); + Assert.That(res[1].Position.Value.Latitude, Is.EqualTo(40.71278259).Within(1.0 / Math.Pow(10, 6))); + ClassicAssert.AreEqual("Philadelphia", (string)res[2].Member); + Assert.That(res[2].Distance, Is.EqualTo(198.424300439725).Within(1.0 / Math.Pow(10, 6))); + Assert.That(res[2].Position.Value.Longitude, Is.EqualTo(-75.1652196).Within(1.0 / Math.Pow(10, 6))); + Assert.That(res[2].Position.Value.Latitude, Is.EqualTo(39.95258287).Within(1.0 / Math.Pow(10, 6))); + ClassicAssert.AreEqual("Washington", (string)res[3].Member); + Assert.That(res[3].Distance, Is.EqualTo(0).Within(1.0 / Math.Pow(10, 6))); + Assert.That(res[3].Position.Value.Longitude, Is.EqualTo(-77.03687042).Within(1.0 / Math.Pow(10, 6))); + Assert.That(res[3].Position.Value.Latitude, Is.EqualTo(38.9071919).Within(1.0 / Math.Pow(10, 6))); + + res = db.GeoSearch(key, new RedisValue("Washington"), new GeoSearchCircle(530, GeoUnit.Kilometers), + options: GeoRadiusOptions.WithDistance | GeoRadiusOptions.WithCoordinates); + ClassicAssert.AreEqual(4, res.Length); } [Test] @@ -376,6 +400,9 @@ public void CanUseGeoSearchStore() Assert.That(actualValues[1].Score, Is.EqualTo(198.424300439725).Within(1.0 / Math.Pow(10, 6))); ClassicAssert.AreEqual("New York", (string)actualValues[2].Element); Assert.That(actualValues[2].Score, Is.EqualTo(327.676458633557).Within(1.0 / Math.Pow(10, 6))); + + actualCount = db.GeoSearchAndStore(key, destinationKey, new RedisValue("Washington"), new GeoSearchBox(800, 800, GeoUnit.Kilometers), storeDistances: true); + ClassicAssert.AreEqual(3, actualCount); } [Test] @@ -394,6 +421,12 @@ public void CanUseGeoSearchStoreWithDeleteKeyWhenSourceNotFound() var actualValues = db.SortedSetRangeByScoreWithScores(destinationKey); ClassicAssert.AreEqual(0, actualValues.Length); + + actualCount = db.GeoSearchAndStore(key, destinationKey, new RedisValue("Washington"), new GeoSearchBox(800, 800, GeoUnit.Kilometers)); + ClassicAssert.AreEqual(0, actualCount); + + actualValues = db.SortedSetRangeByScoreWithScores(destinationKey); + ClassicAssert.AreEqual(0, actualValues.Length); } //end region of SE tests @@ -410,8 +443,17 @@ public void CanUseGeoSearchWithCities(int bytesSent) using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); var db = redis.GetDatabase(0); var entries = new GeoEntry[cities.GetLength(0)]; - for (int j = 0; j < cities.GetLength(0); j++) + + string lat = "0", lon = "0"; + for (var j = 0; j < cities.GetLength(0); j++) { + if (string.Compare(cities[j, 2], "Washington", + CultureInfo.InvariantCulture, CompareOptions.IgnoreCase) == 0) + { + lon = cities[j, 0].TrimEnd(); + lat = cities[j, 1].TrimEnd(); + } + entries[j] = new GeoEntry( double.Parse(cities[j, 0], CultureInfo.InvariantCulture), double.Parse(cities[j, 1], CultureInfo.InvariantCulture), @@ -423,15 +465,37 @@ public void CanUseGeoSearchWithCities(int bytesSent) //TODO: Assert values for latitude and longitude //TODO: Review precision to use for all framework versions using var lightClientRequest = TestUtils.CreateRequest(); - var responseBuf = lightClientRequest.SendCommands("GEOSEARCH cities FROMMEMBER Washington BYBOX 800 800 km WITHCOORD WITHDIST", "PING"); + var responseBuf = lightClientRequest.SendCommands("GEOSEARCH cities FROMMEMBER Washington BYBOX 800 800 km WITHCOORD WITHDIST ASC", "PING"); var expectedResponse = "*3\r\n*3\r\n$10\r\nWashington\r\n$1\r\n0\r\n*2\r\n$17\r\n-77.0368704199791\r\n$17\r\n38.90719190239906\r\n*3\r\n$12\r\nPhiladelphia\r\n$17\r\n198.4242996738795\r\n*2\r\n$18\r\n-75.16521960496902\r\n$18\r\n39.952582865953445\r\n*3\r\n$8\r\nNew York\r\n$18\r\n327.67645879712575\r\n*2\r\n$17\r\n-74.0059420466423\r\n$18\r\n40.712782591581345\r\n+PONG\r\n"; - var actualValue = Encoding.ASCII.GetString(responseBuf).Substring(0, expectedResponse.Length); + var actualValue = Encoding.ASCII.GetString(responseBuf, 0, expectedResponse.Length); ClassicAssert.IsTrue(actualValue.IndexOf("Washington") != -1); //Send command in chunks - responseBuf = lightClientRequest.SendCommandChunks("GEOSEARCH cities FROMMEMBER Washington BYBOX 800 800 km COUNT 3 ANY WITHCOORD WITHDIST", bytesSent, 16); + responseBuf = lightClientRequest.SendCommandChunks("GEOSEARCH cities FROMMEMBER Washington BYBOX 800 800 km COUNT 3 ANY WITHCOORD WITHDIST ASC", bytesSent, 16); + expectedResponse = "*3\r\n*3\r\n$10\r\nWashington\r\n$1\r\n0\r\n*2\r\n$17\r\n-77.0368704199791\r\n$17\r\n38.90719190239906\r\n*3\r\n$12\r\nPhiladelphia\r\n$17\r\n198.4242996738795\r\n*2\r\n$18\r\n-75.16521960496902\r\n$18\r\n39.952582865953445\r\n*3\r\n$8\r\nNew York\r\n$18\r\n327.67645879712575\r\n*2\r\n$17\r\n-74.0059420466423\r\n$18\r\n40.712782591581345\r\n+PONG\r\n"; + actualValue = Encoding.ASCII.GetString(responseBuf, 0, expectedResponse.Length); + ClassicAssert.IsTrue(actualValue.IndexOf("Washington") != -1); + + using var lightClientRequest2 = TestUtils.CreateRequest(); + responseBuf = lightClientRequest2.SendCommands("GEOSEARCH cities FROMMEMBER Washington BYRADIUS 500 km WITHCOORD WITHDIST ASC", "PING"); + expectedResponse = "*3\r\n*3\r\n$10\r\nWashington\r\n$1\r\n0\r\n*2\r\n$17\r\n-77.0368704199791\r\n$17\r\n38.90719190239906\r\n*3\r\n$12\r\nPhiladelphia\r\n$17\r\n198.4242996738795\r\n*2\r\n$18\r\n-75.16521960496902\r\n$18\r\n39.952582865953445\r\n*3\r\n$8\r\nNew York\r\n$18\r\n327.67645879712575\r\n*2\r\n$17\r\n-74.0059420466423\r\n$18\r\n40.712782591581345\r\n+PONG\r\n"; + actualValue = Encoding.ASCII.GetString(responseBuf, 0, expectedResponse.Length); + ClassicAssert.IsTrue(actualValue.IndexOf("Washington") != -1); + + responseBuf = lightClientRequest2.SendCommandChunks($"GEOSEARCH cities FROMLONLAT {lon} {lat} BYRADIUS 500 km COUNT 3 ANY WITHCOORD WITHDIST ASC", bytesSent, 16); expectedResponse = "*3\r\n*3\r\n$10\r\nWashington\r\n$1\r\n0\r\n*2\r\n$17\r\n-77.0368704199791\r\n$17\r\n38.90719190239906\r\n*3\r\n$12\r\nPhiladelphia\r\n$17\r\n198.4242996738795\r\n*2\r\n$18\r\n-75.16521960496902\r\n$18\r\n39.952582865953445\r\n*3\r\n$8\r\nNew York\r\n$18\r\n327.67645879712575\r\n*2\r\n$17\r\n-74.0059420466423\r\n$18\r\n40.712782591581345\r\n+PONG\r\n"; - actualValue = Encoding.ASCII.GetString(responseBuf).Substring(0, expectedResponse.Length); + actualValue = Encoding.ASCII.GetString(responseBuf, 0, expectedResponse.Length); + ClassicAssert.IsTrue(actualValue.IndexOf("Washington") != -1); + + using var lightClientRequest3 = TestUtils.CreateRequest(); + responseBuf = lightClientRequest3.SendCommands("GEORADIUSBYMEMBER_RO cities Washington 500 km WITHCOORD WITHDIST ASC", "PING"); + expectedResponse = "*3\r\n*3\r\n$10\r\nWashington\r\n$1\r\n0\r\n*2\r\n$17\r\n-77.0368704199791\r\n$17\r\n38.90719190239906\r\n*3\r\n$12\r\nPhiladelphia\r\n$17\r\n198.4242996738795\r\n*2\r\n$18\r\n-75.16521960496902\r\n$18\r\n39.952582865953445\r\n*3\r\n$8\r\nNew York\r\n$18\r\n327.67645879712575\r\n*2\r\n$17\r\n-74.0059420466423\r\n$18\r\n40.712782591581345\r\n+PONG\r\n"; + actualValue = Encoding.ASCII.GetString(responseBuf, 0, expectedResponse.Length); + ClassicAssert.IsTrue(actualValue.IndexOf("Washington") != -1); + + responseBuf = lightClientRequest3.SendCommandChunks($"GEORADIUS_RO cities FROMLONLAT {lon} {lat} 500 km COUNT 3 ANY WITHCOORD WITHDIST ASC", bytesSent, 16); + expectedResponse = "*3\r\n*3\r\n$10\r\nWashington\r\n$1\r\n0\r\n*2\r\n$17\r\n-77.0368704199791\r\n$17\r\n38.90719190239906\r\n*3\r\n$12\r\nPhiladelphia\r\n$17\r\n198.4242996738795\r\n*2\r\n$18\r\n-75.16521960496902\r\n$18\r\n39.952582865953445\r\n*3\r\n$8\r\nNew York\r\n$18\r\n327.67645879712575\r\n*2\r\n$17\r\n-74.0059420466423\r\n$18\r\n40.712782591581345\r\n+PONG\r\n"; + actualValue = Encoding.ASCII.GetString(responseBuf, 0, expectedResponse.Length); ClassicAssert.IsTrue(actualValue.IndexOf("Washington") != -1); } @@ -468,7 +532,6 @@ public void CanDoGeoAddWhenInvalidPairLC(int bytesSent) ClassicAssert.AreEqual(expectedResponse, actualValue); } - [Test] [TestCase(10)] [TestCase(50)] @@ -619,6 +682,37 @@ public void CanContinueWhenNotEnoughParametersInGeoAdd(int bytesSent) actualValue = Encoding.ASCII.GetString(response).Substring(0, expectedResponse.Length); ClassicAssert.AreEqual(expectedResponse, actualValue); } + + [Test] + public void InvalidGeoSearches() + { + using var lightClientRequest = TestUtils.CreateRequest(); + + var response = lightClientRequest.SendCommand("GEOADD Sicily NX 13.361389 38.115556 Palermo 15.087269 37.502669 Catania"); + var expectedResponse = ":2\r\n"; + var actualValue = Encoding.ASCII.GetString(response, 0, expectedResponse.Length); + ClassicAssert.AreEqual(expectedResponse, actualValue); + + response = lightClientRequest.SendCommand("GEOSEARCH Sicily FROMLONLAT 15 37 FROMMEMBER a BYRADIUS 0 km"); + expectedResponse = "-ERR syntax error\r\n"; + actualValue = Encoding.ASCII.GetString(response, 0, expectedResponse.Length); + ClassicAssert.AreEqual(expectedResponse, actualValue); + + response = lightClientRequest.SendCommand("GEOSEARCH Sicily FROMLONLAT 15 37 BYRADIUS 100 km BYBOX 400 400 km"); + expectedResponse = "-ERR syntax error\r\n"; + actualValue = Encoding.ASCII.GetString(response, 0, expectedResponse.Length); + ClassicAssert.AreEqual(expectedResponse, actualValue); + + response = lightClientRequest.SendCommand("GEOSEARCH Sicily FROMLONLAT 15 37 BYBOX 400 400 km COUNT 0 ANY"); + expectedResponse = "-ERR value is out of range, must be positive.\r\n"; + actualValue = Encoding.ASCII.GetString(response, 0, expectedResponse.Length); + ClassicAssert.AreEqual(expectedResponse, actualValue); + + response = lightClientRequest.SendCommand("GEOSEARCH foo FROMMEMBER bar BYRADIUS 0 m"); + expectedResponse = "*0\r\n"; + actualValue = Encoding.ASCII.GetString(response, 0, expectedResponse.Length); + ClassicAssert.AreEqual(expectedResponse, actualValue); + } #endregion } } \ No newline at end of file diff --git a/website/docs/commands/api-compatibility.md b/website/docs/commands/api-compatibility.md index 118d0122b4..c4031caee8 100644 --- a/website/docs/commands/api-compatibility.md +++ b/website/docs/commands/api-compatibility.md @@ -155,12 +155,12 @@ Note that this list is subject to change as we continue to expand our API comman | | [GEODIST](data-structures.md#geodist) | ➕ | | | | [GEOHASH](data-structures.md#geohash) | ➕ | | | | [GEOPOS](data-structures.md#geopos) | ➕ | | -| | GEORADIUS | ➖ | (Deprecated) | -| | GEORADIUS_RO | ➖ | (Deprecated) | -| | GEORADIUSBYMEMBER | ➖ | (Deprecated) | -| | GEORADIUSBYMEMBER_RO | ➖ | (Deprecated) | -| | [GEOSEARCH](data-structures.md#geosearch) | ➕ | Partially Implemented | -| | [GEOSEARCHSTORE](data-structures.md#geosearchstore) | ➕ | Partially Implemented | +| | [GEORADIUS](data-structures.md#georadius) | ➕ | (Deprecated) | +| | [GEORADIUS_RO](data-structures.md#georadius_ro) | ➕ | (Deprecated) | +| | [GEORADIUSBYMEMBER](data-structures.md#georadiusbymember) | ➕ | (Deprecated) | +| | [GEORADIUSBYMEMBER_RO](data-structures.md#georadiusbymember_ro) | ➕ | (Deprecated) | +| | [GEOSEARCH](data-structures.md#geosearch) | ➕ | | +| | [GEOSEARCHSTORE](data-structures.md#geosearchstore) | ➕ | | | **HASH** | [HDEL](data-structures.md#hdel) | ➕ | | | | [HEXISTS](data-structures.md#hexists) | ➕ | | | | [HEXPIRE](data-structures.md#hexpire) | ➕ | | diff --git a/website/docs/commands/data-structures.md b/website/docs/commands/data-structures.md index 3992413920..3cdd9ade43 100644 --- a/website/docs/commands/data-structures.md +++ b/website/docs/commands/data-structures.md @@ -1611,6 +1611,113 @@ The command can accept a variable number of arguments so it always returns an ar --- +### GEORADIUS + +#### Syntax + +```bash +GEORADIUS_RO key longitude latitude radius + [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count [ANY]] [ASC | DESC] + [STORE key | STOREDIST key] +``` + +Return the members of a sorted set populated with geospatial information using GEOADD, which are within the borders of the area specified with the center location and the maximum distance from the center (the radius). + +The common use case for this command is to retrieve geospatial items near a specified point not farther than a given amount of meters (or other units). This allows, for example, to suggest mobile users of an application nearby places. + +The radius is specified in one of the following units: + + m for meters. + km for kilometers. + mi for miles. + ft for feet. + +The command optionally returns additional information using the following options: + + WITHDIST: Also return the distance of the returned items from the specified center. The distance is returned in the same unit as the unit specified as the radius argument of the command. + WITHCOORD: Also return the longitude,latitude coordinates of the matching items. + WITHHASH: Also return the raw geohash-encoded sorted set score of the item, in the form of a 52 bit unsigned integer. This is only useful for low level hacks or debugging and is otherwise of little interest for the general user. + +The command default is to return unsorted items. Two different sorting methods can be invoked using the following two options: + + ASC: Sort returned items from the nearest to the farthest, relative to the center. + DESC: Sort returned items from the farthest to the nearest, relative to the center. + +By default all the matching items are returned. It is possible to limit the results to the first N matching items by using the COUNT option. + +By default the command returns the items to the client. It is possible to store the results with one of these options: + + STORE: Store the items in a sorted set populated with their geospatial information. + STOREDIST: Store the items in a sorted set populated with their distance from the center as a floating point number, in the same unit specified in the radius. + +--- + +### GEORADIUS_RO + +#### Syntax + +```bash +GEORADIUS_RO key longitude latitude radius + [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count [ANY]] [ASC | DESC] +``` + +Return the members of a sorted set populated with geospatial information using GEOADD, which are within the borders of the area specified with the center location and the maximum distance from the center (the radius). + +The common use case for this command is to retrieve geospatial items near a specified point not farther than a given amount of meters (or other units). This allows, for example, to suggest mobile users of an application nearby places. + +The radius is specified in one of the following units: + + m for meters. + km for kilometers. + mi for miles. + ft for feet. + +The command optionally returns additional information using the following options: + + WITHDIST: Also return the distance of the returned items from the specified center. The distance is returned in the same unit as the unit specified as the radius argument of the command. + WITHCOORD: Also return the longitude,latitude coordinates of the matching items. + WITHHASH: Also return the raw geohash-encoded sorted set score of the item, in the form of a 52 bit unsigned integer. This is only useful for low level hacks or debugging and is otherwise of little interest for the general user. + +The command default is to return unsorted items. Two different sorting methods can be invoked using the following two options: + + ASC: Sort returned items from the nearest to the farthest, relative to the center. + DESC: Sort returned items from the farthest to the nearest, relative to the center. + +By default all the matching items are returned. It is possible to limit the results to the first N matching items by using the COUNT option. When ANY is provided the command will return as soon as enough matches are found, so the results may not be the ones closest to the specified point, but on the other hand, the effort invested by the server is significantly lower. When ANY is not provided, the command will perform an effort that is proportional to the number of items matching the specified area and sort them, so to query very large areas with a very small COUNT option may be slow even if just a few results are returned. + +--- + +### GEORADIUSBYMEMBER + +#### Syntax + +```bash +GEORADIUSBYMEMBER_RO key member radius + [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count [ANY]] [ASC | DESC] + [STORE key | STOREDIST key] +``` + +This command is exactly like [GEORADIUS](#georadius) with the sole difference that instead of taking, as the center of the area to query, a longitude and latitude value, it takes the name of a member already existing inside the geospatial index represented by the sorted set. + +The position of the specified member is used as the center of the query. + +--- + +### GEORADIUSBYMEMBER_RO + +#### Syntax + +```bash +GEORADIUSBYMEMBER_RO key member radius + [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count [ANY]] [ASC | DESC] +``` + +This command is exactly like [GEORADIUS_RO](#georadius_ro) with the sole difference that instead of taking, as the center of the area to query, a longitude and latitude value, it takes the name of a member already existing inside the geospatial index represented by the sorted set. + +The position of the specified member is used as the center of the query. + +--- + ### GEOSEARCH #### Syntax