diff --git a/CHANGELOG.md b/CHANGELOG.md index 6791a5bccd..a1510be7f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ * Java: Added `FT.PROFILE` ([#2473](https://github.com/valkey-io/valkey-glide/pull/2473)) * Java: Added `JSON.SET` and `JSON.GET` ([#2462](https://github.com/valkey-io/valkey-glide/pull/2462)) * Node: Added `FT.CREATE` ([#2501](https://github.com/valkey-io/valkey-glide/pull/2501)) +* Node: Added `FT.INFO` ([#2540](https://github.com/valkey-io/valkey-glide/pull/2540)) * Java: Added `JSON.DEBUG` ([#2520](https://github.com/valkey-io/valkey-glide/pull/2520)) * Java: Added `JSON.ARRINSERT` and `JSON.ARRLEN` ([#2476](https://github.com/valkey-io/valkey-glide/pull/2476)) * Java: Added `JSON.ARRPOP` ([#2486](https://github.com/valkey-io/valkey-glide/pull/2486)) diff --git a/java/client/src/main/java/glide/api/commands/servermodules/FT.java b/java/client/src/main/java/glide/api/commands/servermodules/FT.java index 9d1a75e9ea..ff0d80f0c7 100644 --- a/java/client/src/main/java/glide/api/commands/servermodules/FT.java +++ b/java/client/src/main/java/glide/api/commands/servermodules/FT.java @@ -540,7 +540,7 @@ public static CompletableFuture profile( * Map response = client.ftinfo("myIndex").get(); * // the response contains data in the following format: * Map data = Map.of( - * "index_name", gs("bcd97d68-4180-4bc5-98fe-5125d0abbcb8"), + * "index_name", gs("myIndex"), * "index_status", gs("AVAILABLE"), * "key_type", gs("JSON"), * "creation_timestamp", 1728348101728771L, @@ -566,7 +566,7 @@ public static CompletableFuture profile( * gs("dimension", 6L, * gs("block_size", 1024L, * gs("algorithm", gs("FLAT") - * ) + * ) * ), * Map.of( * gs("identifier"), gs("name"), @@ -599,7 +599,7 @@ public static CompletableFuture> info( * Map response = client.ftinfo(gs("myIndex")).get(); * // the response contains data in the following format: * Map data = Map.of( - * "index_name", gs("bcd97d68-4180-4bc5-98fe-5125d0abbcb8"), + * "index_name", gs("myIndex"), * "index_status", gs("AVAILABLE"), * "key_type", gs("JSON"), * "creation_timestamp", 1728348101728771L, @@ -625,7 +625,7 @@ public static CompletableFuture> info( * gs("dimension", 6L, * gs("block_size", 1024L, * gs("algorithm", gs("FLAT") - * ) + * ) * ), * Map.of( * gs("identifier"), gs("name"), diff --git a/java/integTest/src/test/java/glide/modules/VectorSearchTests.java b/java/integTest/src/test/java/glide/modules/VectorSearchTests.java index 75151f103b..9f02df3680 100644 --- a/java/integTest/src/test/java/glide/modules/VectorSearchTests.java +++ b/java/integTest/src/test/java/glide/modules/VectorSearchTests.java @@ -723,14 +723,6 @@ public void ft_aggregate() { @Test @SneakyThrows public void ft_info() { - // TODO use FT.LIST when it is done - var indices = (Object[]) client.customCommand(new String[] {"FT._LIST"}).get().getSingleValue(); - - // check that we can get a response for all indices (no crashes on value conversion or so) - for (var idx : indices) { - FT.info(client, (String) idx).get(); - } - var index = UUID.randomUUID().toString(); assertEquals( OK, diff --git a/node/npm/glide/index.ts b/node/npm/glide/index.ts index 7539524e32..4c370588df 100644 --- a/node/npm/glide/index.ts +++ b/node/npm/glide/index.ts @@ -126,6 +126,7 @@ function initialize() { VectorFieldAttributesFlat, VectorFieldAttributesHnsw, FtCreateOptions, + FtInfoReturnType, GlideRecord, GlideString, JsonGetOptions, @@ -244,6 +245,7 @@ function initialize() { VectorFieldAttributesFlat, VectorFieldAttributesHnsw, FtCreateOptions, + FtInfoReturnType, GlideRecord, GlideJson, GlideString, diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index a756a4a877..d0e0e1494e 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -5513,7 +5513,6 @@ export class BaseClient { * attributes of a consumer group for the stream at `key`. * @example * ```typescript - *
{@code
      * const result = await client.xinfoGroups("my_stream");
      * console.log(result); // Output:
      * // [
@@ -5967,13 +5966,11 @@ export class BaseClient {
      *
      * @example
      * ```typescript
-     *  
{@code
      * const entryId = await client.xadd("mystream", ["myfield", "mydata"]);
      * // read messages from streamId
      * const readResult = await client.xreadgroup(["myfield", "mydata"], "mygroup", "my0consumer");
      * // acknowledge messages on stream
      * console.log(await client.xack("mystream", "mygroup", [entryId])); // Output: 1
-     * 
* ``` */ public async xack( diff --git a/node/src/server-modules/GlideFt.ts b/node/src/server-modules/GlideFt.ts index e78f961c8c..0d58cbfeb0 100644 --- a/node/src/server-modules/GlideFt.ts +++ b/node/src/server-modules/GlideFt.ts @@ -2,12 +2,28 @@ * Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ -import { Decoder, DecoderOption, GlideString } from "../BaseClient"; +import { + convertGlideRecordToRecord, + Decoder, + DecoderOption, + GlideRecord, + GlideReturnType, + GlideString, +} from "../BaseClient"; import { GlideClient } from "../GlideClient"; import { GlideClusterClient } from "../GlideClusterClient"; import { Field, FtCreateOptions } from "./GlideFtOptions"; -/** Module for Vector Search commands */ +/** Data type of {@link GlideFt.info | info} command response. */ +type FtInfoReturnType = Record< + string, + | GlideString + | number + | GlideString[] + | Record[]> +>; + +/** Module for Vector Search commands. */ export class GlideFt { /** * Creates an index and initiates a backfill of that index. @@ -187,16 +203,82 @@ export class GlideFt { decoder: Decoder.String, }) as Promise<"OK">; } + + /** + * Returns information about a given index. + * + * @param client - The client to execute the command. + * @param indexName - The index name. + * @param options - (Optional) See {@link DecoderOption}. + * + * @returns Nested maps with info about the index. See example for more details. + * + * @example + * ```typescript + * const info = await GlideFt.info(client, "myIndex"); + * console.log(info); // Output: + * // { + * // index_name: 'myIndex', + * // index_status: 'AVAILABLE', + * // key_type: 'JSON', + * // creation_timestamp: 1728348101728771, + * // key_prefixes: [ 'json:' ], + * // num_indexed_vectors: 0, + * // space_usage: 653471, + * // num_docs: 0, + * // vector_space_usage: 653471, + * // index_degradation_percentage: 0, + * // fulltext_space_usage: 0, + * // current_lag: 0, + * // fields: [ + * // { + * // identifier: '$.vec', + * // type: 'VECTOR', + * // field_name: 'VEC', + * // option: '', + * // vector_params: { + * // data_type: 'FLOAT32', + * // initial_capacity: 1000, + * // current_capacity: 1000, + * // distance_metric: 'L2', + * // dimension: 6, + * // block_size: 1024, + * // algorithm: 'FLAT' + * // } + * // }, + * // { + * // identifier: 'name', + * // type: 'TEXT', + * // field_name: 'name', + * // option: '' + * // }, + * // ] + * // } + * ``` + */ + static async info( + client: GlideClient | GlideClusterClient, + indexName: GlideString, + options?: DecoderOption, + ): Promise { + const args: GlideString[] = ["FT.INFO", indexName]; + + return ( + _handleCustomCommand(client, args, options) as Promise< + GlideRecord + > + ).then(convertGlideRecordToRecord); + } } /** * @internal */ -function _handleCustomCommand( +async function _handleCustomCommand( client: GlideClient | GlideClusterClient, args: GlideString[], - decoderOption: DecoderOption, -) { + decoderOption?: DecoderOption, +): Promise { return client instanceof GlideClient ? (client as GlideClient).customCommand(args, decoderOption) : (client as GlideClusterClient).customCommand(args, decoderOption); diff --git a/node/tests/ServerModules.test.ts b/node/tests/ServerModules.test.ts index 4ec5757fd7..1016e2378c 100644 --- a/node/tests/ServerModules.test.ts +++ b/node/tests/ServerModules.test.ts @@ -12,6 +12,7 @@ import { import { v4 as uuidv4 } from "uuid"; import { ConditionalChange, + Decoder, GlideClusterClient, GlideFt, GlideJson, @@ -48,16 +49,16 @@ describe("Server Module Tests", () => { await cluster.close(); }, TIMEOUT); - describe("GlideJson", () => { - let client: GlideClusterClient; + describe.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + "GlideJson", + (protocol) => { + let client: GlideClusterClient; - afterEach(async () => { - await flushAndCloseClient(true, cluster.getAddresses(), client); - }); + afterEach(async () => { + await flushAndCloseClient(true, cluster.getAddresses(), client); + }); - it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( - "check modules loaded", - async (protocol) => { + it("check modules loaded", async () => { client = await GlideClusterClient.createClient( getClientConfigurationOption( cluster.getAddresses(), @@ -70,12 +71,9 @@ describe("Server Module Tests", () => { }); expect(info).toContain("# json_core_metrics"); expect(info).toContain("# search_index_stats"); - }, - ); + }); - it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( - "json.set and json.get tests", - async (protocol) => { + it("json.set and json.get tests", async () => { client = await GlideClusterClient.createClient( getClientConfigurationOption( cluster.getAddresses(), @@ -118,12 +116,9 @@ describe("Server Module Tests", () => { // JSON.get with non-existing path result = await GlideJson.get(client, key, { paths: ["$.d"] }); expect(result).toEqual("[]"); - }, - ); + }); - it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( - "json.set and json.get tests with multiple value", - async (protocol) => { + it("json.set and json.get tests with multiple value", async () => { client = await GlideClusterClient.createClient( getClientConfigurationOption( cluster.getAddresses(), @@ -164,12 +159,9 @@ describe("Server Module Tests", () => { "new_value", "new_value", ]); - }, - ); + }); - it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( - "json.set conditional set", - async (protocol) => { + it("json.set conditional set", async () => { client = await GlideClusterClient.createClient( getClientConfigurationOption( cluster.getAddresses(), @@ -210,12 +202,9 @@ describe("Server Module Tests", () => { ).toBe("OK"); result = await GlideJson.get(client, key, { paths: [".a"] }); expect(result).toEqual("4.5"); - }, - ); + }); - it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( - "json.get formatting", - async (protocol) => { + it("json.get formatting", async () => { client = await GlideClusterClient.createClient( getClientConfigurationOption( cluster.getAddresses(), @@ -254,12 +243,9 @@ describe("Server Module Tests", () => { const expectedResult2 = '[\n~{\n~~"a":*1,\n~~"b":*2,\n~~"c":*{\n~~~"d":*3,\n~~~"e":*4\n~~}\n~}\n]'; expect(result).toEqual(expectedResult2); - }, - ); + }); - it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( - "json.toggle tests", - async (protocol) => { + it("json.toggle tests", async () => { client = await GlideClusterClient.createClient( getClientConfigurationOption( cluster.getAddresses(), @@ -312,12 +298,9 @@ describe("Server Module Tests", () => { await expect( GlideJson.toggle(client, "non_existing_key", { path: "$" }), ).rejects.toThrow(RequestError); - }, - ); + }); - it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( - "json.del tests", - async (protocol) => { + it("json.del tests", async () => { client = await GlideClusterClient.createClient( getClientConfigurationOption( cluster.getAddresses(), @@ -414,12 +397,9 @@ describe("Server Module Tests", () => { path: ".", }), ).toBe(0); - }, - ); + }); - it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( - "json.forget tests", - async (protocol) => { + it("json.forget tests", async () => { client = await GlideClusterClient.createClient( getClientConfigurationOption( cluster.getAddresses(), @@ -520,12 +500,9 @@ describe("Server Module Tests", () => { path: ".", }), ).toBe(0); - }, - ); + }); - it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( - "json.type tests", - async (protocol) => { + it("json.type tests", async () => { client = await GlideClusterClient.createClient( getClientConfigurationOption( cluster.getAddresses(), @@ -588,12 +565,9 @@ describe("Server Module Tests", () => { expect( await GlideJson.type(client, "non_existing", { path: "." }), ).toBeNull(); - }, - ); + }); - it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( - "json.resp tests", - async (protocol) => { + it("json.resp tests", async () => { client = await GlideClusterClient.createClient( getClientConfigurationOption( cluster.getAddresses(), @@ -717,9 +691,9 @@ describe("Server Module Tests", () => { expect( await GlideJson.resp(client, "nonexistent_key"), ).toBeNull(); - }, - ); - }); + }); + }, + ); describe("GlideFt", () => { let client: GlideClusterClient; @@ -940,5 +914,78 @@ describe("Server Module Tests", () => { expect((e as Error).message).toContain("Index does not exist"); } }); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + "FT.INFO ft.info", + async (protocol) => { + client = await GlideClusterClient.createClient( + getClientConfigurationOption( + cluster.getAddresses(), + protocol, + ), + ); + + const index = uuidv4(); + expect( + await GlideFt.create( + client, + Buffer.from(index), + [ + { + type: "VECTOR", + name: "$.vec", + alias: "VEC", + attributes: { + algorithm: "HNSW", + distanceMetric: "COSINE", + dimensions: 42, + }, + }, + { type: "TEXT", name: "$.name" }, + ], + { dataType: "JSON", prefixes: ["123"] }, + ), + ).toEqual("OK"); + + let response = await GlideFt.info(client, Buffer.from(index)); + + expect(response).toMatchObject({ + index_name: index, + key_type: "JSON", + key_prefixes: ["123"], + fields: [ + { + identifier: "$.name", + type: "TEXT", + field_name: "$.name", + option: "", + }, + { + identifier: "$.vec", + type: "VECTOR", + field_name: "VEC", + option: "", + vector_params: { + distance_metric: "COSINE", + dimension: 42, + }, + }, + ], + }); + + response = await GlideFt.info(client, index, { + decoder: Decoder.Bytes, + }); + expect(response).toMatchObject({ + index_name: Buffer.from(index), + }); + + expect(await GlideFt.dropindex(client, index)).toEqual("OK"); + // querying a missing index + await expect(GlideFt.info(client, index)).rejects.toThrow( + "Index not found", + ); + }, + ); }); });