From c9d9fa1ada0cf932f89c4febac6919e3b512d3c8 Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand <yury.fridlyand@improving.com> Date: Wed, 9 Oct 2024 08:53:00 -0700 Subject: [PATCH 1/5] `FT.CREATE` Signed-off-by: Yury-Fridlyand <yury.fridlyand@improving.com> --- .github/workflows/java.yml | 5 +- CHANGELOG.md | 2 + java/client/build.gradle | 4 +- .../glide/api/commands/servermodules/FT.java | 79 ++++ .../models/commands/vss/FTCreateOptions.java | 384 ++++++++++++++++++ java/client/src/main/java/module-info.java | 2 + java/integTest/build.gradle | 3 - .../java/glide/modules/VectorSearchTests.java | 176 +++++++- 8 files changed, 644 insertions(+), 11 deletions(-) create mode 100644 java/client/src/main/java/glide/api/commands/servermodules/FT.java create mode 100644 java/client/src/main/java/glide/api/models/commands/vss/FTCreateOptions.java diff --git a/.github/workflows/java.yml b/.github/workflows/java.yml index d5d0697abb..d87c9a0453 100644 --- a/.github/workflows/java.yml +++ b/.github/workflows/java.yml @@ -197,9 +197,8 @@ jobs: name: lint java rust test-modules: - if: github.event.pull_request.head.repo.owner.login == 'valkey-io' - environment: AWS_ACTIONS - name: Running Module Tests + if: github.repository_owner == 'valkey-io' + name: Modules Tests runs-on: [self-hosted, linux, ARM64] timeout-minutes: 15 steps: diff --git a/CHANGELOG.md b/CHANGELOG.md index 72d205fefb..d08720c9e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ #### Changes +* Java: Added `FT.CREATE` ([#2414](https://github.com/valkey-io/valkey-glide/pull/2414)) + #### Breaking Changes #### Fixes diff --git a/java/client/build.gradle b/java/client/build.gradle index 46fa8f4cee..364b09ca1e 100644 --- a/java/client/build.gradle +++ b/java/client/build.gradle @@ -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" diff --git a/java/client/src/main/java/glide/api/commands/servermodules/FT.java b/java/client/src/main/java/glide/api/commands/servermodules/FT.java new file mode 100644 index 0000000000..950602e74f --- /dev/null +++ b/java/client/src/main/java/glide/api/commands/servermodules/FT.java @@ -0,0 +1,79 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.commands.servermodules; + +import glide.api.BaseClient; +import glide.api.GlideClient; +import glide.api.GlideClusterClient; +import glide.api.models.ClusterValue; +import glide.api.models.GlideString; +import glide.api.models.commands.vss.FTCreateOptions; +import glide.api.models.commands.vss.FTCreateOptions.FieldInfo; +import java.util.Arrays; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Stream; + +public class FT { + /** + * Creates an index and initiates a backfill of that index. + * + * @param indexName The index name. + * @param options Additional parameters for the command - see {@link FTCreateOptions}. + * @param fields Fields to populate into the index. + * @return <code>OK</code>. + * @example + * <pre>{@code + * // Create an index for vectors of size 2: + * FT.create(client, "hash_idx1", FTCreateOptions.empty(), new FieldInfo[] { + * new FieldInfo("vec", VectorFieldFlat.builder(DistanceMetric.L2, 2).build()) + * }).get(); + * // Create a 6-dimensional JSON index using the HNSW algorithm: + * FT.create( + * client, + * "json_idx1", + * FTCreateOptions.builder().indexType(JSON).prefixes(new String[] {"json:"}).build(), + * new FieldInfo[] { new FieldInfo( + * "$.vec", + * "VEC", + * VectorFieldHnsw.builder(DistanceMetric.L2, 6).numberOfEdges(32).build()) + * }).get(); + * }</pre> + */ + public static CompletableFuture<String> create( + BaseClient client, String indexName, FTCreateOptions options, FieldInfo[] fields) { + var args = + Stream.of( + new String[] {"FT.CREATE", indexName}, + options.toArgs(), + new String[] {"SCHEMA"}, + Arrays.stream(fields) + .map(FieldInfo::toArgs) + .flatMap(Arrays::stream) + .toArray(String[]::new)) + .flatMap(Arrays::stream) + .map(GlideString::gs) + .toArray(GlideString[]::new); + return executeCommand(client, args, false); + } + + /** + * A wrapper for custom command API. + * + * @param client + * @param args + * @param returnsMap - true if command returns a map + */ + @SuppressWarnings("unchecked") + private static <T> CompletableFuture<T> executeCommand( + BaseClient client, GlideString[] args, boolean returnsMap) { + if (client instanceof GlideClient) { + return ((GlideClient) client).customCommand(args).thenApply(r -> (T) r); + } else if (client instanceof GlideClusterClient) { + return ((GlideClusterClient) client) + .customCommand(args) + .thenApply(returnsMap ? ClusterValue::getMultiValue : ClusterValue::getSingleValue) + .thenApply(r -> (T) r); + } + throw new IllegalArgumentException( + "Unknown type of client, should be either `GlideClient` or `GlideClusterClient`"); + } +} 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 new file mode 100644 index 0000000000..c0e0144c3a --- /dev/null +++ b/java/client/src/main/java/glide/api/models/commands/vss/FTCreateOptions.java @@ -0,0 +1,384 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.models.commands.vss; + +import glide.api.commands.servermodules.FT; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.NonNull; + +/** Optional parameters for {@link FT#create} command. */ +@Builder +public class FTCreateOptions { + /** The index type. If not given a {@link IndexType#HASH} index is created. */ + private final IndexType indexType; + + /** A list of prefixes of index definitions. */ + private final String[] prefixes; + + /** Create an empty options if parametrization is not needed. */ + public static FTCreateOptions empty() { + // Node: bug in meme DB - command fails if cmd is too short even though all mandatory args are + // present + // TODO confirm is it fixed or not and update docs if needed + return builder().build(); + } + + public String[] toArgs() { + var args = new ArrayList<String>(); + if (indexType != null) { + args.add("ON"); + args.add(indexType.toString()); + } + if (prefixes != null && prefixes.length > 0) { + args.add("PREFIX"); + args.add(Integer.toString(prefixes.length)); + args.addAll(List.of(prefixes)); + } + return args.toArray(String[]::new); + } + + /** 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 + } + + /** + * A vector search field. Could be one of the following: + * + * <ul> + * <li>{@link NumericField} + * <li>{@link TextField} + * <li>{@link TagField} + * <li>{@link VectorFieldHnsw} + * <li>{@link VectorFieldFlat} + * </ul> + */ + public interface Field { + /** Convert to module API. */ + String[] toArgs(); + } + + private enum FieldType { + NUMERIC, + TEXT, + TAG, + VECTOR + } + + /** Field contains a number. */ + public static class NumericField implements Field { + @Override + public String[] toArgs() { + return new String[] {FieldType.NUMERIC.toString()}; + } + } + + /** Field contains any blob of data. */ + public static class TextField implements Field { + @Override + public String[] toArgs() { + return new String[] {FieldType.TEXT.toString()}; + } + } + + /** + * Tag fields are similar to full-text fields, but they interpret the text as a simple list of + * tags delimited by a separator character.<br> + * For {@link IndexType#HASH} fields, separator default is a comma (<code>,</code>). For {@link + * IndexType#JSON} fields, there is no default separator; you must declare one explicitly if + * needed. + */ + public static class TagField implements Field { + private Optional<Character> separator; + private final boolean caseSensitive; + + /** Create a <code>TAG</code> field. */ + public TagField() { + this.separator = Optional.empty(); + this.caseSensitive = false; + } + + /** + * Create a <code>TAG</code> field. + * + * @param separator The tag separator. + */ + public TagField(char separator) { + this.separator = Optional.of(separator); + this.caseSensitive = false; + } + + /** + * Create a <code>TAG</code> field. + * + * @param separator The tag separator. + * @param caseSensitive Whether to keep the original case. + */ + public TagField(char separator, boolean caseSensitive) { + this.separator = Optional.of(separator); + this.caseSensitive = caseSensitive; + } + + /** + * Create a <code>TAG</code> field. + * + * @param caseSensitive Whether to keep the original case. + */ + public TagField(boolean caseSensitive) { + this.caseSensitive = caseSensitive; + } + + @Override + public String[] toArgs() { + var args = new ArrayList<String>(); + args.add(FieldType.TAG.toString()); + if (separator.isPresent()) { + args.add("SEPARATOR"); + args.add(separator.get().toString()); + } + if (caseSensitive) { + args.add("CASESENSITIVE"); + } + return args.toArray(String[]::new); + } + } + + /** Vector index algorithm. */ + public enum Algorithm { + /** + * 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. + */ + FLAT + } + + /** + * Distance metrics to measure the degree of similarity between two vectors.<br> + * 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<VectorAlgorithmParam, String> params; + private final VectorAlgorithm Algorithm; + + @Override + public String[] toArgs() { + var args = new ArrayList<String>(); + args.add(FieldType.VECTOR.toString()); + args.add(Algorithm.toString()); + args.add(Integer.toString(params.size() * 2)); + params.forEach( + (name, value) -> { + args.add(name.toString()); + args.add(value); + }); + return args.toArray(String[]::new); + } + } + + private enum VectorAlgorithm { + HNSW, + FLAT + } + + private enum VectorAlgorithmParam { + M, + EF_CONSTRUCTION, + EF_RUNTIME, + TYPE, + DIM, + DISTANCE_METRIC, + INITIAL_CAP + } + + /** + * Vector field that supports vector search by <code>HNSM</code> (Hierarchical Navigable Small + * World) algorithm.<br> + * The algorithm provides an approximation of the correct answer in exchange for substantially + * lower execution times. + */ + public static class VectorFieldHnsw extends VectorField { + private VectorFieldHnsw(Map<VectorAlgorithmParam, String> params) { + super(params, VectorAlgorithm.HNSW); + } + + /** + * Init a builder. + * + * @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( + @NonNull DistanceMetric distanceMetric, int dimensions) { + return new VectorFieldHnswBuilder(distanceMetric, dimensions); + } + } + + public static class VectorFieldHnswBuilder extends VectorFieldBuilder<VectorFieldHnswBuilder> { + VectorFieldHnswBuilder(DistanceMetric distanceMetric, int dimensions) { + super(distanceMetric, dimensions); + } + + @Override + public VectorFieldHnsw build() { + return new VectorFieldHnsw(params); + } + + /** + * Number of maximum allowed outgoing edges for each node in the graph in each layer. On layer + * zero the maximal number of outgoing edges is doubled. Default is 16 Maximum is 512. + */ + public VectorFieldHnswBuilder numberOfEdges(int numberOfEdges) { + params.put(VectorAlgorithmParam.M, Integer.toString(numberOfEdges)); + return this; + } + + /** + * (Optional) The number of vectors examined during index construction. Higher values for this + * parameter will improve recall ratio at the expense of longer index creation times. Default + * value is 200. Maximum value is 4096. + */ + public VectorFieldHnswBuilder vectorsExaminedOnConstruction(int vectorsExaminedOnConstruction) { + params.put( + VectorAlgorithmParam.EF_CONSTRUCTION, Integer.toString(vectorsExaminedOnConstruction)); + return this; + } + + /** + * (Optional) The number of vectors examined during query operations. Higher values for this + * parameter can yield improved recall at the expense of longer query times. The value of this + * parameter can be overriden on a per-query basis. Default value is 10. Maximum value is 4096. + */ + public VectorFieldHnswBuilder vectorsExaminedOnRuntime(int vectorsExaminedOnRuntime) { + params.put(VectorAlgorithmParam.EF_RUNTIME, Integer.toString(vectorsExaminedOnRuntime)); + return this; + } + } + + /** + * Vector field that supports vector search by <code>FLAT</code> (brute force) algorithm.<br> + * The 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. + */ + public static class VectorFieldFlat extends VectorField { + + private VectorFieldFlat(Map<VectorAlgorithmParam, String> params) { + super(params, VectorAlgorithm.FLAT); + } + + /** + * Init a builder. + * + * @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( + @NonNull DistanceMetric distanceMetric, int dimensions) { + return new VectorFieldFlatBuilder(distanceMetric, dimensions); + } + } + + public static class VectorFieldFlatBuilder extends VectorFieldBuilder<VectorFieldFlatBuilder> { + VectorFieldFlatBuilder(DistanceMetric distanceMetric, int dimensions) { + super(distanceMetric, dimensions); + } + + @Override + public VectorFieldFlat build() { + return new VectorFieldFlat(params); + } + } + + abstract static class VectorFieldBuilder<T extends VectorFieldBuilder<T>> { + final Map<VectorAlgorithmParam, String> params = new HashMap<>(); + + VectorFieldBuilder(DistanceMetric distanceMetric, int dimensions) { + params.put(VectorAlgorithmParam.TYPE, "FLOAT32"); + params.put(VectorAlgorithmParam.DIM, Integer.toString(dimensions)); + params.put(VectorAlgorithmParam.DISTANCE_METRIC, distanceMetric.toString()); + } + + /** + * Initial vector capacity in the index affecting memory allocation size of the index. Defaults + * to 1024. + */ + @SuppressWarnings("unchecked") + public T initialCapacity(int initialCapacity) { + params.put(VectorAlgorithmParam.INITIAL_CAP, Integer.toString(initialCapacity)); + return (T) this; + } + + public abstract VectorField build(); + } + + /** 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(@NonNull String identifier, @NonNull 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(@NonNull String identifier, @NonNull String alias, @NonNull Field field) { + this.identifier = identifier; + this.alias = alias; + this.field = field; + } + + /** Convert to module API. */ + public String[] toArgs() { + var args = new ArrayList<String>(); + args.add(identifier); + if (alias != null) { + args.add("AS"); + args.add(alias); + } + args.addAll(List.of(field.toArgs())); + return args.toArray(String[]::new); + } + } +} diff --git a/java/client/src/main/java/module-info.java b/java/client/src/main/java/module-info.java index 99c4655082..6f1bd5e14a 100644 --- a/java/client/src/main/java/module-info.java +++ b/java/client/src/main/java/module-info.java @@ -9,8 +9,10 @@ exports glide.api.models.commands.function; exports glide.api.models.commands.scan; exports glide.api.models.commands.stream; + exports glide.api.models.commands.vss; exports glide.api.models.configuration; exports glide.api.models.exceptions; + exports glide.api.commands.servermodules; requires com.google.protobuf; requires io.netty.codec; diff --git a/java/integTest/build.gradle b/java/integTest/build.gradle index d467b4ebbb..c2032d05d1 100644 --- a/java/integTest/build.gradle +++ b/java/integTest/build.gradle @@ -102,7 +102,6 @@ tasks.register('startStandalone') { } } - test.dependsOn 'stopAllBeforeTests' stopAllBeforeTests.finalizedBy 'clearDirs' clearDirs.finalizedBy 'startStandalone' @@ -112,8 +111,6 @@ test.dependsOn ':client:buildRustRelease' tasks.withType(Test) { doFirst { - println "Cluster hosts = ${clusterHosts}" - println "Standalone hosts = ${standaloneHosts}" systemProperty 'test.server.standalone', standaloneHosts systemProperty 'test.server.cluster', clusterHosts systemProperty 'test.server.tls', System.getProperty("tls") diff --git a/java/integTest/src/test/java/glide/modules/VectorSearchTests.java b/java/integTest/src/test/java/glide/modules/VectorSearchTests.java index 07b0946b3d..db58c99ff5 100644 --- a/java/integTest/src/test/java/glide/modules/VectorSearchTests.java +++ b/java/integTest/src/test/java/glide/modules/VectorSearchTests.java @@ -2,23 +2,193 @@ package glide.modules; import static glide.TestUtilities.commonClusterClientConfig; +import static glide.api.BaseClient.OK; +import static glide.api.models.configuration.RequestRoutingConfiguration.SimpleMultiNodeRoute.ALL_PRIMARIES; import static glide.api.models.configuration.RequestRoutingConfiguration.SimpleSingleNodeRoute.RANDOM; +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.GlideClusterClient; +import glide.api.commands.servermodules.FT; +import glide.api.models.commands.FlushMode; import glide.api.models.commands.InfoOptions.Section; +import glide.api.models.commands.vss.FTCreateOptions; +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.UUID; +import java.util.concurrent.ExecutionException; import lombok.SneakyThrows; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; public class VectorSearchTests { - @Test + private static GlideClusterClient client; + + @BeforeAll @SneakyThrows - public void check_module_loaded() { - var client = + public static void init() { + client = GlideClusterClient.createClient(commonClusterClientConfig().requestTimeout(5000).build()) .get(); + client.flushall(FlushMode.SYNC, ALL_PRIMARIES).get(); + } + + @AfterAll + @SneakyThrows + public static void teardown() { + client.close(); + } + + @Test + @SneakyThrows + public void check_module_loaded() { var info = client.info(new Section[] {Section.MODULES}, RANDOM).get().getSingleValue(); assertTrue(info.contains("# search_index_stats")); } + + @SneakyThrows + @Test + public void ft_create() { + // create few simple indices + assertEquals( + OK, + FT.create( + client, + UUID.randomUUID().toString(), + FTCreateOptions.empty(), + new FieldInfo[] { + new FieldInfo("vec", "VEC", VectorFieldHnsw.builder(DistanceMetric.L2, 2).build()) + }) + .get()); + assertEquals( + OK, + FT.create( + client, + UUID.randomUUID().toString(), + FTCreateOptions.builder() + .indexType(IndexType.JSON) + .prefixes(new String[] {"json:"}) + .build(), + new FieldInfo[] { + new FieldInfo( + "$.vec", "VEC", VectorFieldFlat.builder(DistanceMetric.L2, 6).build()) + }) + .get()); + + // create an index with NSFW vector with additional parameters + assertEquals( + OK, + FT.create( + client, + UUID.randomUUID().toString(), + FTCreateOptions.builder() + .indexType(IndexType.HASH) + .prefixes(new String[] {"docs:"}) + .build(), + 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, + FT.create( + client, + UUID.randomUUID().toString(), + FTCreateOptions.builder() + .indexType(IndexType.HASH) + .prefixes(new String[] {"blog:post:"}) + .build(), + 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, + FT.create( + client, + name, + FTCreateOptions.builder() + .indexType(IndexType.HASH) + .prefixes(new String[] {"author:details:", "book:details:"}) + .build(), + 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, + () -> + FT.create( + client, + name, + FTCreateOptions.empty(), + new FieldInfo[] { + new FieldInfo("title", new TextField()), + new FieldInfo("name", new TextField()) + }) + .get()); + assertInstanceOf(RequestException.class, exception.getCause()); + assertTrue(exception.getMessage().contains("already exists")); + + // create an index without fields + exception = + assertThrows( + ExecutionException.class, + () -> + FT.create( + client, + UUID.randomUUID().toString(), + FTCreateOptions.empty(), + new FieldInfo[0]) + .get()); + assertInstanceOf(RequestException.class, exception.getCause()); + assertTrue(exception.getMessage().contains("wrong number of arguments")); + + // duplicated field name + exception = + assertThrows( + ExecutionException.class, + () -> + FT.create( + client, + UUID.randomUUID().toString(), + FTCreateOptions.empty(), + new FieldInfo[] { + new FieldInfo("name", new TextField()), + new FieldInfo("name", new TextField()) + }) + .get()); + assertInstanceOf(RequestException.class, exception.getCause()); + assertTrue(exception.getMessage().contains("already exists")); + } } From cd9ceef50b998609642dc55c2fb4281008660b1c Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand <yury.fridlyand@improving.com> Date: Wed, 9 Oct 2024 13:14:13 -0700 Subject: [PATCH 2/5] Address PR review. Signed-off-by: Yury-Fridlyand <yury.fridlyand@improving.com> --- .github/workflows/java.yml | 5 +- .../glide/api/commands/servermodules/FT.java | 53 ++++++++++---- .../commands/{vss => FT}/FTCreateOptions.java | 16 ++--- java/client/src/main/java/module-info.java | 2 +- .../java/glide/modules/VectorSearchTests.java | 69 ++++++++----------- 5 files changed, 79 insertions(+), 66 deletions(-) rename java/client/src/main/java/glide/api/models/commands/{vss => FT}/FTCreateOptions.java (96%) diff --git a/.github/workflows/java.yml b/.github/workflows/java.yml index d87c9a0453..d5d0697abb 100644 --- a/.github/workflows/java.yml +++ b/.github/workflows/java.yml @@ -197,8 +197,9 @@ jobs: name: lint java rust test-modules: - if: github.repository_owner == 'valkey-io' - name: Modules Tests + if: github.event.pull_request.head.repo.owner.login == 'valkey-io' + environment: AWS_ACTIONS + name: Running Module Tests runs-on: [self-hosted, linux, ARM64] timeout-minutes: 15 steps: diff --git a/java/client/src/main/java/glide/api/commands/servermodules/FT.java b/java/client/src/main/java/glide/api/commands/servermodules/FT.java index 950602e74f..3e9aaae095 100644 --- a/java/client/src/main/java/glide/api/commands/servermodules/FT.java +++ b/java/client/src/main/java/glide/api/commands/servermodules/FT.java @@ -6,40 +6,65 @@ import glide.api.GlideClusterClient; import glide.api.models.ClusterValue; import glide.api.models.GlideString; -import glide.api.models.commands.vss.FTCreateOptions; -import glide.api.models.commands.vss.FTCreateOptions.FieldInfo; +import glide.api.models.commands.FT.FTCreateOptions; +import glide.api.models.commands.FT.FTCreateOptions.FieldInfo; import java.util.Arrays; import java.util.concurrent.CompletableFuture; import java.util.stream.Stream; +import lombok.NonNull; +/** Module for vector search commands. */ public class FT { /** * Creates an index and initiates a backfill of that index. * * @param indexName The index name. - * @param options Additional parameters for the command - see {@link FTCreateOptions}. * @param fields Fields to populate into the index. * @return <code>OK</code>. * @example * <pre>{@code * // Create an index for vectors of size 2: - * FT.create(client, "hash_idx1", FTCreateOptions.empty(), new FieldInfo[] { + * FT.create(client, "my_idx1", new FieldInfo[] { * new FieldInfo("vec", VectorFieldFlat.builder(DistanceMetric.L2, 2).build()) * }).get(); * // Create a 6-dimensional JSON index using the HNSW algorithm: - * FT.create( - * client, - * "json_idx1", - * FTCreateOptions.builder().indexType(JSON).prefixes(new String[] {"json:"}).build(), - * new FieldInfo[] { new FieldInfo( - * "$.vec", - * "VEC", + * FT.create(client, "my_idx2", + * new FieldInfo[] { new FieldInfo("$.vec", "VEC", * VectorFieldHnsw.builder(DistanceMetric.L2, 6).numberOfEdges(32).build()) * }).get(); * }</pre> */ public static CompletableFuture<String> create( - BaseClient client, String indexName, FTCreateOptions options, FieldInfo[] fields) { + @NonNull BaseClient client, @NonNull String indexName, @NonNull FieldInfo[] fields) { + // Node: bug in meme DB - command fails if cmd is too short even though all mandatory args are + // present + // TODO confirm is it fixed or not and update docs if needed + return create(client, indexName, fields, FTCreateOptions.builder().build()); + } + + /** + * Creates an index and initiates a backfill of that index. + * + * @param indexName The index name. + * @param fields Fields to populate into the index. + * @param options Additional parameters for the command - see {@link FTCreateOptions}. + * @return <code>OK</code>. + * @example + * <pre>{@code + * // Create a 6-dimensional JSON index using the HNSW algorithm: + * FT.create(client, "json_idx1", + * new FieldInfo[] { new FieldInfo("$.vec", "VEC", + * VectorFieldHnsw.builder(DistanceMetric.L2, 6).numberOfEdges(32).build()) + * }, + * FTCreateOptions.builder().indexType(JSON).prefixes(new String[] {"json:"}).build(), + * ).get(); + * }</pre> + */ + public static CompletableFuture<String> create( + @NonNull BaseClient client, + @NonNull String indexName, + @NonNull FieldInfo[] fields, + @NonNull FTCreateOptions options) { var args = Stream.of( new String[] {"FT.CREATE", indexName}, @@ -58,8 +83,8 @@ public static CompletableFuture<String> create( /** * A wrapper for custom command API. * - * @param client - * @param args + * @param client The client to execute the command. + * @param args The command line. * @param returnsMap - true if command returns a map */ @SuppressWarnings("unchecked") 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/FT/FTCreateOptions.java similarity index 96% rename from java/client/src/main/java/glide/api/models/commands/vss/FTCreateOptions.java rename to java/client/src/main/java/glide/api/models/commands/FT/FTCreateOptions.java index c0e0144c3a..fab4edee25 100644 --- a/java/client/src/main/java/glide/api/models/commands/vss/FTCreateOptions.java +++ b/java/client/src/main/java/glide/api/models/commands/FT/FTCreateOptions.java @@ -1,6 +1,7 @@ /** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ -package glide.api.models.commands.vss; +package glide.api.models.commands.FT; +import glide.api.BaseClient; import glide.api.commands.servermodules.FT; import java.util.ArrayList; import java.util.HashMap; @@ -12,7 +13,10 @@ import lombok.Builder; import lombok.NonNull; -/** Optional parameters for {@link FT#create} command. */ +/** + * Additional parameters for {@link FT#create(BaseClient, String, FieldInfo[], FTCreateOptions)} + * command. + */ @Builder public class FTCreateOptions { /** The index type. If not given a {@link IndexType#HASH} index is created. */ @@ -21,14 +25,6 @@ public class FTCreateOptions { /** A list of prefixes of index definitions. */ private final String[] prefixes; - /** Create an empty options if parametrization is not needed. */ - public static FTCreateOptions empty() { - // Node: bug in meme DB - command fails if cmd is too short even though all mandatory args are - // present - // TODO confirm is it fixed or not and update docs if needed - return builder().build(); - } - public String[] toArgs() { var args = new ArrayList<String>(); if (indexType != null) { diff --git a/java/client/src/main/java/module-info.java b/java/client/src/main/java/module-info.java index 6f1bd5e14a..183e6c0410 100644 --- a/java/client/src/main/java/module-info.java +++ b/java/client/src/main/java/module-info.java @@ -9,7 +9,7 @@ exports glide.api.models.commands.function; exports glide.api.models.commands.scan; exports glide.api.models.commands.stream; - exports glide.api.models.commands.vss; + exports glide.api.models.commands.FT; exports glide.api.models.configuration; exports glide.api.models.exceptions; exports glide.api.commands.servermodules; diff --git a/java/integTest/src/test/java/glide/modules/VectorSearchTests.java b/java/integTest/src/test/java/glide/modules/VectorSearchTests.java index db58c99ff5..8d951137c4 100644 --- a/java/integTest/src/test/java/glide/modules/VectorSearchTests.java +++ b/java/integTest/src/test/java/glide/modules/VectorSearchTests.java @@ -12,17 +12,17 @@ import glide.api.GlideClusterClient; import glide.api.commands.servermodules.FT; +import glide.api.models.commands.FT.FTCreateOptions; +import glide.api.models.commands.FT.FTCreateOptions.DistanceMetric; +import glide.api.models.commands.FT.FTCreateOptions.FieldInfo; +import glide.api.models.commands.FT.FTCreateOptions.IndexType; +import glide.api.models.commands.FT.FTCreateOptions.NumericField; +import glide.api.models.commands.FT.FTCreateOptions.TagField; +import glide.api.models.commands.FT.FTCreateOptions.TextField; +import glide.api.models.commands.FT.FTCreateOptions.VectorFieldFlat; +import glide.api.models.commands.FT.FTCreateOptions.VectorFieldHnsw; import glide.api.models.commands.FlushMode; import glide.api.models.commands.InfoOptions.Section; -import glide.api.models.commands.vss.FTCreateOptions; -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.UUID; import java.util.concurrent.ExecutionException; @@ -66,7 +66,6 @@ public void ft_create() { FT.create( client, UUID.randomUUID().toString(), - FTCreateOptions.empty(), new FieldInfo[] { new FieldInfo("vec", "VEC", VectorFieldHnsw.builder(DistanceMetric.L2, 2).build()) }) @@ -76,14 +75,14 @@ public void ft_create() { FT.create( client, UUID.randomUUID().toString(), - FTCreateOptions.builder() - .indexType(IndexType.JSON) - .prefixes(new String[] {"json:"}) - .build(), new FieldInfo[] { new FieldInfo( "$.vec", "VEC", VectorFieldFlat.builder(DistanceMetric.L2, 6).build()) - }) + }, + FTCreateOptions.builder() + .indexType(IndexType.JSON) + .prefixes(new String[] {"json:"}) + .build()) .get()); // create an index with NSFW vector with additional parameters @@ -92,10 +91,6 @@ public void ft_create() { FT.create( client, UUID.randomUUID().toString(), - FTCreateOptions.builder() - .indexType(IndexType.HASH) - .prefixes(new String[] {"docs:"}) - .build(), new FieldInfo[] { new FieldInfo( "doc_embedding", @@ -104,7 +99,11 @@ public void ft_create() { .vectorsExaminedOnConstruction(250) .vectorsExaminedOnRuntime(40) .build()) - }) + }, + FTCreateOptions.builder() + .indexType(IndexType.HASH) + .prefixes(new String[] {"docs:"}) + .build()) .get()); // create an index with multiple fields @@ -113,15 +112,15 @@ public void ft_create() { FT.create( client, UUID.randomUUID().toString(), - FTCreateOptions.builder() - .indexType(IndexType.HASH) - .prefixes(new String[] {"blog:post:"}) - .build(), new FieldInfo[] { new FieldInfo("title", new TextField()), new FieldInfo("published_at", new NumericField()), new FieldInfo("category", new TagField()) - }) + }, + FTCreateOptions.builder() + .indexType(IndexType.HASH) + .prefixes(new String[] {"blog:post:"}) + .build()) .get()); // create an index with multiple prefixes @@ -131,16 +130,16 @@ public void ft_create() { FT.create( client, name, - FTCreateOptions.builder() - .indexType(IndexType.HASH) - .prefixes(new String[] {"author:details:", "book:details:"}) - .build(), new FieldInfo[] { new FieldInfo("author_id", new TagField()), new FieldInfo("author_ids", new TagField()), new FieldInfo("title", new TextField()), new FieldInfo("name", new TextField()) - }) + }, + FTCreateOptions.builder() + .indexType(IndexType.HASH) + .prefixes(new String[] {"author:details:", "book:details:"}) + .build()) .get()); // create a duplicating index @@ -151,7 +150,6 @@ public void ft_create() { FT.create( client, name, - FTCreateOptions.empty(), new FieldInfo[] { new FieldInfo("title", new TextField()), new FieldInfo("name", new TextField()) @@ -164,13 +162,7 @@ public void ft_create() { exception = assertThrows( ExecutionException.class, - () -> - FT.create( - client, - UUID.randomUUID().toString(), - FTCreateOptions.empty(), - new FieldInfo[0]) - .get()); + () -> FT.create(client, UUID.randomUUID().toString(), new FieldInfo[0]).get()); assertInstanceOf(RequestException.class, exception.getCause()); assertTrue(exception.getMessage().contains("wrong number of arguments")); @@ -182,7 +174,6 @@ public void ft_create() { FT.create( client, UUID.randomUUID().toString(), - FTCreateOptions.empty(), new FieldInfo[] { new FieldInfo("name", new TextField()), new FieldInfo("name", new TextField()) From 6aece715afd05ca605b616ac027625062e80e3b5 Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand <yury.fridlyand@improving.com> Date: Thu, 10 Oct 2024 10:16:03 -0700 Subject: [PATCH 3/5] docs Signed-off-by: Yury-Fridlyand <yury.fridlyand@improving.com> --- .../src/main/java/glide/api/commands/servermodules/FT.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/java/client/src/main/java/glide/api/commands/servermodules/FT.java b/java/client/src/main/java/glide/api/commands/servermodules/FT.java index 3e9aaae095..229a606221 100644 --- a/java/client/src/main/java/glide/api/commands/servermodules/FT.java +++ b/java/client/src/main/java/glide/api/commands/servermodules/FT.java @@ -18,6 +18,7 @@ public class FT { /** * Creates an index and initiates a backfill of that index. * + * @param client The client to execute the command. * @param indexName The index name. * @param fields Fields to populate into the index. * @return <code>OK</code>. @@ -27,6 +28,7 @@ public class FT { * FT.create(client, "my_idx1", new FieldInfo[] { * new FieldInfo("vec", VectorFieldFlat.builder(DistanceMetric.L2, 2).build()) * }).get(); + * * // Create a 6-dimensional JSON index using the HNSW algorithm: * FT.create(client, "my_idx2", * new FieldInfo[] { new FieldInfo("$.vec", "VEC", @@ -45,6 +47,7 @@ public static CompletableFuture<String> create( /** * Creates an index and initiates a backfill of that index. * + * @param client The client to execute the command. * @param indexName The index name. * @param fields Fields to populate into the index. * @param options Additional parameters for the command - see {@link FTCreateOptions}. From 707a07980737217e9fa0a78150bbb0bf981cd046 Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand <yury.fridlyand@improving.com> Date: Thu, 10 Oct 2024 11:59:22 -0700 Subject: [PATCH 4/5] glidestring Signed-off-by: Yury-Fridlyand <yury.fridlyand@improving.com> --- .../glide/api/commands/servermodules/FT.java | 65 ++++++++++++++- .../models/commands/FT/FTCreateOptions.java | 79 +++++++++++++++---- 2 files changed, 124 insertions(+), 20 deletions(-) diff --git a/java/client/src/main/java/glide/api/commands/servermodules/FT.java b/java/client/src/main/java/glide/api/commands/servermodules/FT.java index 229a606221..51bde7a03d 100644 --- a/java/client/src/main/java/glide/api/commands/servermodules/FT.java +++ b/java/client/src/main/java/glide/api/commands/servermodules/FT.java @@ -1,6 +1,8 @@ /** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api.commands.servermodules; +import static glide.api.models.GlideString.gs; + import glide.api.BaseClient; import glide.api.GlideClient; import glide.api.GlideClusterClient; @@ -68,17 +70,72 @@ public static CompletableFuture<String> create( @NonNull String indexName, @NonNull FieldInfo[] fields, @NonNull FTCreateOptions options) { + return create(client, gs(indexName), fields, options); + } + + /** + * Creates an index and initiates a backfill of that index. + * + * @param client The client to execute the command. + * @param indexName The index name. + * @param fields Fields to populate into the index. + * @return <code>OK</code>. + * @example + * <pre>{@code + * // Create an index for vectors of size 2: + * FT.create(client, gs("my_idx1"), new FieldInfo[] { + * new FieldInfo("vec", VectorFieldFlat.builder(DistanceMetric.L2, 2).build()) + * }).get(); + * + * // Create a 6-dimensional JSON index using the HNSW algorithm: + * FT.create(client, gs("my_idx2"), + * new FieldInfo[] { new FieldInfo(gs("$.vec"), gs("VEC"), + * VectorFieldHnsw.builder(DistanceMetric.L2, 6).numberOfEdges(32).build()) + * }).get(); + * }</pre> + */ + public static CompletableFuture<String> create( + @NonNull BaseClient client, @NonNull GlideString indexName, @NonNull FieldInfo[] fields) { + // Node: bug in meme DB - command fails if cmd is too short even though all mandatory args are + // present + // TODO confirm is it fixed or not and update docs if needed + return create(client, indexName, fields, FTCreateOptions.builder().build()); + } + + /** + * Creates an index and initiates a backfill of that index. + * + * @param client The client to execute the command. + * @param indexName The index name. + * @param fields Fields to populate into the index. + * @param options Additional parameters for the command - see {@link FTCreateOptions}. + * @return <code>OK</code>. + * @example + * <pre>{@code + * // Create a 6-dimensional JSON index using the HNSW algorithm: + * FT.create(client, gs("json_idx1"), + * new FieldInfo[] { new FieldInfo(gs("$.vec"), gs("VEC"), + * VectorFieldHnsw.builder(DistanceMetric.L2, 6).numberOfEdges(32).build()) + * }, + * FTCreateOptions.builder().indexType(JSON).prefixes(new String[] {"json:"}).build(), + * ).get(); + * }</pre> + */ + public static CompletableFuture<String> create( + @NonNull BaseClient client, + @NonNull GlideString indexName, + @NonNull FieldInfo[] fields, + @NonNull FTCreateOptions options) { var args = Stream.of( - new String[] {"FT.CREATE", indexName}, + new GlideString[] {gs("FT.CREATE"), indexName}, options.toArgs(), - new String[] {"SCHEMA"}, + new GlideString[] {gs("SCHEMA")}, Arrays.stream(fields) .map(FieldInfo::toArgs) .flatMap(Arrays::stream) - .toArray(String[]::new)) + .toArray(GlideString[]::new)) .flatMap(Arrays::stream) - .map(GlideString::gs) .toArray(GlideString[]::new); return executeCommand(client, args, false); } diff --git a/java/client/src/main/java/glide/api/models/commands/FT/FTCreateOptions.java b/java/client/src/main/java/glide/api/models/commands/FT/FTCreateOptions.java index fab4edee25..67451b1f65 100644 --- a/java/client/src/main/java/glide/api/models/commands/FT/FTCreateOptions.java +++ b/java/client/src/main/java/glide/api/models/commands/FT/FTCreateOptions.java @@ -1,13 +1,18 @@ /** 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.BaseClient; import glide.api.commands.servermodules.FT; +import glide.api.models.GlideString; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; @@ -23,20 +28,36 @@ public class FTCreateOptions { private final IndexType indexType; /** A list of prefixes of index definitions. */ - private final String[] prefixes; + private final GlideString[] prefixes; + + FTCreateOptions(IndexType indexType, GlideString[] prefixes) { + this.indexType = indexType; + this.prefixes = prefixes; + } + + public static FTCreateOptionsBuilder builder() { + return new FTCreateOptionsBuilder(); + } - public String[] toArgs() { - var args = new ArrayList<String>(); + public GlideString[] toArgs() { + var args = new ArrayList<GlideString>(); if (indexType != null) { - args.add("ON"); - args.add(indexType.toString()); + args.add(gs("ON")); + args.add(gs(indexType.toString())); } if (prefixes != null && prefixes.length > 0) { - args.add("PREFIX"); - args.add(Integer.toString(prefixes.length)); + args.add(gs("PREFIX")); + args.add(gs(Integer.toString(prefixes.length))); args.addAll(List.of(prefixes)); } - return args.toArray(String[]::new); + return args.toArray(GlideString[]::new); + } + + public static class FTCreateOptionsBuilder { + public FTCreateOptionsBuilder prefixes(String[] prefixes) { + this.prefixes = Stream.of(prefixes).map(GlideString::gs).toArray(GlideString[]::new); + return this; + } } /** Type of the index dataset. */ @@ -336,8 +357,8 @@ public T initialCapacity(int initialCapacity) { /** Field definition to be added into index schema. */ public static class FieldInfo { - private final String identifier; - private final String alias; + private final GlideString identifier; + private final GlideString alias; private final Field field; /** @@ -347,7 +368,7 @@ public static class FieldInfo { * @param field The {@link Field} itself. */ public FieldInfo(@NonNull String identifier, @NonNull Field field) { - this.identifier = identifier; + this.identifier = gs(identifier); this.field = field; this.alias = null; } @@ -360,21 +381,47 @@ public FieldInfo(@NonNull String identifier, @NonNull Field field) { * @param field The {@link Field} itself. */ public FieldInfo(@NonNull String identifier, @NonNull String alias, @NonNull Field field) { + this.identifier = gs(identifier); + this.alias = gs(alias); + this.field = field; + } + + /** + * Field definition to be added into index schema. + * + * @param identifier Field identifier (name). + * @param field The {@link Field} itself. + */ + public FieldInfo(@NonNull GlideString identifier, @NonNull 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( + @NonNull GlideString identifier, @NonNull GlideString alias, @NonNull Field field) { this.identifier = identifier; this.alias = alias; this.field = field; } /** Convert to module API. */ - public String[] toArgs() { - var args = new ArrayList<String>(); + public GlideString[] toArgs() { + var args = new ArrayList<GlideString>(); args.add(identifier); if (alias != null) { - args.add("AS"); + args.add(gs("AS")); args.add(alias); } - args.addAll(List.of(field.toArgs())); - return args.toArray(String[]::new); + args.addAll(Stream.of(field.toArgs()).map(GlideString::gs).collect(Collectors.toList())); + return args.toArray(GlideString[]::new); } } } From a9669a298f88d19c777fe5aaaae029b25141c025 Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand <yury.fridlyand@improving.com> Date: Fri, 11 Oct 2024 10:20:55 -0700 Subject: [PATCH 5/5] Address PR review. Signed-off-by: Yury-Fridlyand <yury.fridlyand@improving.com> --- .../models/commands/FT/FTCreateOptions.java | 18 ++---------------- .../java/glide/modules/VectorSearchTests.java | 2 +- 2 files changed, 3 insertions(+), 17 deletions(-) diff --git a/java/client/src/main/java/glide/api/models/commands/FT/FTCreateOptions.java b/java/client/src/main/java/glide/api/models/commands/FT/FTCreateOptions.java index 67451b1f65..1cdb6c77d0 100644 --- a/java/client/src/main/java/glide/api/models/commands/FT/FTCreateOptions.java +++ b/java/client/src/main/java/glide/api/models/commands/FT/FTCreateOptions.java @@ -169,20 +169,6 @@ public String[] toArgs() { } } - /** Vector index algorithm. */ - public enum Algorithm { - /** - * 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. - */ - FLAT - } - /** * Distance metrics to measure the degree of similarity between two vectors.<br> * The above metrics calculate distance between two vectors, where the smaller the value is, the @@ -201,13 +187,13 @@ public enum DistanceMetric { @AllArgsConstructor(access = AccessLevel.PROTECTED) abstract static class VectorField implements Field { private final Map<VectorAlgorithmParam, String> params; - private final VectorAlgorithm Algorithm; + private final VectorAlgorithm algorithm; @Override public String[] toArgs() { var args = new ArrayList<String>(); args.add(FieldType.VECTOR.toString()); - args.add(Algorithm.toString()); + args.add(algorithm.toString()); args.add(Integer.toString(params.size() * 2)); params.forEach( (name, value) -> { diff --git a/java/integTest/src/test/java/glide/modules/VectorSearchTests.java b/java/integTest/src/test/java/glide/modules/VectorSearchTests.java index 8d951137c4..67387026bd 100644 --- a/java/integTest/src/test/java/glide/modules/VectorSearchTests.java +++ b/java/integTest/src/test/java/glide/modules/VectorSearchTests.java @@ -85,7 +85,7 @@ public void ft_create() { .build()) .get()); - // create an index with NSFW vector with additional parameters + // create an index with HNSW vector with additional parameters assertEquals( OK, FT.create(