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

FT.SEARCH #423

Closed
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
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
1 change: 1 addition & 0 deletions glide-core/src/protobuf/command_request.proto
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,7 @@ enum RequestType {
ScriptShow = 218;

FtCreate = 2000;
FtSearch = 2001;
}

message Command {
Expand Down
3 changes: 3 additions & 0 deletions glide-core/src/request_type.rs
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,7 @@ pub enum RequestType {
ScriptKill = 217,
ScriptShow = 218,
FtCreate = 2000,
FtSearch = 2001,
}

fn get_two_word_command(first: &str, second: &str) -> Cmd {
Expand Down Expand Up @@ -459,6 +460,7 @@ impl From<::protobuf::EnumOrUnknown<ProtobufRequestType>> for RequestType {
ProtobufRequestType::ScriptKill => RequestType::ScriptKill,
ProtobufRequestType::ScriptShow => RequestType::ScriptShow,
ProtobufRequestType::FtCreate => RequestType::FtCreate,
ProtobufRequestType::FtSearch => RequestType::FtSearch,
}
}
}
Expand Down Expand Up @@ -688,6 +690,7 @@ impl RequestType {
RequestType::ScriptFlush => Some(get_two_word_command("SCRIPT", "FLUSH")),
RequestType::ScriptKill => Some(get_two_word_command("SCRIPT", "KILL")),
RequestType::FtCreate => Some(cmd("FT.CREATE")),
RequestType::FtSearch => Some(cmd("FT.SEARCH")),
}
}
}
4 changes: 2 additions & 2 deletions java/client/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -165,8 +165,8 @@ jar.dependsOn('copyNativeLib')
javadoc.dependsOn('copyNativeLib')
copyNativeLib.dependsOn('buildRustRelease')
compileTestJava.dependsOn('copyNativeLib')
test.dependsOn('buildRust')
testFfi.dependsOn('buildRust')
test.dependsOn('buildRustRelease')
testFfi.dependsOn('buildRustRelease')

test {
exclude "glide/ffi/FfiTest.class"
Expand Down
13 changes: 13 additions & 0 deletions java/client/src/main/java/glide/api/BaseClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import static command_request.CommandRequestOuterClass.RequestType.FCall;
import static command_request.CommandRequestOuterClass.RequestType.FCallReadOnly;
import static command_request.CommandRequestOuterClass.RequestType.FtCreate;
import static command_request.CommandRequestOuterClass.RequestType.FtSearch;
import static command_request.CommandRequestOuterClass.RequestType.GeoAdd;
import static command_request.CommandRequestOuterClass.RequestType.GeoDist;
import static command_request.CommandRequestOuterClass.RequestType.GeoHash;
Expand Down Expand Up @@ -268,6 +269,7 @@
import glide.api.models.commands.stream.StreamTrimOptions;
import glide.api.models.commands.vss.FTCreateOptions.FieldInfo;
import glide.api.models.commands.vss.FTCreateOptions.IndexType;
import glide.api.models.commands.vss.FTSearchOptions;
import glide.api.models.configuration.BaseClientConfiguration;
import glide.api.models.configuration.BaseSubscriptionConfiguration;
import glide.api.models.exceptions.ConfigurationError;
Expand Down Expand Up @@ -5167,4 +5169,15 @@ public CompletableFuture<String> ftcreate(
return commandManager.submitNewCommand(
FtCreate, args.toArray(String[]::new), this::handleStringResponse);
}

@Override
public CompletableFuture<Object[]> ftsearch(
String indexName, String query, FTSearchOptions options) {
var args =
concatenateArrays(
new GlideString[] {gs(indexName), gs(query)},
options.toArgs(),
new GlideString[] {gs("DIALECT"), gs("2")});
return commandManager.submitNewCommand(FtSearch, args, this::handleArrayResponseBinary);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

import glide.api.models.commands.vss.FTCreateOptions.FieldInfo;
import glide.api.models.commands.vss.FTCreateOptions.IndexType;
import glide.api.models.commands.vss.FTSearchOptions;
import glide.api.models.commands.vss.FTSearchOptions.FTSearchOptionsBuilder;
import java.util.concurrent.CompletableFuture;

public interface VectorSearchBaseCommands {
Expand Down Expand Up @@ -30,4 +32,34 @@ public interface VectorSearchBaseCommands {
*/
CompletableFuture<String> ftcreate(
String indexName, IndexType indexType, String[] prefixes, FieldInfo[] fields);

/**
* 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.
*
* @see TODO
jonathanl-bq marked this conversation as resolved.
Show resolved Hide resolved
* @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>, is a mapping between
* document names and map of their attributes.<br>
* If {@link FTSearchOptionsBuilder#count()} or {@link 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 = client.ftsearch("json_idx1", "*=>[KNN 2 @VEC $query_vec]",
* FTSearchOptions.builder().params(Map.of("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>
*/
CompletableFuture<Object[]> ftsearch(String indexName, String query, FTSearchOptions options);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */
package glide.api.models.commands.vss;

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

import glide.api.commands.VectorSearchBaseCommands;
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 VectorSearchBaseCommands#ftsearch}. */
@Builder
public class FTSearchOptions {
/**
* Which fields of a key to be returned.<br>
* Map keys are field names and their values are aliases. Aliases are optional, use <code>null
* </code> to omit.
*/
@Builder.Default private final Map<String, String> 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<String, GlideString> params = new HashMap<>();

// TODO maxstale?

Choose a reason for hiding this comment

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

What is maxstale? I don't see it in the list of options in the Redis docs.

Copy link
Author

Choose a reason for hiding this comment

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

Undocumented option in aws implementation.

Choose a reason for hiding this comment

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

Hmmm, I wonder why it's not documented. Is there anyone we can ask about this?

Copy link
Author

Choose a reason for hiding this comment

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

I found this in one of the examples. I hope it will be documented when the module comes to open source.

// 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(gs(pair.getKey()));
if (pair.getValue() != null) {
tokenCount += 2;
args.add(gs("AS"));
args.add(gs(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(gs(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) {}
jonathanl-bq marked this conversation as resolved.
Show resolved Hide resolved

void count(boolean count) {}

/**
* 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;
}
}
}
4 changes: 2 additions & 2 deletions java/integTest/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,8 @@ test {

tasks.register('modulesTest', Test) {
doFirst {
systemProperty 'test.server.standalone.ports', 6379
systemProperty 'test.server.cluster.ports', 7000
clusterPorts = [7000]
standalonePorts = [6379]
}

filter {
Expand Down
Loading
Loading