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"); }