Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Java: add FT.INFO #2405

Merged
merged 3 commits into from
Oct 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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"));
}
}
Loading