From b6d6d4acd74ada63a5d147d0d42f243a7769c9f1 Mon Sep 17 00:00:00 2001
From: Tihomir Mateev <tihomir.mateev@gmail.com>
Date: Sun, 2 Feb 2025 13:39:30 +0100
Subject: [PATCH 1/3] Introducing the FT.CREATE command

---
 .../core/AbstractRedisAsyncCommands.java      |  13 +-
 .../core/AbstractRedisReactiveCommands.java   |  22 +-
 .../core/RediSearchCommandBuilder.java        |  62 ++
 .../api/async/RediSearchAsyncCommands.java    |  35 ++
 .../core/api/async/RedisAsyncCommands.java    |   2 +-
 .../reactive/RediSearchReactiveCommands.java  |  36 ++
 .../core/api/sync/RediSearchCommands.java     |  34 ++
 .../lettuce/core/api/sync/RedisCommands.java  |   2 +-
 .../api/async/RediSearchAsyncCommands.java    |  34 ++
 .../cluster/api/sync/RediSearchCommands.java  |  35 ++
 .../api/sync/RedisClusterCommands.java        |   1 +
 .../lettuce/core/protocol/CommandKeyword.java |   6 +-
 .../io/lettuce/core/protocol/CommandType.java |   3 +
 .../lettuce/core/search/DocumentLanguage.java | 144 +++++
 .../java/io/lettuce/core/search/Field.java    | 518 +++++++++++++++++
 .../java/io/lettuce/core/search/Fields.java   |  39 ++
 .../core/search/arguments/CreateArgs.java     | 535 ++++++++++++++++++
 .../io/lettuce/core/search/package-info.java  |  10 +
 .../RediSearchCoroutinesCommands.kt           |  37 ++
 .../lettuce/core/api/RediSearchCommands.java  |  34 ++
 .../io/lettuce/apigenerator/Constants.java    |   2 +-
 .../RediSearchCommandBuilderUnitTests.java    |  92 +++
 .../core/json/RediSearchIntegrationTests.java |  84 +++
 23 files changed, 1769 insertions(+), 11 deletions(-)
 create mode 100644 src/main/java/io/lettuce/core/RediSearchCommandBuilder.java
 create mode 100644 src/main/java/io/lettuce/core/api/async/RediSearchAsyncCommands.java
 create mode 100644 src/main/java/io/lettuce/core/api/reactive/RediSearchReactiveCommands.java
 create mode 100644 src/main/java/io/lettuce/core/api/sync/RediSearchCommands.java
 create mode 100644 src/main/java/io/lettuce/core/cluster/api/async/RediSearchAsyncCommands.java
 create mode 100644 src/main/java/io/lettuce/core/cluster/api/sync/RediSearchCommands.java
 create mode 100644 src/main/java/io/lettuce/core/search/DocumentLanguage.java
 create mode 100644 src/main/java/io/lettuce/core/search/Field.java
 create mode 100644 src/main/java/io/lettuce/core/search/Fields.java
 create mode 100644 src/main/java/io/lettuce/core/search/arguments/CreateArgs.java
 create mode 100644 src/main/java/io/lettuce/core/search/package-info.java
 create mode 100644 src/main/kotlin/io/lettuce/core/api/coroutines/RediSearchCoroutinesCommands.kt
 create mode 100644 src/main/templates/io/lettuce/core/api/RediSearchCommands.java
 create mode 100644 src/test/java/io/lettuce/core/RediSearchCommandBuilderUnitTests.java
 create mode 100644 src/test/java/io/lettuce/core/json/RediSearchIntegrationTests.java

diff --git a/src/main/java/io/lettuce/core/AbstractRedisAsyncCommands.java b/src/main/java/io/lettuce/core/AbstractRedisAsyncCommands.java
index de095893fa..b1f3d1650e 100644
--- a/src/main/java/io/lettuce/core/AbstractRedisAsyncCommands.java
+++ b/src/main/java/io/lettuce/core/AbstractRedisAsyncCommands.java
@@ -48,6 +48,8 @@
 import io.lettuce.core.protocol.CommandType;
 import io.lettuce.core.protocol.ProtocolKeyword;
 import io.lettuce.core.protocol.RedisCommand;
+import io.lettuce.core.search.Fields;
+import io.lettuce.core.search.arguments.CreateArgs;
 import reactor.core.publisher.Mono;
 
 import java.time.Duration;
@@ -79,7 +81,8 @@ public abstract class AbstractRedisAsyncCommands<K, V> implements RedisAclAsyncC
         RedisKeyAsyncCommands<K, V>, RedisStringAsyncCommands<K, V>, RedisListAsyncCommands<K, V>, RedisSetAsyncCommands<K, V>,
         RedisSortedSetAsyncCommands<K, V>, RedisScriptingAsyncCommands<K, V>, RedisServerAsyncCommands<K, V>,
         RedisHLLAsyncCommands<K, V>, BaseRedisAsyncCommands<K, V>, RedisTransactionalAsyncCommands<K, V>,
