Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Node: add GEOSEARCH #2007

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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))
Yury-Fridlyand marked this conversation as resolved.
Show resolved Hide resolved
* 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 @@ -3639,6 +3647,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:
*
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just curious...
Are we going to have extra line for each sub-item?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is needed for line breaks in the list. Withot that, all bullet points are rendered in one messy line without a list formatting.

* - {@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) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we have a different check here? Checking for string seems incorrect here.

Copy link
Collaborator Author

@Yury-Fridlyand Yury-Fridlyand Jul 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It checks for the named property/field in the variable. typeof/instanceof doesn't work there unfortunately. Do you have a better idea how to do a runtime type check?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is the standard way to do it

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
Loading