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.SEARCH #2439

Merged
merged 11 commits into from
Oct 17, 2024
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.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.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))
* Core: Update routing for commands from server modules ([#2461](https://github.com/valkey-io/valkey-glide/pull/2461))

#### Breaking Changes
Expand Down
50 changes: 49 additions & 1 deletion glide-core/src/client/value_conversion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ pub(crate) enum ExpectedReturnType<'a> {
ArrayOfStrings,
ArrayOfBools,
ArrayOfDoubleOrNull,
FTSearchReturnType,
Lolwut,
ArrayOfStringAndArrays,
ArrayOfArraysOfDoubleOrNull,
Expand Down Expand Up @@ -891,7 +892,53 @@ pub(crate) fn convert_to_expected_type(
format!("(response was {:?})", get_value_type(&value)),
)
.into()),
}
},
ExpectedReturnType::FTSearchReturnType => match value {
/*
Example of the response
1) (integer) 2
2) "json:2"
3) 1) "__VEC_score"
2) "11.1100006104"
3) "$"
4) "{\"vec\":[1.1,1.2,1.3,1.4,1.5,1.6]}"
4) "json:0"
5) 1) "__VEC_score"
2) "91"
3) "$"
4) "{\"vec\":[1,2,3,4,5,6]}"

Converting response to
1) (integer) 2
2) 1# "json:2" =>
1# "__VEC_score" => "11.1100006104"
2# "$" => "{\"vec\":[1.1,1.2,1.3,1.4,1.5,1.6]}"
2# "json:0" =>
1# "__VEC_score" => "91"
2# "$" => "{\"vec\":[1,2,3,4,5,6]}"

Response may contain only 1 element, no conversion in that case.
*/
Value::Array(ref array) if array.len() == 1 => Ok(value),
Value::Array(mut array) => {
Ok(Value::Array(vec![
array.remove(0),
convert_to_expected_type(Value::Array(array), Some(ExpectedReturnType::Map {
key_type: &Some(ExpectedReturnType::BulkString),
value_type: &Some(ExpectedReturnType::Map {
key_type: &Some(ExpectedReturnType::BulkString),
value_type: &Some(ExpectedReturnType::BulkString),
}),
}))?
]))
},
_ => Err((
ErrorKind::TypeError,
"Response couldn't be converted to Pair",
format!("(response was {:?})", get_value_type(&value)),
)
.into())
},
}
}

Expand Down Expand Up @@ -1256,6 +1303,7 @@ pub(crate) fn expected_type_for_cmd(cmd: &Cmd) -> Option<ExpectedReturnType> {
key_type: &None,
value_type: &None,
}),
b"FT.SEARCH" => Some(ExpectedReturnType::FTSearchReturnType),
_ => None,
}
}
Expand Down
135 changes: 135 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 @@ -2,6 +2,7 @@
package glide.api.commands.servermodules;

import static glide.api.models.GlideString.gs;
import static glide.utils.ArrayTransformUtils.concatenateArrays;

import glide.api.BaseClient;
import glide.api.GlideClient;
Expand All @@ -10,6 +11,7 @@
import glide.api.models.GlideString;
import glide.api.models.commands.FT.FTCreateOptions;
import glide.api.models.commands.FT.FTCreateOptions.FieldInfo;
import glide.api.models.commands.FT.FTSearchOptions;
import java.util.Arrays;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Stream;
Expand Down Expand Up @@ -140,6 +142,139 @@ public static CompletableFuture<String> create(
return executeCommand(client, args, false);
}

/**
* Uses the provided query expression to locate keys within an index. Once located, the count
* and/or content of indexed fields within those keys can be returned.
*
* @param client The client to execute the command.
* @param indexName The index name to search into.
* @param query The text query to search.
* @param options The search options - see {@link FTSearchOptions}.
* @return A two element array, where first element is count of documents in result set, and the
* second element, which has format <code>
* {@literal Map<GlideString, Map<GlideString, GlideString>>}</code> - a mapping between
* document names and map of their attributes.<br>
* If {@link FTSearchOptions.FTSearchOptionsBuilder#count()} or {@link
* FTSearchOptions.FTSearchOptionsBuilder#limit(int, int)} with values <code>0, 0</code> is
* set, the command returns array with only one element - the count of the documents.
* @example
* <pre>{@code
* byte[] vector = new byte[24];
* Arrays.fill(vector, (byte) 0);
* var result = FT.search(client, "json_idx1", "*=>[KNN 2 @VEC $query_vec]",
* FTSearchOptions.builder().params(Map.of(gs("query_vec"), gs(vector))).build())
* .get();
* assertArrayEquals(result, new Object[] { 2L, Map.of(
* gs("json:2"), Map.of(gs("__VEC_score"), gs("11.1100006104"), gs("$"), gs("{\"vec\":[1.1,1.2,1.3,1.4,1.5,1.6]}")),
* gs("json:0"), Map.of(gs("__VEC_score"), gs("91"), gs("$"), gs("{\"vec\":[1,2,3,4,5,6]}")))
* });
* }</pre>
*/
public static CompletableFuture<Object[]> search(
@NonNull BaseClient client,
@NonNull String indexName,
@NonNull String query,
@NonNull FTSearchOptions options) {
var args =
concatenateArrays(
new GlideString[] {gs("FT.SEARCH"), gs(indexName), gs(query)}, options.toArgs());
return executeCommand(client, args, false);
}

