Skip to content

Commit

Permalink
Node: Add GEOSEARCHSTORE command. (valkey-io#2080)
Browse files Browse the repository at this point in the history
* Add `GEOSEARCHSTORE` command.

Signed-off-by: Yury-Fridlyand <yury.fridlyand@improving.com>

* Signed-off-by: Yury-Fridlyand <yury.fridlyand@improving.com>

---------

Signed-off-by: Yury-Fridlyand <yury.fridlyand@improving.com>
  • Loading branch information
Yury-Fridlyand authored and GumpacG committed Aug 7, 2024
1 parent 97ec890 commit db1a655
Show file tree
Hide file tree
Showing 9 changed files with 445 additions and 49 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
* Node: Added EXPIRETIME and PEXPIRETIME commands ([#2063](https://github.com/valkey-io/valkey-glide/pull/2063))
* Node: Added SORT commands ([#2028](https://github.com/valkey-io/valkey-glide/pull/2028))
* Node: Added LASTSAVE command ([#2059](https://github.com/valkey-io/valkey-glide/pull/2059))
* Node: Added GEOSEARCHSTORE command ([#2080](https://github.com/valkey-io/valkey-glide/pull/2080))
* Node: Added EXPIRETIME and PEXPIRETIME commands ([]())
* Node: Added LCS command ([#2049](https://github.com/valkey-io/valkey-glide/pull/2049))
* Node: Added MSETNX command ([#2046](https://github.com/valkey-io/valkey-glide/pull/2046))
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 @@ -71,6 +72,7 @@ import {
createGeoHash,
createGeoPos,
createGeoSearch,
createGeoSearchStore,
createGet,
createGetBit,
createGetDel,
Expand Down Expand Up @@ -4202,22 +4204,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 @@ -4271,12 +4266,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 @@ -2699,7 +2699,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 @@ -2709,6 +2709,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 @@ -2759,16 +2775,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 @@ -2796,9 +2835,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)
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 @@ -2809,7 +2853,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 @@ -215,6 +215,8 @@ import {
createZRevRankWithScore,
createZScan,
createZScore,
createGeoSearchStore,
GeoSearchStoreResultOptions,
} from "./Commands";
import { command_request } from "./ProtobufMessage";

Expand Down Expand Up @@ -2575,6 +2577,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

0 comments on commit db1a655

Please sign in to comment.