diff --git a/CHANGELOG.md b/CHANGELOG.md index dc7d93523e..250a33679b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,8 @@ * Python: Python: Added FT.CREATE command([#2413](https://github.com/valkey-io/valkey-glide/pull/2413)) * Python: Add JSON.ARRLEN command ([#2403](https://github.com/valkey-io/valkey-glide/pull/2403)) * Python: Add JSON.CLEAR command ([#2418](https://github.com/valkey-io/valkey-glide/pull/2418)) - * Java: Added `FT.CREATE` ([#2414](https://github.com/valkey-io/valkey-glide/pull/2414)) +* Java: Added `FT.INFO` ([#2441](https://github.com/valkey-io/valkey-glide/pull/2441)) #### Breaking Changes diff --git a/glide-core/src/client/value_conversion.rs b/glide-core/src/client/value_conversion.rs index 4a43da7da7..2a03f98e28 100644 --- a/glide-core/src/client/value_conversion.rs +++ b/glide-core/src/client/value_conversion.rs @@ -22,6 +22,7 @@ pub(crate) enum ExpectedReturnType<'a> { ArrayOfStrings, ArrayOfBools, ArrayOfDoubleOrNull, + FTInfoReturnType, Lolwut, ArrayOfStringAndArrays, ArrayOfArraysOfDoubleOrNull, @@ -891,7 +892,115 @@ pub(crate) fn convert_to_expected_type( 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!() }; + for pair in map.iter_mut() { + if pair.0 == Value::SimpleString("fields".into()) { + let Value::Array(mut fields) = pair.1.clone() else { + return Err(( + ErrorKind::TypeError, + "Response couldn't be converted for FT.INFO", + format!("(`fields` was {:?})", get_value_type(&pair.1.clone())), + ) + .into()); + }; + + for field in fields.iter_mut() { + let Value::Map(mut field_params) = convert_to_expected_type(field.clone(), Some(ExpectedReturnType::Map { + key_type: &None, + value_type: &None, + })).unwrap() else { unreachable!() }; + + for pair in field_params.iter_mut() { + if pair.0 == Value::SimpleString("vector_params".into()) { + *pair = (pair.0.clone(), convert_to_expected_type(pair.1.clone(), Some(ExpectedReturnType::Map { + key_type: &None, + value_type: &None, + }))?); + break; + } + } + + *field = Value::Map(field_params); + } + + *pair = (pair.0.clone(), Value::Array(fields)); + break; + } + } + Ok(Value::Map(map)) + }, + _ => Err(( + ErrorKind::TypeError, + "Response couldn't be converted to Pair", + format!("(response was {:?})", get_value_type(&value)), + ) + .into()) + }, } } @@ -1256,6 +1365,7 @@ pub(crate) fn expected_type_for_cmd(cmd: &Cmd) -> Option { key_type: &None, value_type: &None, }), + 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 51bde7a03d..2f00c29e72 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 @@ -11,6 +11,7 @@ import glide.api.models.commands.FT.FTCreateOptions; import glide.api.models.commands.FT.FTCreateOptions.FieldInfo; import java.util.Arrays; +import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.stream.Stream; import lombok.NonNull; @@ -140,6 +141,114 @@ public static CompletableFuture create( return executeCommand(client, args, false); } + /** + * 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", "bcd97d68-4180-4bc5-98fe-5125d0abbcb8",
+     *     "index_status", "AVAILABLE",
+     *     "key_type", "JSON",
+     *     "creation_timestamp", 1728348101728771L,
+     *     "key_prefixes", new String[] { "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(
+     *             "identifier", "$.vec",
+     *             "type", "VECTOR",
+     *             "field_name", "VEC",
+     *             "option", ""
+     *             "vector_params", Map.of(
+     *                 "data_type", "FLOAT32",
+     *                 "initial_capacity", 1000L,
+     *                 "current_capacity", 1000L,
+     *                 "distance_metric", "L2",
+     *                 "dimension", 6L,
+     *                 "block_size", 1024L,
+     *                 "algorithm", "FLAT"
+     *           )
+     *         ),
+     *         Map.of(
+     *             "identifier", "name",
+     *             "type", "TEXT",
+     *             "field_name", "name",
+     *             "option", ""
+     *         ),
+     *     }
+     * );
+     * }
+ */ + public static CompletableFuture> info( + @NonNull BaseClient client, @NonNull String indexName) { + 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("myIndex").get();
+     * // the response contains data in the following format:
+     * Map data = Map.of(
+     *     "index_name", "bcd97d68-4180-4bc5-98fe-5125d0abbcb8",
+     *     "index_status", "AVAILABLE",
+     *     "key_type", "JSON",
+     *     "creation_timestamp", 1728348101728771L,
+     *     "key_prefixes", new String[] { "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(
+     *             "identifier", "$.vec",
+     *             "type", "VECTOR",
+     *             "field_name", "VEC",
+     *             "option", ""
+     *             "vector_params", Map.of(
+     *                 "data_type", "FLOAT32",
+     *                 "initial_capacity", 1000L,
+     *                 "current_capacity", 1000L,
+     *                 "distance_metric", "L2",
+     *                 "dimension", 6L,
+     *                 "block_size", 1024L,
+     *                 "algorithm", "FLAT"
+     *           )
+     *         ),
+     *         Map.of(
+     *             "identifier", "name",
+     *             "type", "TEXT",
+     *             "field_name", "name",
+     *             "option", ""
+     *         ),
+     *     }
+     * );
+     * }
+ */ + 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 67387026bd..2604719c48 100644 --- a/java/integTest/src/test/java/glide/modules/VectorSearchTests.java +++ b/java/integTest/src/test/java/glide/modules/VectorSearchTests.java @@ -5,6 +5,7 @@ import static glide.api.BaseClient.OK; import static glide.api.models.configuration.RequestRoutingConfiguration.SimpleMultiNodeRoute.ALL_PRIMARIES; import static glide.api.models.configuration.RequestRoutingConfiguration.SimpleSingleNodeRoute.RANDOM; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -24,8 +25,11 @@ import glide.api.models.commands.FlushMode; import glide.api.models.commands.InfoOptions.Section; import glide.api.models.exceptions.RequestException; +import java.util.Arrays; +import java.util.Map; import java.util.UUID; import java.util.concurrent.ExecutionException; +import java.util.stream.Collectors; import lombok.SneakyThrows; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; @@ -124,12 +128,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()), @@ -149,7 +153,7 @@ public void ft_create() { () -> FT.create( client, - name, + index, new FieldInfo[] { new FieldInfo("title", new TextField()), new FieldInfo("name", new TextField()) @@ -182,4 +186,64 @@ public void ft_create() { assertInstanceOf(RequestException.class, exception.getCause()); assertTrue(exception.getMessage().contains("already exists")); } + + @SneakyThrows + @Test + @SuppressWarnings("unchecked") + public void ft_info() { + var indices = + client + .customCommand(new String[] {"FT._LIST"}, ALL_PRIMARIES) + .get() + .getMultiValue() + .values() + .stream() + .flatMap(s -> Arrays.stream((Object[]) s)) + .collect(Collectors.toSet()); + + // check that we can get a response for all existing 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(index, response.get("index_name")); + assertEquals("JSON", response.get("key_type")); + assertArrayEquals(new String[] {"123"}, (Object[]) response.get("key_prefixes")); + var fields = (Object[]) response.get("fields"); + assertEquals(2, fields.length); + var f1 = (Map) fields[1]; + assertEquals("$.vec", f1.get("identifier")); + assertEquals("VECTOR", f1.get("type")); + assertEquals("VEC", f1.get("field_name")); + var f1params = (Map) f1.get("vector_params"); + assertEquals("COSINE", f1params.get("distance_metric")); + assertEquals(42L, f1params.get("dimension")); + + assertEquals( + Map.of("identifier", "$.name", "type", "TEXT", "field_name", "$.name", "option", ""), + fields[0]); + + var exception = assertThrows(ExecutionException.class, () -> FT.info(client, index).get()); + assertInstanceOf(RequestException.class, exception.getCause()); + assertTrue(exception.getMessage().contains("Index not found")); + } } diff --git a/submodules/redis-rs b/submodules/redis-rs index 396536db31..8e89ad8f3d 160000 --- a/submodules/redis-rs +++ b/submodules/redis-rs @@ -1 +1 @@ -Subproject commit 396536db31fbf2de0f272d8179d68286329fa70e +Subproject commit 8e89ad8f3d4a872ceb9bd87027e55ea7f1477b83