/**
* Uses the provided query expression to locate keys within an index. Once located, the count
* and/or content of indexed fields within those keys can be returned.
*
* @param client The client to execute the command.
* @param indexName The index name to search into.
* @param query The text query to search.
* @param options The search options - see {@link FTSearchOptions}.
* @return A two element array, where first element is count of documents in result set, and the
* second element, which has format <code>
* {@literal Map<GlideString, Map<GlideString, GlideString>>}</code> - a mapping between
* document names and map of their attributes.<br>
* If {@link FTSearchOptions.FTSearchOptionsBuilder#count()} or {@link
* FTSearchOptions.FTSearchOptionsBuilder#limit(int, int)} with values <code>0, 0</code> is
* set, the command returns array with only one element - the count of the documents.
* @example
* <pre>{@code
* byte[] vector = new byte[24];
* Arrays.fill(vector, (byte) 0);
* var result = FT.search(client, gs("json_idx1"), gs("*=>[KNN 2 @VEC $query_vec]"),
* FTSearchOptions.builder().params(Map.of(gs("query_vec"), gs(vector))).build())
* .get();
* assertArrayEquals(result, new Object[] { 2L, Map.of(
* gs("json:2"), Map.of(gs("__VEC_score"), gs("11.1100006104"), gs("$"), gs("{\"vec\":[1.1,1.2,1.3,1.4,1.5,1.6]}")),
* gs("json:0"), Map.of(gs("__VEC_score"), gs("91"), gs("$"), gs("{\"vec\":[1,2,3,4,5,6]}")))
* });
* }</pre>
*/
public static CompletableFuture<Object[]> search(
@NonNull BaseClient client,
@NonNull GlideString indexName,
@NonNull GlideString query,
@NonNull FTSearchOptions options) {
var args =
concatenateArrays(new GlideString[] {gs("FT.SEARCH"), indexName, query}, options.toArgs());
return executeCommand(client, args, false);
}

/**
* Uses the provided query expression to locate keys within an index. Once located, the count
* and/or content of indexed fields within those keys can be returned.
*
* @param client The client to execute the command.
* @param indexName The index name to search into.
* @param query The text query to search.
* @return A two element array, where first element is count of documents in result set, and the
* second element, which has format <code>
* {@literal Map<GlideString, Map<GlideString, GlideString>>}</code> - a mapping between
* document names and map of their attributes.
* @example
* <pre>{@code
* byte[] vector = new byte[24];
* Arrays.fill(vector, (byte) 0);
* var result = FT.search(client, "json_idx1", "*=>[KNN 2 @VEC $query_vec]").get();
* assertArrayEquals(result, new Object[] { 2L, Map.of(
* gs("json:2"), Map.of(gs("__VEC_score"), gs("11.1100006104"), gs("$"), gs("{\"vec\":[1.1,1.2,1.3,1.4,1.5,1.6]}")),
* gs("json:0"), Map.of(gs("__VEC_score"), gs("91"), gs("$"), gs("{\"vec\":[1,2,3,4,5,6]}")))
* });
* }</pre>
*/
public static CompletableFuture<Object[]> search(
@NonNull BaseClient client, @NonNull String indexName, @NonNull String query) {
var args = new GlideString[] {gs("FT.SEARCH"), gs(indexName), gs(query)};
return executeCommand(client, args, false);
}

