Skip to content

Commit

Permalink
Java: FT.EXPLAIN and FT.EXPLAINCLI (#2515)
Browse files Browse the repository at this point in the history
Signed-off-by: Chloe <chloe.yip@improving.com>
Signed-off-by: Chloe Yip <chloe.yip@improving.com>
Signed-off-by: Andrew Carbonetto <andrew.carbonetto@improving.com>
Co-authored-by: Andrew Carbonetto <andrew.carbonetto@improving.com>
  • Loading branch information
cyip10 and acarbonetto authored Oct 28, 2024
1 parent 5e86b7a commit ddca067
Show file tree
Hide file tree
Showing 3 changed files with 207 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,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.NUMINCRBY` and `JSON.NUMMULTBY` ([#2511](https://github.com/valkey-io/valkey-glide/pull/2511))
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 @@ -751,6 +751,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}");
* }</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
* String[] 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
* GlideString[] result = FT.explaincli(client, gs("myIndex"), gs("@price:[0 10]")).get();
* assert Arrays.equals(result, new GlideString[]{
* gs("Field {"),
* gs(" price"),
* gs(" 0"),
* gs(" 10"),
* gs("}")
* });
* }</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 @@ -38,11 +39,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());
}
}

0 comments on commit ddca067

Please sign in to comment.