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(