-        RedisGeoAsyncCommands<K, V>, RedisClusterAsyncCommands<K, V>, RedisJsonAsyncCommands<K, V> {
+        RedisGeoAsyncCommands<K, V>, RedisClusterAsyncCommands<K, V>, RedisJsonAsyncCommands<K, V>,
+        RediSearchAsyncCommands<K, V> {
 
     private final StatefulConnection<K, V> connection;
 
@@ -87,6 +90,8 @@ public abstract class AbstractRedisAsyncCommands<K, V> implements RedisAclAsyncC
 
     private final RedisJsonCommandBuilder<K, V> jsonCommandBuilder;
 
+    private final RediSearchCommandBuilder<K, V> searchCommandBuilder;
+
     private final Mono<JsonParser> parser;
 
     /**
@@ -101,6 +106,7 @@ public AbstractRedisAsyncCommands(StatefulConnection<K, V> connection, RedisCode
         this.connection = connection;
         this.commandBuilder = new RedisCommandBuilder<>(codec);
         this.jsonCommandBuilder = new RedisJsonCommandBuilder<>(codec, parser);
+        this.searchCommandBuilder = new RediSearchCommandBuilder<>(codec);
     }
 
     /**
@@ -1478,6 +1484,11 @@ public boolean isOpen() {
         return connection.isOpen();
     }
 
+    @Override
+    public RedisFuture<String> ftCreate(K index, CreateArgs<K, V> options, Fields<K> fields) {
+        return dispatch(searchCommandBuilder.ftCreate(index, options, fields));
+    }
+
     @Override
     public RedisFuture<List<Long>> jsonArrappend(K key, JsonPath jsonPath, JsonValue... values) {
         return dispatch(jsonCommandBuilder.jsonArrappend(key, jsonPath, values));
diff --git a/src/main/java/io/lettuce/core/AbstractRedisReactiveCommands.java b/src/main/java/io/lettuce/core/AbstractRedisReactiveCommands.java
index 1e9365821f..2deb31e78c 100644
--- a/src/main/java/io/lettuce/core/AbstractRedisReactiveCommands.java
+++ b/src/main/java/io/lettuce/core/AbstractRedisReactiveCommands.java
@@ -49,6 +49,8 @@
 import io.lettuce.core.protocol.RedisCommand;
 import io.lettuce.core.protocol.TracedCommand;
 import io.lettuce.core.resource.ClientResources;
+import io.lettuce.core.search.Fields;
+import io.lettuce.core.search.arguments.CreateArgs;
 import io.lettuce.core.tracing.TraceContext;
 import io.lettuce.core.tracing.TraceContextProvider;
 import io.lettuce.core.tracing.Tracing;
@@ -84,12 +86,12 @@
  * @author Tihomir Mateev
  * @since 4.0
  */
-public abstract class AbstractRedisReactiveCommands<K, V>
-        implements RedisAclReactiveCommands<K, V>, RedisHashReactiveCommands<K, V>, RedisKeyReactiveCommands<K, V>,
-        RedisStringReactiveCommands<K, V>, RedisListReactiveCommands<K, V>, RedisSetReactiveCommands<K, V>,
-        RedisSortedSetReactiveCommands<K, V>, RedisScriptingReactiveCommands<K, V>, RedisServerReactiveCommands<K, V>,
-        RedisHLLReactiveCommands<K, V>, BaseRedisReactiveCommands<K, V>, RedisTransactionalReactiveCommands<K, V>,
-        RedisGeoReactiveCommands<K, V>, RedisClusterReactiveCommands<K, V>, RedisJsonReactiveCommands<K, V> {
+public abstract class AbstractRedisReactiveCommands<K, V> implements RedisAclReactiveCommands<K, V>,
+        RedisHashReactiveCommands<K, V>, RedisKeyReactiveCommands<K, V>, RedisStringReactiveCommands<K, V>,
+        RedisListReactiveCommands<K, V>, RedisSetReactiveCommands<K, V>, RedisSortedSetReactiveCommands<K, V>,
+        RedisScriptingReactiveCommands<K, V>, RedisServerReactiveCommands<K, V>, RedisHLLReactiveCommands<K, V>,
+        BaseRedisReactiveCommands<K, V>, RedisTransactionalReactiveCommands<K, V>, RedisGeoReactiveCommands<K, V>,
+        RedisClusterReactiveCommands<K, V>, RedisJsonReactiveCommands<K, V>, RediSearchReactiveCommands<K, V> {
 
     private final StatefulConnection<K, V> connection;
 
@@ -97,6 +99,8 @@ public abstract class AbstractRedisReactiveCommands<K, V>
 
     private final RedisJsonCommandBuilder<K, V> jsonCommandBuilder;
 
+    private final RediSearchCommandBuilder<K, V> searchCommandBuilder;
+
     private final Mono<JsonParser> parser;
 
     private final ClientResources clientResources;
@@ -117,6 +121,7 @@ public AbstractRedisReactiveCommands(StatefulConnection<K, V> connection, RedisC
         this.parser = parser;
         this.commandBuilder = new RedisCommandBuilder<>(codec);
         this.jsonCommandBuilder = new RedisJsonCommandBuilder<>(codec, parser);
+        this.searchCommandBuilder = new RediSearchCommandBuilder<>(codec);
         this.clientResources = connection.getResources();
         this.tracingEnabled = clientResources.tracing().isEnabled();
     }
@@ -1543,6 +1548,11 @@ public boolean isOpen() {
         return connection.isOpen();
     }
 
+    @Override
+    public Mono<String> ftCreate(K index, CreateArgs<K, V> options, Fields<K> fields) {
+        return createMono(() -> searchCommandBuilder.ftCreate(index, options, fields));
+    }
+
     @Override
     public Flux<Long> jsonArrappend(K key, JsonPath jsonPath, JsonValue... values) {
         return createDissolvingFlux(() -> jsonCommandBuilder.jsonArrappend(key, jsonPath, values));
diff --git a/src/main/java/io/lettuce/core/RediSearchCommandBuilder.java b/src/main/java/io/lettuce/core/RediSearchCommandBuilder.java
new file mode 100644
index 0000000000..163c199a4c
--- /dev/null
+++ b/src/main/java/io/lettuce/core/RediSearchCommandBuilder.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2025, Redis Ltd. and Contributors
+ * All rights reserved.
+ *
+ * Licensed under the MIT License.
+ */
+package io.lettuce.core;
+
+import io.lettuce.core.codec.RedisCodec;
+import io.lettuce.core.output.StatusOutput;
+import io.lettuce.core.protocol.BaseRedisCommandBuilder;
+import io.lettuce.core.protocol.Command;
+import io.lettuce.core.protocol.CommandArgs;
+import io.lettuce.core.protocol.CommandKeyword;
+import io.lettuce.core.search.Fields;
+import io.lettuce.core.search.arguments.CreateArgs;
+import io.lettuce.core.search.Field;
+
+import static io.lettuce.core.protocol.CommandType.*;
+
+/**
+ * Command builder for RediSearch commands.
+ *
+ * @param <K> Key type.
+ * @param <V> Value type.
+ * @since 6.6
+ */
+class RediSearchCommandBuilder<K, V> extends BaseRedisCommandBuilder<K, V> {
+
+    RediSearchCommandBuilder(RedisCodec<K, V> codec) {
+        super(codec);
+    }
+
+    /**
+     * Create a new index with the given name, index options and fields.
+     *
+     * @param index the index name
+     * @param createArgs the index options
+     * @param fields the fields
+     * @return the result of the create command
+     */
+    public Command<K, V, String> ftCreate(K index, CreateArgs<K, V> createArgs, Fields<K> fields) {
+        notNullKey(index);
+        notEmpty(fields.getFields().toArray());
+
+        CommandArgs<K, V> args = new CommandArgs<>(codec).addKey(index);
+
+        if (createArgs != null) {
+            createArgs.build(args);
+        }
+
+        args.add(CommandKeyword.SCHEMA);
+
+        for (Field<K> field : fields.getFields()) {
+            field.build(args);
+        }
+
+        return createCommand(FT_CREATE, new StatusOutput<>(codec), args);
+
+    }
+
+}
diff --git a/src/main/java/io/lettuce/core/api/async/RediSearchAsyncCommands.java b/src/main/java/io/lettuce/core/api/async/RediSearchAsyncCommands.java
new file mode 100644
index 0000000000..bef4cb4aa0
--- /dev/null
+++ b/src/main/java/io/lettuce/core/api/async/RediSearchAsyncCommands.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2025, Redis Ltd. and Contributors
+ * All rights reserved.
+ *
+ * Licensed under the MIT License.
+ */
+package io.lettuce.core.api.async;
+
+import io.lettuce.core.RedisFuture;
+import io.lettuce.core.search.Fields;
+import io.lettuce.core.search.arguments.CreateArgs;
+
+/**
+ * Asynchronous executed commands for RediSearch functionality
+ *
+ * @param <K> Key type.
+ * @param <V> Value type.
+ * @author Tihomir Mateev
+ * @see <a href="https://redis.io/docs/latest/operate/oss_and_stack/stack-with-enterprise/search/">RediSearch</a>
+ * @since 6.6
+ * @generated by io.lettuce.apigenerator.CreateAsyncApi
+ */
+public interface RediSearchAsyncCommands<K, V> {
+
+    /**
+     * Create a new index with the given name, index options and fields.
+     *
+     * @param index the index name
+     * @param options the index options
+     * @param fields the fields
+     * @return the result of the create command
+     */
+    RedisFuture<String> ftCreate(K index, CreateArgs<K, V> options, Fields<K> fields);
+
+}
diff --git a/src/main/java/io/lettuce/core/api/async/RedisAsyncCommands.java b/src/main/java/io/lettuce/core/api/async/RedisAsyncCommands.java
index 6ff3ef9ad1..5689de96f5 100644
--- a/src/main/java/io/lettuce/core/api/async/RedisAsyncCommands.java
+++ b/src/main/java/io/lettuce/core/api/async/RedisAsyncCommands.java
@@ -37,7 +37,7 @@ public interface RedisAsyncCommands<K, V> extends BaseRedisAsyncCommands<K, V>,
         RedisHashAsyncCommands<K, V>, RedisHLLAsyncCommands<K, V>, RedisKeyAsyncCommands<K, V>, RedisListAsyncCommands<K, V>,
         RedisScriptingAsyncCommands<K, V>, RedisServerAsyncCommands<K, V>, RedisSetAsyncCommands<K, V>,
         RedisSortedSetAsyncCommands<K, V>, RedisStreamAsyncCommands<K, V>, RedisStringAsyncCommands<K, V>,
-        RedisTransactionalAsyncCommands<K, V>, RedisJsonAsyncCommands<K, V> {
+        RedisTransactionalAsyncCommands<K, V>, RedisJsonAsyncCommands<K, V>, RediSearchAsyncCommands<K, V> {
 
     /**
      * Authenticate to the server.
diff --git a/src/main/java/io/lettuce/core/api/reactive/RediSearchReactiveCommands.java b/src/main/java/io/lettuce/core/api/reactive/RediSearchReactiveCommands.java
new file mode 100644
index 0000000000..c9ecac34b5
--- /dev/null
+++ b/src/main/java/io/lettuce/core/api/reactive/RediSearchReactiveCommands.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2025, Redis Ltd. and Contributors
+ * All rights reserved.
+ *
+ * Licensed under the MIT License.
+ */
+package io.lettuce.core.api.reactive;
+
+import io.lettuce.core.search.Field;
+import io.lettuce.core.search.Fields;
+import io.lettuce.core.search.arguments.CreateArgs;
+import reactor.core.publisher.Mono;
+
+/**
+ * Reactive executed commands for RediSearch functionality
+ *
+ * @param <K> Key type.
+ * @param <V> Value type.
+ * @author Tihomir Mateev
+ * @see <a href="https://redis.io/docs/latest/operate/oss_and_stack/stack-with-enterprise/search/">RediSearch</a>
+ * @since 6.6
+ * @generated by io.lettuce.apigenerator.CreateReactiveApi
+ */
+public interface RediSearchReactiveCommands<K, V> {
+
+    /**
+     * Create a new index with the given name, index options and fields.
+     *
+     * @param index the index name
+     * @param options the index options
+     * @param fields the fields
+     * @return the result of the create command
+     */
+    Mono<String> ftCreate(K index, CreateArgs<K, V> options, Fields<K> fields);
+
+}
diff --git a/src/main/java/io/lettuce/core/api/sync/RediSearchCommands.java b/src/main/java/io/lettuce/core/api/sync/RediSearchCommands.java
new file mode 100644
index 0000000000..e4a746b097
--- /dev/null
+++ b/src/main/java/io/lettuce/core/api/sync/RediSearchCommands.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2025, Redis Ltd. and Contributors
+ * All rights reserved.
+ *
+ * Licensed under the MIT License.
+ */
+package io.lettuce.core.api.sync;
+
+import io.lettuce.core.search.Fields;
+import io.lettuce.core.search.arguments.CreateArgs;
+
+/**
+ * Synchronous executed commands for RediSearch functionality
+ *
+ * @param <K> Key type.
+ * @param <V> Value type.
+ * @author Tihomir Mateev
+ * @see <a href="https://redis.io/docs/latest/operate/oss_and_stack/stack-with-enterprise/search/">RediSearch</a>
+ * @since 6.6
+ * @generated by io.lettuce.apigenerator.CreateSyncApi
+ */
+public interface RediSearchCommands<K, V> {
+
+    /**
+     * Create a new index with the given name, index options and fields.
+     *
+     * @param index the index name
+     * @param options the index options
+     * @param fields the fields
+     * @return the result of the create command
+     */
+    String ftCreate(K index, CreateArgs<K, V> options, Fields<K> fields);
+
+}
diff --git a/src/main/java/io/lettuce/core/api/sync/RedisCommands.java b/src/main/java/io/lettuce/core/api/sync/RedisCommands.java
index 98f21b4cb2..e7f74d5378 100644
--- a/src/main/java/io/lettuce/core/api/sync/RedisCommands.java
+++ b/src/main/java/io/lettuce/core/api/sync/RedisCommands.java
@@ -36,7 +36,7 @@ public interface RedisCommands<K, V> extends BaseRedisCommands<K, V>, RedisAclCo
         RedisFunctionCommands<K, V>, RedisGeoCommands<K, V>, RedisHashCommands<K, V>, RedisHLLCommands<K, V>,
         RedisKeyCommands<K, V>, RedisListCommands<K, V>, RedisScriptingCommands<K, V>, RedisServerCommands<K, V>,
         RedisSetCommands<K, V>, RedisSortedSetCommands<K, V>, RedisStreamCommands<K, V>, RedisStringCommands<K, V>,
-        RedisTransactionalCommands<K, V>, RedisJsonCommands<K, V> {
+        RedisTransactionalCommands<K, V>, RedisJsonCommands<K, V>, RediSearchCommands<K, V> {
 
     /**
      * Authenticate to the server.
diff --git a/src/main/java/io/lettuce/core/cluster/api/async/RediSearchAsyncCommands.java b/src/main/java/io/lettuce/core/cluster/api/async/RediSearchAsyncCommands.java
new file mode 100644
index 0000000000..3747f8a26a
--- /dev/null
+++ b/src/main/java/io/lettuce/core/cluster/api/async/RediSearchAsyncCommands.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2025, Redis Ltd. and Contributors
+ * All rights reserved.
+ *
+ * Licensed under the MIT License.
+ */
+package io.lettuce.core.cluster.api.async;
+
+import io.lettuce.core.search.Fields;
+import io.lettuce.core.search.arguments.CreateArgs;
+
+/**
+ * Asynchronous executed commands on a node selection for RediSearch functionality
+ *
+ * @param <K> Key type.
+ * @param <V> Value type.
+ * @author Tihomir Mateev
+ * @see <a href="https://redis.io/docs/latest/operate/oss_and_stack/stack-with-enterprise/search/">RediSearch</a>
+ * @since 6.6
+ * @generated by io.lettuce.apigenerator.CreateAsyncNodeSelectionClusterApi
+ */
+public interface RediSearchAsyncCommands<K, V> {
+
+    /**
+     * Create a new index with the given name, index options and fields.
+     *
+     * @param index the index name
+     * @param options the index options
+     * @param fields the fields
+     * @return the result of the create command
+     */
+    AsyncExecutions<String> ftCreate(K index, CreateArgs<K, V> options, Fields<K> fields);
+
+}
diff --git a/src/main/java/io/lettuce/core/cluster/api/sync/RediSearchCommands.java b/src/main/java/io/lettuce/core/cluster/api/sync/RediSearchCommands.java
new file mode 100644
index 0000000000..9970fc9200
--- /dev/null
+++ b/src/main/java/io/lettuce/core/cluster/api/sync/RediSearchCommands.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2025, Redis Ltd. and Contributors
+ * All rights reserved.
+ *
+ * Licensed under the MIT License.
+ */
+package io.lettuce.core.cluster.api.sync;
+
+import io.lettuce.core.search.Field;
+import io.lettuce.core.search.Fields;
+import io.lettuce.core.search.arguments.CreateArgs;
+
+/**
+ * Synchronous executed commands on a node selection for RediSearch functionality
+ *
+ * @param <K> Key type.
+ * @param <V> Value type.
+ * @author Tihomir Mateev
+ * @see <a href="https://redis.io/docs/latest/operate/oss_and_stack/stack-with-enterprise/search/">RediSearch</a>
+ * @since 6.6
+ * @generated by io.lettuce.apigenerator.CreateSyncNodeSelectionClusterApi
+ */
+public interface RediSearchCommands<K, V> {
+
+    /**
+     * Create a new index with the given name, index options and fields.
+     *
+     * @param index the index name
+     * @param options the index options
+     * @param fields the fields
+     * @return the result of the create command
+     */
+    Executions<String> ftCreate(K index, CreateArgs<K, V> options, Fields<K> fields);
+
+}
diff --git a/src/main/java/io/lettuce/core/cluster/api/sync/RedisClusterCommands.java b/src/main/java/io/lettuce/core/cluster/api/sync/RedisClusterCommands.java
index 988975740c..d66093759f 100644
--- a/src/main/java/io/lettuce/core/cluster/api/sync/RedisClusterCommands.java
+++ b/src/main/java/io/lettuce/core/cluster/api/sync/RedisClusterCommands.java
@@ -34,6 +34,7 @@
  * @param <V> Value type.
  * @author Mark Paluch
  * @author dengliming
+ * @author Tihomir Mateev
  * @since 4.0
  */
 public interface RedisClusterCommands<K, V>
diff --git a/src/main/java/io/lettuce/core/protocol/CommandKeyword.java b/src/main/java/io/lettuce/core/protocol/CommandKeyword.java
index c9d782afd8..62c6e84cd0 100644
--- a/src/main/java/io/lettuce/core/protocol/CommandKeyword.java
+++ b/src/main/java/io/lettuce/core/protocol/CommandKeyword.java
@@ -49,7 +49,11 @@ public enum CommandKeyword implements ProtocolKeyword {
 
     MIGRATING, IMPORTING, SAVE, SKIPME, SLAVES, STREAM, STORE, SUM, SEGFAULT, SETUSER, TAKEOVER, TRACKING, TRACKINGINFO, TYPE, UNBLOCK, USERS, USAGE, WEIGHTS, WHOAMI,
 
-    WITHMATCHLEN, WITHSCORE, WITHSCORES, WITHVALUES, XOR, XX, YES, INDENT, NEWLINE, SPACE, GT, LT;
+    WITHMATCHLEN, WITHSCORE, WITHSCORES, WITHVALUES, XOR, XX, YES, INDENT, NEWLINE, SPACE, GT, LT,
+
+    MAXTEXTFIELDS, PREFIX, FILTER, LANGUAGE, LANGUAGE_FIELD, SCORE, SCORE_FIELD, PAYLOAD_FIELD, TEMPORARY, NOOFFSETS, NOHL, NOFIELDS, NOFREQS, SKIPINITIALSCAN, STOPWORDS, AS, SORTABLE, SCHEMA, UNF, NOINDEX,
+
+    NOSTEM, PHONETIC, WEIGHT, SEPARATOR, CASESENSITIVE, WITHSUFFIXTRIE, INDEXEMPTY, INDEXMISSING;
 
     public final byte[] bytes;
 
diff --git a/src/main/java/io/lettuce/core/protocol/CommandType.java b/src/main/java/io/lettuce/core/protocol/CommandType.java
index 9a2fcf83f6..3d5b9f4e9d 100644
--- a/src/main/java/io/lettuce/core/protocol/CommandType.java
+++ b/src/main/java/io/lettuce/core/protocol/CommandType.java
@@ -112,6 +112,9 @@ public enum CommandType implements ProtocolKeyword {
                                     "JSON.OBJLEN"), JSON_SET("JSON.SET"), JSON_STRAPPEND("JSON.STRAPPEND"), JSON_STRLEN(
                                             "JSON.STRLEN"), JSON_TOGGLE("JSON.TOGGLE"), JSON_TYPE("JSON.TYPE"),
 
+    // RediSearch
+    FT_CREATE("FT.CREATE"),
+
     // Others
 
     TIME, WAIT,
diff --git a/src/main/java/io/lettuce/core/search/DocumentLanguage.java b/src/main/java/io/lettuce/core/search/DocumentLanguage.java
new file mode 100644
index 0000000000..ba3dd1d161
--- /dev/null
+++ b/src/main/java/io/lettuce/core/search/DocumentLanguage.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright 2025, Redis Ltd. and Contributors
+ * All rights reserved.
+ *
+ * Licensed under the MIT License.
+ */
+package io.lettuce.core.search;
+
+import java.util.Locale;
+
+/**
+ * Supported document languages.
+ *
+ * @since 6.6
+ * @author Tihomir Mateev
+ * @see <a href="https://redis.io/docs/latest/develop/interact/search-and-query/advanced-concepts/stemming/">Stemming</a>
+ */
+public enum DocumentLanguage {
+
+    /**
+     * Arabic
+     */
+    ARABIC("arabic", new Locale("ar")),
+    /**
+     * Armenian
+     */
+    ARMENIAN("armenian", new Locale("hy")),
+    /**
+     * Danish
+     */
+    DANISH("danish", new Locale("da")),
+    /**
+     * Dutch
+     */
+    DUTCH("dutch", new Locale("nl")),
+    /**
+     * English
+     */
+    ENGLISH("english", Locale.ENGLISH),
+    /**
+     * Finnish
+     */
+    FINNISH("finnish", new Locale("fi")),
+    /**
+     * French
+     */
+    FRENCH("french", Locale.FRENCH),
+    /**
+     * German
+     */
+    GERMAN("german", Locale.GERMAN),
+    /**
+     * Hungarian
+     */
+    HUNGARIAN("hungarian", new Locale("hu")),
+    /**
+     * Italian
+     */
+    ITALIAN("italian", Locale.ITALIAN),
+    /**
+     * Norwegian
+     */
+    NORWEGIAN("norwegian", new Locale("no")),
+    /**
+     * Portuguese
+     */
+    PORTUGUESE("portuguese", new Locale("pt")),
+    /**
+     * Romanian
+     */
+    ROMANIAN("romanian", new Locale("ro")),
+    /**
+     * Russian
+     */
+    RUSSIAN("russian", new Locale("ru")),
+    /**
+     * Serbian
+     */
+    SERBIAN("serbian", new Locale("sr")),
+    /**
+     * Spanish
+     */
+    SPANISH("spanish", new Locale("es")),
+    /**
+     * Swedish
+     */
+    SWEDISH("swedish", new Locale("sv")),
+    /**
+     * Tamil
+     */
+    TAMIL("tamil", new Locale("ta")),
+    /**
+     * Turkish
+     */
+    TURKISH("turkish", new Locale("tr")),
+    /**
+     * Yiddish
+     */
+    YIDDISH("yiddish", new Locale("yi")),
+    /**
+     * Chinese
+     * 
+     * @see <a href="https://redis.io/docs/latest/develop/interact/search-and-query/advanced-concepts/chinese/">Chinese
+     *      support</a>
+     */
+    CHINESE("chinese", Locale.CHINESE);
+
+    private final String language;
+
+    private final Locale locale;
+
+    DocumentLanguage(String language, Locale locale) {
+        this.language = language;
+        this.locale = locale;
+    }
+
+    @Override
+    public String toString() {
+        return language;
+    }
+
+    /**
+     * @return the {@link DocumentLanguage} as a {@link Locale}
+     */
+    public Locale getLocale() {
+        return locale;
+    }
+
+    /**
+     * Retrieve the {@link DocumentLanguage} for a given {@link Locale}.
+     * 
+     * @param locale the locale
+     * @return the {@link DocumentLanguage}
+     */
+    public static DocumentLanguage getLanguage(Locale locale) {
+        for (DocumentLanguage language : DocumentLanguage.values()) {
+            if (language.getLocale().getLanguage().equals(locale.getLanguage())) {
+                return language;
+            }
+        }
+        throw new UnsupportedOperationException("No language found for locale: " + locale);
+    }
+
+}
diff --git a/src/main/java/io/lettuce/core/search/Field.java b/src/main/java/io/lettuce/core/search/Field.java
new file mode 100644
index 0000000000..6bc5294435
--- /dev/null
+++ b/src/main/java/io/lettuce/core/search/Field.java
@@ -0,0 +1,518 @@
+/*
+ * Copyright 2025, Redis Ltd. and Contributors
+ * All rights reserved.
+ *
+ * Licensed under the MIT License.
+ */
+package io.lettuce.core.search;
+
+import io.lettuce.core.protocol.CommandArgs;
+
+import java.util.Optional;
+
+import static io.lettuce.core.protocol.CommandKeyword.*;
+
+/**
+ * Representation of a field in a RediSearch index.
+ *
+ * @param <K> Key type
+ * @see <a href="https://redis.io/docs/latest/develop/interact/search-and-query/basic-constructs/field-and-type-options/">Field
+ *      and type options</a>
+ * @since 6.6
+ * @author Tihomir Mateev
+ */
+@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
+public class Field<K> {
+
+    /**
+     * Field types
+     * 
+     * @see <a href=
+     *      "https://redis.io/docs/latest/develop/interact/search-and-query/basic-constructs/field-and-type-options/">Field and
+     *      type options</a>
+     */
+    public enum Type {
+        /**
+         * Allows full-text search queries against the value in this attribute.
+         */
+        TEXT,
+        /**
+         * Allows exact-match queries, 1 as categories or primary keys, against the value in this attribute.
+         * 
+         * @see <a href="https://redis.io/docs/latest/develop/interact/search-and-query/advanced-concepts/tags/">Tag Fields</a>
+         */
+        TAG,
+        /**
+         * Allows numeric range queries against the value in this attribute. See query syntax docs for details on how to use
+         * numeric ranges.
+         */
+        NUMERIC,
+        /**
+         * Allows radius range queries against the value (point) in this attribute. The value of the attribute must be a string
+         * containing a longitude (first) and latitude separated by a comma.
+         */
+        GEO,
+        /**
+         * Allows vector queries against the value in this attribute. Requires query dialect 2 or above (introduced in
+         * RediSearch v2.4).
+         * 
+         * @see <a href="https://redis.io/docs/latest/develop/interact/search-and-query/advanced-concepts/vectors/">Vector
+         *      Fields</a>
+         * @see <a href=
+         *      "https://redis.io/docs/latest/develop/interact/search-and-query/advanced-concepts/dialects/#dialect-2">Query
+         *      Dialect v2</a>
+         */
+        VECTOR,
+        /**
+         * Allows polygon queries against the value in this attribute. The value of the attribute must follow a
+         * <a href="https://en.wikipedia.org/wiki/Well-known_text_representation_of_geometry">WKT notation</a> list of 2D points
+         * representing the polygon edges POLYGON((x1 y1, x2 y2, ...) separated by a comma.
+         * <p/>
+         * A GEOSHAPE field type can be followed by one of the following coordinate systems:
+         * <ul>
+         * <li>SPHERICAL for Geographic longitude and latitude coordinates</li>
+         * <li>FLAT for Cartesian X Y coordinates</li>
+         * <li>The default coordinate system is SPHERICAL.</li>
+         * </ul>
+         *
+         * Currently GEOSHAPE doesn't support JSON multi-value and SORTABLE option.
+         */
+        GEOSHAPE
+    }
+
+    /**
+     * Phonetic matchers
+     *
+     * @see <a href=
+     *      "https://redis.io/docs/latest/develop/interact/search-and-query/advanced-concepts/phonetic_matching/">Phonetic
+     *      Matching</a>
+     */
+    public enum PhoneticMatcher {
+
+        ENGLISH("dm:en"), FRENCH("dm:fr"), PORTUGUESE("dm:pt"), SPANISH("dm:es");
+
+        PhoneticMatcher(String matcher) {
+            this.matcher = matcher;
+        }
+
+        private final String matcher;
+
+        /**
+         * @return the {@link String} representation of the matcher
+         */
+        public String getMatcher() {
+            return matcher;
+        }
+
+    }
+
+    private K name;
+
+    private Optional<K> as = Optional.empty();
+
+    private Type type;
+
+    private boolean sortable;
+
+    private boolean unNormalizedForm;
+
+    private boolean noStemming;
+
+    private boolean noIndex;
+
+    private Optional<PhoneticMatcher> phonetic = Optional.empty();;
+
+    private boolean caseSensitive;
+
+    private boolean withSuffixTrie;
+
+    private boolean indexEmpty;
+
+    private boolean indexMissing;
+
+    private Optional<Long> weight = Optional.empty();;
+
+    private Optional<String> separator = Optional.empty();;
+
+    private Field() {
+    }
+
+    /**
+     * Create a new {@link Field} using the builder pattern.
+     * <p/>
+     * One needs to call {@link Builder#build()} to build a single {@link Field} or {@link Builder#buildFields()} to build a
+     * {@link java.util.List} of {@link Field}s.
+     * 
+     * @param <K> Key type
+     * @return a new {@link Builder}
+     */
+    public static <K> Builder<K> builder() {
+        return new Builder<>();
+    }
+
+    /**
+     * Builder for {@link Field}.
+     * 
+     * @param <K> Key type
+     */
+    public static class Builder<K> {
+
+        private final Field<K> instance = new Field<>();
+
+        /**
+         * The name of the field in a hash the index is going to be based on.
+         * 
+         * @param name the name of the field
+         * @return the instance of the {@link Builder} for the purpose of method chaining
+         */
+        public Builder<K> name(K name) {
+            instance.name = name;
+            return this;
+        }
+
+        // TODO handling JsonPath
+        // public Builder<K> name(JsonPath path) {
+        // instance.name = path.toString();
+        // return this;
+        // }
+
+        /**
+         * The type of the field.
+         * 
+         * @param type the type of the field
+         * @return the instance of the {@link Builder} for the purpose of method chaining
+         * @see Type
+         */
+        public Builder<K> type(Type type) {
+            instance.type = type;
+            return this;
+        }
+
+        /**
+         * Defines the attribute associated to the identifier. For example, you can use this feature to alias a complex JSONPath
+         * expression with more memorable (and easier to type) name.
+         * 
+         * @param as the field name to be used in queries
+         * @return the instance of the {@link Builder} for the purpose of method chaining
+         */
+        public Builder<K> as(K as) {
+            instance.as = Optional.of(as);
+            return this;
+        }
+
+        /**
+         * {@link Type#NUMERIC}, {@link Type#TAG}, {@link Type#TEXT}, or {@link Type#GEO} attributes can have an optional
+         * SORTABLE argument. As the user sorts the results by the value of this attribute, the results are available with very
+         * low latency. Default is false (not sortable).
+         * <p/>
+         * Note that this adds memory overhead, so consider not declaring it on large text attributes. You can sort an attribute
+         * without the SORTABLE option, but the latency is not as good as with SORTABLE.
+         *
+         * @return the instance of the {@link Builder} for the purpose of method chaining
+         */
+        public Builder<K> sortable() {
+            instance.sortable = true;
+            return this;
+        }
+
+        /**
+         * By default, for hashes (not with JSON) SORTABLE applies normalization to the indexed value (characters set to
+         * lowercase, removal of diacritics). When using the unnormalized form (UNF), you can disable the normalization and keep
+         * the original form of the value. With JSON, UNF is implicit with SORTABLE (normalization is disabled).
+         * <p/>
+         * Default is false (normalized form).
+         *
+         * @return the instance of the {@link Builder} for the purpose of method chaining
+         */
+        public Builder<K> unNormalizedForm() {
+            instance.sortable = true;
+            instance.unNormalizedForm = true;
+            return this;
+        }
+
+        /**
+         * By default, the index applies stemming to {@link Type#TEXT} fields. If you don't want to apply stemming to the field,
+         * you can use the NOSTEM argument. This may be ideal for things like proper names.
+         *
+         * @return the instance of the {@link Builder} for the purpose of method chaining
+         */
+        public Builder<K> noStemming() {
+            instance.noStemming = true;
+            return this;
+        }
+
+        /**
+         * Attributes can have the NOINDEX option, which means they will not be indexed. This is useful in conjunction with
+         * {@link Builder#sortable()}, to create attributes whose update using PARTIAL will not cause full reindexing of the
+         * document. If an attribute has NOINDEX and doesn't have SORTABLE, it will just be ignored by the index.
+         *
+         * @return the instance of the {@link Builder} for the purpose of method chaining
+         */
+        public Builder<K> noIndex() {
+            instance.noIndex = true;
+            return this;
+        }
+
+        /**
+         * Phonetic matching is a feature that allows you to search for similar-sounding words. For example, a search for
+         * "Smith" will also return results for "Smyth". Phonetic matching is language-specific, and you can specify the
+         * language using the PHONETIC argument.
+         * <p/>
+         * The following languages are supported:
+         * <ul>
+         * <li>ENGLISH</li>
+         * <li>FRENCH</li>
+         * <li>PORTUGUESE</li>x
+         * <li>SPANISH</li>
+         * </ul>
+         *
+         * @see <a href=
+         *      "https://redis.io/docs/latest/develop/interact/search-and-query/advanced-concepts/phonetic_matching/">Phonetic
+         *      Matching</a>
+         * @param matcher the phonetic matcher
+         * @return the instance of the {@link Builder} for the purpose of method chaining
+         */
+        public Builder<K> phonetic(PhoneticMatcher matcher) {
+            instance.phonetic = Optional.of(matcher);
+            return this;
+        }
+
+        /**
+         * The weight of the field. Works with {@link Type#TEXT} attributes, declares the importance of this attribute when
+         * calculating result accuracy. This is a multiplication factor. The default weight is 1.
+         *
+         * @param weight the weight of the field
+         * @return the instance of the {@link Builder} for the purpose of method chaining
+         */
+        public Builder<K> weight(long weight) {
+            instance.weight = Optional.of(weight);
+            return this;
+        }
+
+        /**
+         * The separator for {@link Type#TAG} attributes. The default separator is a comma.
+         *
+         * @param separator the separator for tag fields
+         * @return the instance of the {@link Builder} for the purpose of method chaining
+         */
+        public Builder<K> separator(String separator) {
+            instance.separator = Optional.of(separator);
+            return this;
+        }
+
+        /**
+         * Keeps the original letter cases of the tags. If not specified, the characters are converted to lowercase. Works with
+         * {@link Type#TAG} attributes.
+         *
+         * @return the instance of the {@link Builder} for the purpose of method chaining
+         */
+        public Builder<K> caseSensitive() {
+            instance.caseSensitive = true;
+            return this;
+        }
+
+        /**
+         * For {@link Type#TEXT} and {@link Type#TAG} attributes, keeps a suffix trie with all terms which match the suffix. It
+         * is used to optimize contains (foo) and suffix (*foo) queries. Otherwise, a brute-force search on the trie is
+         * performed. If the suffix trie exists for some fields, these queries will be disabled for other fields.
+         *
+         * @return the instance of the {@link Builder} for the purpose of method chaining
+         */
+        public Builder<K> withSuffixTrie() {
+            instance.withSuffixTrie = true;
+            return this;
+        }
+
+        /**
+         * For {@link Type#TEXT} and {@link Type#TAG} attributes, introduced in v2.10, allows you to index and search for empty
+         * strings. By default, empty strings are not indexed.
+         * 
+         * @return the instance of the {@link Builder} for the purpose of method chaining
+         */
+        public Builder<K> indexEmpty() {
+            instance.indexEmpty = true;
+            return this;
+        }
+
+        /**
+         * For all field types, introduced in v2.10, allows you to search for missing values, that is, documents that do not
+         * contain a specific field. Note the difference between a field with an empty value and a document with a missing
+         * value. By default, missing values are not indexed.
+         */
+        public Builder<K> indexMissing() {
+            instance.indexMissing = true;
+            return this;
+        }
+
+        /**
+         * Build a single {@link Field}.
+         *
+         * @return the instance of the {@link Field}
+         */
+        public Field<K> build() {
+            return instance;
+        }
+
+        /**
+         * Build a {@link java.util.List} of {@link Field}s, containing the current {@link Field} as the only element of the list.
+         *
+         * @return the instance of the {@link Field}
+         */
+        public Fields<K> buildFields() {
+            Fields<K> fields = new Fields<>();
+            return fields.add(instance);
+        }
+
+    }
+
+    /**
+     * @return the type of the field
+     * @see Builder#type(Type)
+     */
+    public Type getType() {
+        return type;
+    }
+
+    /**
+     * @return the name of the field
+     * @see Builder#name(Object)
+     */
+    public K getName() {
+        return name;
+    }
+
+    /**
+     * @return the alias of the field
+     * @see Builder#as(Object)
+     */
+    public Optional<K> getAs() {
+        return as;
+    }
+
+    /**
+     * @return if the field should be sortable
+     * @see Builder#sortable()
+     */
+    public boolean isSortable() {
+        return sortable;
+    }
+
+    /**
+     * @return if the field should be in unnormalized form
+     * @see Builder#unNormalizedForm()
+     */
+    public boolean isUnNormalizedForm() {
+        return unNormalizedForm;
+    }
+
+    /**
+     * @return if the field should not be indexed
+     * @see Builder#noIndex()
+     */
+    public boolean isNoIndex() {
+        return noIndex;
+    }
+
+    /**
+     * @return if the field should not be stemmed
+     * @see Builder#noStemming()
+     */
+    public boolean isNoStemming() {
+        return noStemming;
+    }
+
+    /**
+     * @return the setting for phonetic matching
+     * @see Builder#phonetic(PhoneticMatcher)
+     */
+    public Optional<PhoneticMatcher> isPhonetic() {
+        return phonetic;
+    }
+
+    /**
+     * @return if the field should be case sensitive
+     * @see Builder#caseSensitive()
+     */
+    public boolean isCaseSensitive() {
+        return caseSensitive;
+    }
+
+    /**
+     * @return if the field should have a suffix trie
+     * @see Builder#withSuffixTrie()
+     */
+    public boolean isWithSuffixTrie() {
+        return withSuffixTrie;
+    }
+
+    /**
+     * @return if the field should index empty values
+     * @see Builder#indexEmpty()
+     */
+    public boolean isIndexEmpty() {
+        return indexEmpty;
+    }
+
+    /**
+     * @return if the field should index missing values
+     * @see Builder#indexMissing()
+     */
+    public boolean isIndexMissing() {
+        return indexMissing;
+    }
+
+    /**
+     * @return the weight of the field
+     * @see Builder#weight(long)
+     */
+    public Optional<Long> getWeight() {
+        return weight;
+    }
+
+    /**
+     * @return the separator for tag fields
+     * @see Builder#separator(String)
+     */
+    public Optional<String> getSeparator() {
+        return separator;
+    }
+
+    /**
+     * Add all configured arguments to the final command
+     *
+     * @param args the command arguments to modify
+     */
+    public void build(CommandArgs<K, ?> args) {
+        args.addKey(name);
+        as.ifPresent(a -> args.add(AS).addKey(a));
+        args.add(type.toString());
+        if (sortable) {
+            args.add(SORTABLE);
+            if (unNormalizedForm) {
+                args.add(UNF);
+            }
+        }
+        if (noStemming) {
+            args.add(NOSTEM);
+        }
+        if (noIndex) {
+            args.add(NOINDEX);
+        }
+        phonetic.ifPresent(p -> args.add(PHONETIC).add(p.getMatcher()));
+        weight.ifPresent(w -> args.add(WEIGHT).add(w));
+        separator.ifPresent(s -> args.add(SEPARATOR).add(s));
+        if (caseSensitive) {
+            args.add(CASESENSITIVE);
+        }
+        if (withSuffixTrie) {
+            args.add(WITHSUFFIXTRIE);
+        }
+        if (indexEmpty) {
+            args.add(INDEXEMPTY);
+        }
+        if (indexMissing) {
+            args.add(INDEXMISSING);
+        }
+    }
+
+}
diff --git a/src/main/java/io/lettuce/core/search/Fields.java b/src/main/java/io/lettuce/core/search/Fields.java
new file mode 100644
index 0000000000..b338c01a62
--- /dev/null
+++ b/src/main/java/io/lettuce/core/search/Fields.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2025, Redis Ltd. and Contributors
+ * All rights reserved.
+ *
+ * Licensed under the MIT License.
+ */
+package io.lettuce.core.search;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class Fields<K> {
+
+    private final List<Field<K>> fields = new ArrayList<>();
+
+    @SafeVarargs
+    public static Fields<String> from(Field<String>... field) {
+        Fields<String> fields = new Fields<>();
+        for (Field<String> f : field) {
+            fields.add(f);
+        }
+        return fields;
+    }
+
+    public Fields<K> add(Field<K> field) {
+        fields.add(field);
+        return this;
+    }
+
+    public Fields<K> addAll(List<Field<K>> field) {
+        fields.addAll(field);
+        return this;
+    }
+
+    public List<Field<K>> getFields() {
+        return fields;
+    }
+
+}
diff --git a/src/main/java/io/lettuce/core/search/arguments/CreateArgs.java b/src/main/java/io/lettuce/core/search/arguments/CreateArgs.java
new file mode 100644
index 0000000000..9cbb71d6c6
--- /dev/null
+++ b/src/main/java/io/lettuce/core/search/arguments/CreateArgs.java
@@ -0,0 +1,535 @@
+/*
+ * Copyright 2025, Redis Ltd. and Contributors
+ * All rights reserved.
+ *
+ * Licensed under the MIT License.
+ */
+package io.lettuce.core.search.arguments;
+
+import io.lettuce.core.protocol.CommandArgs;
+import io.lettuce.core.search.DocumentLanguage;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.OptionalDouble;
+import java.util.OptionalLong;
+
+import static io.lettuce.core.protocol.CommandKeyword.*;
+
+/**
+ * Argument list builder for {@code FT.CREATE}.
+ *
+ * @param <K> Key type.
+ * @param <V> Value type.
+ * @see <a href="https://redis.io/docs/latest/commands/ft.create/">FT.CREATE</a>
+ * @since 6.6
+ * @author Tihomir Mateev
+ */
+@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
+public class CreateArgs<K, V> {
+
+    /**
+     * Possible target types for the index.
+     */
+    public enum TargetType {
+        HASH, JSON
+    }
+
+    private Optional<TargetType> on = Optional.of(TargetType.HASH);
+
+    private final List<K> prefixes = new ArrayList<>();
+
+    private Optional<V> filter = Optional.empty();
+
+    private Optional<DocumentLanguage> defaultLanguage = Optional.empty();
+
+    private Optional<K> languageField = Optional.empty();
+
+    private OptionalDouble defaultScore = OptionalDouble.empty();
+
+    private Optional<K> scoreField = Optional.empty();
+
+    private Optional<K> payloadField = Optional.empty();
+
+    private boolean maxTextFields;
+
+    private OptionalLong temporary = OptionalLong.empty();
+
+    private boolean noOffsets;
+
+    private boolean noHighlight;
+
+    private boolean noFields;
+
+    private boolean noFrequency;
+
+    private boolean skipInitialScan;
+
+    private Optional<List<V>> stopWords = Optional.empty();
+
+    /**
+     * Used to build a new instance of the {@link CreateArgs}.
+     *
+     * @return a {@link Builder} that provides the option to build up a new instance of the {@link CreateArgs}
+     * @param <K> the key type
+     * @param <V> the value type
+     */
+    public static <K, V> Builder<K, V> builder() {
+        return new Builder<>();
+    }
+
+    /**
+     * Builder for {@link CreateArgs}.
+     * <p>
+     * As a final step the {@link Builder#build()} method needs to be executed to create the final {@link CreateArgs} instance.
+     * 
+     * @param <K> the key type
+     * @param <V> the value type
+     * @see <a href="https://redis.io/docs/latest/commands/ft.create/">FT.CREATE</a>
+     */
+    public static class Builder<K, V> {
+
+        private final CreateArgs<K, V> instance = new CreateArgs<>();
+
+        /**
+         * Set the {@link TargetType} type for the index. Defaults to {@link TargetType#HASH}.
+         * 
+         * @param targetType the target type
+         * @return the instance of the current {@link Builder} for the purpose of method chaining
+         */
+        public Builder<K, V> on(TargetType targetType) {
+            instance.on = Optional.of(targetType);
+            return this;
+        }
+
+        /**
+         * Add a prefix to the index. You can add several prefixes to index. Default setting is * (all keys).
+         * 
+         * @param prefix the prefix
+         * @return the instance of the current {@link Builder} for the purpose of method chaining
+         * @see {@link Builder#addPrefixes(List)}
+         */
+        public Builder<K, V> addPrefix(K prefix) {
+            instance.prefixes.add(prefix);
+            return this;
+        }
+
+        /**
+         * Add a list of prefixes to the index. You can add several prefixes to index. Default setting is * (all keys).
+         * 
+         * @param prefixes a {@link List} of prefixes
+         * @return the instance of the current {@link Builder} for the purpose of method chaining
+         */
+        public Builder<K, V> addPrefixes(List<K> prefixes) {
+            instance.prefixes.addAll(prefixes);
+            return this;
+        }
+
+        /**
+         * Set a filter for the index. Default setting is to have no filter.
+         * <p/>
+         * It is possible to use @__key to access the key that was just added/changed. A field can be used to set field name by
+         * passing 'FILTER @indexName=="myindexname"'.
+         * 
+         * @param filter a filter expression with the full RediSearch aggregation expression language
+         * @return the instance of the current {@link Builder} for the purpose of method chaining
+         * @see <a href="https://redis.io/docs/latest/develop/interact/search-and-query/query/">RediSearch Query</a>
+         */
+        public Builder<K, V> filter(V filter) {
+            instance.filter = Optional.of(filter);
+            return this;
+        }
+
+        /**
+         * Set the default language for the documents in the index. The default setting is English.
+         * 
+         * @param language the default language
+         * @return the instance of the current {@link Builder} for the purpose of method chaining
+         */
+        public Builder<K, V> defaultLanguage(DocumentLanguage language) {
+            instance.defaultLanguage = Optional.of(language);
+            return this;
+        }
+
+        /**
+         * Set the field that contains the language setting for the documents in the index. The default setting is to have no
+         * language field.
+         * 
+         * @param field the language field
+         * @return the instance of the current {@link Builder} for the purpose of method chaining
+         * @see <a href=
+         *      "https://redis.io/docs/latest/develop/interact/search-and-query/advanced-concepts/stemming/">Stemming</a>
+         */
+        public Builder<K, V> languageField(K field) {
+            instance.languageField = Optional.of(field);
+            return this;
+        }
+
+        /**
+         * Set the default score for the documents in the index. The default setting is 1.0.
+         * 
+         * @param score the default score
+         * @return the instance of the current {@link Builder} for the purpose of method chaining
+         * @see <a href="https://redis.io/docs/latest/develop/interact/search-and-query/advanced-concepts/scoring/">Scoring</a>
+         */
+        public Builder<K, V> defaultScore(double score) {
+            instance.defaultScore = OptionalDouble.of(score);
+            return this;
+        }
+
+        /**
+         * Set the field that contains the score setting for the documents in the index. The default setting is a score of 1.0.
+         * 
+         * @param field the score field
+         * @return the instance of the current {@link Builder} for the purpose of method chaining
+         * @see <a href="https://redis.io/docs/latest/develop/interact/search-and-query/advanced-concepts/scoring/">Scoring</a>
+         */
+        public Builder<K, V> scoreField(K field) {
+            instance.scoreField = Optional.of(field);
+            return this;
+        }
+
+        /**
+         * Set the field that contains the payload setting for the documents in the index. The default setting is to have no
+         * payload field.
+         * <p/>
+         * This should be a document attribute that you use as a binary safe payload string to the document that can be
+         * evaluated at query time by a custom scoring function or retrieved to the client
+         * 
+         * @param field the payload field
+         * @return the instance of the current {@link Builder} for the purpose of method chaining
+         * @see <a href="https://redis.io/docs/latest/develop/interact/search-and-query/advanced-concepts/scoring/">Scoring</a>
+         */
+        public Builder<K, V> payloadField(K field) {
+            instance.payloadField = Optional.of(field);
+            return this;
+        }
+
+        /**
+         * Set the maximum number of text fields in the index. The default setting is to have no limit.
+         * <p/>
+         * Forces RediSearch to encode indexes as if there were more than 32 text attributes, which allows you to add additional
+         * attributes (beyond 32) using FT.ALTER. For efficiency, RediSearch encodes indexes differently if they are created
+         * with less than 32 text attributes.
+         * 
+         * @param maxTextFields the maximum number of text fields
+         * @return the instance of the current {@link Builder} for the purpose of method chaining
+         */
+        public Builder<K, V> maxTextFields(boolean maxTextFields) {
+            instance.maxTextFields = maxTextFields;
+            return this;
+        }
+
+        /**
+         * Set the temporary index expiration time in seconds. The default setting is to have no expiration time.
+         * <p/>
+         * Creates a lightweight temporary index that expires after a specified period of inactivity, in seconds. The internal
+         * idle timer is reset whenever the index is searched or added to. Because such indexes are lightweight, you can create
+         * thousands of such indexes without negative performance implications and, therefore, you should consider using
+         * {@link Builder#skipInitialScan(boolean)} to avoid costly scanning.
+         * <p/>
+         * Warning: When temporary indexes expire, they drop all the records associated with them. FT.DROPINDEX was introduced
+         * with a default of not deleting docs and a DD flag that enforced deletion. However, for temporary indexes, documents
+         * are deleted along with the index. Historically, RediSearch used an FT.ADD command, which made a connection between
+         * the document and the index. Then, FT.DROP, also a hystoric command, deleted documents by default. In version 2.x,
+         * RediSearch indexes hashes and JSONs, and the dependency between the index and documents no longer exists.
+         * 
+         * @param seconds the temporary index expiration time in seconds
+         * @return the instance of the current {@link Builder} for the purpose of method chaining
+         */
+        public Builder<K, V> temporary(long seconds) {
+            instance.temporary = OptionalLong.of(seconds);
+            return this;
+        }
+
+        /**
+         * Set the no offsets flag. The default setting is to have offsets.
+         * <p/>
+         * It saves memory, but does not allow exact searches or highlighting. It implies
+         * {@link Builder#noHighlighting(boolean)} is set to true.
+         * 
+         * @param noOffsets the no offsets flag
+         * @return the instance of the current {@link Builder} for the purpose of method chaining
+         */
+        public Builder<K, V> noOffsets(boolean noOffsets) {
+            instance.noOffsets = noOffsets;
+            return this;
+        }
+
+        /**
+         * Set the no highlighting flag. The default setting is to have highlighting.
+         * <p/>
+         * Conserves storage space and memory by disabling highlighting support. If set, the corresponding byte offsets for term
+         * positions are not stored. NOHL is also implied by NOOFFSETS.
+         * 
+         * @param noHL the no highlighting flag
+         * @return the instance of the current {@link Builder} for the purpose of method chaining
+         */
+        public Builder<K, V> noHighlighting(boolean noHL) {
+            instance.noHighlight = noHL;
+            return this;
+        }
+
+        /**
+         * Set the no fields flag. The default setting is to have fields.
+         * <p/>
+         * Does not store attribute bits for each term. It saves memory, but it does not allow filtering by specific attributes.
+         *
+         * @param noFields the no fields flag
+         * @return the instance of the current {@link Builder} for the purpose of method chaining
+         */
+        public Builder<K, V> noFields(boolean noFields) {
+            instance.noFields = noFields;
+            return this;
+        }
+
+        /**
+         * Set the no frequency flag. The default setting is to have frequencies.
+         * <p/>
+         * Does not store the frequency of each term. It saves memory, but it does not allow sorting by frequency of a given
+         * term.
+         *
+         * @param noFreqs the no frequency flag
+         * @return the instance of the current {@link Builder} for the purpose of method chaining
+         */
+        public Builder<K, V> noFrequency(boolean noFreqs) {
+            instance.noFrequency = noFreqs;
+            return this;
+        }
+
+        /**
+         * Set the skip initial scan flag. The default setting is to scan initially.
+         *
+         * @param skipInitialScan the skip initial scan flag
+         * @return the instance of the current {@link Builder} for the purpose of method chaining
+         */
+        public Builder<K, V> skipInitialScan(boolean skipInitialScan) {
+            instance.skipInitialScan = skipInitialScan;
+            return this;
+        }
+
+        /**
+         * Set the index with a custom stopword list, to be ignored during indexing and search time.
+         * <p/>
+         * If not set, FT.CREATE takes the default list of stopwords. If {count} is set to 0, the index does not have stopwords.
+         *
+         * @param stopWords a list of stop words
+         * @return the instance of the current {@link Builder} for the purpose of method chaining
+         */
+        public Builder<K, V> stopWords(List<V> stopWords) {
+            instance.stopWords = Optional.of(stopWords);
+            return this;
+        }
+
+        public CreateArgs<K, V> build() {
+            return instance;
+        }
+
+    }
+
+    /**
+     * Get the target type for the index.
+     *
+     * @return the target type
+     * @see TargetType
+     * @see Builder#on(TargetType)
+     */
+    public Optional<TargetType> getOn() {
+        return on;
+    }
+
+    /**
+     * Get the prefixes for the index.
+     *
+     * @return the prefixes
+     * @see Builder#addPrefix(Object)
+     * @see Builder#addPrefixes(List)
+     */
+    public List<K> getPrefixes() {
+        return prefixes;
+    }
+
+    /**
+     * Get the filter for the index.
+     *
+     * @return the filter
+     * @see Builder#filter(Object)
+     */
+    public Optional<V> getFilter() {
+        return filter;
+    }
+
+    /**
+     * Get the default language for the documents in the index.
+     *
+     * @return the default language
+     * @see Builder#defaultLanguage(DocumentLanguage)
+     */
+    public Optional<DocumentLanguage> getDefaultLanguage() {
+        return defaultLanguage;
+    }
+
+    /**
+     * Get the field that contains the language setting for the documents in the index.
+     *
+     * @return the language field
+     * @see Builder#languageField(Object)
+     */
+    public Optional<K> getLanguageField() {
+        return languageField;
+    }
+
+    /**
+     * Get the default score for the documents in the index.
+     *
+     * @return the default score
+     * @see Builder#defaultScore(double)
+     */
+    public OptionalDouble getDefaultScore() {
+        return defaultScore;
+    }
+
+    /**
+     * Get the field that contains the score setting for the documents in the index.
+     *
+     * @return the score field
+     * @see Builder#scoreField(Object)
+     */
+    public Optional<K> getScoreField() {
+        return scoreField;
+    }
+
+    /**
+     * Get the field that contains the payload setting for the documents in the index.
+     *
+     * @return the payload field
+     * @see Builder#payloadField(Object)
+     */
+    public Optional<K> getPayloadField() {
+        return payloadField;
+    }
+
+    /**
+     * Get the maximum number of text fields in the index.
+     *
+     * @return the maximum number of text fields
+     * @see Builder#maxTextFields(boolean)
+     */
+    public boolean isMaxTextFields() {
+        return maxTextFields;
+    }
+
+    /**
+     * Get the temporary index expiration time in seconds.
+     *
+     * @return the temporary index expiration time in seconds
+     * @see Builder#temporary(long)
+     */
+    public OptionalLong getTemporary() {
+        return temporary;
+    }
+
+    /**
+     * Get the no offsets flag.
+     *
+     * @return the no offsets flag
+     * @see Builder#noOffsets(boolean)
+     */
+    public boolean isNoOffsets() {
+        return noOffsets;
+    }
+
+    /**
+     * Get the no highlighting flag.
+     *
+     * @return the no highlighting flag
+     * @see Builder#noHighlighting(boolean)
+     */
+    public boolean isNoHighlight() {
+        return noHighlight;
+    }
+
+    /**
+     * Get the no fields flag.
+     *
+     * @return the no fields flag
+     * @see Builder#noFields(boolean)
+     */
+    public boolean isNoFields() {
+        return noFields;
+    }
+
+    /**
+     * Get the no frequency flag.
+     *
+     * @return the no frequency flag
+     * @see Builder#noFrequency(boolean)
+     */
+    public boolean isNoFrequency() {
+        return noFrequency;
+    }
+
+    /**
+     * Get the skip initial scan flag.
+     *
+     * @return the skip initial scan flag
+     * @see Builder#skipInitialScan(boolean)
+     */
+    public boolean isSkipInitialScan() {
+        return skipInitialScan;
+    }
+
+    /**
+     * Get the stop words for the index.
+     *
+     * @return the stop words
+     * @see Builder#stopWords(List)
+     */
+    public Optional<List<V>> getStopWords() {
+        return stopWords;
+    }
+
+    /**
+     * Build a {@link CommandArgs} object that contains all the arguments.
+     *
+     * @param args the {@link CommandArgs} object
+     */
+    public void build(CommandArgs<K, V> args) {
+        on.ifPresent(targetType -> args.add(ON).add(targetType.name()));
+        if (!prefixes.isEmpty()) {
+            args.add(PREFIX).add(prefixes.size());
+            prefixes.forEach(args::addKey);
+        }
+        filter.ifPresent(filter -> args.add(FILTER).addValue(filter));
+        defaultLanguage.ifPresent(language -> args.add(LANGUAGE).add(language.toString()));
+        languageField.ifPresent(field -> args.add(LANGUAGE_FIELD).addKey(field));
+        defaultScore.ifPresent(score -> args.add(SCORE).add(score));
+        scoreField.ifPresent(field -> args.add(SCORE_FIELD).addKey(field));
+        payloadField.ifPresent(field -> args.add(PAYLOAD_FIELD).addKey(field));
+        if (maxTextFields) {
+            args.add(MAXTEXTFIELDS);
+        }
+        temporary.ifPresent(seconds -> args.add(TEMPORARY).add(seconds));
+        if (noOffsets) {
+            args.add(NOOFFSETS);
+        }
+        if (noHighlight) {
+            args.add(NOHL);
+        }
+        if (noFields) {
+            args.add(NOFIELDS);
+        }
+        if (noFrequency) {
+            args.add(NOFREQS);
+        }
+        if (skipInitialScan) {
+            args.add(SKIPINITIALSCAN);
+        }
+        stopWords.ifPresent(words -> {
+            args.add(STOPWORDS).add(words.size());
+            words.forEach(args::addValue);
+        });
+    }
+
+}
diff --git a/src/main/java/io/lettuce/core/search/package-info.java b/src/main/java/io/lettuce/core/search/package-info.java
new file mode 100644
index 0000000000..0d4f7f5cdd
--- /dev/null
+++ b/src/main/java/io/lettuce/core/search/package-info.java
@@ -0,0 +1,10 @@
+/*
+ * Copyright 2025, Redis Ltd. and Contributors
+ * All rights reserved.
+ *
+ * Licensed under the MIT License.
+ */
+/**
+ * Support for the RediSearch features.
+ */
+package io.lettuce.core.search;
diff --git a/src/main/kotlin/io/lettuce/core/api/coroutines/RediSearchCoroutinesCommands.kt b/src/main/kotlin/io/lettuce/core/api/coroutines/RediSearchCoroutinesCommands.kt
new file mode 100644
index 0000000000..faa2985815
--- /dev/null
+++ b/src/main/kotlin/io/lettuce/core/api/coroutines/RediSearchCoroutinesCommands.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2025, Redis Ltd. and Contributors
+ * All rights reserved.
+ *
+ * Licensed under the MIT License.
+ */
+package io.lettuce.core.api.coroutines
+
+import io.lettuce.core.ExperimentalLettuceCoroutinesApi
+import io.lettuce.core.search.Fields
+import io.lettuce.core.search.arguments.CreateArgs
+
+/**
+ * Coroutine executed commands for RediSearch functionality
+ *
+ * @param <K> Key type.
+ * @param <V> Value type.
+ * @author Tihomir Mateev
+ * @see <a href="https://redis.io/docs/latest/operate/oss_and_stack/stack-with-enterprise/search/">RediSearch</a>
+ * @since 6.6
+ * @generated by io.lettuce.apigenerator.CreateKotlinCoroutinesApi
+ */
+@ExperimentalLettuceCoroutinesApi
+interface RediSearchCoroutinesCommands<K : Any, V : Any> {
+
+    /**
+     * Create a new index with the given name, index options and fields.
+     *
+     * @param index the index name
+     * @param options the index options
+     * @param fields the fields
+     * @return the result of the create command
+     */
+    suspend fun ftCreate(index: K, options: CreateArgs<K, V>, fields: Fields<K>): String?
+
+}
+
diff --git a/src/main/templates/io/lettuce/core/api/RediSearchCommands.java b/src/main/templates/io/lettuce/core/api/RediSearchCommands.java
new file mode 100644
index 0000000000..b17706a4d7
--- /dev/null
+++ b/src/main/templates/io/lettuce/core/api/RediSearchCommands.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2025, Redis Ltd. and Contributors
+ * All rights reserved.
+ *
+ * Licensed under the MIT License.
+ */
+package io.lettuce.core.api;
+
+import io.lettuce.core.search.Fields;
+import io.lettuce.core.search.arguments.CreateArgs;
+import io.lettuce.core.search.Field;
+
+/**
+ * ${intent} for RediSearch functionality
+ *
+ * @param <K> Key type.
+ * @param <V> Value type.
+ * @author Tihomir Mateev
+ * @see <a href="https://redis.io/docs/latest/operate/oss_and_stack/stack-with-enterprise/search/">RediSearch</a>
+ * @since 6.6
+ */
+public interface RediSearchCommands<K, V> {
+
+    /**
+     * Create a new index with the given name, index options and fields.
+     *
+     * @param index the index name
+     * @param options the index options
+     * @param fields the fields
+     * @return the result of the create command
+     */
+    String ftCreate(K index, CreateArgs<K, V> options, Fields<K> fields);
+
+}
diff --git a/src/test/java/io/lettuce/apigenerator/Constants.java b/src/test/java/io/lettuce/apigenerator/Constants.java
index 896b939951..4f28a33d95 100644
--- a/src/test/java/io/lettuce/apigenerator/Constants.java
+++ b/src/test/java/io/lettuce/apigenerator/Constants.java
@@ -30,7 +30,7 @@ class Constants {
             "RedisGeoCommands", "RedisHashCommands", "RedisHLLCommands", "RedisKeyCommands", "RedisListCommands",
             "RedisScriptingCommands", "RedisSentinelCommands", "RedisServerCommands", "RedisSetCommands",
             "RedisSortedSetCommands", "RedisStreamCommands", "RedisStringCommands", "RedisTransactionalCommands",
-            "RedisJsonCommands" };
+            "RedisJsonCommands", "RediSearchCommands" };
 
     public static final File TEMPLATES = new File("src/main/templates");
 
diff --git a/src/test/java/io/lettuce/core/RediSearchCommandBuilderUnitTests.java b/src/test/java/io/lettuce/core/RediSearchCommandBuilderUnitTests.java
new file mode 100644
index 0000000000..036e9ca853
--- /dev/null
+++ b/src/test/java/io/lettuce/core/RediSearchCommandBuilderUnitTests.java
@@ -0,0 +1,92 @@
+package io.lettuce.core;
+
+/*
+ * Copyright 2025, Redis Ltd. and Contributors
+ * All rights reserved.
+ *
+ * Licensed under the MIT License.
+ */
+import io.lettuce.core.codec.StringCodec;
+import io.lettuce.core.protocol.Command;
+import io.lettuce.core.search.Field;
+import io.lettuce.core.search.Fields;
+import io.lettuce.core.search.arguments.CreateArgs;
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+import java.nio.charset.StandardCharsets;
+
+import static io.lettuce.TestTags.UNIT_TEST;
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Unit tests for {@link RediSearchCommandBuilder}.
+ *
+ * @author Tihomir Mateev
+ */
+@Tag(UNIT_TEST)
+class RediSearchCommandBuilderUnitTests {
+
+    private static final String MY_KEY = "idx";
+
+    private static final String FIELD1_NAME = "title";
+
+    private static final String FIELD2_NAME = "published_at";
+
+    private static final String FIELD3_NAME = "category";
+
+    private static final String FIELD4_NAME = "sku";
+
+    private static final String FIELD4_ALIAS1 = "sku_text";
+
+    private static final String FIELD4_ALIAS2 = "sku_tag";
+
+    private static final String PREFIX = "blog:post:";
+
+    RediSearchCommandBuilder<String, String> builder = new RediSearchCommandBuilder<>(StringCodec.UTF8);
+
+    // FT.CREATE idx ON HASH PREFIX 1 blog:post: SCHEMA title TEXT SORTABLE published_at NUMERIC SORTABLE category TAG SORTABLE
+    @Test
+    void shouldCorrectlyConstructFtCreateCommandScenario1() {
+        Field<String> field1 = Field.<String> builder().name(FIELD1_NAME).type(Field.Type.TEXT).sortable().build();
+        Field<String> field2 = Field.<String> builder().name(FIELD2_NAME).type(Field.Type.NUMERIC).sortable().build();
+        Field<String> field3 = Field.<String> builder().name(FIELD3_NAME).type(Field.Type.TAG).sortable().build();
+        CreateArgs<String, String> createArgs = CreateArgs.<String, String> builder().addPrefix(PREFIX)
+                .on(CreateArgs.TargetType.HASH).build();
+        Command<String, String, String> command = builder.ftCreate(MY_KEY, createArgs, Fields.from(field1, field2, field3));
+        ByteBuf buf = Unpooled.directBuffer();
+        command.encode(buf);
+
+        String result = "*17\r\n" + "$9\r\n" + "FT.CREATE\r\n" + "$3\r\n" + MY_KEY + "\r\n" + "$2\r\n" + "ON\r\n" + "$4\r\n"
+                + "HASH\r\n" + "$6\r\n" + "PREFIX\r\n" + "$1\r\n" + "1\r\n" + "$10\r\n" + PREFIX + "\r\n" + "$6\r\n"
+                + "SCHEMA\r\n" + "$5\r\n" + FIELD1_NAME + "\r\n" + "$4\r\n" + "TEXT\r\n" + "$8\r\n" + "SORTABLE\r\n" + "$12\r\n"
+                + FIELD2_NAME + "\r\n" + "$7\r\n" + "NUMERIC\r\n" + "$8\r\n" + "SORTABLE\r\n" + "$8\r\n" + FIELD3_NAME + "\r\n"
+                + "$3\r\n" + "TAG\r\n" + "$8\r\n" + "SORTABLE\r\n";
+
+        assertThat(buf.toString(StandardCharsets.UTF_8)).isEqualTo(result);
+    }
+
+    // FT.CREATE idx ON HASH PREFIX 1 blog:post: SCHEMA sku AS sku_text TEXT sku AS sku_tag TAG SORTABLE
+    @Test
+    void shouldCorrectlyConstructFtCreateCommandScenario2() {
+        Field<String> field1 = Field.<String> builder().name(FIELD4_NAME).as(FIELD4_ALIAS1).type(Field.Type.TEXT).build();
+        Field<String> field2 = Field.<String> builder().name(FIELD4_NAME).as(FIELD4_ALIAS2).type(Field.Type.TAG).sortable()
+                .build();
+        CreateArgs<String, String> createArgs = CreateArgs.<String, String> builder().addPrefix(PREFIX)
+                .on(CreateArgs.TargetType.HASH).build();
+        Command<String, String, String> command = builder.ftCreate(MY_KEY, createArgs, Fields.from(field1, field2));
+        ByteBuf buf = Unpooled.directBuffer();
+        command.encode(buf);
+
+        String result = "*17\r\n" + "$9\r\n" + "FT.CREATE\r\n" + "$3\r\n" + MY_KEY + "\r\n" + "$2\r\n" + "ON\r\n" + "$4\r\n"
+                + "HASH\r\n" + "$6\r\n" + "PREFIX\r\n" + "$1\r\n" + "1\r\n" + "$10\r\n" + PREFIX + "\r\n" + "$6\r\n"
+                + "SCHEMA\r\n" + "$3\r\n" + FIELD4_NAME + "\r\n" + "$2\r\n" + "AS\r\n" + "$8\r\n" + FIELD4_ALIAS1 + "\r\n"
+                + "$4\r\n" + "TEXT\r\n" + "$3\r\n" + FIELD4_NAME + "\r\n" + "$2\r\n" + "AS\r\n" + "$7\r\n" + FIELD4_ALIAS2
+                + "\r\n" + "$3\r\n" + "TAG\r\n" + "$8\r\n" + "SORTABLE\r\n";
+
+        assertThat(buf.toString(StandardCharsets.UTF_8)).isEqualTo(result);
+    }
+
+}
diff --git a/src/test/java/io/lettuce/core/json/RediSearchIntegrationTests.java b/src/test/java/io/lettuce/core/json/RediSearchIntegrationTests.java
new file mode 100644
index 0000000000..a778467233
--- /dev/null
+++ b/src/test/java/io/lettuce/core/json/RediSearchIntegrationTests.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2025, Redis Ltd. and Contributors
+ * All rights reserved.
+ *
+ * Licensed under the MIT License.
+ */
+
+package io.lettuce.core.json;
+
+import io.lettuce.core.RedisClient;
+import io.lettuce.core.RedisContainerIntegrationTests;
+import io.lettuce.core.RedisURI;
+import io.lettuce.core.api.sync.RedisCommands;
+import io.lettuce.core.search.Field;
+import io.lettuce.core.search.Fields;
+import io.lettuce.core.search.arguments.CreateArgs;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+import static io.lettuce.TestTags.INTEGRATION_TEST;
+import static org.assertj.core.api.Assertions.assertThat;
+
+@Tag(INTEGRATION_TEST)
+public class RediSearchIntegrationTests extends RedisContainerIntegrationTests {
+
+    private static final String GENERIC_INDEX = "idx";
+
+    private static final String FIELD1_NAME = "title";
+
+    private static final String FIELD2_NAME = "published_at";
+
+    private static final String FIELD3_NAME = "category";
+
+    private static final String PREFIX = "blog:post:";
+
+    protected static RedisClient client;
+
+    protected static RedisCommands<String, String> redis;
+
+    public RediSearchIntegrationTests() {
+        RedisURI redisURI = RedisURI.Builder.redis("127.0.0.1").withPort(16379).build();
+
+        client = RedisClient.create(redisURI);
+        redis = client.connect().sync();
+    }
+
+    @BeforeEach
+    public void prepare() throws IOException {
+        redis.flushall();
+
+        Path path = Paths.get("src/test/resources/bike-inventory.json");
+        String read = String.join("", Files.readAllLines(path));
+        JsonValue value = redis.getJsonParser().createJsonValue(read);
+
+        redis.jsonSet("bikes:inventory", JsonPath.ROOT_PATH, value);
+    }
+
+    @AfterAll
+    static void teardown() {
+        if (client != null) {
+            client.shutdown();
+        }
+    }
+
+    @Test
+    void ftCreateScenario1() {
+        Field<String> field1 = Field.<String> builder().name(FIELD1_NAME).type(Field.Type.TEXT).sortable().build();
+        Field<String> field2 = Field.<String> builder().name(FIELD2_NAME).type(Field.Type.NUMERIC).sortable().build();
+        Field<String> field3 = Field.<String> builder().name(FIELD3_NAME).type(Field.Type.TAG).sortable().build();
+        CreateArgs<String, String> createArgs = CreateArgs.<String, String> builder().addPrefix(PREFIX)
+                .on(CreateArgs.TargetType.HASH).build();
+
+        String result = redis.ftCreate(GENERIC_INDEX, createArgs, Fields.from(field1, field2, field3));
+        assertThat(result).isEqualTo("OK");
+    }
+
+}

From 986cab72d1f8435a0f87817a93f8e96ca0f31f8f Mon Sep 17 00:00:00 2001
From: Tihomir Mateev <tihomir.mateev@gmail.com>
Date: Sun, 2 Feb 2025 15:22:07 +0100
Subject: [PATCH 2/3] Formatter issues

---
 src/main/java/io/lettuce/core/search/Field.java | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/main/java/io/lettuce/core/search/Field.java b/src/main/java/io/lettuce/core/search/Field.java
index 6bc5294435..4b7090cf94 100644
--- a/src/main/java/io/lettuce/core/search/Field.java
+++ b/src/main/java/io/lettuce/core/search/Field.java
@@ -354,7 +354,8 @@ public Field<K> build() {
         }
 
         /**
-         * Build a {@link java.util.List} of {@link Field}s, containing the current {@link Field} as the only element of the list.
+         * Build a {@link java.util.List} of {@link Field}s, containing the current {@link Field} as the only element of the
+         * list.
          *
          * @return the instance of the {@link Field}
          */

From dada00ea6aefd84baa5a6f0ddbf7bbaaf37ddcf2 Mon Sep 17 00:00:00 2001
From: Tihomir Mateev <tihomir.mateev@gmail.com>
Date: Mon, 3 Feb 2025 11:41:55 +0100
Subject: [PATCH 3/3] Polishing #1

---
 .../core/AbstractRedisAsyncCommands.java      |  4 +-
 .../core/AbstractRedisReactiveCommands.java   |  4 +-
 .../core/RediSearchCommandBuilder.java        |  9 +++--
 .../api/async/RediSearchAsyncCommands.java    | 15 ++++---
 .../reactive/RediSearchReactiveCommands.java  | 14 ++++---
 .../api/reactive/RedisReactiveCommands.java   | 13 ++++---
 .../core/api/sync/RediSearchCommands.java     | 15 ++++---
 .../api/async/RediSearchAsyncCommands.java    | 15 ++++---
 .../cluster/api/sync/RediSearchCommands.java  | 14 ++++---
 .../java/io/lettuce/core/search/Field.java    |  9 +++--
 .../java/io/lettuce/core/search/Fields.java   | 39 -------------------
 .../core/search/arguments/CreateArgs.java     |  2 +
 .../RediSearchCoroutinesCommands.kt           | 18 +++++----
 .../lettuce/core/api/RediSearchCommands.java  | 17 ++++----
 .../RediSearchCommandBuilderUnitTests.java    |  6 +--
 .../core/json/RediSearchIntegrationTests.java |  4 +-
 16 files changed, 93 insertions(+), 105 deletions(-)
 delete mode 100644 src/main/java/io/lettuce/core/search/Fields.java

diff --git a/src/main/java/io/lettuce/core/AbstractRedisAsyncCommands.java b/src/main/java/io/lettuce/core/AbstractRedisAsyncCommands.java
index b1f3d1650e..1a8994d28d 100644
--- a/src/main/java/io/lettuce/core/AbstractRedisAsyncCommands.java
+++ b/src/main/java/io/lettuce/core/AbstractRedisAsyncCommands.java
@@ -48,7 +48,7 @@
 import io.lettuce.core.protocol.CommandType;
 import io.lettuce.core.protocol.ProtocolKeyword;
 import io.lettuce.core.protocol.RedisCommand;
-import io.lettuce.core.search.Fields;
+import io.lettuce.core.search.Field;
 import io.lettuce.core.search.arguments.CreateArgs;
 import reactor.core.publisher.Mono;
 
@@ -1485,7 +1485,7 @@ public boolean isOpen() {
     }
 
     @Override
-    public RedisFuture<String> ftCreate(K index, CreateArgs<K, V> options, Fields<K> fields) {
+    public RedisFuture<String> ftCreate(K index, CreateArgs<K, V> options, List<Field<K>> fields) {
         return dispatch(searchCommandBuilder.ftCreate(index, options, fields));
     }
 
diff --git a/src/main/java/io/lettuce/core/AbstractRedisReactiveCommands.java b/src/main/java/io/lettuce/core/AbstractRedisReactiveCommands.java
index 2deb31e78c..02f37afd4d 100644
--- a/src/main/java/io/lettuce/core/AbstractRedisReactiveCommands.java
+++ b/src/main/java/io/lettuce/core/AbstractRedisReactiveCommands.java
@@ -49,7 +49,7 @@
 import io.lettuce.core.protocol.RedisCommand;
 import io.lettuce.core.protocol.TracedCommand;
 import io.lettuce.core.resource.ClientResources;
-import io.lettuce.core.search.Fields;
+import io.lettuce.core.search.Field;
 import io.lettuce.core.search.arguments.CreateArgs;
 import io.lettuce.core.tracing.TraceContext;
 import io.lettuce.core.tracing.TraceContextProvider;
@@ -1549,7 +1549,7 @@ public boolean isOpen() {
     }
 
     @Override
-    public Mono<String> ftCreate(K index, CreateArgs<K, V> options, Fields<K> fields) {
+    public Mono<String> ftCreate(K index, CreateArgs<K, V> options, List<Field<K>> fields) {
         return createMono(() -> searchCommandBuilder.ftCreate(index, options, fields));
     }
 
diff --git a/src/main/java/io/lettuce/core/RediSearchCommandBuilder.java b/src/main/java/io/lettuce/core/RediSearchCommandBuilder.java
index 163c199a4c..1f8f25d303 100644
--- a/src/main/java/io/lettuce/core/RediSearchCommandBuilder.java
+++ b/src/main/java/io/lettuce/core/RediSearchCommandBuilder.java
@@ -12,10 +12,11 @@
 import io.lettuce.core.protocol.Command;
 import io.lettuce.core.protocol.CommandArgs;
 import io.lettuce.core.protocol.CommandKeyword;
-import io.lettuce.core.search.Fields;
 import io.lettuce.core.search.arguments.CreateArgs;
 import io.lettuce.core.search.Field;
 
+import java.util.List;
+
 import static io.lettuce.core.protocol.CommandType.*;
 
 /**
@@ -39,9 +40,9 @@ class RediSearchCommandBuilder<K, V> extends BaseRedisCommandBuilder<K, V> {
      * @param fields the fields
      * @return the result of the create command
      */
-    public Command<K, V, String> ftCreate(K index, CreateArgs<K, V> createArgs, Fields<K> fields) {
+    public Command<K, V, String> ftCreate(K index, CreateArgs<K, V> createArgs, List<Field<K>> fields) {
         notNullKey(index);
-        notEmpty(fields.getFields().toArray());
+        notEmpty(fields.toArray());
 
         CommandArgs<K, V> args = new CommandArgs<>(codec).addKey(index);
 
@@ -51,7 +52,7 @@ public Command<K, V, String> ftCreate(K index, CreateArgs<K, V> createArgs, Fiel
 
         args.add(CommandKeyword.SCHEMA);
 
-        for (Field<K> field : fields.getFields()) {
+        for (Field<K> field : fields) {
             field.build(args);
         }
 
diff --git a/src/main/java/io/lettuce/core/api/async/RediSearchAsyncCommands.java b/src/main/java/io/lettuce/core/api/async/RediSearchAsyncCommands.java
index bef4cb4aa0..7b549160cb 100644
--- a/src/main/java/io/lettuce/core/api/async/RediSearchAsyncCommands.java
+++ b/src/main/java/io/lettuce/core/api/async/RediSearchAsyncCommands.java
@@ -6,8 +6,9 @@
  */
 package io.lettuce.core.api.async;
 
+import java.util.List;
 import io.lettuce.core.RedisFuture;
-import io.lettuce.core.search.Fields;
+import io.lettuce.core.search.Field;
 import io.lettuce.core.search.arguments.CreateArgs;
 
 /**
@@ -23,13 +24,15 @@
 public interface RediSearchAsyncCommands<K, V> {
 
     /**
-     * Create a new index with the given name, index options and fields.
+     * Create a new index with the given name, index options, and fields.
      *
-     * @param index the index name
-     * @param options the index options
-     * @param fields the fields
+     * @param index the index name, as a key
+     * @param options the index {@link CreateArgs}
+     * @param fields the {@link Field}s of the index
      * @return the result of the create command
+     * @since 6.6
+     * @see <a href="https://redis.io/docs/latest/commands/ft.create/">FT.CREATE</a>
      */
-    RedisFuture<String> ftCreate(K index, CreateArgs<K, V> options, Fields<K> fields);
+    RedisFuture<String> ftCreate(K index, CreateArgs<K, V> options, List<Field<K>> fields);
 
 }
diff --git a/src/main/java/io/lettuce/core/api/reactive/RediSearchReactiveCommands.java b/src/main/java/io/lettuce/core/api/reactive/RediSearchReactiveCommands.java
index c9ecac34b5..ba2268cca3 100644
--- a/src/main/java/io/lettuce/core/api/reactive/RediSearchReactiveCommands.java
+++ b/src/main/java/io/lettuce/core/api/reactive/RediSearchReactiveCommands.java
@@ -6,8 +6,8 @@
  */
 package io.lettuce.core.api.reactive;
 
+import java.util.List;
 import io.lettuce.core.search.Field;
-import io.lettuce.core.search.Fields;
 import io.lettuce.core.search.arguments.CreateArgs;
 import reactor.core.publisher.Mono;
 
@@ -24,13 +24,15 @@
 public interface RediSearchReactiveCommands<K, V> {
 
     /**
-     * Create a new index with the given name, index options and fields.
+     * Create a new index with the given name, index options, and fields.
      *
-     * @param index the index name
-     * @param options the index options
-     * @param fields the fields
+     * @param index the index name, as a key
+     * @param options the index {@link CreateArgs}
+     * @param fields the {@link Field}s of the index
      * @return the result of the create command
+     * @since 6.6
+     * @see <a href="https://redis.io/docs/latest/commands/ft.create/">FT.CREATE</a>
      */
-    Mono<String> ftCreate(K index, CreateArgs<K, V> options, Fields<K> fields);
+    Mono<String> ftCreate(K index, CreateArgs<K, V> options, List<Field<K>> fields);
 
 }
diff --git a/src/main/java/io/lettuce/core/api/reactive/RedisReactiveCommands.java b/src/main/java/io/lettuce/core/api/reactive/RedisReactiveCommands.java
index 2f75efcc92..76d24ddf10 100644
--- a/src/main/java/io/lettuce/core/api/reactive/RedisReactiveCommands.java
+++ b/src/main/java/io/lettuce/core/api/reactive/RedisReactiveCommands.java
@@ -31,12 +31,13 @@
  * @author Mark Paluch
  * @since 5.0
  */
-public interface RedisReactiveCommands<K, V> extends BaseRedisReactiveCommands<K, V>, RedisAclReactiveCommands<K, V>,
-        RedisClusterReactiveCommands<K, V>, RedisFunctionReactiveCommands<K, V>, RedisGeoReactiveCommands<K, V>,
-        RedisHashReactiveCommands<K, V>, RedisHLLReactiveCommands<K, V>, RedisKeyReactiveCommands<K, V>,
-        RedisListReactiveCommands<K, V>, RedisScriptingReactiveCommands<K, V>, RedisServerReactiveCommands<K, V>,
-        RedisSetReactiveCommands<K, V>, RedisSortedSetReactiveCommands<K, V>, RedisStreamReactiveCommands<K, V>,
-        RedisStringReactiveCommands<K, V>, RedisTransactionalReactiveCommands<K, V>, RedisJsonReactiveCommands<K, V> {
+public interface RedisReactiveCommands<K, V>
+        extends BaseRedisReactiveCommands<K, V>, RedisAclReactiveCommands<K, V>, RedisClusterReactiveCommands<K, V>,
+        RedisFunctionReactiveCommands<K, V>, RedisGeoReactiveCommands<K, V>, RedisHashReactiveCommands<K, V>,
+        RedisHLLReactiveCommands<K, V>, RedisKeyReactiveCommands<K, V>, RedisListReactiveCommands<K, V>,
+        RedisScriptingReactiveCommands<K, V>, RedisServerReactiveCommands<K, V>, RedisSetReactiveCommands<K, V>,
+        RedisSortedSetReactiveCommands<K, V>, RedisStreamReactiveCommands<K, V>, RedisStringReactiveCommands<K, V>,
+        RedisTransactionalReactiveCommands<K, V>, RedisJsonReactiveCommands<K, V>, RediSearchReactiveCommands<K, V> {
 
     /**
      * Authenticate to the server.
diff --git a/src/main/java/io/lettuce/core/api/sync/RediSearchCommands.java b/src/main/java/io/lettuce/core/api/sync/RediSearchCommands.java
index e4a746b097..c76f9867e6 100644
--- a/src/main/java/io/lettuce/core/api/sync/RediSearchCommands.java
+++ b/src/main/java/io/lettuce/core/api/sync/RediSearchCommands.java
@@ -6,7 +6,8 @@
  */
 package io.lettuce.core.api.sync;
 
-import io.lettuce.core.search.Fields;
+import java.util.List;
+import io.lettuce.core.search.Field;
 import io.lettuce.core.search.arguments.CreateArgs;
 
 /**
@@ -22,13 +23,15 @@
 public interface RediSearchCommands<K, V> {
 
     /**
-     * Create a new index with the given name, index options and fields.
+     * Create a new index with the given name, index options, and fields.
      *
-     * @param index the index name
-     * @param options the index options
-     * @param fields the fields
+     * @param index the index name, as a key
+     * @param options the index {@link CreateArgs}
+     * @param fields the {@link Field}s of the index
      * @return the result of the create command
+     * @since 6.6
+     * @see <a href="https://redis.io/docs/latest/commands/ft.create/">FT.CREATE</a>
      */
-    String ftCreate(K index, CreateArgs<K, V> options, Fields<K> fields);
+    String ftCreate(K index, CreateArgs<K, V> options, List<Field<K>> fields);
 
 }
diff --git a/src/main/java/io/lettuce/core/cluster/api/async/RediSearchAsyncCommands.java b/src/main/java/io/lettuce/core/cluster/api/async/RediSearchAsyncCommands.java
index 3747f8a26a..d9fb189253 100644
--- a/src/main/java/io/lettuce/core/cluster/api/async/RediSearchAsyncCommands.java
+++ b/src/main/java/io/lettuce/core/cluster/api/async/RediSearchAsyncCommands.java
@@ -6,7 +6,8 @@
  */
 package io.lettuce.core.cluster.api.async;
 
-import io.lettuce.core.search.Fields;
+import java.util.List;
+import io.lettuce.core.search.Field;
 import io.lettuce.core.search.arguments.CreateArgs;
 
 /**
@@ -22,13 +23,15 @@
 public interface RediSearchAsyncCommands<K, V> {
 
     /**
-     * Create a new index with the given name, index options and fields.
+     * Create a new index with the given name, index options, and fields.
      *
-     * @param index the index name
-     * @param options the index options
-     * @param fields the fields
+     * @param index the index name, as a key
+     * @param options the index {@link CreateArgs}
+     * @param fields the {@link Field}s of the index
      * @return the result of the create command
+     * @since 6.6
+     * @see <a href="https://redis.io/docs/latest/commands/ft.create/">FT.CREATE</a>
      */
-    AsyncExecutions<String> ftCreate(K index, CreateArgs<K, V> options, Fields<K> fields);
+    AsyncExecutions<String> ftCreate(K index, CreateArgs<K, V> options, List<Field<K>> fields);
 
 }
diff --git a/src/main/java/io/lettuce/core/cluster/api/sync/RediSearchCommands.java b/src/main/java/io/lettuce/core/cluster/api/sync/RediSearchCommands.java
index 9970fc9200..00cbc7b8bc 100644
--- a/src/main/java/io/lettuce/core/cluster/api/sync/RediSearchCommands.java
+++ b/src/main/java/io/lettuce/core/cluster/api/sync/RediSearchCommands.java
@@ -6,8 +6,8 @@
  */
 package io.lettuce.core.cluster.api.sync;
 
+import java.util.List;
 import io.lettuce.core.search.Field;
-import io.lettuce.core.search.Fields;
 import io.lettuce.core.search.arguments.CreateArgs;
 
 /**
@@ -23,13 +23,15 @@
 public interface RediSearchCommands<K, V> {
 
     /**
-     * Create a new index with the given name, index options and fields.
+     * Create a new index with the given name, index options, and fields.
      *
-     * @param index the index name
-     * @param options the index options
-     * @param fields the fields
+     * @param index the index name, as a key
+     * @param options the index {@link CreateArgs}
+     * @param fields the {@link Field}s of the index
      * @return the result of the create command
+     * @since 6.6
+     * @see <a href="https://redis.io/docs/latest/commands/ft.create/">FT.CREATE</a>
      */
-    Executions<String> ftCreate(K index, CreateArgs<K, V> options, Fields<K> fields);
+    Executions<String> ftCreate(K index, CreateArgs<K, V> options, List<Field<K>> fields);
 
 }
diff --git a/src/main/java/io/lettuce/core/search/Field.java b/src/main/java/io/lettuce/core/search/Field.java
index 4b7090cf94..51049936d0 100644
--- a/src/main/java/io/lettuce/core/search/Field.java
+++ b/src/main/java/io/lettuce/core/search/Field.java
@@ -8,6 +8,8 @@
 
 import io.lettuce.core.protocol.CommandArgs;
 
+import java.util.ArrayList;
+import java.util.List;
 import java.util.Optional;
 
 import static io.lettuce.core.protocol.CommandKeyword.*;
@@ -359,9 +361,10 @@ public Field<K> build() {
          *
          * @return the instance of the {@link Field}
          */
-        public Fields<K> buildFields() {
-            Fields<K> fields = new Fields<>();
-            return fields.add(instance);
+        public List<Field<K>> buildFields() {
+            List<Field<K>> fields = new ArrayList<>();
+            fields.add(instance);
+            return fields;
         }
 
     }
diff --git a/src/main/java/io/lettuce/core/search/Fields.java b/src/main/java/io/lettuce/core/search/Fields.java
deleted file mode 100644
index b338c01a62..0000000000
--- a/src/main/java/io/lettuce/core/search/Fields.java
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- * Copyright 2025, Redis Ltd. and Contributors
- * All rights reserved.
- *
- * Licensed under the MIT License.
- */
-package io.lettuce.core.search;
-
-import java.util.ArrayList;
-import java.util.List;
-
-public class Fields<K> {
-
-    private final List<Field<K>> fields = new ArrayList<>();
-
-    @SafeVarargs
-    public static Fields<String> from(Field<String>... field) {
-        Fields<String> fields = new Fields<>();
-        for (Field<String> f : field) {
-            fields.add(f);
-        }
-        return fields;
-    }
-
-    public Fields<K> add(Field<K> field) {
-        fields.add(field);
-        return this;
-    }
-
-    public Fields<K> addAll(List<Field<K>> field) {
-        fields.addAll(field);
-        return this;
-    }
-
-    public List<Field<K>> getFields() {
-        return fields;
-    }
-
-}
diff --git a/src/main/java/io/lettuce/core/search/arguments/CreateArgs.java b/src/main/java/io/lettuce/core/search/arguments/CreateArgs.java
index 9cbb71d6c6..2e61affba3 100644
--- a/src/main/java/io/lettuce/core/search/arguments/CreateArgs.java
+++ b/src/main/java/io/lettuce/core/search/arguments/CreateArgs.java
@@ -316,6 +316,8 @@ public Builder<K, V> skipInitialScan(boolean skipInitialScan) {
          *
          * @param stopWords a list of stop words
          * @return the instance of the current {@link Builder} for the purpose of method chaining
+         * @see <a href="https://redis.io/docs/latest/develop/interact/search-and-query/advanced-concepts/stopwords/">Stop
+         *      words</a>
          */
         public Builder<K, V> stopWords(List<V> stopWords) {
             instance.stopWords = Optional.of(stopWords);
diff --git a/src/main/kotlin/io/lettuce/core/api/coroutines/RediSearchCoroutinesCommands.kt b/src/main/kotlin/io/lettuce/core/api/coroutines/RediSearchCoroutinesCommands.kt
index faa2985815..f6a24da5a0 100644
--- a/src/main/kotlin/io/lettuce/core/api/coroutines/RediSearchCoroutinesCommands.kt
+++ b/src/main/kotlin/io/lettuce/core/api/coroutines/RediSearchCoroutinesCommands.kt
@@ -4,10 +4,12 @@
  *
  * Licensed under the MIT License.
  */
+
 package io.lettuce.core.api.coroutines
 
 import io.lettuce.core.ExperimentalLettuceCoroutinesApi
-import io.lettuce.core.search.Fields
+import kotlinx.coroutines.flow.Flow
+import io.lettuce.core.search.Field
 import io.lettuce.core.search.arguments.CreateArgs
 
 /**
@@ -18,20 +20,22 @@ import io.lettuce.core.search.arguments.CreateArgs
  * @author Tihomir Mateev
  * @see <a href="https://redis.io/docs/latest/operate/oss_and_stack/stack-with-enterprise/search/">RediSearch</a>
  * @since 6.6
- * @generated by io.lettuce.apigenerator.CreateKotlinCoroutinesApi
+ * @generated by io.lettuce.apigenerator.CreateKotlinCoroutinesApi
  */
 @ExperimentalLettuceCoroutinesApi
 interface RediSearchCoroutinesCommands<K : Any, V : Any> {
 
     /**
-     * Create a new index with the given name, index options and fields.
+     * Create a new index with the given name, index options, and fields.
      *
-     * @param index the index name
-     * @param options the index options
-     * @param fields the fields
+     * @param index the index name, as a key
+     * @param options the index [CreateArgs]
+     * @param fields the [Field]s of the index
      * @return the result of the create command
+     * @since 6.6
+     * @see <a href="https://redis.io/docs/latest/commands/ft.create/">FT.CREATE</a>
      */
-    suspend fun ftCreate(index: K, options: CreateArgs<K, V>, fields: Fields<K>): String?
+    suspend fun ftCreate(index: K, options: CreateArgs<K, V>, fields: List<Field<K>>): String?
 
 }
 
diff --git a/src/main/templates/io/lettuce/core/api/RediSearchCommands.java b/src/main/templates/io/lettuce/core/api/RediSearchCommands.java
index b17706a4d7..9c348db9c9 100644
--- a/src/main/templates/io/lettuce/core/api/RediSearchCommands.java
+++ b/src/main/templates/io/lettuce/core/api/RediSearchCommands.java
@@ -6,9 +6,10 @@
  */
 package io.lettuce.core.api;
 
-import io.lettuce.core.search.Fields;
-import io.lettuce.core.search.arguments.CreateArgs;
 import io.lettuce.core.search.Field;
+import io.lettuce.core.search.arguments.CreateArgs;
+
+import java.util.List;
 
 /**
  * ${intent} for RediSearch functionality
@@ -22,13 +23,15 @@
 public interface RediSearchCommands<K, V> {
 
     /**
-     * Create a new index with the given name, index options and fields.
+     * Create a new index with the given name, index options, and fields.
      *
-     * @param index the index name
-     * @param options the index options
-     * @param fields the fields
+     * @param index the index name, as a key
+     * @param options the index {@link CreateArgs}
+     * @param fields the {@link Field}s of the index
      * @return the result of the create command
+     * @since 6.6
+     * @see <a href="https://redis.io/docs/latest/commands/ft.create/">FT.CREATE</a>
      */
-    String ftCreate(K index, CreateArgs<K, V> options, Fields<K> fields);
+    String ftCreate(K index, CreateArgs<K, V> options, List<Field<K>> fields);
 
 }
diff --git a/src/test/java/io/lettuce/core/RediSearchCommandBuilderUnitTests.java b/src/test/java/io/lettuce/core/RediSearchCommandBuilderUnitTests.java
index 036e9ca853..c84e232ebf 100644
--- a/src/test/java/io/lettuce/core/RediSearchCommandBuilderUnitTests.java
+++ b/src/test/java/io/lettuce/core/RediSearchCommandBuilderUnitTests.java
@@ -9,7 +9,6 @@
 import io.lettuce.core.codec.StringCodec;
 import io.lettuce.core.protocol.Command;
 import io.lettuce.core.search.Field;
-import io.lettuce.core.search.Fields;
 import io.lettuce.core.search.arguments.CreateArgs;
 import io.netty.buffer.ByteBuf;
 import io.netty.buffer.Unpooled;
@@ -17,6 +16,7 @@
 import org.junit.jupiter.api.Test;
 
 import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
 
 import static io.lettuce.TestTags.UNIT_TEST;
 import static org.assertj.core.api.Assertions.assertThat;
@@ -55,7 +55,7 @@ void shouldCorrectlyConstructFtCreateCommandScenario1() {
         Field<String> field3 = Field.<String> builder().name(FIELD3_NAME).type(Field.Type.TAG).sortable().build();
         CreateArgs<String, String> createArgs = CreateArgs.<String, String> builder().addPrefix(PREFIX)
                 .on(CreateArgs.TargetType.HASH).build();
-        Command<String, String, String> command = builder.ftCreate(MY_KEY, createArgs, Fields.from(field1, field2, field3));
+        Command<String, String, String> command = builder.ftCreate(MY_KEY, createArgs, Arrays.asList(field1, field2, field3));
         ByteBuf buf = Unpooled.directBuffer();
         command.encode(buf);
 
@@ -76,7 +76,7 @@ void shouldCorrectlyConstructFtCreateCommandScenario2() {
                 .build();
         CreateArgs<String, String> createArgs = CreateArgs.<String, String> builder().addPrefix(PREFIX)
                 .on(CreateArgs.TargetType.HASH).build();
-        Command<String, String, String> command = builder.ftCreate(MY_KEY, createArgs, Fields.from(field1, field2));
+        Command<String, String, String> command = builder.ftCreate(MY_KEY, createArgs, Arrays.asList(field1, field2));
         ByteBuf buf = Unpooled.directBuffer();
         command.encode(buf);
 
diff --git a/src/test/java/io/lettuce/core/json/RediSearchIntegrationTests.java b/src/test/java/io/lettuce/core/json/RediSearchIntegrationTests.java
index a778467233..c87472bc64 100644
--- a/src/test/java/io/lettuce/core/json/RediSearchIntegrationTests.java
+++ b/src/test/java/io/lettuce/core/json/RediSearchIntegrationTests.java
@@ -12,7 +12,6 @@
 import io.lettuce.core.RedisURI;
 import io.lettuce.core.api.sync.RedisCommands;
 import io.lettuce.core.search.Field;
-import io.lettuce.core.search.Fields;
 import io.lettuce.core.search.arguments.CreateArgs;
 import org.junit.jupiter.api.AfterAll;
 import org.junit.jupiter.api.BeforeEach;
@@ -23,6 +22,7 @@
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
+import java.util.Arrays;
 
 import static io.lettuce.TestTags.INTEGRATION_TEST;
 import static org.assertj.core.api.Assertions.assertThat;
@@ -77,7 +77,7 @@ void ftCreateScenario1() {
         CreateArgs<String, String> createArgs = CreateArgs.<String, String> builder().addPrefix(PREFIX)
                 .on(CreateArgs.TargetType.HASH).build();
 
-        String result = redis.ftCreate(GENERIC_INDEX, createArgs, Fields.from(field1, field2, field3));
+        String result = redis.ftCreate(GENERIC_INDEX, createArgs, Arrays.asList(field1, field2, field3));
         assertThat(result).isEqualTo("OK");
     }