Skip to content

Commit

Permalink
Java: add FT.INFO (#2405)
Browse files Browse the repository at this point in the history
* `FT.INFO`

Signed-off-by: Yury-Fridlyand <yury.fridlyand@improving.com>
  • Loading branch information
Yury-Fridlyand authored Oct 18, 2024
1 parent 87412e8 commit d291d87
Show file tree
Hide file tree
Showing 4 changed files with 284 additions and 4 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
105 changes: 104 additions & 1 deletion glide-core/src/client/value_conversion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ pub(crate) enum ExpectedReturnType<'a> {
ArrayOfDoubleOrNull,
FTAggregateReturnType,
FTSearchReturnType,
FTInfoReturnType,
Lolwut,
ArrayOfStringAndArrays,
ArrayOfArraysOfDoubleOrNull,
Expand Down Expand Up @@ -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::<RedisResult<Vec<Value>>>()?;
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())
Expand Down Expand Up @@ -1370,6 +1472,7 @@ pub(crate) fn expected_type_for_cmd(cmd: &Cmd) -> Option<ExpectedReturnType> {
}),
b"FT.AGGREGATE" => Some(ExpectedReturnType::FTAggregateReturnType),
b"FT.SEARCH" => Some(ExpectedReturnType::FTSearchReturnType),
b"FT.INFO" => Some(ExpectedReturnType::FTInfoReturnType),
_ => None,
}
}
Expand Down
113 changes: 113 additions & 0 deletions java/client/src/main/java/glide/api/commands/servermodules/FT.java
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,119 @@ public static CompletableFuture<Map<GlideString, Object>[]> 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
* <pre>{@code
* // example of using the API:
* Map<String, Object> response = client.ftinfo("myIndex").get();
* // the response contains data in the following format:
* Map<String, Object> 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("")
* ),
* }
* );
* }</pre>
*/
public static CompletableFuture<Map<String, Object>> info(
@NonNull BaseClient client, @NonNull String indexName) {
// TODO inconsistency: the outer map is `Map<String, T>`,
// while inner maps are `Map<GlideString, T>`
// The outer map converted from `Map<GlideString, T>` 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
* <pre>{@code
* // example of using the API:
* Map<String, Object> response = client.ftinfo(gs("myIndex")).get();
* // the response contains data in the following format:
* Map<String, Object> 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("")
* ),
* }
* );
* }</pre>
*/
public static CompletableFuture<Map<String, Object>> info(
@NonNull BaseClient client, @NonNull GlideString indexName) {
return executeCommand(client, new GlideString[] {gs("FT.INFO"), indexName}, true);
}

/**
* A wrapper for custom command API.
*
Expand Down
69 changes: 66 additions & 3 deletions java/integTest/src/test/java/glide/modules/VectorSearchTests.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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()),
Expand All @@ -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())
Expand Down Expand Up @@ -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<GlideString, Object>) 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<GlideString, Object>) 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"));
}
}

0 comments on commit d291d87

Please sign in to comment.