From 834a932e7fb2f8f6ac125102d22da5dd50a4c484 Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Wed, 7 Aug 2024 08:51:33 -0700 Subject: [PATCH] Node: Add `GEOSEARCHSTORE` command. (#2080) * Add `GEOSEARCHSTORE` command. Signed-off-by: Yury-Fridlyand * Signed-off-by: Yury-Fridlyand --------- Signed-off-by: Yury-Fridlyand --- CHANGELOG.md | 1 + .../GeospatialIndicesBaseCommands.java | 16 +- .../geospatial/GeoSearchStoreOptions.java | 9 +- node/src/BaseClient.ts | 92 ++++++- node/src/Commands.ts | 62 ++++- node/src/Transaction.ts | 44 +++ node/tests/GlideClusterClient.test.ts | 8 +- node/tests/SharedTests.ts | 250 ++++++++++++++++-- node/tests/TestUtilities.ts | 12 + 9 files changed, 445 insertions(+), 49 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3403cb5bc9..a149637d19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 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)) * Node: Added BLMOVE command ([#2027](https://github.com/valkey-io/valkey-glide/pull/2027)) diff --git a/java/client/src/main/java/glide/api/commands/GeospatialIndicesBaseCommands.java b/java/client/src/main/java/glide/api/commands/GeospatialIndicesBaseCommands.java index 7abb252747..86ae648faa 100644 --- a/java/client/src/main/java/glide/api/commands/GeospatialIndicesBaseCommands.java +++ b/java/client/src/main/java/glide/api/commands/GeospatialIndicesBaseCommands.java @@ -768,7 +768,7 @@ CompletableFuture geosearch( * axis-aligned rectangle, determined by height and width. * * - * @return The number of elements in the resulting set. + * @return The number of elements in the resulting sorted set stored at destination. * @example *
{@code
      * Long result = client
@@ -812,7 +812,7 @@ CompletableFuture geosearchstore(
      *           axis-aligned rectangle, determined by height and width.
      *     
      *
-     * @return The number of elements in the resulting set.
+     * @return The number of elements in the resulting sorted set stored at destination.
      * @example
      *     
{@code
      * Long result = client
@@ -861,7 +861,7 @@ CompletableFuture 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 destination.
      * @example
      *     
{@code
      * Long result = client
@@ -912,7 +912,7 @@ CompletableFuture 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 destination.
      * @example
      *     
{@code
      * Long result = client
@@ -962,7 +962,7 @@ CompletableFuture geosearchstore(
      *     
      *
      * @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 destination.
      * @example
      *     
{@code
      * Long result = client
@@ -1012,7 +1012,7 @@ CompletableFuture geosearchstore(
      *     
      *
      * @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 destination.
      * @example
      *     
{@code
      * Long result = client
@@ -1064,7 +1064,7 @@ CompletableFuture 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 destination.
      * @example
      *     
{@code
      * Long result = client
@@ -1118,7 +1118,7 @@ CompletableFuture 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 destination.
      * @example
      *     
{@code
      * Long result = client
diff --git a/java/client/src/main/java/glide/api/models/commands/geospatial/GeoSearchStoreOptions.java b/java/client/src/main/java/glide/api/models/commands/geospatial/GeoSearchStoreOptions.java
index 047273aaa3..384567b56d 100644
--- a/java/client/src/main/java/glide/api/models/commands/geospatial/GeoSearchStoreOptions.java
+++ b/java/client/src/main/java/glide/api/models/commands/geospatial/GeoSearchStoreOptions.java
@@ -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 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. + */ private final boolean storeDist; /** diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index ffb1c97aa8..1b3d85be63 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -29,6 +29,7 @@ import { GeoCircleShape, // eslint-disable-line @typescript-eslint/no-unused-vars GeoSearchResultOptions, GeoSearchShape, + GeoSearchStoreResultOptions, GeoUnit, GeospatialData, InsertPosition, @@ -71,6 +72,7 @@ import { createGeoHash, createGeoPos, createGeoSearch, + createGeoSearchStore, createGet, createGetBit, createGetDel, @@ -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`. @@ -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 { + 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`. diff --git a/node/src/Commands.ts b/node/src/Commands.ts index fd0402b299..f807d8234e 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -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; /** @@ -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. */ @@ -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( @@ -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()); @@ -2809,7 +2853,7 @@ export function createGeoSearch( if (resultOptions.sortOrder) args.push(resultOptions.sortOrder); } - return createCommand(RequestType.GeoSearch, args); + return args; } /** diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 2795f85d2e..e2babf630f 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -215,6 +215,8 @@ import { createZRevRankWithScore, createZScan, createZScore, + createGeoSearchStore, + GeoSearchStoreResultOptions, } from "./Commands"; import { command_request } from "./ProtobufMessage"; @@ -2575,6 +2577,48 @@ export class BaseTransaction> { ); } + /** + * 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`. diff --git a/node/tests/GlideClusterClient.test.ts b/node/tests/GlideClusterClient.test.ts index 930df79c62..1edb82e84b 100644 --- a/node/tests/GlideClusterClient.test.ts +++ b/node/tests/GlideClusterClient.test.ts @@ -25,7 +25,7 @@ import { ScoreFilter, } from ".."; import { RedisCluster } from "../../utils/TestUtils.js"; -import { FlushMode, SortOrder } from "../build-ts/src/Commands"; +import { FlushMode, GeoUnit, SortOrder } from "../build-ts/src/Commands"; import { runBaseTests } from "./SharedTests"; import { checkClusterResponse, @@ -354,6 +354,12 @@ describe("GlideClusterClient", () => { client.zdiffWithScores(["abc", "zxy", "lkn"]), client.zdiffstore("abc", ["zxy", "lkn"]), client.copy("abc", "zxy", true), + client.geosearchstore( + "abc", + "zxy", + { member: "_" }, + { radius: 5, unit: GeoUnit.METERS }, + ), ); } diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index b045b866c4..1ef69cc727 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -5603,12 +5603,14 @@ export function runBaseTests(config: { ); it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( - `geosearch test_%p`, + `geosearch geosearchstore test_%p`, async (protocol) => { await runTest(async (client: BaseClient, cluster) => { if (cluster.checkIfServerVersionLessThan("6.2.0")) return; - const key = uuidv4(); + const key1 = "{geosearch}" + uuidv4(); + const key2 = "{geosearch}" + uuidv4(); + const key3 = "{geosearch}" + uuidv4(); const members: string[] = [ "Catania", @@ -5672,30 +5674,55 @@ export function runBaseTests(config: { ]; // geoadd - expect(await client.geoadd(key, membersToCoordinates)).toBe( + expect(await client.geoadd(key1, membersToCoordinates)).toBe( members.length, ); let searchResult = await client.geosearch( - key, + key1, { position: { longitude: 15, latitude: 37 } }, { width: 400, height: 400, unit: GeoUnit.KILOMETERS }, ); // using set to compare, because results are reordrered expect(new Set(searchResult)).toEqual(membersSet); + // same with geosearchstore + expect( + await client.geosearchstore( + key2, + key1, + { position: { longitude: 15, latitude: 37 } }, + { width: 400, height: 400, unit: GeoUnit.KILOMETERS }, + ), + ).toEqual(4); + expect( + await client.zrange(key2, { start: 0, stop: -1 }), + ).toEqual(searchResult); // order search result searchResult = await client.geosearch( - key, + key1, { position: { longitude: 15, latitude: 37 } }, { width: 400, height: 400, unit: GeoUnit.KILOMETERS }, { sortOrder: SortOrder.ASC }, ); expect(searchResult).toEqual(members); + // same with geosearchstore + expect( + await client.geosearchstore( + key2, + key1, + { position: { longitude: 15, latitude: 37 } }, + { width: 400, height: 400, unit: GeoUnit.KILOMETERS }, + { sortOrder: SortOrder.ASC, storeDist: true }, + ), + ).toEqual(4); + expect( + await client.zrange(key2, { start: 0, stop: -1 }), + ).toEqual(searchResult); // order and query all extra data searchResult = await client.geosearch( - key, + key1, { position: { longitude: 15, latitude: 37 } }, { width: 400, height: 400, unit: GeoUnit.KILOMETERS }, { @@ -5709,7 +5736,7 @@ export function runBaseTests(config: { // order, query and limit by 1 searchResult = await client.geosearch( - key, + key1, { position: { longitude: 15, latitude: 37 } }, { width: 400, height: 400, unit: GeoUnit.KILOMETERS }, { @@ -5721,11 +5748,28 @@ export function runBaseTests(config: { }, ); expect(searchResult).toEqual(expectedResult.slice(0, 1)); + // same with geosearchstore + expect( + await client.geosearchstore( + key2, + key1, + { position: { longitude: 15, latitude: 37 } }, + { width: 400, height: 400, unit: GeoUnit.KILOMETERS }, + { + sortOrder: SortOrder.ASC, + count: 1, + storeDist: true, + }, + ), + ).toEqual(1); + expect( + await client.zrange(key2, { start: 0, stop: -1 }), + ).toEqual([members[0]]); // test search by box, unit: meters, from member, with distance const meters = 400 * 1000; searchResult = await client.geosearch( - key, + key1, { member: "Catania" }, { width: meters, height: meters, unit: GeoUnit.METERS }, { @@ -5739,11 +5783,33 @@ export function runBaseTests(config: { ["Palermo", [166274.1516]], ["Catania", [0.0]], ]); + // same with geosearchstore + expect( + await client.geosearchstore( + key2, + key1, + { member: "Catania" }, + { width: meters, height: meters, unit: GeoUnit.METERS }, + { sortOrder: SortOrder.DESC, storeDist: true }, + ), + ).toEqual(3); + // TODO deep close to https://github.com/maasencioh/jest-matcher-deep-close-to + expect( + await client.zrangeWithScores( + key2, + { start: 0, stop: -1 }, + true, + ), + ).toEqual({ + edge2: 236529.17986494553, + Palermo: 166274.15156960033, + Catania: 0.0, + }); // test search by box, unit: feet, from member, with limited count 2, with hash const feet = 400 * 3280.8399; searchResult = await client.geosearch( - key, + key1, { member: "Palermo" }, { width: feet, height: feet, unit: GeoUnit.FEET }, { @@ -5758,39 +5824,97 @@ export function runBaseTests(config: { ["Palermo", [3479099956230698]], ["edge1", [3479273021651468]], ]); + // same with geosearchstore + expect( + await client.geosearchstore( + key2, + key1, + { member: "Palermo" }, + { width: feet, height: feet, unit: GeoUnit.FEET }, + { + sortOrder: SortOrder.ASC, + count: 2, + }, + ), + ).toEqual(2); + expect( + await client.zrangeWithScores(key2, { start: 0, stop: -1 }), + ).toEqual({ + Palermo: 3479099956230698, + edge1: 3479273021651468, + }); // test search by box, unit: miles, from geospatial position, with limited ANY count to 1 const miles = 250; searchResult = await client.geosearch( - key, + key1, { position: { longitude: 15, latitude: 37 } }, { width: miles, height: miles, unit: GeoUnit.MILES }, { count: 1, isAny: true }, ); expect(members).toContainEqual(searchResult[0]); + // same with geosearchstore + expect( + await client.geosearchstore( + key2, + key1, + { position: { longitude: 15, latitude: 37 } }, + { width: miles, height: miles, unit: GeoUnit.MILES }, + { count: 1, isAny: true }, + ), + ).toEqual(1); + expect( + await client.zrange(key2, { start: 0, stop: -1 }), + ).toEqual(searchResult); // test search by radius, units: feet, from member const feetRadius = 200 * 3280.8399; searchResult = await client.geosearch( - key, + key1, { member: "Catania" }, { radius: feetRadius, unit: GeoUnit.FEET }, { sortOrder: SortOrder.ASC }, ); expect(searchResult).toEqual(["Catania", "Palermo"]); + // same with geosearchstore + expect( + await client.geosearchstore( + key2, + key1, + { member: "Catania" }, + { radius: feetRadius, unit: GeoUnit.FEET }, + { sortOrder: SortOrder.ASC, storeDist: true }, + ), + ).toEqual(2); + expect( + await client.zrange(key2, { start: 0, stop: -1 }), + ).toEqual(searchResult); // Test search by radius, unit: meters, from member const metersRadius = 200 * 1000; searchResult = await client.geosearch( - key, + key1, { member: "Catania" }, { radius: metersRadius, unit: GeoUnit.METERS }, { sortOrder: SortOrder.DESC }, ); expect(searchResult).toEqual(["Palermo", "Catania"]); + // same with geosearchstore + expect( + await client.geosearchstore( + key2, + key1, + { member: "Catania" }, + { radius: metersRadius, unit: GeoUnit.METERS }, + { sortOrder: SortOrder.DESC, storeDist: true }, + ), + ).toEqual(2); + expect( + await client.zrange(key2, { start: 0, stop: -1 }, true), + ).toEqual(searchResult); searchResult = await client.geosearch( - key, + key1, { member: "Catania" }, { radius: metersRadius, unit: GeoUnit.METERS }, { @@ -5805,7 +5929,7 @@ export function runBaseTests(config: { // Test search by radius, unit: miles, from geospatial data searchResult = await client.geosearch( - key, + key1, { position: { longitude: 15, latitude: 37 } }, { radius: 175, unit: GeoUnit.MILES }, { sortOrder: SortOrder.DESC }, @@ -5816,10 +5940,23 @@ export function runBaseTests(config: { "Palermo", "Catania", ]); + // same with geosearchstore + expect( + await client.geosearchstore( + key2, + key1, + { position: { longitude: 15, latitude: 37 } }, + { radius: 175, unit: GeoUnit.MILES }, + { sortOrder: SortOrder.DESC, storeDist: true }, + ), + ).toEqual(4); + expect( + await client.zrange(key2, { start: 0, stop: -1 }, true), + ).toEqual(searchResult); // Test search by radius, unit: kilometers, from a geospatial data, with limited count to 2 searchResult = await client.geosearch( - key, + key1, { position: { longitude: 15, latitude: 37 } }, { radius: 200, unit: GeoUnit.KILOMETERS }, { @@ -5831,10 +5968,27 @@ export function runBaseTests(config: { }, ); expect(searchResult).toEqual(expectedResult.slice(0, 2)); + // same with geosearchstore + expect( + await client.geosearchstore( + key2, + key1, + { position: { longitude: 15, latitude: 37 } }, + { radius: 200, unit: GeoUnit.KILOMETERS }, + { + sortOrder: SortOrder.ASC, + count: 2, + storeDist: true, + }, + ), + ).toEqual(2); + expect( + await client.zrange(key2, { start: 0, stop: -1 }), + ).toEqual(members.slice(0, 2)); // Test search by radius, unit: kilometers, from a geospatial data, with limited ANY count to 1 searchResult = await client.geosearch( - key, + key1, { position: { longitude: 15, latitude: 37 } }, { radius: 200, unit: GeoUnit.KILOMETERS }, { @@ -5847,40 +6001,94 @@ export function runBaseTests(config: { }, ); expect(members).toContainEqual(searchResult[0][0]); + // same with geosearchstore + expect( + await client.geosearchstore( + key2, + key1, + { position: { longitude: 15, latitude: 37 } }, + { radius: 200, unit: GeoUnit.KILOMETERS }, + { + sortOrder: SortOrder.ASC, + count: 1, + isAny: true, + }, + ), + ).toEqual(1); + expect( + await client.zrange(key2, { start: 0, stop: -1 }), + ).toEqual([searchResult[0][0]]); // no members within the area searchResult = await client.geosearch( - key, + key1, { position: { longitude: 15, latitude: 37 } }, { width: 50, height: 50, unit: GeoUnit.METERS }, { sortOrder: SortOrder.ASC }, ); expect(searchResult).toEqual([]); + // same with geosearchstore + expect( + await client.geosearchstore( + key2, + key1, + { position: { longitude: 15, latitude: 37 } }, + { width: 50, height: 50, unit: GeoUnit.METERS }, + { sortOrder: SortOrder.ASC }, + ), + ).toEqual(0); + expect(await client.zcard(key2)).toEqual(0); // no members within the area searchResult = await client.geosearch( - key, + key1, { position: { longitude: 15, latitude: 37 } }, { radius: 5, unit: GeoUnit.METERS }, { sortOrder: SortOrder.ASC }, ); expect(searchResult).toEqual([]); + // same with geosearchstore + expect( + await client.geosearchstore( + key2, + key1, + { position: { longitude: 15, latitude: 37 } }, + { radius: 5, unit: GeoUnit.METERS }, + { sortOrder: SortOrder.ASC }, + ), + ).toEqual(0); + expect(await client.zcard(key2)).toEqual(0); // member does not exist await expect( client.geosearch( - key, + key1, + { member: "non-existing-member" }, + { radius: 100, unit: GeoUnit.METERS }, + ), + ).rejects.toThrow(RequestError); + await expect( + client.geosearchstore( + key2, + key1, { member: "non-existing-member" }, { radius: 100, unit: GeoUnit.METERS }, ), ).rejects.toThrow(RequestError); // key exists but holds a non-ZSET value - const key2 = uuidv4(); - expect(await client.set(key2, uuidv4())).toEqual("OK"); + expect(await client.set(key3, uuidv4())).toEqual("OK"); await expect( client.geosearch( + key3, + { position: { longitude: 15, latitude: 37 } }, + { radius: 100, unit: GeoUnit.METERS }, + ), + ).rejects.toThrow(RequestError); + await expect( + client.geosearchstore( key2, + key3, { position: { longitude: 15, latitude: 37 } }, { radius: 100, unit: GeoUnit.METERS }, ), diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index c1983f38cf..f8def33988 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -472,6 +472,7 @@ export async function transactionTest( const key22 = "{key}" + uuidv4(); // list for sort const key23 = "{key}" + uuidv4(); // zset random const key24 = "{key}" + uuidv4(); // list value + const key25 = "{key}" + uuidv4(); // Geospatial Data/ZSET const field = uuidv4(); const value = uuidv4(); const groupName1 = uuidv4(); @@ -1104,6 +1105,17 @@ export async function transactionTest( ], ], ]); + + baseTransaction.geosearchstore( + key25, + key18, + { position: { longitude: 15, latitude: 37 } }, + { width: 400, height: 400, unit: GeoUnit.KILOMETERS }, + ); + responseData.push([ + "geosearchstore(key25, key18, (15, 37), 400x400 KM)", + 2, + ]); } const libName = "mylib1C" + uuidv4().replaceAll("-", "");