/**
* Uses the provided query expression to locate keys within an index. Once located, the count
* and/or content of indexed fields within those keys can be returned.
*
* @param client The client to execute the command.
* @param indexName The index name to search into.
* @param query The text query to search.
* @return A two element array, where first element is count of documents in result set, and the
* second element, which has format <code>
* {@literal Map<GlideString, Map<GlideString, GlideString>>}</code> - a mapping between
* document names and map of their attributes.
* @example
* <pre>{@code
* byte[] vector = new byte[24];
* Arrays.fill(vector, (byte) 0);
* var result = FT.search(client, gs("json_idx1"), gs("*=>[KNN 2 @VEC $query_vec]")).get();
* assertArrayEquals(result, new Object[] { 2L, Map.of(
* gs("json:2"), Map.of(gs("__VEC_score"), gs("11.1100006104"), gs("$"), gs("{\"vec\":[1.1,1.2,1.3,1.4,1.5,1.6]}")),
* gs("json:0"), Map.of(gs("__VEC_score"), gs("91"), gs("$"), gs("{\"vec\":[1,2,3,4,5,6]}")))
* });
* }</pre>
*/
public static CompletableFuture<Object[]> search(
@NonNull BaseClient client, @NonNull GlideString indexName, @NonNull GlideString query) {
var args = new GlideString[] {gs("FT.SEARCH"), indexName, query};
return executeCommand(client, args, false);
}

/**
* Deletes an index and associated content. Indexed document keys are unaffected.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */
package glide.api.models.commands.FT;

import static glide.api.models.GlideString.gs;

import glide.api.commands.servermodules.FT;
import glide.api.models.GlideString;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import lombok.Builder;
import org.apache.commons.lang3.tuple.Pair;

/** Mandatory parameters for {@link FT#search}. */
@Builder
public class FTSearchOptions {

@Builder.Default private final Map<GlideString, GlideString> identifiers = new HashMap<>();
prateek-kumar-improving marked this conversation as resolved.
Show resolved Hide resolved

/** Query timeout in milliseconds. */
private final Integer timeout;

private final Pair<Integer, Integer> limit;

@Builder.Default private final boolean count = false;

/**
* Query parameters, which could be referenced in the query by <code>$</code> sign, followed by
* the parameter name.
*/
@Builder.Default private final Map<GlideString, GlideString> params = new HashMap<>();

// TODO maxstale?
// dialect is no-op

/** Convert to module API. */
public GlideString[] toArgs() {
var args = new ArrayList<GlideString>();
if (!identifiers.isEmpty()) {
args.add(gs("RETURN"));
int tokenCount = 0;
for (var pair : identifiers.entrySet()) {
tokenCount++;
args.add(pair.getKey());
if (pair.getValue() != null) {
tokenCount += 2;
args.add(gs("AS"));
args.add(pair.getValue());
}
}
args.add(1, gs(Integer.toString(tokenCount)));
}
if (timeout != null) {
args.add(gs("TIMEOUT"));
args.add(gs(timeout.toString()));
}
if (!params.isEmpty()) {
args.add(gs("PARAMS"));
args.add(gs(Integer.toString(params.size() * 2)));
params.forEach(
(name, value) -> {
args.add(name);
args.add(value);
});
}
if (limit != null) {
args.add(gs("LIMIT"));
args.add(gs(Integer.toString(limit.getLeft())));
args.add(gs(Integer.toString(limit.getRight())));
} else if (count) {
Yury-Fridlyand marked this conversation as resolved.
Show resolved Hide resolved
args.add(gs("COUNT"));
}
return args.toArray(GlideString[]::new);
}

public static class FTSearchOptionsBuilder {

// private - hiding this API from user
void limit(Pair<Integer, Integer> limit) {}

void count(boolean count) {}

void identifiers(Map<GlideString, GlideString> identifiers) {}

/** Add a field to be returned. */
public FTSearchOptionsBuilder addReturnField(String field) {
this.identifiers$value.put(gs(field), null);
return this;
}

/** Add a field with an alias to be returned. */
public FTSearchOptionsBuilder addReturnField(String field, String alias) {
this.identifiers$value.put(gs(field), gs(alias));
return this;
}

/** Add a field to be returned. */
public FTSearchOptionsBuilder addReturnField(GlideString field) {
this.identifiers$value.put(field, null);
return this;
}

/** Add a field with an alias to be returned. */
public FTSearchOptionsBuilder addReturnField(GlideString field, GlideString alias) {
this.identifiers$value.put(field, alias);
return this;
}

/**
* Configure query pagination. By default only first 10 documents are returned.
*
* @param offset Zero-based offset.
* @param count Number of elements to return.
*/
public FTSearchOptionsBuilder limit(int offset, int count) {
this.limit = Pair.of(offset, count);
return this;
}

/**
* Once set, the query will return only number of documents in the result set without actually
* returning them.
*/
public FTSearchOptionsBuilder count() {
this.count$value = true;
this.count$set = true;
return this;
}
}
}
Loading
Loading