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
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)}.
cyip10 marked this conversation as resolved.
Show resolved Hide resolved
* @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):
cyip10 marked this conversation as resolved.
Show resolved Hide resolved
* // 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)});
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
var args = concatenateArrays(new GlideString[] {gs("FT.EXPLAIN"), gs(indexName), gs(query)});
GlideString[] args = new GlideString[] {gs("FT.EXPLAIN"), gs(indexName), gs(query)};

concatenateArrays() is not needed, as we are creating only one array. Also try not to use var.
Please check all other occurences as well.

return executeCommand(client, args, false)
.thenApply(result -> ((GlideString) result).toString());
cyip10 marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* 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.
cyip10 marked this conversation as resolved.
Show resolved Hide resolved
*
* @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.
cyip10 marked this conversation as resolved.
Show resolved Hide resolved
* @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));
cyip10 marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* Same as the {@link FT#explain(BaseClient, String, String)} except that the results are
* displayed in a different format. More useful with cli.
cyip10 marked this conversation as resolved.
Show resolved Hide resolved
*
* @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));
Yury-Fridlyand marked this conversation as resolved.
Show resolved Hide resolved
return result;
}

/**
* A wrapper for custom command API.
*
Expand Down
109 changes: 109 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,15 @@
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.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 +830,108 @@ 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 = 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"));

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
Copy link
Collaborator

Choose a reason for hiding this comment

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

why not using stream again here with resultListGS2?

}
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
acarbonetto marked this conversation as resolved.
Show resolved Hide resolved
public void createIndexHelper(String indexName) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

If this is a helper/utility method, then maybe it doesn't need to be public.

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