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: FT.EXPLAIN and FT.EXPLAINCLI #2515

Merged
merged 16 commits into from
Oct 28, 2024
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
* Java: Added `JSON.OBJLEN` and `JSON.OBJKEYS` ([#2492](https://github.com/valkey-io/valkey-glide/pull/2492))
* Java: Added `JSON.DEL` and `JSON.FORGET` ([#2490](https://github.com/valkey-io/valkey-glide/pull/2490))
* Java: Added `FT.ALIASADD`, `FT.ALIASDEL`, `FT.ALIASUPDATE` ([#2442](https://github.com/valkey-io/valkey-glide/pull/2442))
* Java: Added `FT.EXPLAIN`, `FT.EXPLAINCLI` ([#2515](https://github.com/valkey-io/valkey-glide/pull/2515))
* Core: Update routing for commands from server modules ([#2461](https://github.com/valkey-io/valkey-glide/pull/2461))
* Node: Added `JSON.SET` and `JSON.GET` ([#2427](https://github.com/valkey-io/valkey-glide/pull/2427))
* Java: Added `JSON.ARRAPPEND` ([#2489](https://github.com/valkey-io/valkey-glide/pull/2489))
Expand Down
100 changes: 100 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 @@ -703,6 +703,106 @@ public static CompletableFuture<String> aliasupdate(
return executeCommand(client, args, false);
}

/**
* Parse a query and return information about how that query was parsed.
*
* @param client The client to execute the command.
* @param indexName The index name to search into.
* @param query The text query to search. It is the same as the query passed as an argument to
* {@link FT#search(BaseClient, String, String)} and {@link FT#aggregate(BaseClient, String,
* String)}.
* @return A <code>String</code> representing the execution plan.
* @example
* <pre>{@code
* String result = FT.explain(client, "myIndex", "@price:[0 10]").get();
* assert result.equals("Field {\n\tprice\n\t0\n\t10\n}");
* }</pre>
*/
public static CompletableFuture<String> explain(
@NonNull BaseClient client, @NonNull String indexName, @NonNull String query) {
GlideString[] args = {gs("FT.EXPLAIN"), gs(indexName), gs(query)};
return FT.<GlideString>executeCommand(client, args, false).thenApply(GlideString::toString);
}

/**
* Parse a query and return information about how that query was parsed.
*
* @param client The client to execute the command.
* @param indexName The index name to search into.
* @param query The text query to search. It is the same as the query passed as an argument to
* {@link FT#search(BaseClient, GlideString, GlideString)} and {@link FT#aggregate(BaseClient,
* GlideString, GlideString)}.
* @return A <code>GlideString</code> representing the execution plan.
* @example
* <pre>{@code
* GlideString result = FT.explain(client, gs("myIndex"), gs("@price:[0 10]")).get();
* assert result.equals("Field {\n\tprice\n\t0\n\t10\n}");
acarbonetto marked this conversation as resolved.
Show resolved Hide resolved
* }</pre>
*/
public static CompletableFuture<GlideString> explain(
@NonNull BaseClient client, @NonNull GlideString indexName, @NonNull GlideString query) {
GlideString[] args = {gs("FT.EXPLAIN"), indexName, query};
return executeCommand(client, args, false);
}

/**
* Same as the {@link FT#explain(BaseClient, String, String)} except that the results are
* displayed in a different format.
*
* @param client The client to execute the command.
* @param indexName The index name to search into.
* @param query The text query to search. It is the same as the query passed as an argument to
* {@link FT#search(BaseClient, String, String)} and {@link FT#aggregate(BaseClient, String,
* String)}.
* @return A <code>String[]</code> representing the execution plan.
* @example
* <pre>{@code
* var result = FT.explaincli(client, "myIndex", "@price:[0 10]").get();
* assert Arrays.equals(result, new String[]{
* "Field {",
* " price",
* " 0",
* " 10",
* "}"
* });
* }</pre>
*/
public static CompletableFuture<String[]> explaincli(
@NonNull BaseClient client, @NonNull String indexName, @NonNull String query) {
CompletableFuture<GlideString[]> result = explaincli(client, gs(indexName), gs(query));
return result.thenApply(
ret -> Arrays.stream(ret).map(GlideString::toString).toArray(String[]::new));
}

/**
* Same as the {@link FT#explain(BaseClient, String, String)} except that the results are
* displayed in a different format.
*
* @param client The client to execute the command.
* @param indexName The index name to search into.
* @param query The text query to search. It is the same as the query passed as an argument to
* {@link FT#search(BaseClient, GlideString, GlideString)} and {@link FT#aggregate(BaseClient,
* GlideString, GlideString)}.
* @return A <code>GlideString[]</code> representing the execution plan.
* @example
* <pre>{@code
* var result = FT.explaincli(client, "myIndex", "@price:[0 10]").get();
* assert Arrays.equals(result, new GlideString[]{
* "Field {",
acarbonetto marked this conversation as resolved.
Show resolved Hide resolved
* " price",
* " 0",
* " 10",
* "}"
* });
* }</pre>
*/
public static CompletableFuture<GlideString[]> explaincli(
@NonNull BaseClient client, @NonNull GlideString indexName, @NonNull GlideString query) {
GlideString[] args = new GlideString[] {gs("FT.EXPLAINCLI"), indexName, query};
return FT.<Object[]>executeCommand(client, args, false)
.thenApply(ret -> castArray(ret, GlideString.class));
}

/**
* A wrapper for custom command API.
*
Expand Down
106 changes: 106 additions & 0 deletions java/integTest/src/test/java/glide/modules/VectorSearchTests.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import glide.api.models.commands.FT.FTAggregateOptions.SortBy.SortOrder;
import glide.api.models.commands.FT.FTAggregateOptions.SortBy.SortProperty;
import glide.api.models.commands.FT.FTCreateOptions;
import glide.api.models.commands.FT.FTCreateOptions.DataType;
import glide.api.models.commands.FT.FTCreateOptions.DistanceMetric;
import glide.api.models.commands.FT.FTCreateOptions.FieldInfo;
import glide.api.models.commands.FT.FTCreateOptions.NumericField;
Expand All @@ -37,11 +38,14 @@
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.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
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;
Expand Down Expand Up @@ -825,4 +829,106 @@ public void ft_aliasadd_aliasdel_aliasupdate() {
assertInstanceOf(RequestException.class, exception.getCause());
assertTrue(exception.getMessage().contains("Index does not exist"));
}

@SneakyThrows
@Test
public void ft_explain() {

String indexName = UUID.randomUUID().toString();
createIndexHelper(indexName);

// search query containing numeric field.
String query = "@price:[0 10]";
String result = FT.explain(client, indexName, query).get();
assertTrue(result.contains("price"));
assertTrue(result.contains("0"));
assertTrue(result.contains("10"));

GlideString resultGS = FT.explain(client, gs(indexName), gs(query)).get();
assertTrue((resultGS).toString().contains("price"));
assertTrue((resultGS).toString().contains("0"));
assertTrue((resultGS).toString().contains("10"));

// search query that returns all data.
GlideString resultGSAllData = FT.explain(client, gs(indexName), gs("*")).get();
assertTrue(resultGSAllData.toString().contains("*"));

assertEquals(OK, FT.dropindex(client, indexName).get());

// missing index throws an error.
var exception =
assertThrows(
ExecutionException.class,
() -> FT.explain(client, UUID.randomUUID().toString(), "*").get());
assertInstanceOf(RequestException.class, exception.getCause());
assertTrue(exception.getMessage().contains("Index not found"));
}

@SneakyThrows
@Test
public void ft_explaincli() {

String indexName = UUID.randomUUID().toString();
createIndexHelper(indexName);

// search query containing numeric field.
String query = "@price:[0 10]";
String[] result = FT.explaincli(client, indexName, query).get();
List<String> resultList = Arrays.stream(result).map(String::trim).collect(Collectors.toList());

assertTrue(resultList.contains("price"));
assertTrue(resultList.contains("0"));
assertTrue(resultList.contains("10"));

GlideString[] resultGS = FT.explaincli(client, gs(indexName), gs(query)).get();
List<String> resultListGS =
Arrays.stream(resultGS)
.map(GlideString::toString)
.map(String::trim)
.collect(Collectors.toList());

assertTrue((resultListGS).contains("price"));
assertTrue((resultListGS).contains("0"));
assertTrue((resultListGS).contains("10"));

// search query that returns all data.
GlideString[] resultGSAllData = FT.explaincli(client, gs(indexName), gs("*")).get();
List<String> resultListGSAllData =
Arrays.stream(resultGSAllData)
.map(GlideString::toString)
.map(String::trim)
.collect(Collectors.toList());
assertTrue((resultListGSAllData).contains("*"));

assertEquals(OK, FT.dropindex(client, indexName).get());

// missing index throws an error.
var exception =
assertThrows(
ExecutionException.class,
() -> FT.explaincli(client, UUID.randomUUID().toString(), "*").get());
assertInstanceOf(RequestException.class, exception.getCause());
assertTrue(exception.getMessage().contains("Index not found"));
}

private void createIndexHelper(String indexName) throws ExecutionException, InterruptedException {
FieldInfo numericField = new FieldInfo("price", new NumericField());
FieldInfo textField = new FieldInfo("title", new TextField());

FieldInfo[] fields = new FieldInfo[] {numericField, textField};

String prefix = "{hash-search-" + UUID.randomUUID().toString() + "}:";

assertEquals(
OK,
FT.create(
client,
indexName,
fields,
FTCreateOptions.builder()
.dataType(DataType.HASH)
.prefixes(new String[] {prefix})
.build())
.get());
}
}
Loading