From d291d87bf140efc2f9b9b28a2887b5ffdd8a124c Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Fri, 18 Oct 2024 13:01:53 -0700 Subject: [PATCH] Java: add `FT.INFO` (#2405) * `FT.INFO` Signed-off-by: Yury-Fridlyand --- CHANGELOG.md | 1 + glide-core/src/client/value_conversion.rs | 105 +++++++++++++++- .../glide/api/commands/servermodules/FT.java | 113 ++++++++++++++++++ .../java/glide/modules/VectorSearchTests.java | 69 ++++++++++- 4 files changed, 284 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 429e2d2316..cf0761a6ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ * Python: Add JSON.NUMINCRBY command ([#2448](https://github.com/valkey-io/valkey-glide/pull/2448)) * Python: Add JSON.NUMMULTBY command ([#2458](https://github.com/valkey-io/valkey-glide/pull/2458)) * Java: Added `FT.CREATE` ([#2414](https://github.com/valkey-io/valkey-glide/pull/2414)) +* Java: Added `FT.INFO` ([#2405](https://github.com/valkey-io/valkey-glide/pull/2441)) * Java: Added `FT.DROPINDEX` ([#2440](https://github.com/valkey-io/valkey-glide/pull/2440)) * Java: Added `FT.SEARCH` ([#2439](https://github.com/valkey-io/valkey-glide/pull/2439)) * Java: Added `FT.AGGREGATE` ([#2466](https://github.com/valkey-io/valkey-glide/pull/2466)) diff --git a/glide-core/src/client/value_conversion.rs b/glide-core/src/client/value_conversion.rs index 64b853af9f..d16d4ef939 100644 --- a/glide-core/src/client/value_conversion.rs +++ b/glide-core/src/client/value_conversion.rs @@ -24,6 +24,7 @@ pub(crate) enum ExpectedReturnType<'a> { ArrayOfDoubleOrNull, FTAggregateReturnType, FTSearchReturnType, + FTInfoReturnType, Lolwut, ArrayOfStringAndArrays, ArrayOfArraysOfDoubleOrNull, @@ -999,7 +1000,108 @@ pub(crate) fn convert_to_expected_type( }, _ => Err(( ErrorKind::TypeError, - "Response couldn't be converted to Pair", + "Response couldn't be converted for FT.SEARCH", + format!("(response was {:?})", get_value_type(&value)), + ) + .into()) + }, + ExpectedReturnType::FTInfoReturnType => match value { + /* + Example of the response + 1) index_name + 2) "957fa3ca-2280-467d-873f-8763a36fbd5a" + 3) creation_timestamp + 4) (integer) 1728348101740745 + 5) key_type + 6) HASH + 7) key_prefixes + 8) 1) "blog:post:" + 9) fields + 10) 1) 1) identifier + 2) category + 3) field_name + 4) category + 5) type + 6) TAG + 7) option + 8) + 2) 1) identifier + 2) vec + 3) field_name + 4) VEC + 5) type + 6) VECTOR + 7) option + 8) + 9) vector_params + 10) 1) algorithm + 2) HNSW + 3) data_type + 4) FLOAT32 + 5) dimension + 6) (integer) 2 + ... + + Converting response to + 1# "index_name" => "957fa3ca-2280-467d-873f-8763a36fbd5a" + 2# "creation_timestamp" => 1728348101740745 + 3# "key_type" => "HASH" + 4# "key_prefixes" => + 1) "blog:post:" + 5# "fields" => + 1) 1# "identifier" => "category" + 2# "field_name" => "category" + 3# "type" => "TAG" + 4# "option" => "" + 2) 1# "identifier" => "vec" + 2# "field_name" => "VEC" + 3# "type" => "TAVECTORG" + 4# "option" => "" + 5# "vector_params" => + 1# "algorithm" => "HNSW" + 2# "data_type" => "FLOAT32" + 3# "dimension" => 2 + ... + + Map keys (odd array elements) are simple strings, not bulk strings. + */ + Value::Array(_) => { + let Value::Map(mut map) = convert_to_expected_type(value, Some(ExpectedReturnType::Map { + key_type: &None, + value_type: &None, + }))? else { unreachable!() }; + let Some(fields_pair) = map.iter_mut().find(|(key, _)| { + *key == Value::SimpleString("fields".into()) + }) else { return Ok(Value::Map(map)) }; + let (fields_key, fields_value) = std::mem::replace(fields_pair, (Value::Nil, Value::Nil)); + let Value::Array(fields) = fields_value else { + return Err(( + ErrorKind::TypeError, + "Response couldn't be converted for FT.INFO", + format!("(`fields` was {:?})", get_value_type(&fields_value)), + ).into()); + }; + let fields = fields.into_iter().map(|field| { + let Value::Map(mut field_params) = convert_to_expected_type(field, Some(ExpectedReturnType::Map { + key_type: &None, + value_type: &None, + }))? else { unreachable!() }; + let Some(vector_params_pair) = field_params.iter_mut().find(|(key, _)| { + *key == Value::SimpleString("vector_params".into()) + }) else { return Ok(Value::Map(field_params)) }; + let (vector_params_key, vector_params_value) = std::mem::replace(vector_params_pair, (Value::Nil, Value::Nil)); + let _ = std::mem::replace(vector_params_pair, (vector_params_key, convert_to_expected_type(vector_params_value, Some(ExpectedReturnType::Map { + key_type: &None, + value_type: &None, + }))?)); + Ok(Value::Map(field_params)) + }).collect::>>()?; + let _ = std::mem::replace(fields_pair, (fields_key, Value::Array(fields))); + Ok(Value::Map(map)) + }, + _ => Err(( + ErrorKind::TypeError, + "Response couldn't be converted for FT.INFO", format!("(response was {:?})", get_value_type(&value)), ) .into()) @@ -1370,6 +1472,7 @@ pub(crate) fn expected_type_for_cmd(cmd: &Cmd) -> Option { }), b"FT.AGGREGATE" => Some(ExpectedReturnType::FTAggregateReturnType), b"FT.SEARCH" => Some(ExpectedReturnType::FTSearchReturnType), + b"FT.INFO" => Some(ExpectedReturnType::FTInfoReturnType), _ => None, } } 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 106d540f8c..7a5b514816 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 @@ -474,6 +474,119 @@ public static CompletableFuture[]> aggregate( .thenApply(res -> castArray(res, Map.class)); } + /** + * Returns information about a given index. + * + * @param indexName The index name. + * @return Nested maps with info about the index. See example for more details. + * @example + *
{@code
+     * // example of using the API:
+     * 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_status", gs("AVAILABLE"),
+     *     "key_type", gs("JSON"),
+     *     "creation_timestamp", 1728348101728771L,
+     *     "key_prefixes", new String[] { gs("json:") },
+     *     "num_indexed_vectors", 0L,
+     *     "space_usage", 653471L,
+     *     "num_docs", 0L,
+     *     "vector_space_usage", 653471L,
+     *     "index_degradation_percentage", 0L,
+     *     "fulltext_space_usage", 0L,
+     *     "current_lag", 0L,
+     *     "fields", new Object [] {
+     *         Map.of(
+     *             gs("identifier"), gs("$.vec"),
+     *             gs("type"), gs("VECTOR"),
+     *             gs("field_name"), gs("VEC"),
+     *             gs("option"), gs(""),
+     *             gs("vector_params", Map.of(
+     *                 gs("data_type", gs("FLOAT32"),
+     *                 gs("initial_capacity", 1000L,
+     *                 gs("current_capacity", 1000L,
+     *                 gs("distance_metric", gs("L2"),
+     *                 gs("dimension", 6L,
+     *                 gs("block_size", 1024L,
+     *                 gs("algorithm", gs("FLAT")
+     *           )
+     *         ),
+     *         Map.of(
+     *             gs("identifier"), gs("name"),
+     *             gs("type"), gs("TEXT"),
+     *             gs("field_name"), gs("name"),
+     *             gs("option"), gs("")
+     *         ),
+     *     }
+     * );
+     * }
+ */ + public static CompletableFuture> info( + @NonNull BaseClient client, @NonNull String indexName) { + // TODO inconsistency: the outer map is `Map`, + // while inner maps are `Map` + // The outer map converted from `Map` in ClusterValue::ofMultiValueBinary + // TODO server returns all strings as `SimpleString`, we're safe to convert all to + // `GlideString`s to `String` + return executeCommand(client, new GlideString[] {gs("FT.INFO"), gs(indexName)}, true); + } + + /** + * Returns information about a given index. + * + * @param indexName The index name. + * @return Nested maps with info about the index. See example for more details. + * @example + *
{@code
+     * // example of using the API:
+     * 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_status", gs("AVAILABLE"),
+     *     "key_type", gs("JSON"),
+     *     "creation_timestamp", 1728348101728771L,
+     *     "key_prefixes", new String[] { gs("json:") },
+     *     "num_indexed_vectors", 0L,
+     *     "space_usage", 653471L,
+     *     "num_docs", 0L,
+     *     "vector_space_usage", 653471L,
+     *     "index_degradation_percentage", 0L,
+     *     "fulltext_space_usage", 0L,
+     *     "current_lag", 0L,
+     *     "fields", new Object [] {
+     *         Map.of(
+     *             gs("identifier"), gs("$.vec"),
+     *             gs("type"), gs("VECTOR"),
+     *             gs("field_name"), gs("VEC"),
+     *             gs("option"), gs(""),
+     *             gs("vector_params", Map.of(
+     *                 gs("data_type", gs("FLOAT32"),
+     *                 gs("initial_capacity", 1000L,
+     *                 gs("current_capacity", 1000L,
+     *                 gs("distance_metric", gs("L2"),
+     *                 gs("dimension", 6L,
+     *                 gs("block_size", 1024L,
+     *                 gs("algorithm", gs("FLAT")
+     *           )
+     *         ),
+     *         Map.of(
+     *             gs("identifier"), gs("name"),
+     *             gs("type"), gs("TEXT"),
+     *             gs("field_name"), gs("name"),
+     *             gs("option"), gs("")
+     *         ),
+     *     }
+     * );
+     * }
+ */ + public static CompletableFuture> info( + @NonNull BaseClient client, @NonNull GlideString indexName) { + return executeCommand(client, new GlideString[] {gs("FT.INFO"), indexName}, true); + } + /** * A wrapper for custom command API. * diff --git a/java/integTest/src/test/java/glide/modules/VectorSearchTests.java b/java/integTest/src/test/java/glide/modules/VectorSearchTests.java index fbc3eab196..6e690e77c9 100644 --- a/java/integTest/src/test/java/glide/modules/VectorSearchTests.java +++ b/java/integTest/src/test/java/glide/modules/VectorSearchTests.java @@ -16,6 +16,7 @@ import glide.api.GlideClusterClient; import glide.api.commands.servermodules.FT; +import glide.api.models.GlideString; import glide.api.models.commands.FT.FTAggregateOptions; import glide.api.models.commands.FT.FTAggregateOptions.Apply; import glide.api.models.commands.FT.FTAggregateOptions.GroupBy; @@ -142,12 +143,12 @@ public void ft_create() { .get()); // create an index with multiple prefixes - var name = UUID.randomUUID().toString(); + var index = UUID.randomUUID().toString(); assertEquals( OK, FT.create( client, - name, + index, new FieldInfo[] { new FieldInfo("author_id", new TagField()), new FieldInfo("author_ids", new TagField()), @@ -167,7 +168,7 @@ public void ft_create() { () -> FT.create( client, - name, + index, new FieldInfo[] { new FieldInfo("title", new TextField()), new FieldInfo("name", new TextField()) @@ -713,4 +714,66 @@ public void ft_aggregate() { 9.)), Set.of(aggreg)); } + + @SuppressWarnings("unchecked") + @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, + FT.create( + client, + index, + new FieldInfo[] { + new FieldInfo( + "$.vec", "VEC", VectorFieldHnsw.builder(DistanceMetric.COSINE, 42).build()), + new FieldInfo("$.name", new TextField()), + }, + FTCreateOptions.builder() + .indexType(IndexType.JSON) + .prefixes(new String[] {"123"}) + .build()) + .get()); + + var response = FT.info(client, index).get(); + assertEquals(gs(index), response.get("index_name")); + assertEquals(gs("JSON"), response.get("key_type")); + assertArrayEquals(new GlideString[] {gs("123")}, (Object[]) response.get("key_prefixes")); + var fields = (Object[]) response.get("fields"); + assertEquals(2, fields.length); + var f1 = (Map) fields[1]; + assertEquals(gs("$.vec"), f1.get(gs("identifier"))); + assertEquals(gs("VECTOR"), f1.get(gs("type"))); + assertEquals(gs("VEC"), f1.get(gs("field_name"))); + var f1params = (Map) f1.get(gs("vector_params")); + assertEquals(gs("COSINE"), f1params.get(gs("distance_metric"))); + assertEquals(42L, f1params.get(gs("dimension"))); + + assertEquals( + Map.of( + gs("identifier"), + gs("$.name"), + gs("type"), + gs("TEXT"), + gs("field_name"), + gs("$.name"), + gs("option"), + gs("")), + fields[0]); + + // querying a missing index + assertEquals(OK, FT.dropindex(client, index).get()); + var exception = assertThrows(ExecutionException.class, () -> FT.info(client, index).get()); + assertInstanceOf(RequestException.class, exception.getCause()); + assertTrue(exception.getMessage().contains("Index not found")); + } }