) () -> {
+ // TODO @xin 先测试一个
+ switch (platform) {
+ case TONG_YI:
+ return buildTongYiEmbeddingModel(apiKey);
+ default:
+ throw new IllegalArgumentException(StrUtil.format("未知平台({})", platform));
+ }
+ });
+ }
+
private static String buildClientCacheKey(Class> clazz, Object... params) {
if (ArrayUtil.isEmpty(params)) {
return clazz.getName();
@@ -229,8 +254,7 @@ public class AiModelFactoryImpl implements AiModelFactory {
}
/**
- * 可参考 {@link ZhiPuAiAutoConfiguration#zhiPuAiChatModel(
- * ZhiPuAiConnectionProperties, ZhiPuAiChatProperties, RestClient.Builder, List, FunctionCallbackContext, RetryTemplate, ResponseErrorHandler)}
+ * 可参考 {@link ZhiPuAiAutoConfiguration#zhiPuAiChatModel(ZhiPuAiConnectionProperties, ZhiPuAiChatProperties, RestClient.Builder, List, FunctionCallbackContext, RetryTemplate, ResponseErrorHandler)}
*/
private ZhiPuAiChatModel buildZhiPuChatModel(String apiKey, String url) {
url = StrUtil.blankToDefault(url, ZhiPuAiConnectionProperties.DEFAULT_BASE_URL);
@@ -239,8 +263,7 @@ public class AiModelFactoryImpl implements AiModelFactory {
}
/**
- * 可参考 {@link ZhiPuAiAutoConfiguration#zhiPuAiImageModel(
- * ZhiPuAiConnectionProperties, ZhiPuAiImageProperties, RestClient.Builder, RetryTemplate, ResponseErrorHandler)}
+ * 可参考 {@link ZhiPuAiAutoConfiguration#zhiPuAiImageModel(ZhiPuAiConnectionProperties, ZhiPuAiImageProperties, RestClient.Builder, RetryTemplate, ResponseErrorHandler)}
*/
private ZhiPuAiImageModel buildZhiPuAiImageModel(String apiKey, String url) {
url = StrUtil.blankToDefault(url, ZhiPuAiConnectionProperties.DEFAULT_BASE_URL);
@@ -268,6 +291,21 @@ public class AiModelFactoryImpl implements AiModelFactory {
return new OpenAiChatModel(openAiApi);
}
+ /**
+ * 可参考 {@link AzureOpenAiAutoConfiguration}
+ */
+ private static AzureOpenAiChatModel buildAzureOpenAiChatModel(String apiKey, String url) {
+ AzureOpenAiAutoConfiguration azureOpenAiAutoConfiguration = new AzureOpenAiAutoConfiguration();
+ // 创建 OpenAIClient 对象
+ AzureOpenAiConnectionProperties connectionProperties = new AzureOpenAiConnectionProperties();
+ connectionProperties.setApiKey(apiKey);
+ connectionProperties.setEndpoint(url);
+ OpenAIClient openAIClient = azureOpenAiAutoConfiguration.openAIClient(connectionProperties);
+ // 获取 AzureOpenAiChatProperties 对象
+ AzureOpenAiChatProperties chatProperties = SpringUtil.getBean(AzureOpenAiChatProperties.class);
+ return azureOpenAiAutoConfiguration.azureOpenAiChatModel(openAIClient, chatProperties, null, null);
+ }
+
/**
* 可参考 {@link OpenAiAutoConfiguration}
*/
@@ -291,4 +329,15 @@ public class AiModelFactoryImpl implements AiModelFactory {
return new StabilityAiImageModel(stabilityAiApi);
}
+ // ========== 各种创建 EmbeddingModel 的方法 ==========
+
+ /**
+ * 可参考 {@link TongYiAutoConfiguration#tongYiTextEmbeddingClient(TextEmbedding, TongYiConnectionProperties)}
+ */
+ private EmbeddingModel buildTongYiEmbeddingModel(String apiKey) {
+ TongYiConnectionProperties connectionProperties = new TongYiConnectionProperties();
+ connectionProperties.setApiKey(apiKey);
+ return new TongYiAutoConfiguration().tongYiTextEmbeddingClient(SpringUtil.getBean(TextEmbedding.class), connectionProperties);
+ }
+
}
diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiVectorStoreFactory.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiVectorStoreFactory.java
new file mode 100644
index 000000000..dad58a2c0
--- /dev/null
+++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiVectorStoreFactory.java
@@ -0,0 +1,28 @@
+package cn.iocoder.yudao.framework.ai.core.factory;
+
+import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum;
+import org.springframework.ai.embedding.EmbeddingModel;
+import org.springframework.ai.vectorstore.VectorStore;
+
+// TODO @xin:也放到 AiModelFactory 里面好了,后续改成 AiFactory
+/**
+ * AI Vector 模型工厂的接口类
+ *
+ * @author xiaoxin
+ */
+public interface AiVectorStoreFactory {
+
+ /**
+ * 基于指定配置,获得 VectorStore 对象
+ *
+ * 如果不存在,则进行创建
+ *
+ * @param embeddingModel 嵌入模型
+ * @param platform 平台
+ * @param apiKey API KEY
+ * @param url API URL
+ * @return VectorStore 对象
+ */
+ VectorStore getOrCreateVectorStore(EmbeddingModel embeddingModel, AiPlatformEnum platform, String apiKey, String url);
+
+}
diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiVectorStoreFactoryImpl.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiVectorStoreFactoryImpl.java
new file mode 100644
index 000000000..ec04c5e88
--- /dev/null
+++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiVectorStoreFactoryImpl.java
@@ -0,0 +1,52 @@
+package cn.iocoder.yudao.framework.ai.core.factory;
+
+import cn.hutool.core.lang.Singleton;
+import cn.hutool.core.lang.func.Func0;
+import cn.hutool.core.util.ArrayUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum;
+import cn.iocoder.yudao.framework.common.util.spring.SpringUtils;
+import org.springframework.ai.embedding.EmbeddingModel;
+import org.springframework.ai.vectorstore.RedisVectorStore;
+import org.springframework.ai.vectorstore.VectorStore;
+import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
+import redis.clients.jedis.JedisPooled;
+
+/**
+ * AI Vector 模型工厂的实现类
+ * 使用 redisVectorStore 实现 VectorStore
+ *
+ * @author xiaoxin
+ */
+public class AiVectorStoreFactoryImpl implements AiVectorStoreFactory {
+
+ @Override
+ public VectorStore getOrCreateVectorStore(EmbeddingModel embeddingModel, AiPlatformEnum platform, String apiKey, String url) {
+ String cacheKey = buildClientCacheKey(VectorStore.class, platform, apiKey, url);
+ return Singleton.get(cacheKey, (Func0) () -> {
+ // TODO 芋艿 @xin 这两个配置取哪好呢
+ // TODO 不同模型的向量维度可能会不一样,目前看貌似是以 index 来做区分的,维度不一样存不到一个 index 上
+ // TODO 回复:好的哈
+ String index = "default-index";
+ String prefix = "default:";
+ var config = RedisVectorStore.RedisVectorStoreConfig.builder()
+ .withIndexName(index)
+ .withPrefix(prefix)
+ .build();
+ RedisProperties redisProperties = SpringUtils.getBean(RedisProperties.class);
+ RedisVectorStore redisVectorStore = new RedisVectorStore(config, embeddingModel,
+ new JedisPooled(redisProperties.getHost(), redisProperties.getPort()),
+ true);
+ redisVectorStore.afterPropertiesSet();
+ return redisVectorStore;
+ });
+ }
+
+ private static String buildClientCacheKey(Class> clazz, Object... params) {
+ if (ArrayUtil.isEmpty(params)) {
+ return clazz.getName();
+ }
+ return StrUtil.format("{}#{}", clazz.getName(), ArrayUtil.join(params, "_"));
+ }
+
+}
diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/deepseek/DeepSeekChatModel.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/deepseek/DeepSeekChatModel.java
index 1437404e8..e3097b83a 100644
--- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/deepseek/DeepSeekChatModel.java
+++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/deepseek/DeepSeekChatModel.java
@@ -1,5 +1,6 @@
package cn.iocoder.yudao.framework.ai.core.model.deepseek;
+import cn.hutool.core.collection.ListUtil;
import cn.hutool.core.lang.Assert;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.metadata.ChatGenerationMetadata;
@@ -70,12 +71,12 @@ public class DeepSeekChatModel implements ChatModel {
OpenAiApi.ChatCompletion chatCompletion = completionEntity.getBody();
if (chatCompletion == null) {
log.warn("No chat completion returned for prompt: {}", prompt);
- return new ChatResponse(List.of());
+ return new ChatResponse(ListUtil.of());
}
List choices = chatCompletion.choices();
if (choices == null) {
log.warn("No choices returned for prompt: {}", prompt);
- return new ChatResponse(List.of());
+ return new ChatResponse(ListUtil.of());
}
// 2. 转换 ChatResponse 返回
diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/xinghuo/XingHuoChatModel.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/xinghuo/XingHuoChatModel.java
index 60284bf2f..501d916db 100644
--- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/xinghuo/XingHuoChatModel.java
+++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/xinghuo/XingHuoChatModel.java
@@ -1,5 +1,6 @@
package cn.iocoder.yudao.framework.ai.core.model.xinghuo;
+import cn.hutool.core.collection.ListUtil;
import cn.hutool.core.lang.Assert;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.metadata.ChatGenerationMetadata;
@@ -72,12 +73,12 @@ public class XingHuoChatModel implements ChatModel {
OpenAiApi.ChatCompletion chatCompletion = completionEntity.getBody();
if (chatCompletion == null) {
log.warn("No chat completion returned for prompt: {}", prompt);
- return new ChatResponse(List.of());
+ return new ChatResponse(ListUtil.of());
}
List choices = chatCompletion.choices();
if (choices == null) {
log.warn("No choices returned for prompt: {}", prompt);
- return new ChatResponse(List.of());
+ return new ChatResponse(ListUtil.of());
}
// 2. 转换 ChatResponse 返回
diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/util/AiUtils.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/util/AiUtils.java
index b25658c67..e18f10015 100644
--- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/util/AiUtils.java
+++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/util/AiUtils.java
@@ -5,6 +5,7 @@ import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum;
import cn.iocoder.yudao.framework.ai.core.model.deepseek.DeepSeekChatOptions;
import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatOptions;
import com.alibaba.cloud.ai.tongyi.chat.TongYiChatOptions;
+import org.springframework.ai.azure.openai.AzureOpenAiChatOptions;
import org.springframework.ai.chat.messages.*;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.ollama.api.OllamaOptions;
@@ -35,6 +36,9 @@ public class AiUtils {
return XingHuoChatOptions.builder().model(model).temperature(temperatureF).maxTokens(maxTokens).build();
case OPENAI:
return OpenAiChatOptions.builder().withModel(model).withTemperature(temperatureF).withMaxTokens(maxTokens).build();
+ case AZURE_OPENAI:
+ // TODO 芋艿:貌似没 model 字段???!
+ return AzureOpenAiChatOptions.builder().withDeploymentName(model).withTemperature(temperatureF).withMaxTokens(maxTokens).build();
case OLLAMA:
return OllamaOptions.create().withModel(model).withTemperature(temperatureF).withNumPredict(maxTokens);
default:
diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/transcription/TongYiAudioTranscriptionModel.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/transcription/TongYiAudioTranscriptionModel.java
index 2068feeb5..0f0dca9c0 100644
--- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/transcription/TongYiAudioTranscriptionModel.java
+++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/transcription/TongYiAudioTranscriptionModel.java
@@ -16,6 +16,7 @@
package com.alibaba.cloud.ai.tongyi.audio.transcription;
+import cn.hutool.core.collection.ListUtil;
import com.alibaba.cloud.ai.tongyi.audio.AudioTranscriptionModels;
import com.alibaba.cloud.ai.tongyi.audio.transcription.api.AudioTranscriptionPrompt;
import com.alibaba.cloud.ai.tongyi.audio.transcription.api.AudioTranscriptionResponse;
@@ -82,7 +83,7 @@ public class TongYiAudioTranscriptionModel
try {
transcriptionParam = TranscriptionParam.builder()
.model(AudioTranscriptionModels.Paraformer_V1)
- .fileUrls(List.of(String.valueOf(instructions.getURL())))
+ .fileUrls(ListUtil.of(String.valueOf(instructions.getURL())))
.build();
}
catch (IOException e) {
diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/chat/TongYiChatModel.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/chat/TongYiChatModel.java
index c29ffbdfb..11328a02e 100644
--- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/chat/TongYiChatModel.java
+++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/chat/TongYiChatModel.java
@@ -16,6 +16,7 @@
package com.alibaba.cloud.ai.tongyi.chat;
+import cn.hutool.core.collection.ListUtil;
import com.alibaba.cloud.ai.tongyi.common.exception.TongYiException;
import com.alibaba.dashscope.aigc.conversation.ConversationParam;
import com.alibaba.dashscope.aigc.generation.Generation;
@@ -207,7 +208,7 @@ public class TongYiChatModel extends
.getChoices()
.get(0)
));
- return new ChatResponse(List.of(gen));
+ return new ChatResponse(ListUtil.of(gen));
})
)
.publishOn(Schedulers.parallel());
diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/embedding/TongYiTextEmbeddingModel.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/embedding/TongYiTextEmbeddingModel.java
index ce92dae07..99a356fe8 100644
--- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/embedding/TongYiTextEmbeddingModel.java
+++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/embedding/TongYiTextEmbeddingModel.java
@@ -16,6 +16,7 @@
package com.alibaba.cloud.ai.tongyi.embedding;
+import cn.hutool.core.collection.ListUtil;
import com.alibaba.cloud.ai.tongyi.common.exception.TongYiException;
import com.alibaba.cloud.ai.tongyi.metadata.TongYiTextEmbeddingResponseMetadata;
import com.alibaba.dashscope.embeddings.TextEmbedding;
@@ -100,7 +101,7 @@ public class TongYiTextEmbeddingModel extends AbstractEmbeddingModel {
return this.call(
new EmbeddingRequest(
- List.of(document.getFormattedContent(this.metadataMode)),
+ ListUtil.of(document.getFormattedContent(this.metadataMode)),
null)
).getResults().stream()
.map(Embedding::getOutput)
diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/org/springframework/ai/autoconfigure/vectorstore/redis/RedisVectorStoreAutoConfiguration.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/org/springframework/ai/autoconfigure/vectorstore/redis/RedisVectorStoreAutoConfiguration.java
new file mode 100644
index 000000000..a72d50c4a
--- /dev/null
+++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/org/springframework/ai/autoconfigure/vectorstore/redis/RedisVectorStoreAutoConfiguration.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2023 - 2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.ai.autoconfigure.vectorstore.redis;
+
+import org.springframework.ai.embedding.EmbeddingModel;
+import org.springframework.ai.vectorstore.RedisVectorStore;
+import org.springframework.ai.vectorstore.RedisVectorStore.RedisVectorStoreConfig;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
+import redis.clients.jedis.JedisPooled;
+
+/**
+ * TODO @xin 先拿 spring-ai 最新代码覆盖,1.0.0-M1 跟 redis 自动配置会冲突
+ *
+ * TODO 这个官方,有说啥时候 fix 哇?
+ * TODO 看着是列在1.0.0-M2版本
+ *
+ * @author Christian Tzolov
+ * @author Eddú Meléndez
+ */
+@AutoConfiguration(after = RedisAutoConfiguration.class)
+@ConditionalOnClass({JedisPooled.class, JedisConnectionFactory.class, RedisVectorStore.class, EmbeddingModel.class})
+@ConditionalOnBean(JedisConnectionFactory.class)
+@EnableConfigurationProperties(RedisVectorStoreProperties.class)
+public class RedisVectorStoreAutoConfiguration {
+
+ @Bean
+ @ConditionalOnMissingBean
+ public RedisVectorStore vectorStore(EmbeddingModel embeddingModel, RedisVectorStoreProperties properties,
+ JedisConnectionFactory jedisConnectionFactory) {
+
+ var config = RedisVectorStoreConfig.builder()
+ .withIndexName(properties.getIndex())
+ .withPrefix(properties.getPrefix())
+ .build();
+
+ return new RedisVectorStore(config, embeddingModel,
+ new JedisPooled(jedisConnectionFactory.getHostName(), jedisConnectionFactory.getPort()),
+ properties.isInitializeSchema());
+ }
+
+}
diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/org/springframework/ai/vectorstore/RedisVectorStore.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/org/springframework/ai/vectorstore/RedisVectorStore.java
new file mode 100644
index 000000000..de80401ed
--- /dev/null
+++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/org/springframework/ai/vectorstore/RedisVectorStore.java
@@ -0,0 +1,456 @@
+/*
+ * Copyright 2023 - 2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.ai.vectorstore;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.ai.document.Document;
+import org.springframework.ai.embedding.EmbeddingModel;
+import org.springframework.ai.vectorstore.filter.FilterExpressionConverter;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.util.Assert;
+import org.springframework.util.CollectionUtils;
+import redis.clients.jedis.JedisPooled;
+import redis.clients.jedis.Pipeline;
+import redis.clients.jedis.json.Path2;
+import redis.clients.jedis.search.*;
+import redis.clients.jedis.search.Schema.FieldType;
+import redis.clients.jedis.search.schemafields.*;
+import redis.clients.jedis.search.schemafields.VectorField.VectorAlgorithm;
+
+import java.text.MessageFormat;
+import java.util.*;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+/**
+ * The RedisVectorStore is for managing and querying vector data in a Redis database. It
+ * offers functionalities like adding, deleting, and performing similarity searches on
+ * documents.
+ *
+ * The store utilizes RedisJSON and RedisSearch to handle JSON documents and to index and
+ * search vector data. It supports various vector algorithms (e.g., FLAT, HSNW) for
+ * efficient similarity searches. Additionally, it allows for custom metadata fields in
+ * the documents to be stored alongside the vector and content data.
+ *
+ * This class requires a RedisVectorStoreConfig configuration object for initialization,
+ * which includes settings like Redis URI, index name, field names, and vector algorithms.
+ * It also requires an EmbeddingModel to convert documents into embeddings before storing
+ * them.
+ *
+ * @author Julien Ruaux
+ * @author Christian Tzolov
+ * @author Eddú Meléndez
+ * @see VectorStore
+ * @see RedisVectorStoreConfig
+ * @see EmbeddingModel
+ */
+public class RedisVectorStore implements VectorStore, InitializingBean {
+
+ public enum Algorithm {
+
+ FLAT, HSNW
+
+ }
+
+ public record MetadataField(String name, FieldType fieldType) {
+
+ public static MetadataField text(String name) {
+ return new MetadataField(name, FieldType.TEXT);
+ }
+
+ public static MetadataField numeric(String name) {
+ return new MetadataField(name, FieldType.NUMERIC);
+ }
+
+ public static MetadataField tag(String name) {
+ return new MetadataField(name, FieldType.TAG);
+ }
+
+ }
+
+ /**
+ * Configuration for the Redis vector store.
+ */
+ public static final class RedisVectorStoreConfig {
+
+ private final String indexName;
+
+ private final String prefix;
+
+ private final String contentFieldName;
+
+ private final String embeddingFieldName;
+
+ private final Algorithm vectorAlgorithm;
+
+ private final List metadataFields;
+
+ private RedisVectorStoreConfig() {
+ this(builder());
+ }
+
+ private RedisVectorStoreConfig(Builder builder) {
+ this.indexName = builder.indexName;
+ this.prefix = builder.prefix;
+ this.contentFieldName = builder.contentFieldName;
+ this.embeddingFieldName = builder.embeddingFieldName;
+ this.vectorAlgorithm = builder.vectorAlgorithm;
+ this.metadataFields = builder.metadataFields;
+ }
+
+ /**
+ * Start building a new configuration.
+ * @return The entry point for creating a new configuration.
+ */
+ public static Builder builder() {
+
+ return new Builder();
+ }
+
+ /**
+ * {@return the default config}
+ */
+ public static RedisVectorStoreConfig defaultConfig() {
+
+ return builder().build();
+ }
+
+ public static class Builder {
+
+ private String indexName = DEFAULT_INDEX_NAME;
+
+ private String prefix = DEFAULT_PREFIX;
+
+ private String contentFieldName = DEFAULT_CONTENT_FIELD_NAME;
+
+ private String embeddingFieldName = DEFAULT_EMBEDDING_FIELD_NAME;
+
+ private Algorithm vectorAlgorithm = DEFAULT_VECTOR_ALGORITHM;
+
+ private List metadataFields = new ArrayList<>();
+
+ private Builder() {
+ }
+
+ /**
+ * Configures the Redis index name to use.
+ * @param name the index name to use
+ * @return this builder
+ */
+ public Builder withIndexName(String name) {
+ this.indexName = name;
+ return this;
+ }
+
+ /**
+ * Configures the Redis key prefix to use (default: "embedding:").
+ * @param prefix the prefix to use
+ * @return this builder
+ */
+ public Builder withPrefix(String prefix) {
+ this.prefix = prefix;
+ return this;
+ }
+
+ /**
+ * Configures the Redis content field name to use.
+ * @param name the content field name to use
+ * @return this builder
+ */
+ public Builder withContentFieldName(String name) {
+ this.contentFieldName = name;
+ return this;
+ }
+
+ /**
+ * Configures the Redis embedding field name to use.
+ * @param name the embedding field name to use
+ * @return this builder
+ */
+ public Builder withEmbeddingFieldName(String name) {
+ this.embeddingFieldName = name;
+ return this;
+ }
+
+ /**
+ * Configures the Redis vector algorithmto use.
+ * @param algorithm the vector algorithm to use
+ * @return this builder
+ */
+ public Builder withVectorAlgorithm(Algorithm algorithm) {
+ this.vectorAlgorithm = algorithm;
+ return this;
+ }
+
+ public Builder withMetadataFields(MetadataField... fields) {
+ return withMetadataFields(Arrays.asList(fields));
+ }
+
+ public Builder withMetadataFields(List fields) {
+ this.metadataFields = fields;
+ return this;
+ }
+
+ /**
+ * {@return the immutable configuration}
+ */
+ public RedisVectorStoreConfig build() {
+
+ return new RedisVectorStoreConfig(this);
+ }
+
+ }
+
+ }
+
+ private final boolean initializeSchema;
+
+ public static final String DEFAULT_INDEX_NAME = "spring-ai-index";
+
+ public static final String DEFAULT_CONTENT_FIELD_NAME = "content";
+
+ public static final String DEFAULT_EMBEDDING_FIELD_NAME = "embedding";
+
+ public static final String DEFAULT_PREFIX = "embedding:";
+
+ public static final Algorithm DEFAULT_VECTOR_ALGORITHM = Algorithm.HSNW;
+
+ private static final String QUERY_FORMAT = "%s=>[KNN %s @%s $%s AS %s]";
+
+ private static final Path2 JSON_SET_PATH = Path2.of("$");
+
+ private static final String JSON_PATH_PREFIX = "$.";
+
+ private static final Logger logger = LoggerFactory.getLogger(RedisVectorStore.class);
+
+ private static final Predicate