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

Open
wants to merge 10 commits into
base: release-1.2
Choose a base branch
from
Open
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 @@ -21,6 +21,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
110 changes: 110 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,116 @@ 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)}.
* @return A <code>String</code> representing the execution plan.
* @example
* <pre>{@code
* FT.explain(client, "myIndex", "@price:[0 10]").get();
* // the result can look like (the result is a string):
* // Field {
* // price
* // 0
* // 10
* // }
* }</pre>
*/
public static CompletableFuture<String> explain(
@NonNull BaseClient client, @NonNull String indexName, @NonNull String query) {
var args = concatenateArrays(new GlideString[] {gs("FT.EXPLAIN"), gs(indexName), gs(query)});
return executeCommand(client, args, false)
.thenApply(result -> ((GlideString) result).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, String, String)}.
* @return A <code>GlideString</code> representing the execution plan.
* @example
* <pre>{@code
* FT.explain(client, "myIndex", "@price:[0 10]").get();
* // the result can look like (the result is a string):
* // Field {
* // price
* // 0
* // 10
* // }
* }</pre>
*/
public static CompletableFuture<GlideString> explain(
@NonNull BaseClient client, @NonNull GlideString indexName, @NonNull GlideString query) {
var args = concatenateArrays(new GlideString[] {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. More useful with cli.
*
* @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)}.
* @return A <code>String[[</code> representing the execution plan.
* @example
* <pre>{@code
* FT.explaincli(client, "myIndex", "@price:[0 10]").get();
* // the output can look like this (the result is an array)
* // 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(e -> e.getString()).toArray(String[]::new));
}

/**
* Same as the {@link FT#explain(BaseClient, String, String)} except that the results are
* displayed in a different format. More useful with cli.
*
* @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)}.
* @return A <code>GlideString[]</code> representing the execution plan.
* @example
* <pre>{@code
* FT.explaincli(client, "myIndex", "@price:[0 10]").get();
* // the output can look like this (the result is an array)
* // Field {
* // price
* // 0
* // 10
* // }
* }</pre>
*/
public static CompletableFuture<GlideString[]> explaincli(
@NonNull BaseClient client, @NonNull GlideString indexName, @NonNull GlideString query) {
var args = concatenateArrays(new GlideString[] {gs("FT.EXPLAINCLI"), indexName, query});
CompletableFuture<GlideString[]> result =
((GlideClusterClient) client)
.customCommand(args)
.thenApply(ClusterValue::getSingleValue)
.thenApply(ret -> (Object[]) ret)
.thenApply(ret -> castArray(ret, GlideString.class));
return result;
}

/**
* A wrapper for custom command API.
*
Expand Down
111 changes: 111 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,7 +38,10 @@
import glide.api.models.commands.FlushMode;
import glide.api.models.commands.InfoOptions.Section;
import glide.api.models.exceptions.RequestException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
Expand Down Expand Up @@ -825,4 +829,111 @@ 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.
resultGS = FT.explain(client, gs(indexName), gs("*")).get();
result = resultGS.toString();
assertTrue(result.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 = new ArrayList<>();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As an interesting exercise, you can use Stream() to map result to a result list. Something like:

List<String> trimmedResult = result.stream().map(::trim).collect(Collection.toList());

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unfortunately streams only work on lists

Copy link
Collaborator

@acarbonetto acarbonetto Oct 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Arrays.stream() works on arrays. I'll point you to an example, and IntelliJ has a lot of hints to help create streams.

https://github.com/valkey-io/valkey-glide/blob/main/java/client/src/main/java/glide/utils/ArrayTransformUtils.java#L162-L164


for (String r : result) {
resultList.add(r.trim()); // trim to remove any excess white space
}

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 = new ArrayList<>();
for (GlideString r : resultGS) {
resultListGS.add(r.toString().trim()); // trim to remove any excess white space
}

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

List<String> resultListGS2 = new ArrayList<>();

// search query that returns all data.
resultGS = FT.explaincli(client, gs(indexName), gs("*")).get();
for (GlideString r : resultGS) {
resultListGS2.add(r.toString().trim()); // trim to remove any excess white space
}
assertTrue((resultListGS2).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"));
}

@SneakyThrows
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: instead, you can throw on the method signature

public void createIndexHelper(String indexName) {
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