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 GEOSEARCHSTORE command. #2080

Merged
Merged
Show file tree
Hide file tree
Changes from 2 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,6 +1,7 @@
#### Changes
* Node: Added SORT commands ([#2028](https:/valkey-io/valkey-glide/pull/2028))
* Node: Added LASTSAVE command ([#2059](https:/valkey-io/valkey-glide/pull/2059))
* Node: Added GEOSEARCHSTORE command ([#2080](https:/valkey-io/valkey-glide/pull/2080))
* Node: Added LCS command ([#2049](https:/valkey-io/valkey-glide/pull/2049))
* Node: Added MSETNX command ([#2046](https:/valkey-io/valkey-glide/pull/2046))
* Node: Added BLMOVE command ([#2027](https:/valkey-io/valkey-glide/pull/2027))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -768,7 +768,7 @@ CompletableFuture<Object[]> geosearch(
* axis-aligned rectangle, determined by height and width.
* </ul>
*
* @return The number of elements in the resulting set.
* @return The number of elements in the resulting sorted set stored at <code>destination</code>.
* @example
* <pre>{@code
* Long result = client
Expand Down Expand Up @@ -812,7 +812,7 @@ CompletableFuture<Long> geosearchstore(
* axis-aligned rectangle, determined by height and width.
* </ul>
*
* @return The number of elements in the resulting set.
* @return The number of elements in the resulting sorted set stored at <code>destination</code>.
* @example
* <pre>{@code
* Long result = client
Expand Down Expand Up @@ -861,7 +861,7 @@ CompletableFuture<Long> geosearchstore(
*
* @param resultOptions Optional inputs for sorting/limiting the results. See - {@link
* GeoSearchResultOptions}
* @return The number of elements in the resulting set.
* @return The number of elements in the resulting sorted set stored at <code>destination</code>.
* @example
* <pre>{@code
* Long result = client
Expand Down Expand Up @@ -912,7 +912,7 @@ CompletableFuture<Long> geosearchstore(
*
* @param resultOptions Optional inputs for sorting/limiting the results. See - {@link
* GeoSearchResultOptions}
* @return The number of elements in the resulting set.
* @return The number of elements in the resulting sorted set stored at <code>destination</code>.
* @example
* <pre>{@code
* Long result = client
Expand Down Expand Up @@ -962,7 +962,7 @@ CompletableFuture<Long> geosearchstore(
* </ul>
*
* @param options The optional inputs to request additional information.
* @return The number of elements in the resulting set.
* @return The number of elements in the resulting sorted set stored at <code>destination</code>.
* @example
* <pre>{@code
* Long result = client
Expand Down Expand Up @@ -1012,7 +1012,7 @@ CompletableFuture<Long> geosearchstore(
* </ul>
*
* @param options The optional inputs to request additional information.
* @return The number of elements in the resulting set.
* @return The number of elements in the resulting sorted set stored at <code>destination</code>.
* @example
* <pre>{@code
* Long result = client
Expand Down Expand Up @@ -1064,7 +1064,7 @@ CompletableFuture<Long> geosearchstore(
* @param options The optional inputs to request additional information.
* @param resultOptions Optional inputs for sorting/limiting the results. See - {@link
* GeoSearchResultOptions}
* @return The number of elements in the resulting set.
* @return The number of elements in the resulting sorted set stored at <code>destination</code>.
* @example
* <pre>{@code
* Long result = client
Expand Down Expand Up @@ -1118,7 +1118,7 @@ CompletableFuture<Long> geosearchstore(
* @param options The optional inputs to request additional information.
* @param resultOptions Optional inputs for sorting/limiting the results. See - {@link
* GeoSearchResultOptions}
* @return The number of elements in the resulting set.
* @return The number of elements in the resulting sorted set stored at <code>destination</code>.
* @example
* <pre>{@code
* Long result = client
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,14 @@ public final class GeoSearchStoreOptions {
/** Valkey API keyword for {@link #storeDist} parameter. */
public static final String GEOSEARCHSTORE_VALKEY_API = "STOREDIST";

/** Configure sorting the results with their distance from the center. */
/**
* Determines what is stored as the sorted set score. Defaults to <code>false</code>.<br>
* If set to <code>false</code>, the geohash of the location will be stored as the sorted set
* score.<br>
* If set to <code>true</code>, the distance from the center of the shape (circle or box) will be
* stored as the sorted set score. The distance is represented as a floating-point number in the
* same unit specified for that shape.
*/
private final boolean storeDist;

/**
Expand Down
92 changes: 83 additions & 9 deletions node/src/BaseClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
GeoCircleShape, // eslint-disable-line @typescript-eslint/no-unused-vars
GeoSearchResultOptions,
GeoSearchShape,
GeoSearchStoreResultOptions,
GeoUnit,
GeospatialData,
InsertPosition,
Expand Down Expand Up @@ -69,6 +70,7 @@ import {
createGeoHash,
createGeoPos,
createGeoSearch,
createGeoSearchStore,
createGet,
createGetBit,
createGetDel,
Expand Down Expand Up @@ -4050,22 +4052,15 @@ export class BaseClient {
*
* @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}.
* @param resultOptions - (Optional) Parameters 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`.
Expand Down Expand Up @@ -4119,12 +4114,91 @@ export class BaseClient {
searchFrom: SearchOrigin,
searchBy: GeoSearchShape,
resultOptions?: GeoSearchResultOptions,
): Promise<(Buffer | (number | number[])[])[]> {
): Promise<(string | (number | number[])[])[]> {
return this.createWritePromise(
createGeoSearch(key, searchFrom, searchBy, resultOptions),
);
}

/**
* Searches for members in a sorted set stored at `source` representing geospatial data
* within a circular or rectangular area and stores the result in `destination`.
*
* If `destination` already exists, it is overwritten. Otherwise, a new sorted set will be created.
*
* To get the result directly, see {@link geosearch}.
*
* See https://valkey.io/commands/geosearchstore/ for more details.
*
* since - Valkey 6.2.0 and above.
*
* @remarks When in cluster mode, `destination` and `source` must map to the same hash slot.
*
* @param destination - The key of the destination sorted set.
* @param source - 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 - (Optional) Parameters to request additional information and configure sorting/limiting the results, see {@link GeoSearchStoreResultOptions}.
* @returns The number of elements in the resulting sorted set stored at `destination`.
*
* @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' and store in `destination`:
* await client.geosearchstore("destination", "mySortedSet", { member: "Palermo" }, { radius: 200, unit: GeoUnit.KILOMETERS });
* // query the stored results
* const result1 = await client.zrangeWithScores("destination", { start: 0, stop: -1 });
* console.log(result1); // Output:
* // {
* // Palermo: 3479099956230698, // geohash of the location is stored as element's score
* // Catania: 3479447370796909
* // }
*
* // search for locations in 200x300 mi rectangle centered at coordinate (15, 37), requesting to store distance instead of geohashes,
* // limiting results by 2 best matches, ordered by ascending distance from the search area center
* await client.geosearchstore(
* "destination",
* "mySortedSet",
* { position: { longitude: 15, latitude: 37 } },
* { width: 200, height: 300, unit: GeoUnit.MILES },
* {
* sortOrder: SortOrder.ASC,
* count: 2,
* storeDist: true,
* },
* );
* // query the stored results
* const result2 = await client.zrangeWithScores("destination", { start: 0, stop: -1 });
* console.log(result2); // Output:
* // {
* // Palermo: 190.4424, // distance from the search area center is stored as element's score
* // Catania: 56.4413, // the distance is measured in units used for the search query (miles)
* // }
* ```
*/
public async geosearchstore(
destination: string,
source: string,
searchFrom: SearchOrigin,
searchBy: GeoSearchShape,
resultOptions?: GeoSearchStoreResultOptions,
): Promise<number> {
return this.createWritePromise(
createGeoSearchStore(
destination,
source,
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
62 changes: 53 additions & 9 deletions node/src/Commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2615,7 +2615,7 @@ export function createGeoHash(
* 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 = {
export type GeoSearchResultOptions = GeoSearchCommonResultOptions & {
/** Include the coordinate of the returned items. */
withCoord?: boolean;
/**
Expand All @@ -2625,6 +2625,22 @@ export type GeoSearchResultOptions = {
withDist?: boolean;
/** Include the geohash of the returned items. */
withHash?: boolean;
};

/**
* Optional parameters for {@link BaseClient.geosearchstore|geosearchstore} command which defines what should be included in the
* search results and how results should be ordered and limited.
*/
export type GeoSearchStoreResultOptions = GeoSearchCommonResultOptions & {
/**
* Determines what is stored as the sorted set score. Defaults to `false`.
* - If set to `false`, the geohash of the location will be stored as the sorted set score.
* - If set to `true`, the distance from the center of the shape (circle or box) will be stored as the sorted set score. The distance is represented as a floating-point number in the same unit specified for that shape.
*/
storeDist?: boolean;
};

type GeoSearchCommonResultOptions = {
/** Indicates the order the result should be sorted in. */
sortOrder?: SortOrder;
/** Indicates the number of matches the result should be limited to. */
Expand Down Expand Up @@ -2675,16 +2691,39 @@ export type MemberOrigin = {
member: string;
};

/**
* @internal
*/
/** @internal */
export function createGeoSearch(
key: string,
searchFrom: SearchOrigin,
searchBy: GeoSearchShape,
resultOptions?: GeoSearchResultOptions,
): command_request.Command {
let args: string[] = [key];
const args = [key].concat(
convertGeoSearchOptionsToArgs(searchFrom, searchBy, resultOptions),
);
return createCommand(RequestType.GeoSearch, args);
}

/** @internal */
export function createGeoSearchStore(
destination: string,
source: string,
searchFrom: SearchOrigin,
searchBy: GeoSearchShape,
resultOptions?: GeoSearchStoreResultOptions,
): command_request.Command {
const args = [destination, source].concat(
convertGeoSearchOptionsToArgs(searchFrom, searchBy, resultOptions),
);
return createCommand(RequestType.GeoSearchStore, args);
}

function convertGeoSearchOptionsToArgs(
searchFrom: SearchOrigin,
searchBy: GeoSearchShape,
resultOptions?: GeoSearchCommonResultOptions,
): string[] {
let args: string[] = [];

if ("position" in searchFrom) {
args = args.concat(
Expand Down Expand Up @@ -2712,9 +2751,14 @@ export function createGeoSearch(
}

if (resultOptions) {
if (resultOptions.withCoord) args.push("WITHCOORD");
if (resultOptions.withDist) args.push("WITHDIST");
if (resultOptions.withHash) args.push("WITHHASH");
if ("withCoord" in resultOptions && resultOptions.withCoord)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is the first check needed? Would it be false even if it doesn't exist?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

First check is needed to confirm that I can access to with*. I have to update this code, because there are 2 derived types with different sets of fields.

args.push("WITHCOORD");
if ("withDist" in resultOptions && resultOptions.withDist)
args.push("WITHDIST");
if ("withHash" in resultOptions && resultOptions.withHash)
args.push("WITHHASH");
if ("storeDist" in resultOptions && resultOptions.storeDist)
args.push("STOREDIST");

if (resultOptions.count) {
args.push("COUNT", resultOptions.count?.toString());
Expand All @@ -2725,7 +2769,7 @@ export function createGeoSearch(
if (resultOptions.sortOrder) args.push(resultOptions.sortOrder);
}

return createCommand(RequestType.GeoSearch, args);
return args;
}

/**
Expand Down
44 changes: 44 additions & 0 deletions node/src/Transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,8 @@ import {
createZRevRankWithScore,
createZScan,
createZScore,
createGeoSearchStore,
GeoSearchStoreResultOptions,
} from "./Commands";
import { command_request } from "./ProtobufMessage";

Expand Down Expand Up @@ -2476,6 +2478,48 @@ export class BaseTransaction<T extends BaseTransaction<T>> {
);
}

/**
* Searches for members in a sorted set stored at `source` representing geospatial data
* within a circular or rectangular area and stores the result in `destination`.
*
* If `destination` already exists, it is overwritten. Otherwise, a new sorted set will be created.
*
* To get the result directly, see {@link geosearch}.
*
* See https://valkey.io/commands/geosearchstore/ for more details.
*
* since - Valkey 6.2.0 and above.
*
* @param destination - The key of the destination sorted set.
* @param source - 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 - (Optional) Parameters to request additional information and configure sorting/limiting the results, see {@link GeoSearchStoreResultOptions}.
*
* Command Response - The number of elements in the resulting sorted set stored at `destination`.
*/
public geosearchstore(
destination: string,
source: string,
searchFrom: SearchOrigin,
searchBy: GeoSearchShape,
resultOptions?: GeoSearchStoreResultOptions,
): T {
return this.addAndReturn(
createGeoSearchStore(
destination,
source,
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
Loading
Loading