diff --git a/pom.xml b/pom.xml
index e33dd2972..1f292beee 100644
--- a/pom.xml
+++ b/pom.xml
@@ -30,6 +30,7 @@
spring-ai-spring-boot-starters/spring-ai-starter-pinecone-store
spring-ai-spring-boot-starters/spring-ai-starter-azure-store
spring-ai-spring-boot-starters/spring-ai-starter-weaviate-store
+ spring-ai-spring-boot-starters/spring-ai-starter-redis
spring-ai-docs
vector-stores/spring-ai-pgvector-store
vector-stores/spring-ai-milvus-store
@@ -42,6 +43,7 @@
vector-stores/spring-ai-chroma
vector-stores/spring-ai-azure
vector-stores/spring-ai-weaviate
+ vector-stores/spring-ai-redis
spring-ai-vertex-ai
spring-ai-spring-boot-starters/spring-ai-starter-vertex-ai
diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/nav.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/nav.adoc
index 10e5769c7..8a41bf606 100644
--- a/spring-ai-docs/src/main/antora/modules/ROOT/nav.adoc
+++ b/spring-ai-docs/src/main/antora/modules/ROOT/nav.adoc
@@ -16,6 +16,7 @@
*** xref:api/vectordbs/neo4j.adoc[]
*** xref:api/vectordbs/pgvector.adoc[]
*** xref:api/vectordbs/weaviate.adoc[]
+*** xref:api/vectordbs/redis.adoc[]
** xref:api/testing.adoc[]
* Appendices
** xref:glossary.adoc[]
diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/etl-pipeline.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/etl-pipeline.adoc
index 9dab1eb8e..e37aa43c8 100644
--- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/etl-pipeline.adoc
+++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/etl-pipeline.adoc
@@ -93,6 +93,10 @@ public interface DocumentWriter extends Consumer> {
*Neo4jVectorStore*::
+ Leverages the Neo4j graph database for vector storage.
+*RedisVectorStore*::
++ Provides vector storage capabilities using Redis.
+
+
== Using PDF Reader
diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/vectordbs.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/vectordbs.adoc
index f6e402ba7..3f80c74ab 100644
--- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/vectordbs.adoc
+++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/vectordbs.adoc
@@ -94,6 +94,7 @@ These are the available implementations of the `VectorStore` interface:
* Neo4j [`Neo4jVectorStore`]: The https://neo4j.com/[Neo4j] vector store
* Weaviate [`WeaviateVectorStore`] The https://weaviate.io/[Weaviate] vector store
* Azure Vector Search [`AzureVectorStore`] the https://learn.microsoft.com/en-us/azure/search/vector-search-overview[Azure] vector store
+* Redisj [`RedisVectorStore`]: The https://redis.io/[Redis] vector store
More implementations may be supported in future releases.
diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/vectordbs/redis.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/vectordbs/redis.adoc
new file mode 100644
index 000000000..585ac0897
--- /dev/null
+++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/vectordbs/redis.adoc
@@ -0,0 +1,183 @@
+= Redis
+
+This section walks you through setting up `RedisVectorStore` to store document embeddings and perform similarity searches.
+
+== What is Redis?
+
+link:https://redis.io[Redis] is an open source (BSD licensed), in-memory data structure store used as a database, cache, message broker, and streaming engine. Redis provides data structures such as strings, hashes, lists, sets, sorted sets with range queries, bitmaps, hyperloglogs, geospatial indexes, and streams.
+
+== What is Redis Vector Search?
+
+link:https://redis.io/docs/interact/search-and-query/[Redis Search and Query] extends the core features of Redis OSS and allows you to use Redis as a vector database:
+
+* Store vectors and the associated metadata within hashes or JSON documents
+* Retrieve vectors
+* Perform vector searches
+
+== Prerequisites
+
+1. `EmbeddingClient` instance to compute the document embeddings. Several options are available:
+
+- `Transformers Embedding` - computes the embedding in your local environment. Follow the ONNX Transformers Embedding instructions.
+- `OpenAI Embedding` - uses the OpenAI embedding endpoint. You need to create an account at link:https://platform.openai.com/signup[OpenAI Signup] and generate the api-key token at link:https://platform.openai.com/account/api-keys[API Keys].
+- You can also use the `Azure OpenAI Embedding`.
+
+2. A Redis Stack instance
+a. https://app.redislabs.com/#/[Redis Cloud] (recommended)
+b. link:https://hub.docker.com/r/redis/redis-stack[Docker] image _redis/redis-stack:latest_
+
+
+== Dependencies
+
+Add these dependencies to your project:
+
+* Embedding Client boot starter, required for calculating embeddings.
+
+* Transformers Embedding (Local) and follow the ONNX Transformers Embedding instructions.
+
+[source,xml]
+----
+
+ org.springframework.ai
+ spring-ai-transformers-embedding-spring-boot-starter
+ 0.8.0-SNAPSHOT
+
+----
+
+or use OpenAI (Cloud)
+
+[source,xml]
+----
+
+ org.springframework.ai
+ spring-ai-openai-spring-boot-starter
+ 0.8.0-SNAPSHOT
+
+----
+
+You'll need to provide your OpenAI API Key. Set it as an environment variable like so:
+
+[source,bash]
+----
+export SPRING_AI_OPENAI_API_KEY='Your_OpenAI_API_Key'
+----
+
+* Add the Redis Vector Store and Jedis dependencies
+
+[source,xml]
+----
+
+ org.springframework.ai
+ spring-ai-redis-store
+ 0.8.0-SNAPSHOT
+
+
+
+ redis.clients
+ jedis
+ 5.1.0
+
+----
+
+== Usage
+
+Create a RedisVectorStore instance connected to your Redis database:
+
+[source,java]
+----
+@Bean
+public VectorStore vectorStore(EmbeddingClient embeddingClient) {
+ RedisVectorStoreConfig config = RedisVectorStoreConfig.builder()
+ .withURI("redis://localhost:6379")
+ // Define the metadata fields to be used
+ // in the similarity search filters.
+ .withMetadataFields(
+ MetadataField.tag("country"),
+ MetadataField.numeric("year"))
+ .build();
+
+ return new RedisVectorStore(config, embeddingClient);
+}
+----
+
+> [NOTE]
+> You must list explicitly all metadata field names and types (`TAG`, `TEXT`, or `NUMERIC`) for any metadata field used in filter expression.
+> The `withMetadataFields` above registers filterable metadata fields: `country` of type `TAG`, `year` of type `NUMERIC`.
+>
+
+Then in your main code, create some documents:
+
+[source,java]
+----
+List documents = List.of(
+ new Document("Spring AI rocks!! Spring AI rocks!! Spring AI rocks!! Spring AI rocks!! Spring AI rocks!!", Map.of("country", "UK", "year", 2020)),
+ new Document("The World is Big and Salvation Lurks Around the Corner", Map.of()),
+ new Document("You walk forward facing the past and you turn back toward the future.", Map.of("country", "NL", "year", 2023)));
+----
+
+Now add the documents to your vector store:
+
+
+[source,java]
+----
+vectorStore.add(List.of(document));
+----
+
+And finally, retrieve documents similar to a query:
+
+[source,java]
+----
+List results = vectorStore.similaritySearch(
+ SearchRequest
+ .query("Spring")
+ .withTopK(5));
+----
+
+If all goes well, you should retrieve the document containing the text "Spring AI rocks!!".
+
+=== Metadata filtering
+
+You can leverage the generic, portable link:https://docs.spring.io/spring-ai/reference/api/vectordbs.html#_metadata_filters[metadata filters] with RedisVectorStore as well.
+
+For example, you can use either the text expression language:
+
+[source,java]
+----
+vectorStore.similaritySearch(
+ SearchRequest
+ .query("The World")
+ .withTopK(TOP_K)
+ .withSimilarityThreshold(SIMILARITY_THRESHOLD)
+ .withFilterExpression("country in ['UK', 'NL'] && year >= 2020"));
+----
+
+or programmatically using the expression DSL:
+
+[source,java]
+----
+FilterExpressionBuilder b = Filter.builder();
+
+vectorStore.similaritySearch(
+ SearchRequest
+ .query("The World")
+ .withTopK(TOP_K)
+ .withSimilarityThreshold(SIMILARITY_THRESHOLD)
+ .withFilterExpression(b.and(
+ b.in("country", "UK", "NL"),
+ b.gte("year", 2020)).build()));
+----
+
+The portable filter expressions get automatically converted into link:https://redis.io/docs/interact/search-and-query/query/[Redis search queries].
+For example, the following portable filter expression:
+
+[source,sql]
+----
+country in ['UK', 'NL'] && year >= 2020
+----
+
+is converted into Redis query:
+
+[source]
+----
+@country:{UK | NL} @year:[2020 inf]
+----
diff --git a/spring-ai-spring-boot-autoconfigure/pom.xml b/spring-ai-spring-boot-autoconfigure/pom.xml
index 70fe61656..3828d25cd 100644
--- a/spring-ai-spring-boot-autoconfigure/pom.xml
+++ b/spring-ai-spring-boot-autoconfigure/pom.xml
@@ -132,6 +132,21 @@
${project.parent.version}
true
+
+
+
+ org.springframework.ai
+ spring-ai-redis
+ ${project.parent.version}
+ true
+
+
+
+
+ redis.clients
+ jedis
+ 5.1.0
+
@@ -186,6 +201,13 @@
${testcontainers.version}
test
+
+
+ com.redis
+ testcontainers-redis
+ 2.0.1
+ test
+
org.awaitility
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/redis/RedisVectorStoreAutoConfiguration.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/redis/RedisVectorStoreAutoConfiguration.java
new file mode 100644
index 000000000..507eeab8f
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/redis/RedisVectorStoreAutoConfiguration.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2023-2023 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.EmbeddingClient;
+import org.springframework.ai.vectorstore.RedisVectorStore;
+import org.springframework.ai.vectorstore.RedisVectorStore.RedisVectorStoreConfig;
+import org.springframework.ai.vectorstore.VectorStore;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+
+/**
+ * @author Christian Tzolov
+ */
+@AutoConfiguration
+@ConditionalOnClass({ RedisVectorStore.class, EmbeddingClient.class })
+@EnableConfigurationProperties(RedisVectorStoreProperties.class)
+public class RedisVectorStoreAutoConfiguration {
+
+ @Bean
+ @ConditionalOnMissingBean
+ public VectorStore vectorStore(EmbeddingClient embeddingClient, RedisVectorStoreProperties properties) {
+
+ var config = RedisVectorStoreConfig.builder()
+ .withURI(properties.getUri())
+ .withIndexName(properties.getIndex())
+ .withPrefix(properties.getPrefix())
+ .build();
+
+ return new RedisVectorStore(config, embeddingClient);
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/redis/RedisVectorStoreProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/redis/RedisVectorStoreProperties.java
new file mode 100644
index 000000000..2959336fe
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/redis/RedisVectorStoreProperties.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2023-2023 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.boot.context.properties.ConfigurationProperties;
+
+import static org.springframework.ai.autoconfigure.vectorstore.redis.RedisVectorStoreProperties.CONFIG_PREFIX;
+
+/**
+ * @author Julien Ruaux
+ */
+@ConfigurationProperties(CONFIG_PREFIX)
+public class RedisVectorStoreProperties {
+
+ public static final String CONFIG_PREFIX = "spring.ai.vectorstore.redis";
+
+ private String uri = "redis://localhost:6379";
+
+ private String index;
+
+ private String prefix;
+
+ public String getUri() {
+ return uri;
+ }
+
+ public void setUri(String uri) {
+ this.uri = uri;
+ }
+
+ public String getIndex() {
+ return index;
+ }
+
+ public void setIndex(String name) {
+ this.index = name;
+ }
+
+ public String getPrefix() {
+ return prefix;
+ }
+
+ public void setPrefix(String prefix) {
+ this.prefix = prefix;
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-ai-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
index 90e383788..b3a5b3711 100644
--- a/spring-ai-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
+++ b/spring-ai-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -4,6 +4,7 @@ org.springframework.ai.autoconfigure.azure.openai.AzureOpenAiAutoConfiguration
org.springframework.ai.autoconfigure.vectorstore.pgvector.PgVectorStoreAutoConfiguration
org.springframework.ai.autoconfigure.vectorstore.pinecone.PineconeVectorStoreAutoConfiguration
org.springframework.ai.autoconfigure.vectorstore.milvus.MilvusVectorStoreAutoConfiguration
+org.springframework.ai.autoconfigure.vectorstore.redis.RedisVectorStoreAutoConfiguration
org.springframework.ai.autoconfigure.embedding.transformer.TransformersEmbeddingClientAutoConfiguration
org.springframework.ai.autoconfigure.huggingface.HuggingfaceAutoConfiguration
org.springframework.ai.autoconfigure.vectorstore.chroma.ChromaVectorStoreAutoConfiguration
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vectorstore/redis/RedisVectorStoreAutoConfigurationIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vectorstore/redis/RedisVectorStoreAutoConfigurationIT.java
new file mode 100644
index 000000000..1e91f2041
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vectorstore/redis/RedisVectorStoreAutoConfigurationIT.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2023-2023 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 static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.List;
+import java.util.Map;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.ai.ResourceUtils;
+import org.springframework.ai.document.Document;
+import org.springframework.ai.embedding.EmbeddingClient;
+import org.springframework.ai.embedding.TransformersEmbeddingClient;
+import org.springframework.ai.vectorstore.SearchRequest;
+import org.springframework.ai.vectorstore.VectorStore;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.testcontainers.junit.jupiter.Container;
+import org.testcontainers.junit.jupiter.Testcontainers;
+
+import com.redis.testcontainers.RedisStackContainer;
+
+/**
+ * @author Julien Ruaux
+ */
+@Testcontainers
+class RedisVectorStoreAutoConfigurationIT {
+
+ @Container
+ static RedisStackContainer redisContainer = new RedisStackContainer(
+ RedisStackContainer.DEFAULT_IMAGE_NAME.withTag(RedisStackContainer.DEFAULT_TAG));
+
+ List documents = List.of(
+ new Document(ResourceUtils.getText("classpath:/test/data/spring.ai.txt"), Map.of("spring", "great")),
+ new Document(ResourceUtils.getText("classpath:/test/data/time.shelter.txt")), new Document(
+ ResourceUtils.getText("classpath:/test/data/great.depression.txt"), Map.of("depression", "bad")));
+
+ private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
+ .withConfiguration(AutoConfigurations.of(RedisVectorStoreAutoConfiguration.class))
+ .withUserConfiguration(Config.class)
+ .withPropertyValues("spring.ai.vectorstore.redis.index=myIdx")
+ .withPropertyValues("spring.ai.vectorstore.redis.prefix=doc:");
+
+ @Test
+ void addAndSearch() {
+ contextRunner.withPropertyValues("spring.ai.vectorstore.redis.uri=" + redisContainer.getRedisURI())
+ .run(context -> {
+ VectorStore vectorStore = context.getBean(VectorStore.class);
+ vectorStore.add(documents);
+
+ List results = vectorStore.similaritySearch(SearchRequest.query("Spring").withTopK(1));
+
+ assertThat(results).hasSize(1);
+ Document resultDoc = results.get(0);
+ assertThat(resultDoc.getId()).isEqualTo(documents.get(0).getId());
+ assertThat(resultDoc.getContent()).contains(
+ "Spring AI provides abstractions that serve as the foundation for developing AI applications.");
+
+ // Remove all documents from the store
+ vectorStore.delete(documents.stream().map(doc -> doc.getId()).toList());
+
+ results = vectorStore.similaritySearch(SearchRequest.query("Spring").withTopK(1));
+ assertThat(results).isEmpty();
+ });
+ }
+
+ @Configuration(proxyBeanMethods = false)
+ static class Config {
+
+ @Bean
+ public EmbeddingClient embeddingClient() {
+ return new TransformersEmbeddingClient();
+ }
+
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vectorstore/redis/RedisVectorStorePropertiesTests.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vectorstore/redis/RedisVectorStorePropertiesTests.java
new file mode 100644
index 000000000..dc5eb7244
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vectorstore/redis/RedisVectorStorePropertiesTests.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2023-2023 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 static org.assertj.core.api.Assertions.assertThat;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * @author Julien Ruaux
+ */
+class RedisVectorStorePropertiesTests {
+
+ @Test
+ void defaultValues() {
+ var props = new RedisVectorStoreProperties();
+ assertThat(props.getUri()).isEqualTo("redis://localhost:6379");
+ assertThat(props.getIndex()).isNull();
+ assertThat(props.getPrefix()).isNull();
+ }
+
+ @Test
+ void customValues() {
+ var props = new RedisVectorStoreProperties();
+ props.setUri("redis://redis.com:12345");
+ props.setIndex("myIdx");
+ props.setPrefix("doc:");
+
+ assertThat(props.getUri()).isEqualTo("redis://redis.com:12345");
+ assertThat(props.getIndex()).isEqualTo("myIdx");
+ assertThat(props.getPrefix()).isEqualTo("doc:");
+ }
+
+}
diff --git a/spring-ai-spring-boot-starters/spring-ai-starter-redis/pom.xml b/spring-ai-spring-boot-starters/spring-ai-starter-redis/pom.xml
new file mode 100644
index 000000000..3106a101d
--- /dev/null
+++ b/spring-ai-spring-boot-starters/spring-ai-starter-redis/pom.xml
@@ -0,0 +1,42 @@
+
+
+ 4.0.0
+
+ org.springframework.ai
+ spring-ai
+ 0.8.0-SNAPSHOT
+ ../../pom.xml
+
+ spring-ai-redis-spring-boot-starter
+ jar
+ Spring AI Starter - Redis Vector Store
+ Spring AI Redis Vector Store Auto Configuration
+ https://github.com/spring-projects/spring-ai
+
+
+ https://github.com/spring-projects/spring-ai
+ git://github.com/spring-projects/spring-ai.git
+ git@github.com:spring-projects/spring-ai.git
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter
+
+
+
+ org.springframework.ai
+ spring-ai-spring-boot-autoconfigure
+ ${project.parent.version}
+
+
+
+ org.springframework.ai
+ spring-ai-redis
+ ${project.parent.version}
+
+
+
+
diff --git a/vector-stores/spring-ai-redis/pom.xml b/vector-stores/spring-ai-redis/pom.xml
new file mode 100644
index 000000000..028ab3dca
--- /dev/null
+++ b/vector-stores/spring-ai-redis/pom.xml
@@ -0,0 +1,80 @@
+
+
+ 4.0.0
+
+ org.springframework.ai
+ spring-ai
+ 0.8.0-SNAPSHOT
+ ../../pom.xml
+
+ spring-ai-redis
+ jar
+ spring-ai-redis
+ Spring AI Redis Vector Store
+ https://github.com/spring-projects/spring-ai
+
+
+ https://github.com/spring-projects-experimental/spring-ai
+ git://github.com/spring-projects-experimental/spring-ai.git
+ git@github.com:spring-projects-experimental/spring-ai.git
+
+
+
+ 2.0.1
+ 5.1.0
+ 17
+ 17
+
+
+
+
+ org.springframework.ai
+ spring-ai-core
+ ${parent.version}
+
+
+
+ redis.clients
+ jedis
+ ${jedis.version}
+
+
+
+
+ org.springframework.ai
+ transformers-embedding
+ ${parent.version}
+ test
+
+
+
+ org.springframework.ai
+ spring-ai-test
+ ${parent.version}
+ test
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ com.redis
+ testcontainers-redis
+ ${testcontainers-redis.version}
+ test
+
+
+
+ org.testcontainers
+ junit-jupiter
+ test
+
+
+
+
+
+
\ No newline at end of file
diff --git a/vector-stores/spring-ai-redis/src/main/java/org/springframework/ai/vectorstore/RedisFilterExpressionConverter.java b/vector-stores/spring-ai-redis/src/main/java/org/springframework/ai/vectorstore/RedisFilterExpressionConverter.java
new file mode 100644
index 000000000..a2fdca587
--- /dev/null
+++ b/vector-stores/spring-ai-redis/src/main/java/org/springframework/ai/vectorstore/RedisFilterExpressionConverter.java
@@ -0,0 +1,214 @@
+/*
+ * Copyright 2023-2023 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 java.text.MessageFormat;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+import org.springframework.ai.vectorstore.RedisVectorStore.MetadataField;
+import org.springframework.ai.vectorstore.filter.Filter.Expression;
+import org.springframework.ai.vectorstore.filter.Filter.ExpressionType;
+import org.springframework.ai.vectorstore.filter.Filter.Group;
+import org.springframework.ai.vectorstore.filter.Filter.Key;
+import org.springframework.ai.vectorstore.filter.Filter.Value;
+import org.springframework.ai.vectorstore.filter.converter.AbstractFilterExpressionConverter;
+
+/**
+ * Converts {@link Expression} into Redis search filter expression format.
+ * (https://redis.io/docs/interact/search-and-query/query/)
+ *
+ * @author Julien Ruaux
+ */
+public class RedisFilterExpressionConverter extends AbstractFilterExpressionConverter {
+
+ public static final NumericBoundary POSITIVE_INFINITY = new NumericBoundary(Double.POSITIVE_INFINITY, true);
+
+ public static final NumericBoundary NEGATIVE_INFINITY = new NumericBoundary(Double.NEGATIVE_INFINITY, true);
+
+ private Map metadataFields;
+
+ public RedisFilterExpressionConverter(List metadataFields) {
+ this.metadataFields = metadataFields.stream()
+ .collect(Collectors.toMap(MetadataField::name, Function.identity()));
+ }
+
+ @Override
+ protected void doStartGroup(Group group, StringBuilder context) {
+ context.append("(");
+ }
+
+ @Override
+ protected void doEndGroup(Group group, StringBuilder context) {
+ context.append(")");
+ }
+
+ @Override
+ protected void doKey(Key key, StringBuilder context) {
+ context.append("@").append(key.key()).append(":");
+ }
+
+ @Override
+ protected void doExpression(Expression expression, StringBuilder context) {
+ switch (expression.type()) {
+ case NIN:
+ doExpression(negate(ExpressionType.IN, expression), context);
+ break;
+ case NE:
+ doExpression(negate(ExpressionType.EQ, expression), context);
+ break;
+ case AND:
+ doBinaryOperation(" ", expression, context);
+ break;
+ case OR:
+ doBinaryOperation(" | ", expression, context);
+ break;
+ case NOT:
+ context.append("-");
+ convertOperand(expression.left(), context);
+ break;
+ default:
+ doField(expression, context);
+ break;
+ }
+
+ }
+
+ private Expression negate(ExpressionType expressionType, Expression expression) {
+ return new Expression(ExpressionType.NOT, new Expression(expressionType, expression.left(), expression.right()),
+ null);
+ }
+
+ private void doBinaryOperation(String delimiter, Expression expression, StringBuilder context) {
+ this.convertOperand(expression.left(), context);
+ context.append(delimiter);
+ this.convertOperand(expression.right(), context);
+ }
+
+ private void doField(Expression expression, StringBuilder context) {
+ Key key = (Key) expression.left();
+ doKey(key, context);
+ MetadataField field = metadataFields.getOrDefault(key.key(), MetadataField.tag(key.key()));
+ Value value = (Value) expression.right();
+ switch (field.fieldType()) {
+ case NUMERIC:
+ Numeric numeric = numeric(expression, value);
+ context.append("[");
+ context.append(numeric.lower());
+ context.append(" ");
+ context.append(numeric.upper());
+ context.append("]");
+ break;
+ case TAG:
+ context.append("{");
+ context.append(stringValue(expression, value));
+ context.append("}");
+ break;
+ case TEXT:
+ context.append("(");
+ context.append(stringValue(expression, value));
+ context.append(")");
+ break;
+ default:
+ throw new UnsupportedOperationException(
+ MessageFormat.format("Field type {0} not supported", field.fieldType()));
+ }
+ }
+
+ private Object stringValue(Expression expression, Value value) {
+ String delimiter = tagValueDelimiter(expression);
+ if (value.value() instanceof List> list) {
+ return String.join(delimiter, list.stream().map(String::valueOf).toList());
+ }
+ return value.value();
+ }
+
+ private String tagValueDelimiter(Expression expression) {
+ switch (expression.type()) {
+ case IN:
+ return " | ";
+ case EQ:
+ return " ";
+ default:
+ throw new UnsupportedOperationException(
+ MessageFormat.format("Tag operand {0} not supported", expression.type()));
+ }
+ }
+
+ private Numeric numeric(Expression expression, Value value) {
+ switch (expression.type()) {
+ case EQ:
+ return new Numeric(inclusive(value), inclusive(value));
+ case GT:
+ return new Numeric(exclusive(value), POSITIVE_INFINITY);
+ case GTE:
+ return new Numeric(inclusive(value), POSITIVE_INFINITY);
+ case LT:
+ return new Numeric(NEGATIVE_INFINITY, exclusive(value));
+ case LTE:
+ return new Numeric(NEGATIVE_INFINITY, inclusive(value));
+ default:
+ throw new UnsupportedOperationException(MessageFormat
+ .format("Expression type {0} not supported for numeric fields", expression.type()));
+ }
+ }
+
+ private NumericBoundary inclusive(Value value) {
+ return new NumericBoundary(value.value(), false);
+ }
+
+ private NumericBoundary exclusive(Value value) {
+ return new NumericBoundary(value.value(), true);
+ }
+
+ static record Numeric(NumericBoundary lower, NumericBoundary upper) {
+ }
+
+ static record NumericBoundary(Object value, boolean exclusive) {
+
+ private static final String INFINITY = "inf";
+
+ private static final String MINUS_INFINITY = "-inf";
+
+ private static final String INCLUSIVE_FORMAT = "%s";
+
+ private static final String EXCLUSIVE_FORMAT = "(%s";
+
+ @Override
+ public String toString() {
+ if (this == NEGATIVE_INFINITY) {
+ return MINUS_INFINITY;
+ }
+ if (this == POSITIVE_INFINITY) {
+ return INFINITY;
+ }
+ return String.format(formatString(), value);
+ }
+
+ private String formatString() {
+ if (exclusive) {
+ return EXCLUSIVE_FORMAT;
+ }
+ return INCLUSIVE_FORMAT;
+
+ }
+
+ }
+
+}
diff --git a/vector-stores/spring-ai-redis/src/main/java/org/springframework/ai/vectorstore/RedisVectorStore.java b/vector-stores/spring-ai-redis/src/main/java/org/springframework/ai/vectorstore/RedisVectorStore.java
new file mode 100644
index 000000000..5dff41909
--- /dev/null
+++ b/vector-stores/spring-ai-redis/src/main/java/org/springframework/ai/vectorstore/RedisVectorStore.java
@@ -0,0 +1,473 @@
+/*
+ * Copyright 2023-2023 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 java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.ai.document.Document;
+import org.springframework.ai.embedding.EmbeddingClient;
+import org.springframework.ai.vectorstore.filter.converter.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.FTCreateParams;
+import redis.clients.jedis.search.IndexDataType;
+import redis.clients.jedis.search.Query;
+import redis.clients.jedis.search.RediSearchUtil;
+import redis.clients.jedis.search.Schema.FieldType;
+import redis.clients.jedis.search.SearchResult;
+import redis.clients.jedis.search.schemafields.NumericField;
+import redis.clients.jedis.search.schemafields.SchemaField;
+import redis.clients.jedis.search.schemafields.TagField;
+import redis.clients.jedis.search.schemafields.TextField;
+import redis.clients.jedis.search.schemafields.VectorField;
+import redis.clients.jedis.search.schemafields.VectorField.VectorAlgorithm;
+
+/**
+ * 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 RediSearch 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 EmbeddingClient to convert documents into embeddings before storing
+ * them.
+ *
+ * @author Julien Ruaux
+ * @see VectorStore
+ * @see RedisVectorStoreConfig
+ * @see EmbeddingClient
+ */
+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 uri;
+
+ private final String indexName;
+
+ private final String prefix;
+
+ private final String contentFieldName;
+
+ private final String embeddingFieldName;
+
+ private final Algorithm vectorAlgorithm;
+
+ private final List 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();
+ }
+
+ private RedisVectorStoreConfig(Builder builder) {
+ this.uri = builder.uri;
+ this.indexName = builder.indexName;
+ this.prefix = builder.prefix;
+ this.contentFieldName = builder.contentFieldName;
+ this.embeddingFieldName = builder.embeddingFieldName;
+ this.vectorAlgorithm = builder.vectorAlgorithm;
+ this.metadataFields = builder.metadataFields;
+ }
+
+ public static class Builder {
+
+ private String uri = DEFAULT_URI;
+
+ 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 URI to use.
+ * @param uri the Redis URI to use
+ * @return this builder
+ */
+ public Builder withURI(String uri) {
+ this.uri = uri;
+ return this;
+ }
+
+ /**
+ * 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);
+ }
+
+ }
+
+ }
+
+ public static final String DEFAULT_URI = "redis://localhost:6379";
+
+ 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