diff --git a/src/main/java/redis/clients/jedis/search/Schema.java b/src/main/java/redis/clients/jedis/search/Schema.java index 1403aab556..c48e46980a 100644 --- a/src/main/java/redis/clients/jedis/search/Schema.java +++ b/src/main/java/redis/clients/jedis/search/Schema.java @@ -5,7 +5,9 @@ import java.util.Map; import redis.clients.jedis.CommandArguments; +import redis.clients.jedis.args.Rawable; import redis.clients.jedis.params.IParams; +import redis.clients.jedis.util.SafeEncoder; /** * Schema abstracts the schema definition when creating an index. Documents can contain fields not @@ -138,6 +140,21 @@ public Schema addHNSWVectorField(String name, Map attributes) { return this; } + /** + * Add a Vamana vector field to the schema using the SVS-VAMANA algorithm. + * This method provides a convenient way to add SVS-VAMANA vector fields. + * + * @param name the field's name + * @param attributes the SVS-Vamana algorithm configuration attributes + * @return the schema object + */ + public Schema addSvsVamanaVectorField(String name, Map attributes) { + // Use the existing VectorField with SVS_VAMANA algorithm + Map vamanaAttributes = new java.util.HashMap<>(attributes); + fields.add(new VectorField(name, VectorField.VectorAlgo.SVS_VAMANA, vamanaAttributes)); + return this; + } + public Schema addField(Field field) { fields.add(field); return this; @@ -348,9 +365,65 @@ public String toString() { public static class VectorField extends Field { - public enum VectorAlgo { - FLAT, - HNSW + + /** + * Enumeration of supported vector indexing algorithms in Redis. + * Each algorithm has different performance characteristics and use cases. + */ + public enum VectorAlgo implements Rawable { + + /** + * FLAT algorithm provides exact vector search with perfect accuracy. + * Best suited for smaller datasets (< 1M vectors) where search accuracy + * is more important than search latency. + */ + FLAT("FLAT"), + + /** + * HNSW (Hierarchical Navigable Small World) algorithm provides approximate + * vector search with configurable accuracy-performance trade-offs. + * Best suited for larger datasets (> 1M vectors) where search performance + * and scalability are more important than perfect accuracy. + */ + HNSW("HNSW"), + + /** + * SVS_VAMANA algorithm provides high-performance approximate vector search + * optimized for specific use cases with advanced compression and optimization features. + * + *

Characteristics: + *

    + *
  • High-performance approximate search
  • + *
  • Support for vector compression (LVQ, LeanVec)
  • + *
  • Configurable graph construction and search parameters
  • + *
  • Optimized for Intel platforms with fallback support
  • + *
+ * + *

Note: This algorithm may have specific requirements and limitations. + * Consult the Redis documentation for detailed usage guidelines. + */ + SVS_VAMANA("SVS-VAMANA"); + + private final byte[] raw; + + /** + * Creates a VectorAlgorithm enum value. + * + * @param redisParamName the Redis parameter name for this algorithm + */ + VectorAlgo(String redisParamName) { + raw = SafeEncoder.encode(redisParamName); + } + + /** + * Returns the raw byte representation of the algorithm name for Redis commands. + * + * @return the raw bytes of the algorithm name + */ + @Override + public byte[] getRaw() { + return raw; + } } private final VectorAlgo algorithm; @@ -364,7 +437,9 @@ public VectorField(String name, VectorAlgo algorithm, Map attrib @Override public void addTypeArgs(CommandArguments args) { + args.add(algorithm); + args.add(attributes.size() << 1); for (Map.Entry entry : attributes.entrySet()) { args.add(entry.getKey()); diff --git a/src/main/java/redis/clients/jedis/search/schemafields/VectorField.java b/src/main/java/redis/clients/jedis/search/schemafields/VectorField.java index f550f66e77..04a3cf6e05 100644 --- a/src/main/java/redis/clients/jedis/search/schemafields/VectorField.java +++ b/src/main/java/redis/clients/jedis/search/schemafields/VectorField.java @@ -6,13 +6,77 @@ import java.util.LinkedHashMap; import java.util.Map; import redis.clients.jedis.CommandArguments; +import redis.clients.jedis.args.Rawable; import redis.clients.jedis.search.FieldName; +import redis.clients.jedis.util.SafeEncoder; +/** + * Represents a vector field in a Redis search index schema for performing semantic vector searches. + * Vector fields enable high-performance similarity searches over vector embeddings using various + * algorithms and distance metrics. + * + * @see Redis Vector Search Documentation + */ public class VectorField extends SchemaField { - public enum VectorAlgorithm { - FLAT, - HNSW + /** + * Enumeration of supported vector indexing algorithms in Redis. + * Each algorithm has different performance characteristics and use cases. + */ + public enum VectorAlgorithm implements Rawable { + + /** + * FLAT algorithm provides exact vector search with perfect accuracy. + * Best suited for smaller datasets (< 1M vectors) where search accuracy + * is more important than search latency. + */ + FLAT("FLAT"), + + /** + * HNSW (Hierarchical Navigable Small World) algorithm provides approximate + * vector search with configurable accuracy-performance trade-offs. + * Best suited for larger datasets (> 1M vectors) where search performance + * and scalability are more important than perfect accuracy. + */ + HNSW("HNSW"), + + /** + * SVS_VAMANA algorithm provides high-performance approximate vector search + * optimized for specific use cases with advanced compression and optimization features. + * + *

Characteristics: + *

    + *
  • High-performance approximate search
  • + *
  • Support for vector compression (LVQ, LeanVec)
  • + *
  • Configurable graph construction and search parameters
  • + *
  • Optimized for Intel platforms with fallback support
  • + *
+ * + *

Note: This algorithm may have specific requirements and limitations. + * Consult the Redis documentation for detailed usage guidelines. + */ + SVS_VAMANA("SVS-VAMANA"); + + private final byte[] raw; + + /** + * Creates a VectorAlgorithm enum value. + * + * @param redisParamName the Redis parameter name for this algorithm + */ + VectorAlgorithm(String redisParamName) { + raw = SafeEncoder.encode(redisParamName); + } + + /** + * Returns the raw byte representation of the algorithm name for Redis commands. + * + * @return the raw bytes of the algorithm name + */ + @Override + public byte[] getRaw() { + return raw; + } } private final VectorAlgorithm algorithm; @@ -21,29 +85,70 @@ public enum VectorAlgorithm { private boolean indexMissing; // private boolean noIndex; // throws Field `NOINDEX` does not have a type + /** + * Creates a new VectorField with the specified field name, algorithm, and attributes. + * + * @param fieldName the name of the vector field in the index + * @param algorithm the vector indexing algorithm to use + * @param attributes the algorithm-specific configuration attributes + * @throws IllegalArgumentException if required attributes are missing or invalid + */ public VectorField(String fieldName, VectorAlgorithm algorithm, Map attributes) { super(fieldName); this.algorithm = algorithm; this.attributes = attributes; } + /** + * Creates a new VectorField with the specified field name, algorithm, and attributes. + * + * @param fieldName the field name object containing the field name and optional alias + * @param algorithm the vector indexing algorithm to use + * @param attributes the algorithm-specific configuration attributes + * @throws IllegalArgumentException if required attributes are missing or invalid + * @see #VectorField(String, VectorAlgorithm, Map) for detailed attribute documentation + */ public VectorField(FieldName fieldName, VectorAlgorithm algorithm, Map attributes) { super(fieldName); this.algorithm = algorithm; this.attributes = attributes; } + /** + * Sets an alias for this field that can be used in queries instead of the field name. + * This is useful when the field name contains special characters or when you want + * to use a shorter name in queries. + * + * @param attribute the alias name to use for this field in queries + * @return this VectorField instance for method chaining + */ @Override public VectorField as(String attribute) { super.as(attribute); return this; } + /** + * Configures the field to handle missing values during indexing. + * When enabled, documents that don't contain this vector field will still be indexed, + * but won't participate in vector searches. + * + *

This is useful when not all documents in your dataset contain vector embeddings, + * but you still want to index them for other types of searches. + * + * @return this VectorField instance for method chaining + */ public VectorField indexMissing() { this.indexMissing = true; return this; } + /** + * Adds the vector field parameters to the Redis command arguments. + * This method is used internally when creating the search index. + * + * @param args the command arguments to add parameters to + */ @Override public void addParams(CommandArguments args) { args.addParams(fieldName); @@ -58,19 +163,52 @@ public void addParams(CommandArguments args) { } } + /** + * Creates a new Builder instance for constructing VectorField objects using the builder pattern. + * The builder pattern provides a fluent interface for setting field properties and is especially + * useful when dealing with complex vector field configurations. + * + * @return a new Builder instance + */ public static Builder builder() { return new Builder(); } + /** + * Builder class for constructing VectorField instances using the builder pattern. + * Provides a fluent interface for setting vector field properties and attributes. + * + *

Example usage: + *

{@code
+   * VectorField field = VectorField.builder()
+   *     .fieldName("product_embedding")
+   *     .algorithm(VectorAlgorithm.HNSW)
+   *     .addAttribute("TYPE", "FLOAT32")
+   *     .addAttribute("DIM", 768)
+   *     .addAttribute("DISTANCE_METRIC", "COSINE")
+   *     .addAttribute("M", 32)
+   *     .addAttribute("EF_CONSTRUCTION", 200)
+   *     .build();
+   * }
+ */ public static class Builder { private FieldName fieldName; private VectorAlgorithm algorithm; private Map attributes; + /** + * Private constructor to enforce use of the static builder() method. + */ private Builder() { } + /** + * Builds and returns a new VectorField instance with the configured properties. + * + * @return a new VectorField instance + * @throws IllegalArgumentException if required parameters (fieldName, algorithm, or attributes) are not set + */ public VectorField build() { if (fieldName == null || algorithm == null || attributes == null || attributes.isEmpty()) { throw new IllegalArgumentException("All required VectorField parameters are not set."); @@ -78,31 +216,69 @@ public VectorField build() { return new VectorField(fieldName, algorithm, attributes); } + /** + * Sets the field name for the vector field. + * + * @param fieldName the name of the vector field in the index + * @return this Builder instance for method chaining + */ public Builder fieldName(String fieldName) { this.fieldName = FieldName.of(fieldName); return this; } + /** + * Sets the field name using a FieldName object. + * + * @param fieldName the FieldName object containing the field name and optional alias + * @return this Builder instance for method chaining + */ public Builder fieldName(FieldName fieldName) { this.fieldName = fieldName; return this; } + /** + * Sets an alias for the field that can be used in queries. + * + * @param attribute the alias name to use for this field in queries + * @return this Builder instance for method chaining + */ public Builder as(String attribute) { this.fieldName.as(attribute); return this; } + /** + * Sets the vector indexing algorithm to use. + * + * @param algorithm the vector algorithm (FLAT, HNSW, or SVS_VAMANA) + * @return this Builder instance for method chaining + */ public Builder algorithm(VectorAlgorithm algorithm) { this.algorithm = algorithm; return this; } + /** + * Sets all vector field attributes at once, replacing any previously set attributes. + * + * @param attributes a map of attribute names to values + * @return this Builder instance for method chaining + */ public Builder attributes(Map attributes) { this.attributes = attributes; return this; } + /** + * Adds a single attribute to the vector field configuration. + * If this is the first attribute added, initializes the attributes map. + * + * @param name the attribute name + * @param value the attribute value + * @return this Builder instance for method chaining + */ public Builder addAttribute(String name, Object value) { if (this.attributes == null) { this.attributes = new LinkedHashMap<>(); diff --git a/src/test/java/redis/clients/jedis/modules/search/SchemaTest.java b/src/test/java/redis/clients/jedis/modules/search/SchemaTest.java index abb0495576..90013b4089 100644 --- a/src/test/java/redis/clients/jedis/modules/search/SchemaTest.java +++ b/src/test/java/redis/clients/jedis/modules/search/SchemaTest.java @@ -4,6 +4,8 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import java.util.Collections; +import java.util.HashMap; +import java.util.Map; import org.hamcrest.Matchers; import org.junit.jupiter.api.Test; import redis.clients.jedis.search.FieldName; @@ -36,6 +38,24 @@ public void printSchemaTest() throws Exception { assertThat(schemaPrint, Matchers.containsString("VectorField{name='vector', type=VECTOR, algorithm=HNSW")); } + @Test + public void printSvsVamanaSchemaTest() throws Exception { + Map vamanaAttributes = new HashMap<>(); + vamanaAttributes.put("TYPE", "FLOAT32"); + vamanaAttributes.put("DIM", 128); + vamanaAttributes.put("DISTANCE_METRIC", "COSINE"); + vamanaAttributes.put("COMPRESSION", "LVQ8"); + + Schema sc = new Schema() + .addTextField(TITLE, 5.0) + .addVectorField("embedding", Schema.VectorField.VectorAlgo.SVS_VAMANA, vamanaAttributes); + + String schemaPrint = sc.toString(); + assertThat(schemaPrint, Matchers.containsString("VectorField{name='embedding', type=VECTOR, algorithm=SVS_VAMANA")); + assertThat(schemaPrint, Matchers.containsString("TYPE=FLOAT32")); + assertThat(schemaPrint, Matchers.containsString("COMPRESSION=LVQ8")); + } + @Test public void fieldAttributeNull() { assertThrows(IllegalArgumentException.class, () -> FieldName.of("identifier").as(null)); @@ -46,4 +66,30 @@ public void fieldAttributeMultiple() { assertThrows(IllegalStateException.class, () -> FieldName.of("identifier").as("attribute").as("attribute")); assertThrows(IllegalStateException.class, () -> new FieldName("identifier", "attribute").as("attribute")); } + + @Test + public void addSvsVamanaVectorFieldBasicTest() { + Map attributes = new HashMap<>(); + attributes.put("TYPE", "FLOAT32"); + attributes.put("DIM", 256); + attributes.put("DISTANCE_METRIC", "L2"); + + Schema schema = new Schema() + .addTextField(TITLE, 1.0) + .addSvsVamanaVectorField("embedding", attributes); + + // Verify the schema contains the correct number of fields + assertThat(schema.fields.size(), Matchers.equalTo(2)); + + // Verify the vector field is correctly configured + Schema.VectorField vectorField = (Schema.VectorField) schema.fields.get(1); + assertThat(vectorField.toString(), Matchers.containsString("VectorField{name='embedding', type=VECTOR, algorithm=SVS_VAMANA")); + + // Verify the schema string representation + String schemaString = schema.toString(); + assertThat(schemaString, Matchers.containsString("algorithm=SVS_VAMANA")); + assertThat(schemaString, Matchers.containsString("TYPE=FLOAT32")); + assertThat(schemaString, Matchers.containsString("DIM=256")); + assertThat(schemaString, Matchers.containsString("DISTANCE_METRIC=L2")); + } } diff --git a/src/test/java/redis/clients/jedis/modules/search/SearchTest.java b/src/test/java/redis/clients/jedis/modules/search/SearchTest.java index 50339e1162..d2907ef8f9 100644 --- a/src/test/java/redis/clients/jedis/modules/search/SearchTest.java +++ b/src/test/java/redis/clients/jedis/modules/search/SearchTest.java @@ -17,6 +17,7 @@ import java.util.*; import java.util.stream.Collectors; +import io.redis.test.annotations.SinceRedisVersion; import io.redis.test.utils.RedisVersion; import org.hamcrest.Matchers; import org.junit.jupiter.api.BeforeAll; @@ -1156,7 +1157,7 @@ public void searchProfile() { } @Test - public void testHNSWVVectorSimilarity() { + public void testHNSWVectorSimilarity() { Map attr = new HashMap<>(); attr.put("TYPE", "FLOAT32"); attr.put("DIM", 2); @@ -1179,6 +1180,31 @@ public void testHNSWVVectorSimilarity() { assertEquals("0", doc1.get("__v_score")); } + @Test + @SinceRedisVersion("8.1.240") + public void testSvsVamanaVectorSimilarity() { + Map attr = new HashMap<>(); + attr.put("TYPE", "FLOAT32"); + attr.put("DIM", 2); + attr.put("DISTANCE_METRIC", "L2"); + + Schema sc = new Schema().addSvsVamanaVectorField("v", attr); + assertEquals("OK", client.ftCreate(index, IndexOptions.defaultOptions(), sc)); + + client.hset("a", "v", "aaaaaaaa"); + client.hset("b", "v", "aaaabaaa"); + client.hset("c", "v", "aaaaabaa"); + + Query query = new Query("*=>[KNN 2 @v $vec]") + .addParam("vec", "aaaaaaaa") + .setSortBy("__v_score", true) + .returnFields("__v_score") + .dialect(2); + Document doc1 = client.ftSearch(index, query).getDocuments().get(0); + assertEquals("a", doc1.getId()); + assertEquals("0", doc1.get("__v_score")); + } + @Test public void testFlatVectorSimilarity() { Map attr = new HashMap<>(); diff --git a/src/test/java/redis/clients/jedis/modules/search/SearchWithParamsTest.java b/src/test/java/redis/clients/jedis/modules/search/SearchWithParamsTest.java index 77f80b0e56..705bc71cf9 100644 --- a/src/test/java/redis/clients/jedis/modules/search/SearchWithParamsTest.java +++ b/src/test/java/redis/clients/jedis/modules/search/SearchWithParamsTest.java @@ -38,6 +38,7 @@ import redis.clients.jedis.exceptions.JedisDataException; import redis.clients.jedis.json.Path; import redis.clients.jedis.search.*; +import redis.clients.jedis.search.RediSearchUtil; import redis.clients.jedis.search.schemafields.*; import redis.clients.jedis.search.schemafields.GeoShapeField.CoordinateSystem; import redis.clients.jedis.search.schemafields.VectorField.VectorAlgorithm; @@ -1346,6 +1347,58 @@ public void uint8StorageType() { .addAttribute("DISTANCE_METRIC", "L2").build())); } + @Test + @SinceRedisVersion("8.1.240") + public void testSvsVamanaVectorSimilarity() { + Map attr = new HashMap<>(); + attr.put("TYPE", "FLOAT32"); + attr.put("DIM", 2); + attr.put("DISTANCE_METRIC", "L2"); + + assertOK(client.ftCreate(index, VectorField.builder().fieldName("v") + .algorithm(VectorAlgorithm.SVS_VAMANA).attributes(attr).build())); + + // Create proper float vectors + float[] vectorA = {1.0f, 2.0f}; + float[] vectorB = {1.1f, 2.1f}; + float[] vectorC = {2.0f, 3.0f}; + + // Convert to byte arrays using RediSearchUtil + byte[] bytesA = RediSearchUtil.toByteArray(vectorA); + byte[] bytesB = RediSearchUtil.toByteArray(vectorB); + byte[] bytesC = RediSearchUtil.toByteArray(vectorC); + + client.hset("a".getBytes(), "v".getBytes(), bytesA); + client.hset("b".getBytes(), "v".getBytes(), bytesB); + client.hset("c".getBytes(), "v".getBytes(), bytesC); + + FTSearchParams searchParams = FTSearchParams.searchParams() + .addParam("vec", bytesA) + .sortBy("__v_score", SortingOrder.ASC) + .returnFields("__v_score") + .dialect(2); + Document doc1 = client.ftSearch(index, "*=>[KNN 2 @v $vec]", searchParams).getDocuments().get(0); + assertEquals("a", doc1.getId()); + assertEquals("0", doc1.get("__v_score")); + } + + @Test + @SinceRedisVersion("8.1.240") + public void testSvsVamanaVectorWithAdvancedParameters() { + assertOK(client.ftCreate(index, + VectorField.builder().fieldName("v") + .algorithm(VectorAlgorithm.SVS_VAMANA) + .addAttribute("TYPE", "FLOAT32") + .addAttribute("DIM", 4) + .addAttribute("DISTANCE_METRIC", "L2") + .addAttribute("CONSTRUCTION_WINDOW_SIZE", 200) + .addAttribute("GRAPH_MAX_DEGREE", 64) + .addAttribute("SEARCH_WINDOW_SIZE", 100) + .addAttribute("EPSILON", 0.01) + .build() + )); + } + @Test public void searchProfile() { assertOK(client.ftCreate(index, TextField.of("t1"), TextField.of("t2")));