Skip to content

Commit 7ce425d

Browse files
uglidea-TODO-rov
andauthored
Add support for EPSILON and WITHATTRIBS arguments in VSIM command (#3449)
* Add support for EPSILON and WITHATTRIBS arguments in VSIM * Fix integration test to require min redis 8.0 * Fix API doc * Rm since in response --------- Co-authored-by: aleksandar.todorov <[email protected]>
1 parent c9bbd5a commit 7ce425d

22 files changed

+1428
-7
lines changed

.github/wordlist.txt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -340,4 +340,6 @@ SVS
340340
Hitless
341341
hitless
342342
noninstantiability
343-
Changelog
343+
Changelog
344+
WITHATTRIBS
345+
vsimWithScoreWithAttribs

RELEASE-NOTES.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,4 +63,4 @@ If you need any support, meet Lettuce at
6363

6464
---
6565

66-
**Full Changelog**: [6.8.0.RELEASE...7.0.0.BETA1](https://github.com/redis/lettuce/compare/6.8.0.RELEASE...v7.0.0.BETA1)
66+
**Full Changelog**: [6.8.0.RELEASE...7.0.0.BETA1](https://github.com/redis/lettuce/compare/6.8.0.RELEASE...v7.0.0.BETA1)

docs/user-guide/vector-sets.md

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,10 +121,47 @@ VSimArgs simArgs = VSimArgs.Builder
121121
.build();
122122

123123
Map<String, Double> resultsWithScores = vectorSet.vsimWithScore("points", simArgs, 0.9, 0.1);
124-
resultsWithScores.forEach((element, score) ->
124+
resultsWithScores.forEach((element, score) ->
125125
System.out.println(element + ": " + score));
126126
```
127127

128+
### Similarity Cutoff with EPSILON
129+
130+
Use EPSILON to apply a maximum distance cutoff so that only sufficiently similar results are returned.
131+
The cutoff is defined as a distance threshold epsilon in [0.0, 1.0]; results must have similarity ≥ 1 − epsilon.
132+
Smaller epsilon values yield fewer, more similar results.
133+
134+
```java
135+
VSimArgs simArgs = VSimArgs.Builder
136+
.count(10)
137+
.epsilon(0.2) // distance cutoff; results have similarity >= 0.8
138+
.build();
139+
140+
Map<String, Double> results = vectorSet.vsimWithScore("points", simArgs, 0.9, 0.1);
141+
```
142+
143+
144+
### Including Attributes in Results with WITHATTRIBS
145+
146+
Attributes are included by using the API variant that emits WITHATTRIBS. Use vsimWithScoreWithAttribs(...) to obtain scores and attributes per element.
147+
148+
```java
149+
VSimArgs args = VSimArgs.Builder
150+
.count(10)
151+
.epsilon(0.2)
152+
.build();
153+
154+
Map<String, VSimScoreAttribs> results = vectorSet.vsimWithScoreWithAttribs("points", args, 0.9, 0.1);
155+
results.forEach((element, sa) -> {
156+
double score = sa.getScore();
157+
String attrs = sa.getAttributes();
158+
System.out.println(element + ": score=" + score + ", attrs=" + attrs);
159+
});
160+
```
161+
162+
Note: WITHATTRIBS requires a Redis version that supports returning attributes (Redis 8.2+). Methods are marked @Experimental and subject to change.
163+
164+
128165
## Element Attributes and Filtering
129166

130167
### Setting and Getting Attributes
@@ -761,4 +798,4 @@ public class VectorMigrationService {
761798
public Map<String, Object> getMetadata() { return metadata; }
762799
}
763800
}
764-
```
801+
```

src/main/java/io/lettuce/core/AbstractRedisAsyncCommands.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363
import io.lettuce.core.search.arguments.SugGetArgs;
6464
import io.lettuce.core.search.arguments.SynUpdateArgs;
6565
import io.lettuce.core.vector.RawVector;
66+
import io.lettuce.core.vector.VSimScoreAttribs;
6667
import io.lettuce.core.vector.VectorMetadata;
6768

6869
import java.time.Duration;
@@ -2073,6 +2074,26 @@ public RedisFuture<Map<V, Double>> vsimWithScore(K key, VSimArgs args, V element
20732074
return dispatch(vectorSetCommandBuilder.vsimWithScore(key, args, element));
20742075
}
20752076

2077+
@Override
2078+
public RedisFuture<Map<V, VSimScoreAttribs>> vsimWithScoreWithAttribs(K key, Double... vectors) {
2079+
return dispatch(vectorSetCommandBuilder.vsimWithScoreWithAttribs(key, null, vectors));
2080+
}
2081+
2082+
@Override
2083+
public RedisFuture<Map<V, VSimScoreAttribs>> vsimWithScoreWithAttribs(K key, V element) {
2084+
return dispatch(vectorSetCommandBuilder.vsimWithScoreWithAttribs(key, null, element));
2085+
}
2086+
2087+
@Override
2088+
public RedisFuture<Map<V, VSimScoreAttribs>> vsimWithScoreWithAttribs(K key, VSimArgs args, Double... vectors) {
2089+
return dispatch(vectorSetCommandBuilder.vsimWithScoreWithAttribs(key, args, vectors));
2090+
}
2091+
2092+
@Override
2093+
public RedisFuture<Map<V, VSimScoreAttribs>> vsimWithScoreWithAttribs(K key, VSimArgs args, V element) {
2094+
return dispatch(vectorSetCommandBuilder.vsimWithScoreWithAttribs(key, args, element));
2095+
}
2096+
20762097
@Override
20772098
public RedisFuture<List<K>> keys(K pattern) {
20782099
return dispatch(commandBuilder.keys(pattern));

src/main/java/io/lettuce/core/AbstractRedisReactiveCommands.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@
6767
import io.lettuce.core.tracing.TraceContextProvider;
6868
import io.lettuce.core.tracing.Tracing;
6969
import io.lettuce.core.vector.RawVector;
70+
import io.lettuce.core.vector.VSimScoreAttribs;
7071
import io.lettuce.core.vector.VectorMetadata;
7172
import io.netty.util.concurrent.EventExecutorGroup;
7273
import io.netty.util.concurrent.ImmediateEventExecutor;
@@ -2138,6 +2139,26 @@ public Mono<Map<V, Double>> vsimWithScore(K key, VSimArgs args, V element) {
21382139
return createMono(() -> vectorSetCommandBuilder.vsimWithScore(key, args, element));
21392140
}
21402141

2142+
@Override
2143+
public Mono<Map<V, VSimScoreAttribs>> vsimWithScoreWithAttribs(K key, Double... vectors) {
2144+
return createMono(() -> vectorSetCommandBuilder.vsimWithScoreWithAttribs(key, null, vectors));
2145+
}
2146+
2147+
@Override
2148+
public Mono<Map<V, VSimScoreAttribs>> vsimWithScoreWithAttribs(K key, V element) {
2149+
return createMono(() -> vectorSetCommandBuilder.vsimWithScoreWithAttribs(key, null, element));
2150+
}
2151+
2152+
@Override
2153+
public Mono<Map<V, VSimScoreAttribs>> vsimWithScoreWithAttribs(K key, VSimArgs args, Double... vectors) {
2154+
return createMono(() -> vectorSetCommandBuilder.vsimWithScoreWithAttribs(key, args, vectors));
2155+
}
2156+
2157+
@Override
2158+
public Mono<Map<V, VSimScoreAttribs>> vsimWithScoreWithAttribs(K key, VSimArgs args, V element) {
2159+
return createMono(() -> vectorSetCommandBuilder.vsimWithScoreWithAttribs(key, args, element));
2160+
}
2161+
21412162
@Override
21422163
public Flux<K> keys(K pattern) {
21432164
return createDissolvingFlux(() -> commandBuilder.keys(pattern));

src/main/java/io/lettuce/core/RedisVectorSetCommandBuilder.java

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import io.lettuce.core.protocol.RedisCommand;
1919
import io.lettuce.core.vector.RawVector;
2020
import io.lettuce.core.vector.VectorMetadata;
21+
import io.lettuce.core.vector.VSimScoreAttribs;
2122

2223
import java.util.Arrays;
2324
import java.util.List;
@@ -383,7 +384,7 @@ public Command<K, V, List<V>> vsim(K key, VSimArgs vSimArgs, Double[] vectors) {
383384
if (vectors.length > 1) {
384385
args.add(CommandKeyword.VALUES);
385386
args.add(vectors.length);
386-
Arrays.stream(vectors).map(Object::toString).forEach(args::add);
387+
Arrays.stream(vectors).forEach(args::add);
387388
} else {
388389
args.add(vectors[0]);
389390
}
@@ -463,7 +464,7 @@ public Command<K, V, Map<V, Double>> vsimWithScore(K key, VSimArgs vSimArgs, Dou
463464
if (vectors.length > 1) {
464465
args.add(CommandKeyword.VALUES);
465466
args.add(vectors.length);
466-
Arrays.stream(vectors).map(Object::toString).forEach(args::add);
467+
Arrays.stream(vectors).forEach(args::add);
467468
} else {
468469
args.add(vectors[0]);
469470
}
@@ -500,4 +501,50 @@ public Command<K, V, Map<V, Double>> vsimWithScore(K key, VSimArgs vSimArgs, V e
500501
return createCommand(VSIM, new ValueDoubleMapOutput<>(codec), args);
501502
}
502503

504+
// WITHSCORES WITHATTRIBS variants
505+
public Command<K, V, Map<V, VSimScoreAttribs>> vsimWithScoreWithAttribs(K key, Double[] vectors) {
506+
return vsimWithScoreWithAttribs(key, null, vectors);
507+
}
508+
509+
public Command<K, V, Map<V, VSimScoreAttribs>> vsimWithScoreWithAttribs(K key, V element) {
510+
return vsimWithScoreWithAttribs(key, null, element);
511+
}
512+
513+
public Command<K, V, Map<V, VSimScoreAttribs>> vsimWithScoreWithAttribs(K key, VSimArgs vSimArgs, Double[] vectors) {
514+
notNullKey(key);
515+
notEmpty(vectors);
516+
517+
CommandArgs<K, V> args = new CommandArgs<>(codec).addKey(key);
518+
519+
if (vectors.length > 1) {
520+
args.add(CommandKeyword.VALUES);
521+
args.add(vectors.length);
522+
Arrays.stream(vectors).forEach(args::add);
523+
} else {
524+
args.add(vectors[0]);
525+
}
526+
527+
args.add(WITHSCORES).add(WITHATTRIBS);
528+
529+
if (vSimArgs != null) {
530+
vSimArgs.build(args);
531+
}
532+
533+
return createCommand(VSIM, new VSimScoreAttribsMapOutput<>(codec), args);
534+
}
535+
536+
public Command<K, V, Map<V, VSimScoreAttribs>> vsimWithScoreWithAttribs(K key, VSimArgs vSimArgs, V element) {
537+
notNullKey(key);
538+
notNullKey(element);
539+
540+
CommandArgs<K, V> args = new CommandArgs<>(codec).addKey(key).add(ELE).addValue(element).add(WITHSCORES)
541+
.add(WITHATTRIBS);
542+
543+
if (vSimArgs != null) {
544+
vSimArgs.build(args);
545+
}
546+
547+
return createCommand(VSIM, new VSimScoreAttribsMapOutput<>(codec), args);
548+
}
549+
503550
}

src/main/java/io/lettuce/core/VSimArgs.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import io.lettuce.core.protocol.CommandKeyword;
1212

1313
import java.util.Optional;
14+
import java.util.OptionalDouble;
1415

1516
/**
1617
* Argument list builder for the Redis <a href="https://redis.io/docs/latest/commands/vsim/">VSIM</a> command. Static import the
@@ -36,6 +37,8 @@ public class VSimArgs implements CompositeArgument {
3637

3738
private Optional<Boolean> noThread = Optional.empty();
3839

40+
private Optional<Double> epsilon = Optional.empty();
41+
3942
/**
4043
* Builder entry points for {@link VSimArgs}.
4144
* <p>
@@ -266,10 +269,28 @@ public VSimArgs noThread(boolean noThread) {
266269
return this;
267270
}
268271

272+
/**
273+
* Sets the EPSILON distance cutoff for approximate vector similarity matching; results must have similarity ≥ 1 − epsilon.
274+
* In other words, this is a maximum distance threshold used to filter VSIM results.
275+
*
276+
* @param delta the similarity threshold delta value, must be within [0.0, 1.0] inclusive
277+
* @return {@code this}
278+
* @throws IllegalArgumentException if delta is outside the valid range [0.0, 1.0]
279+
*/
280+
public VSimArgs epsilon(double delta) {
281+
if (delta < 0.0 || delta > 1.0) {
282+
throw new IllegalArgumentException("EPSILON must be in range [0.0, 1.0], got: " + delta);
283+
}
284+
this.epsilon = Optional.of(delta);
285+
return this;
286+
}
287+
269288
@Override
270289
public <K, V> void build(CommandArgs<K, V> args) {
271290
count.ifPresent(Long -> args.add(CommandKeyword.COUNT).add(Long));
272291

292+
epsilon.ifPresent(d -> args.add(CommandKeyword.EPSILON).add(d));
293+
273294
explorationFactor.ifPresent(Long -> args.add(CommandKeyword.EF).add(Long));
274295

275296
filter.ifPresent(s -> args.add(CommandKeyword.FILTER).add(s));

src/main/java/io/lettuce/core/api/async/RedisVectorSetAsyncCommands.java

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import io.lettuce.core.annotations.Experimental;
1515
import io.lettuce.core.json.JsonValue;
1616
import io.lettuce.core.vector.RawVector;
17+
import io.lettuce.core.vector.VSimScoreAttribs;
1718
import io.lettuce.core.vector.VectorMetadata;
1819

1920
/**
@@ -487,4 +488,78 @@ public interface RedisVectorSetAsyncCommands<K, V> {
487488
@Experimental
488489
RedisFuture<Map<V, Double>> vsimWithScore(K key, VSimArgs args, V element);
489490

491+
/**
492+
* Finds the most similar vectors to the given query vector in the vector set stored at {@code key} and returns them with
493+
* their similarity scores and attributes.
494+
* <p>
495+
* The similarity scores represent the distance between the query vector and the result vectors. Attributes are returned as
496+
* a server-provided string.
497+
* <p>
498+
* Time complexity: O(log(N)) where N is the number of elements in the vector set
499+
*
500+
* @param key the key of the vector set
501+
* @param vectors the query vector values as floating point numbers
502+
* @return a map of elements to their (score, attributes), or an empty map if the key does not exist
503+
* @since 7.0
504+
* @see <a href="https://redis.io/docs/latest/commands/vsim/">Redis Documentation: VSIM</a>
505+
*/
506+
@Experimental
507+
RedisFuture<Map<V, VSimScoreAttribs>> vsimWithScoreWithAttribs(K key, Double... vectors);
508+
509+
/**
510+
* Finds the most similar vectors to the given element's vector in the vector set stored at {@code key} and returns them
511+
* with their similarity scores and attributes.
512+
* <p>
513+
* The similarity scores represent the distance between the specified element's vector and the result vectors. Attributes
514+
* are returned as a server-provided string.
515+
* <p>
516+
* Time complexity: O(log(N)) where N is the number of elements in the vector set
517+
*
518+
* @param key the key of the vector set
519+
* @param element the name of the element whose vector will be used as the query
520+
* @return a map of elements to their (score, attributes), or an empty map if the key or element does not exist
521+
* @since 7.0
522+
* @see <a href="https://redis.io/docs/latest/commands/vsim/">Redis Documentation: VSIM</a>
523+
*/
524+
@Experimental
525+
RedisFuture<Map<V, VSimScoreAttribs>> vsimWithScoreWithAttribs(K key, V element);
526+
527+
/**
528+
* Finds the most similar vectors to the given query vector in the vector set stored at {@code key} with additional options
529+
* and returns them with their similarity scores and attributes.
530+
* <p>
531+
* The {@link VSimArgs} allows configuring various options such as the number of results ({@code COUNT}), epsilon cutoff
532+
* ({@code EPSILON}), exploration factor ({@code EF}), and filtering.
533+
* <p>
534+
* Time complexity: O(log(N)) where N is the number of elements in the vector set
535+
*
536+
* @param key the key of the vector set
537+
* @param args the additional arguments for the VSIM command
538+
* @param vectors the query vector values as floating point numbers
539+
* @return a map of elements to their (score, attributes), or an empty map if the key does not exist
540+
* @since 7.0
541+
* @see <a href="https://redis.io/docs/latest/commands/vsim/">Redis Documentation: VSIM</a>
542+
*/
543+
@Experimental
544+
RedisFuture<Map<V, VSimScoreAttribs>> vsimWithScoreWithAttribs(K key, VSimArgs args, Double... vectors);
545+
546+
/**
547+
* Finds the most similar vectors to the given element's vector in the vector set stored at {@code key} with additional
548+
* options and returns them with their similarity scores and attributes.
549+
* <p>
550+
* This method combines using an existing element's vector as the query with the ability to specify additional options via
551+
* {@link VSimArgs}. Attributes are returned as a server-provided string.
552+
* <p>
553+
* Time complexity: O(log(N)) where N is the number of elements in the vector set
554+
*
555+
* @param key the key of the vector set
556+
* @param element the name of the element whose vector will be used as the query
557+
* @param args the additional arguments for the VSIM command
558+
* @return a map of elements to their (score, attributes), or an empty map if the key or element does not exist
559+
* @since 7.0
560+
* @see <a href="https://redis.io/docs/latest/commands/vsim/">Redis Documentation: VSIM</a>
561+
*/
562+
@Experimental
563+
RedisFuture<Map<V, VSimScoreAttribs>> vsimWithScoreWithAttribs(K key, VSimArgs args, V element);
564+
490565
}

0 commit comments

Comments
 (0)