diff --git a/java/client/src/main/java/glide/api/commands/VectorSearchBaseCommands.java b/java/client/src/main/java/glide/api/commands/VectorSearchBaseCommands.java index b9177f63ea..e5a72bc7f7 100644 --- a/java/client/src/main/java/glide/api/commands/VectorSearchBaseCommands.java +++ b/java/client/src/main/java/glide/api/commands/VectorSearchBaseCommands.java @@ -18,7 +18,14 @@ public interface VectorSearchBaseCommands { * @return OK. * @example *
{@code
-     * // TODO
+     * // Create an index for vectors of size 2:
+     * client.ftcreate("hash_idx1", IndexType.HASH, new String[] {"hash:"}, new FieldInfo[] {
+     *     new FieldInfo("vec", "VEC", VectorFieldFlat.builder(DistanceMetric.L2, 2).build())
+     * }).get();
+     * // Create a 6-dimensional JSON index using the HNSW algorithm:
+     * client.ftcreate("json_idx1", IndexType.JSON, new String[] {"json:"}, new FieldInfo[] {
+     *     new FieldInfo("$.vec", "VEC", VectorFieldHnsw.builder(DistanceMetric.L2, 6).numberOfEdges(32).build())
+     * }).get();
      * }
*/ CompletableFuture ftcreate( diff --git a/java/client/src/main/java/glide/api/models/commands/vss/FTCreateOptions.java b/java/client/src/main/java/glide/api/models/commands/vss/FTCreateOptions.java index 64b1758d3c..7105a21bdb 100644 --- a/java/client/src/main/java/glide/api/models/commands/vss/FTCreateOptions.java +++ b/java/client/src/main/java/glide/api/models/commands/vss/FTCreateOptions.java @@ -8,12 +8,14 @@ import java.util.Optional; import lombok.AccessLevel; import lombok.AllArgsConstructor; -import lombok.RequiredArgsConstructor; // TODO examples public class FTCreateOptions { + /** Type of the index dataset. */ public enum IndexType { + /** Data stored in hashes, so field identifiers are field names within the hashes. */ HASH, + /** Data stored in JSONs, so field identifiers are JSON Path expressions. */ JSON } @@ -60,6 +62,22 @@ public static class TagField implements Field { private Optional separator; private final boolean caseSensitive; + /** Create a TAG field. */ + public TagField() { + this.separator = Optional.empty(); + this.caseSensitive = false; + } + + /** + * Create a TAG field. + * + * @param separator The tag separator. + */ + public TagField(char separator) { + this.separator = Optional.of(separator); + this.caseSensitive = false; + } + /** * Create a TAG field. * @@ -95,24 +113,35 @@ public String[] toArgs() { } } + /** Vector index algorithm. */ public enum Algorithm { - /** Hierarchical Navigable Small World */ + /** + * Hierarchical Navigable Small World provides an approximation of nearest neighbors algorithm + * that uses a multi-layered graph. + */ HNSW, /** * The Flat algorithm is a brute force linear processing of each vector in the index, yielding - * exact answers within the bounds of the precision of the distance computations. Because of the - * linear processing of the index, run times for this algorithm can be very high for large - * indexes. + * exact answers within the bounds of the precision of the distance computations. */ FLAT } + /** + * Distance metrics to measure the degree of similarity between two vectors.
+ * The above metrics calculate distance between two vectors, where the smaller the value is, the + * closer the two vectors are in the vector space. + */ public enum DistanceMetric { + /** Euclidean distance between two vectors. */ L2, + /** Inner product of two vectors. */ IP, + /** Cosine distance of two vectors. */ COSINE } + /** Superclass for vector field implementations, contains common logic. */ @AllArgsConstructor(access = AccessLevel.PROTECTED) abstract static class VectorField implements Field { private final Map params; @@ -123,7 +152,7 @@ public String[] toArgs() { var args = new ArrayList(); args.add("VECTOR"); args.add(Algorithm); - args.add(Integer.toString(params.size())); + args.add(Integer.toString(params.size() * 2)); params.forEach( (name, value) -> { args.add(name); @@ -147,7 +176,8 @@ protected VectorFieldHnsw(Map params) { /** * Init a {@link VectorFieldHnsw}'s builder. * - * @param distanceMetric {@link DistanceMetric} + * @param distanceMetric {@link DistanceMetric} to measure the degree of similarity between two + * vectors. * @param dimensions Vector dimension, specified as a positive integer. Maximum: 32768 */ public static VectorFieldHnswBuilder builder( @@ -157,7 +187,7 @@ public static VectorFieldHnswBuilder builder( } public static class VectorFieldHnswBuilder extends VectorFieldBuilder { - public VectorFieldHnswBuilder(DistanceMetric distanceMetric, int dimensions) { + VectorFieldHnswBuilder(DistanceMetric distanceMetric, int dimensions) { super(distanceMetric, dimensions); } @@ -210,7 +240,8 @@ protected VectorFieldFlat(Map params) { /** * Init a {@link VectorFieldFlat}'s builder. * - * @param distanceMetric {@link DistanceMetric} + * @param distanceMetric {@link DistanceMetric} to measure the degree of similarity between two + * vectors. * @param dimensions Vector dimension, specified as a positive integer. Maximum: 32768 */ public static VectorFieldFlatBuilder builder( @@ -220,7 +251,7 @@ public static VectorFieldFlatBuilder builder( } public static class VectorFieldFlatBuilder extends VectorFieldBuilder { - public VectorFieldFlatBuilder(DistanceMetric distanceMetric, int dimensions) { + VectorFieldFlatBuilder(DistanceMetric distanceMetric, int dimensions) { super(distanceMetric, dimensions); } @@ -233,7 +264,7 @@ public VectorFieldFlat build() { abstract static class VectorFieldBuilder> { protected final Map params = new HashMap<>(); - public VectorFieldBuilder(DistanceMetric distanceMetric, int dimensions) { + VectorFieldBuilder(DistanceMetric distanceMetric, int dimensions) { params.put("TYPE", "FLOAT32"); params.put("DIM", Integer.toString(dimensions)); params.put("DISTANCE_METRIC", distanceMetric.toString()); @@ -252,18 +283,37 @@ public T initialCapacity(int initialCapacity) { public abstract VectorField build(); } - @RequiredArgsConstructor + /** Field definition to be added into index schema. */ public static class FieldInfo { private final String identifier; private final String alias; private final Field field; + /** + * Field definition to be added into index schema. + * + * @param identifier Field identifier (name). + * @param field The {@link Field} itself. + */ public FieldInfo(String identifier, Field field) { this.identifier = identifier; this.field = field; this.alias = null; } + /** + * Field definition to be added into index schema. + * + * @param identifier Field identifier (name). + * @param alias Field alias. + * @param field The {@link Field} itself. + */ + public FieldInfo(String identifier, String alias, Field field) { + this.identifier = identifier; + this.alias = alias; + this.field = field; + } + /** Convert to module API. */ public String[] toArgs() { var args = new ArrayList(); diff --git a/java/integTest/build.gradle b/java/integTest/build.gradle index 2b56978a08..304f58f2d9 100644 --- a/java/integTest/build.gradle +++ b/java/integTest/build.gradle @@ -97,7 +97,7 @@ tasks.register('startStandalone') { test.dependsOn 'stopAllBeforeTests' stopAllBeforeTests.finalizedBy 'clearDirs' clearDirs.finalizedBy 'startStandalone' -clearDirs.finalizedBy 'startCluster' +// clearDirs.finalizedBy 'startCluster' test.finalizedBy 'stopAllAfterTests' test.dependsOn ':client:buildRustRelease' @@ -122,3 +122,20 @@ tasks.withType(Test) { logger.quiet "${desc.className}.${desc.name}: ${result.resultType} ${(result.getEndTime() - result.getStartTime())/1000}s" } } + +test { + filter { + excludeTestsMatching 'glide.modules.*' + } +} + +tasks.register('modulesTest', Test) { + doFirst { + systemProperty 'test.server.standalone.ports', 6379 + systemProperty 'test.server.cluster.ports', 7000 + } + + filter { + includeTestsMatching 'glide.modules.*' + } +} diff --git a/java/integTest/src/test/java/glide/modules/VectorSearchTests.java b/java/integTest/src/test/java/glide/modules/VectorSearchTests.java new file mode 100644 index 0000000000..0ac82199ad --- /dev/null +++ b/java/integTest/src/test/java/glide/modules/VectorSearchTests.java @@ -0,0 +1,170 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.modules; + +import static glide.TestUtilities.commonClientConfig; +import static glide.TestUtilities.commonClusterClientConfig; +import static glide.api.BaseClient.OK; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import glide.api.BaseClient; +import glide.api.GlideClient; +import glide.api.GlideClusterClient; +import glide.api.models.commands.vss.FTCreateOptions.DistanceMetric; +import glide.api.models.commands.vss.FTCreateOptions.FieldInfo; +import glide.api.models.commands.vss.FTCreateOptions.IndexType; +import glide.api.models.commands.vss.FTCreateOptions.NumericField; +import glide.api.models.commands.vss.FTCreateOptions.TagField; +import glide.api.models.commands.vss.FTCreateOptions.TextField; +import glide.api.models.commands.vss.FTCreateOptions.VectorFieldFlat; +import glide.api.models.commands.vss.FTCreateOptions.VectorFieldHnsw; +import glide.api.models.exceptions.RequestException; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.ExecutionException; +import lombok.Getter; +import lombok.SneakyThrows; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +public class VectorSearchTests { + + @Getter private static List clients; + + @BeforeAll + @SneakyThrows + public static void init() { + var standaloneClient = + GlideClient.createClient(commonClientConfig().requestTimeout(5000).build()).get(); + + var clusterClient = + GlideClusterClient.createClient(commonClusterClientConfig().requestTimeout(5000).build()) + .get(); + + clients = List.of(Arguments.of(standaloneClient), Arguments.of(clusterClient)); + } + + @AfterAll + @SneakyThrows + public static void teardown() { + for (var client : clients) { + ((BaseClient) client.get()[0]).close(); + } + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void ft_create(BaseClient client) { + // create few simple indices + assertEquals( + OK, + client + .ftcreate( + UUID.randomUUID().toString(), + IndexType.HASH, + new String[0], + new FieldInfo[] { + new FieldInfo("vec", "vec", VectorFieldHnsw.builder(DistanceMetric.L2, 2).build()) + }) + .get()); + assertEquals( + OK, + client + .ftcreate( + UUID.randomUUID().toString(), + IndexType.JSON, + new String[] {"json:"}, + new FieldInfo[] { + new FieldInfo( + "$.vec", "VEC", VectorFieldFlat.builder(DistanceMetric.L2, 6).build()) + }) + .get()); + + // create an index with NSFW vector with additional parameters + assertEquals( + OK, + client + .ftcreate( + UUID.randomUUID().toString(), + IndexType.HASH, + new String[] {"docs:"}, + new FieldInfo[] { + new FieldInfo( + "doc_embedding", + VectorFieldHnsw.builder(DistanceMetric.COSINE, 1536) + .numberOfEdges(40) + .vectorsExaminedOnConstruction(250) + .vectorsExaminedOnRuntime(40) + .build()) + }) + .get()); + + // create an index with multiple fields + assertEquals( + OK, + client + .ftcreate( + UUID.randomUUID().toString(), + IndexType.HASH, + new String[] {"blog:post:"}, + new FieldInfo[] { + new FieldInfo("title", new TextField()), + new FieldInfo("published_at", new NumericField()), + new FieldInfo("category", new TagField()) + }) + .get()); + + // create an index with multiple prefixes + var name = UUID.randomUUID().toString(); + assertEquals( + OK, + client + .ftcreate( + name, + IndexType.HASH, + new String[] {"author:details:", "book:details:"}, + new FieldInfo[] { + new FieldInfo("author_id", new TagField()), + new FieldInfo("author_ids", new TagField()), + new FieldInfo("title", new TextField()), + new FieldInfo("name", new TextField()) + }) + .get()); + + // create a duplicating index + var exception = + assertThrows( + ExecutionException.class, + () -> + client + .ftcreate( + name, + IndexType.HASH, + new String[0], + new FieldInfo[] {new FieldInfo("title", new TextField())}) + .get()); + assertInstanceOf(RequestException.class, exception.getCause()); + assertTrue(exception.getMessage().contains("already exists")); + + // create an index without fields + exception = + assertThrows( + ExecutionException.class, + () -> + client + .ftcreate( + UUID.randomUUID().toString(), + IndexType.HASH, + new String[0], + new FieldInfo[0]) + .get()); + assertInstanceOf(RequestException.class, exception.getCause()); + assertTrue(exception.getMessage().contains("arguments are missing")); + } +} diff --git a/utils/cluster_manager.py b/utils/cluster_manager.py index 21847cfb8d..03adcaba00 100644 --- a/utils/cluster_manager.py +++ b/utils/cluster_manager.py @@ -315,8 +315,6 @@ def get_server_command() -> str: "yes", "--logfile", f"{node_folder}/redis.log", - "--protected-mode", - "no" ] if load_module: if len(load_module) == 0: