Skip to content

Commit

Permalink
Node: add GEOSEARCH (valkey-io#2007)
Browse files Browse the repository at this point in the history
* Add `GEOSEARCH` 

Signed-off-by: Yury-Fridlyand <yury.fridlyand@improving.com>
Co-authored-by: Yi-Pin Chen <yi-pin.chen@improving.com>
  • Loading branch information
Yury-Fridlyand and yipin-chen authored Jul 27, 2024
1 parent 0672ea3 commit b753216
Show file tree
Hide file tree
Showing 7 changed files with 657 additions and 9 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#### Changes
* Node: Added GEOSEARCH command ([#2007](https://github.com/valkey-io/valkey-glide/pull/2007))
* Node: Added LMOVE command ([#2002](https://github.com/valkey-io/valkey-glide/pull/2002))
* Node: Added GEOPOS command ([#1991](https://github.com/valkey-io/valkey-glide/pull/1991))
* Node: Added BITCOUNT command ([#1982](https://github.com/valkey-io/valkey-glide/pull/1982))
Expand Down
18 changes: 18 additions & 0 deletions node/npm/glide/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,15 @@ function initialize() {
BitwiseOperation,
ConditionalChange,
GeoAddOptions,
CoordOrigin,
MemberOrigin,
SearchOrigin,
GeoBoxShape,
GeoCircleShape,
GeoSearchShape,
GeoSearchResultOptions,
SortOrder,
GeoUnit,
GeospatialData,
GlideClient,
GlideClusterClient,
Expand Down Expand Up @@ -136,6 +145,15 @@ function initialize() {
BitwiseOperation,
ConditionalChange,
GeoAddOptions,
CoordOrigin,
MemberOrigin,
SearchOrigin,
GeoBoxShape,
GeoCircleShape,
GeoSearchShape,
GeoSearchResultOptions,
SortOrder,
GeoUnit,
GeospatialData,
GlideClient,
GlideClusterClient,
Expand Down
96 changes: 96 additions & 0 deletions node/src/BaseClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,26 @@ import {
BitmapIndexType,
BitOffsetOptions,
BitwiseOperation,
CoordOrigin, // eslint-disable-line @typescript-eslint/no-unused-vars
ExpireOptions,
GeoAddOptions,
GeoBoxShape, // eslint-disable-line @typescript-eslint/no-unused-vars
GeoCircleShape, // eslint-disable-line @typescript-eslint/no-unused-vars
GeoSearchResultOptions,
GeoSearchShape,
GeospatialData,
GeoUnit,
InsertPosition,
KeyWeight,
MemberOrigin, // eslint-disable-line @typescript-eslint/no-unused-vars
LPosOptions,
ListDirection,
RangeByIndex,
RangeByLex,
RangeByScore,
ScoreBoundary,
ScoreFilter,
SearchOrigin,
SetOptions,
StreamAddOptions,
StreamReadOptions,
Expand All @@ -51,6 +58,7 @@ import {
createGeoDist,
createGeoHash,
createGeoPos,
createGeoSearch,
createGet,
createGetBit,
createGetDel,
Expand Down Expand Up @@ -3660,6 +3668,94 @@ export class BaseClient {
);
}

/**
* Returns the members of a sorted set populated with geospatial information using {@link geoadd},
* which are within the borders of the area specified by a given shape.
*
* See https://valkey.io/commands/geosearch/ for more details.
*
* since - Valkey 6.2.0 and above.
*
* @param key - The key of the sorted set.
* @param searchFrom - The query's center point options, could be one of:
*
* - {@link MemberOrigin} to use the position of the given existing member in the sorted set.
*
* - {@link CoordOrigin} to use the given longitude and latitude coordinates.
*
* @param searchBy - The query's shape options, could be one of:
*
* - {@link GeoCircleShape} to search inside circular area according to given radius.
*
* - {@link GeoBoxShape} to search inside an axis-aligned rectangle, determined by height and width.
*
* @param resultOptions - The optional inputs to request additional information and configure sorting/limiting the results, see {@link GeoSearchResultOptions}.
* @returns By default, returns an `Array` of members (locations) names.
* If any of `withCoord`, `withDist` or `withHash` are set to `true` in {@link GeoSearchResultOptions}, a 2D `Array` returned,
* where each sub-array represents a single item in the following order:
*
* - The member (location) name.
*
* - The distance from the center as a floating point `number`, in the same unit specified for `searchBy`, if `withDist` is set to `true`.
*
* - The geohash of the location as a integer `number`, if `withHash` is set to `true`.
*
* - The coordinates as a two item `array` of floating point `number`s, if `withCoord` is set to `true`.
*
* @example
* ```typescript
* const data = new Map([["Palermo", { longitude: 13.361389, latitude: 38.115556 }], ["Catania", { longitude: 15.087269, latitude: 37.502669 }]]);
* await client.geoadd("mySortedSet", data);
* // search for locations within 200 km circle around stored member named 'Palermo'
* const result1 = await client.geosearch("mySortedSet", { member: "Palermo" }, { radius: 200, unit: GeoUnit.KILOMETERS });
* console.log(result1); // Output: ['Palermo', 'Catania']
*
* // search for locations in 200x300 mi rectangle centered at coordinate (15, 37), requesting additional info,
* // limiting results by 2 best matches, ordered by ascending distance from the search area center
* const result2 = await client.geosearch(
* "mySortedSet",
* { position: { longitude: 15, latitude: 37 } },
* { width: 200, height: 300, unit: GeoUnit.MILES },
* {
* sortOrder: SortOrder.ASC,
* count: 2,
* withCoord: true,
* withDist: true,
* withHash: true,
* },
* );
* console.log(result2); // Output:
* // [
* // [
* // 'Catania', // location name
* // [
* // 56.4413, // distance
* // 3479447370796909, // geohash of the location
* // [15.087267458438873, 37.50266842333162], // coordinates of the location
* // ],
* // ],
* // [
* // 'Palermo',
* // [
* // 190.4424,
* // 3479099956230698,
* // [13.361389338970184, 38.1155563954963],
* // ],
* // ],
* // ]
* ```
*/
public async geosearch(
key: string,
searchFrom: SearchOrigin,
searchBy: GeoSearchShape,
resultOptions?: GeoSearchResultOptions,
): Promise<(Buffer | (number | number[])[])[]> {
return this.createWritePromise(
createGeoSearch(key, searchFrom, searchBy, resultOptions),
);
}

/**
* Returns the positions (longitude, latitude) of all the specified `members` of the
* geospatial index represented by the sorted set at `key`.
Expand Down
130 changes: 121 additions & 9 deletions node/src/Commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2096,28 +2096,23 @@ export function createGeoAdd(
}

membersToGeospatialData.forEach((coord, member) => {
args = args.concat([
args = args.concat(
coord.longitude.toString(),
coord.latitude.toString(),
]);
args.push(member);
member,
);
});
return createCommand(RequestType.GeoAdd, args);
}

/**
* Enumeration representing distance units options for the {@link geodist} command.
*/
/** Enumeration representing distance units options. */
export enum GeoUnit {
/** Represents distance in meters. */
METERS = "m",

/** Represents distance in kilometers. */
KILOMETERS = "km",

/** Represents distance in miles. */
MILES = "mi",

/** Represents distance in feet. */
FEET = "ft",
}
Expand Down Expand Up @@ -2161,6 +2156,123 @@ export function createGeoHash(
return createCommand(RequestType.GeoHash, args);
}

/**
* Optional parameters for {@link BaseClient.geosearch|geosearch} command which defines what should be included in the
* search results and how results should be ordered and limited.
*/
export type GeoSearchResultOptions = {
/** Include the coordinate of the returned items. */
withCoord?: boolean;
/**
* Include the distance of the returned items from the specified center point.
* The distance is returned in the same unit as specified for the `searchBy` argument.
*/
withDist?: boolean;
/** Include the geohash of the returned items. */
withHash?: boolean;
/** Indicates the order the result should be sorted in. */
sortOrder?: SortOrder;
/** Indicates the number of matches the result should be limited to. */
count?: number;
/** Whether to allow returning as enough matches are found. This requires `count` parameter to be set. */
isAny?: boolean;
};

/** Defines the sort order for nested results. */
export enum SortOrder {
/** Sort by ascending order. */
ASC = "ASC",
/** Sort by descending order. */
DESC = "DESC",
}

export type GeoSearchShape = GeoCircleShape | GeoBoxShape;

/** Circle search shape defined by the radius value and measurement unit. */
export type GeoCircleShape = {
/** The radius to search by. */
radius: number;
/** The measurement unit of the radius. */
unit: GeoUnit;
};

/** Rectangle search shape defined by the width and height and measurement unit. */
export type GeoBoxShape = {
/** The width of the rectangle to search by. */
width: number;
/** The height of the rectangle to search by. */
height: number;
/** The measurement unit of the width and height. */
unit: GeoUnit;
};

export type SearchOrigin = CoordOrigin | MemberOrigin;

/** The search origin represented by a {@link GeospatialData} position. */
export type CoordOrigin = {
/** The pivot location to search from. */
position: GeospatialData;
};

/** The search origin represented by an existing member. */
export type MemberOrigin = {
/** Member (location) name stored in the sorted set to use as a search pivot. */
member: string;
};

/**
* @internal
*/
export function createGeoSearch(
key: string,
searchFrom: SearchOrigin,
searchBy: GeoSearchShape,
resultOptions?: GeoSearchResultOptions,
): command_request.Command {
let args: string[] = [key];

if ("position" in searchFrom) {
args = args.concat(
"FROMLONLAT",
searchFrom.position.longitude.toString(),
searchFrom.position.latitude.toString(),
);
} else {
args = args.concat("FROMMEMBER", searchFrom.member);
}

if ("radius" in searchBy) {
args = args.concat(
"BYRADIUS",
searchBy.radius.toString(),
searchBy.unit,
);
} else {
args = args.concat(
"BYBOX",
searchBy.width.toString(),
searchBy.height.toString(),
searchBy.unit,
);
}

if (resultOptions) {
if (resultOptions.withCoord) args.push("WITHCOORD");
if (resultOptions.withDist) args.push("WITHDIST");
if (resultOptions.withHash) args.push("WITHHASH");

if (resultOptions.count) {
args.push("COUNT", resultOptions.count?.toString());

if (resultOptions.isAny) args.push("ANY");
}

if (resultOptions.sortOrder) args.push(resultOptions.sortOrder);
}

return createCommand(RequestType.GeoSearch, args);
}

/**
* @internal
*/
Expand Down
Loading

0 comments on commit b753216

Please sign in to comment.