Skip to content

Commit

Permalink
Java: FT.SEARCH (valkey-io#2439)
Browse files Browse the repository at this point in the history
* `FT.CREATE`

Signed-off-by: Yury-Fridlyand <yury.fridlyand@improving.com>
  • Loading branch information
Yury-Fridlyand authored and Muhammad-awawdi-amazon committed Oct 22, 2024
1 parent 6904c89 commit 46ff569
Show file tree
Hide file tree
Showing 5 changed files with 426 additions and 1 deletion.
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", "*").get();
* assertArrayEquals(result, new Object[] { 2L, Map.of(
* gs("json:2"), Map.of(gs("$"), gs("{\"vec\":[1.1,1.2,1.3,1.4,1.5,1.6]}")),
* gs("json:0"), Map.of(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("*")).get();
* assertArrayEquals(result, new Object[] { 2L, Map.of(
* gs("json:2"), Map.of(gs("$"), gs("{\"vec\":[1.1,1.2,1.3,1.4,1.5,1.6]}")),
* gs("json:0"), Map.of(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,131 @@
/** 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<>();

/** 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())));
}
if (count) {
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

0 comments on commit 46ff569

Please sign in to comment.