diff --git a/pom.xml b/pom.xml
index 6bf7fb6b9..1aa983322 100644
--- a/pom.xml
+++ b/pom.xml
@@ -34,6 +34,7 @@
spring-ai-spring-boot-starters/spring-ai-starter-azure-openaispring-ai-spring-boot-starters/spring-ai-starter-ollamaspring-ai-spring-boot-starters/spring-ai-starter-transformers
+ spring-ai-spring-boot-starters/spring-ai-starter-cassandraspring-ai-spring-boot-starters/spring-ai-starter-chroma-storespring-ai-spring-boot-starters/spring-ai-starter-milvus-storespring-ai-spring-boot-starters/spring-ai-starter-pgvector-store
@@ -47,6 +48,7 @@
spring-ai-spring-boot-starters/spring-ai-starter-qdrant-storespring-ai-spring-boot-starters/spring-ai-starter-postgresml-embeddingspring-ai-docs
+ vector-stores/spring-ai-cassandravector-stores/spring-ai-pgvector-storevector-stores/spring-ai-hanadb-storevector-stores/spring-ai-milvus-store
@@ -138,6 +140,7 @@
3.25.2
+ 4.18.03.0.10.1.42.20.11
diff --git a/spring-ai-bom/pom.xml b/spring-ai-bom/pom.xml
index 1bc27de0c..d1c857f9d 100644
--- a/spring-ai-bom/pom.xml
+++ b/spring-ai-bom/pom.xml
@@ -132,6 +132,12 @@
${project.version}
+
+ org.springframework.ai
+ spring-ai-cassandra
+ ${project.version}
+
+
org.springframework.aispring-ai-chroma-store
@@ -218,6 +224,12 @@
+
+ org.springframework.ai
+ spring-ai-apache-cassandra-store-spring-boot-starter
+ ${project.version}
+
+
org.springframework.aispring-ai-azure-openai-spring-boot-starter
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 cb34f6bbb..82793cc94 100644
--- a/spring-ai-docs/src/main/antora/modules/ROOT/nav.adoc
+++ b/spring-ai-docs/src/main/antora/modules/ROOT/nav.adoc
@@ -46,23 +46,25 @@
**** xref:api/audio/speech/openai-speech.adoc[OpenAI]
** xref:api/vectordbs.adoc[]
*** xref:api/vectordbs/azure.adoc[]
+*** xref:api/vectordbs/apache-cassandra.adoc[]
*** xref:api/vectordbs/chroma.adoc[]
+*** xref:api/vectordbs/gemfire.adoc[GemFire]
*** xref:api/vectordbs/milvus.adoc[]
*** xref:api/vectordbs/neo4j.adoc[]
*** xref:api/vectordbs/pgvector.adoc[]
-*** xref:api/vectordbs/weaviate.adoc[]
-*** xref:api/vectordbs/redis.adoc[]
*** xref:api/vectordbs/pinecone.adoc[]
*** xref:api/vectordbs/qdrant.adoc[]
-*** xref:api/vectordbs/gemfire.adoc[GemFire]
+*** xref:api/vectordbs/redis.adoc[]
*** xref:api/vectordbs/hana.adoc[SAP Hana]
+*** xref:api/vectordbs/weaviate.adoc[]
+
** xref:api/functions.adoc[Function Calling]
** xref:api/prompt.adoc[]
** xref:api/output-parser.adoc[]
** xref:api/etl-pipeline.adoc[]
** xref:api/testing.adoc[]
** xref:api/generic-model.adoc[]
-* xref:api/testcontainers.adoc[Testcontainers]
* xref:contribution-guidelines.adoc[Contribution Guidelines]
* Appendices
** xref:upgrade-notes.adoc[]
+** xref:api/testcontainers.adoc[Testcontainers]
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 0b4385c14..afc12ba2e 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
@@ -88,14 +88,18 @@ Find more information on the `Filter.Expression` in the <> sec
These are the available implementations of the `VectorStore` interface:
* xref:api/vectordbs/azure.adoc[ Azure Vector Search] - The https://learn.microsoft.com/en-us/azure/search/vector-search-overview[Azure] vector store.
-* xref:api/vectordbs/chroma.adoc[ChromaVectorStore] - The https://www.trychroma.com/[Chroma] vector store.
-* xref:api/vectordbs/milvus.adoc[MilvusVectorStore] - The https://milvus.io/[Milvus] vector store.
-* xref:api/vectordbs/neo4j.adoc[Neo4jVectorStore] - The https://neo4j.com/[Neo4j] vector store.
+* xref:api/vectordbs/apache-cassandra.adoc[Apache Cassandra] - The https://cassandra.apache.org/doc/latest/cassandra/vector-search/overview.html[Apache Cassandra]
+* xref:api/vectordbs/chroma.adoc[Chroma Vector Store] - The https://www.trychroma.com/[Chroma] vector store.
+* xref:api/vectordbs/gemfire.adoc[GemFire Vector Store] - The https://tanzu.vmware.com/content/blog/vmware-gemfire-vector-database-extension[GemFire] vector store.
+* xref:api/vectordbs/milvus.adoc[Milvus Vector Store] - The https://milvus.io/[Milvus] vector store.
+* xref:api/vectordbs/neo4j.adoc[Neo4j Vector Store] - The https://neo4j.com/[Neo4j] vector store.
* xref:api/vectordbs/pgvector.adoc[PgVectorStore] - The https://github.com/pgvector/pgvector[PostgreSQL/PGVector] vector store.
-* xref:api/vectordbs/pinecone.adoc[PineconeVectorStore] - https://www.pinecone.io/[PineCone] vector store.
-* xref:api/vectordbs/qdrant.adoc[QdrantVectorStore] - https://www.qdrant.tech/[Qdrant] vector store.
-* xref:api/vectordbs/redis.adoc[RedisVectorStore] - The https://redis.io/[Redis] vector store.
-* xref:api/vectordbs/weaviate.adoc[WeaviateVectorStore] - The https://weaviate.io/[Weaviate] vector store.
+* xref:api/vectordbs/pinecone.adoc[Pinecone Vector Store] - https://www.pinecone.io/[PineCone] vector store.
+* xref:api/vectordbs/qdrant.adoc[Qdrant Vector Store] - https://www.qdrant.tech/[Qdrant] vector store.
+* xref:api/vectordbs/redis.adoc[Redis Vector Store] - The https://redis.io/[Redis] vector store.
+* xref:api/vectordbs/hana.adoc[SAP Hana Vector Store] - The https://news.sap.com/2024/04/sap-hana-cloud-vector-engine-ai-with-business-context/[SAP HANA] vector store.
+* xref:api/vectordbs/weaviate.adoc[Weaviate Vector Store] - The https://weaviate.io/[Weaviate] vector store.
+vector store.
* link:https://github.com/spring-projects/spring-ai/blob/main/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/SimpleVectorStore.java[SimpleVectorStore] - A simple implementation of persistent vector storage, good for educational purposes.
More implementations may be supported in future releases.
diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/vectordbs/apache-cassandra.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/vectordbs/apache-cassandra.adoc
new file mode 100644
index 000000000..a264c08c1
--- /dev/null
+++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/vectordbs/apache-cassandra.adoc
@@ -0,0 +1,260 @@
+= Apache Cassandra
+
+This section walks you through setting up `CassandraVectorStore` to store document embeddings and perform similarity searches.
+
+== What is Apache Cassandra ?
+
+link:https://cassandra.apache.org[Apache Cassandra] is a true open source distributed database reknown for scalability and high availability without compromising performance.
+
+Linear scalability, proven fault-tolerance and low latency on commodity hardware makes it the perfect platform for mission-critical data. Its Vector Similarity Search (VSS) is based on the JVector library that ensures best-in-class performance and relevancy.
+
+A vector search in Apache Cassandra is done as simply as:
+```
+SELECT content FROM table ORDER BY content_vector ANN OF query_embedding ;
+```
+
+More docs on this can be read https://cassandra.apache.org/doc/latest/cassandra/getting-started/vector-search-quickstart.html[here].
+
+The Spring AI Cassandra Vector Store is designed to work for both brand new RAG applications as well as being able to be retrofitted on top of existing data and tables. This vector store may also equally be used for non-RAG non_AI use-cases, e.g. semantic searcing in an existing database. The Vector Store will automatically create, or enhance, the schema as needed according to its configuration. If you don't want the schema modifications, configure the store with `disallowSchemaChanges`.
+
+== What is JVector Vector Search ?
+
+link:https://github.com/jbellis/jvector[JVector] is a pure Java embedded vector search engine.
+
+It stands out from other HNSW Vector Similarity Search implementations by being
+
+* Algorithmic-fast. JVector uses state of the art graph algorithms inspired by DiskANN and related research that offer high recall and low latency.
+* Implementation-fast. JVector uses the Panama SIMD API to accelerate index build and queries.
+* Memory efficient. JVector compresses vectors using product quantization so they can stay in memory during searches. (As part of our PQ implementation, our SIMD-accelerated kmeans class is 5x faster than the one in Apache Commons Math.)
+* Disk-aware. JVector’s disk layout is designed to do the minimum necessary iops at query time.
+* Concurrent. Index builds scale linearly to at least 32 threads. Double the threads, half the build time.
+* Incremental. Query your index as you build it. No delay between adding a vector and being able to find it in search results.
+* Easy to embed. API designed for easy embedding, by people using it in production.
+
+== Prerequisites
+
+1. A `EmbeddingClient` instance to compute the document embeddings. This is usually configured as a Spring Bean. Several options are available:
+
+- `Transformers Embedding` - computes the embedding in your local environment. The default is via ONNX and the all-MiniLM-L6-v2 Sentence Transformers. This just works.
+- If you want to use OpenAI's Embeddings` - 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].
+- There are many more choices, see `Embeddings API` docs.
+
+2. An Apache Cassandra instance, from version 5.0-beta1
+a. link:https://cassandra.apache.org/_/quickstart.html[DIY Quick Start]
+b. For a managed offering https://astra.datastax.com/[Astra DB] offers a healthy free tier offering.
+
+== Dependencies
+
+Add these dependencies to your project:
+
+* For just the Cassandra Vector Store
+
+[source,xml]
+----
+
+ org.springframework.ai
+ spring-ai-cassandra
+
+----
+
+* Or, for everything you need in a RAG application (using the default ONNX Embedding Client)
+
+[source,xml]
+----
+
+ org.springframework.ai
+ spring-ai-cassandra-spring-boot-starter
+
+----
+
+
+TIP: Refer to the xref:getting-started.adoc#dependency-management[Dependency Management] section to add the Spring AI BOM to your build file.
+
+* If for example you want to use the OpenAI modules, remember 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'
+----
+
+
+== Usage
+
+Create a CassandraVectorStore instance connected to your Apache Cassandra database:
+
+[source,java]
+----
+@Bean
+public VectorStore vectorStore(EmbeddingClient embeddingClient) {
+
+ CassandraVectorStoreConfig config = CassandraVectorStoreConfig.builder().build();
+
+ return new CassandraVectorStore(config, embeddingClient);
+}
+----
+
+NOTE: It is more convenient and preferred to create the `CassandraVectorStore` as a Bean.
+But if you decide you can create it manually.
+
+[NOTE]
+====
+The default configuration connects to Cassandra at localhost:9042 and will automatically create the default schema at `springframework_ai_vector.springframework_ai_vector_store`.
+
+Please see `CassandraVectorStoreConfig.Builder` for all the configuration options.
+====
+
+[NOTE]
+====
+The Cassandra Java Driver is easiest configured via the `application.conf` file on the classpath.
+
+More info can be found link: https://github.com/apache/cassandra-java-driver/tree/4.x/manual/core/configuration[here].
+====
+
+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(documents);
+----
+
+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!!".
+
+You can also limit results based on a similarity threshold:
+[source,java]
+----
+List results = vectorStore.similaritySearch(
+ SearchRequest.query("Spring").withTopK(5)
+ .withSimilarityThreshold(0.5d));
+----
+
+=== Metadata filtering
+
+You can leverage the generic, portable link:https://docs.spring.io/spring-ai/reference/api/vectordbs.html#_metadata_filters[metadata filters] with the CassandraVectorStore as well. Metadata fields must be configured in `CassandraVectorStoreConfig`.
+
+For example, you can use either the text expression language:
+
+[source,java]
+----
+vectorStore.similaritySearch(
+ SearchRequest.query("The World").withTopK(TOP_K)
+ .withFilterExpression("country in ['UK', 'NL'] && year >= 2020"));
+----
+
+or programmatically using the expression DSL:
+
+[source,java]
+----
+Filter.Expression f = new FilterExpressionBuilder()
+ .and(f.in("country", "UK", "NL"), f.gte("year", 2020)).build();
+
+vectorStore.similaritySearch(
+ SearchRequest.query("The World").withTopK(TOP_K)
+ .withFilterExpression(f));
+----
+
+The portable filter expressions get automatically converted into link:https://cassandra.apache.org/doc/latest/cassandra/developing/cql/index.html[CQL queries].
+
+Metadata fields to be searchable need to be either primary key columns or SAI indexed. To do this configure the metadata field with the `SchemaColumnTags.INDEXED`.
+
+
+== Advanced Example: Vector Store ontop full Wikipedia dataset
+
+The following example demonstrates how to use the store on an existing schema. Here we use the schema from the https://github.com/datastax-labs/colbert-wikipedia-data project which comes with the full wikipedia dataset ready vectorised for you.
+
+
+== Usage
+
+Create the schema in the Cassandra database first:
+
+[source,bash]
+----
+wget https://raw.githubusercontent.com/datastax-labs/colbert-wikipedia-data/main/schema.cql -O colbert-wikipedia-schema.cql
+cqlsh -f colbert-wikipedia-schema.cql
+----
+
+Then configure the store like:
+
+[source,java]
+----
+@Bean
+public CassandraVectorStore store(EmbeddingClient embeddingClient) {
+
+ List partitionColumns = List.of(new SchemaColumn("wiki", DataTypes.TEXT),
+ new SchemaColumn("language", DataTypes.TEXT), new SchemaColumn("title", DataTypes.TEXT));
+
+ List clusteringColumns = List.of(new SchemaColumn("chunk_no", DataTypes.INT),
+ new SchemaColumn("bert_embedding_no", DataTypes.INT));
+
+ List extraColumns = List.of(new SchemaColumn("revision", DataTypes.INT),
+ new SchemaColumn("id", DataTypes.INT));
+
+ CassandraVectorStoreConfig conf = CassandraVectorStoreConfig.builder()
+ .withKeyspaceName("wikidata")
+ .withTableName("articles")
+ .withPartitionKeys(partitionColumns)
+ .withClusteringKeys(clusteringColumns)
+ .withContentFieldName("body")
+ .withEmbeddingFieldName("all_minilm_l6_v2_embedding")
+ .withIndexName("all_minilm_l6_v2_ann")
+ .disallowSchemaChanges()
+ .addMetadataFields(extraColumns)
+ .withPrimaryKeyTranslator((List
+
+
+ org.springframework.ai
+ spring-ai-cassandra
+ ${project.parent.version}
+ true
+
+
org.springframework.ai
@@ -309,6 +317,12 @@
test
+
+ org.testcontainers
+ cassandra
+ test
+
+
com.redistestcontainers-redis
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/cassandra/CassandraConnectionDetails.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/cassandra/CassandraConnectionDetails.java
new file mode 100644
index 000000000..b67f90f6a
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/cassandra/CassandraConnectionDetails.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2024 - 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.cassandra;
+
+import java.net.InetSocketAddress;
+import java.util.List;
+
+import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails;
+
+/**
+ * @author Mick Semb Wever
+ * @since 1.0.0
+ */
+public interface CassandraConnectionDetails extends ConnectionDetails {
+
+ boolean hasCassandraContactPoints();
+
+ List getCassandraContactPoints();
+
+ boolean hasCassandraLocalDatacenter();
+
+ String getCassandraLocalDatacenter();
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/cassandra/CassandraVectorStoreAutoConfiguration.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/cassandra/CassandraVectorStoreAutoConfiguration.java
new file mode 100644
index 000000000..1bb93f410
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/cassandra/CassandraVectorStoreAutoConfiguration.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2024 - 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.cassandra;
+
+import java.net.InetSocketAddress;
+import java.util.Arrays;
+import java.util.List;
+
+import com.google.common.base.Preconditions;
+
+import org.springframework.ai.embedding.EmbeddingClient;
+import org.springframework.ai.vectorstore.CassandraVectorStore;
+import org.springframework.ai.vectorstore.CassandraVectorStoreConfig;
+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 Mick Semb Wever
+ * @since 1.0.0
+ */
+@AutoConfiguration
+@ConditionalOnClass({ CassandraVectorStore.class, EmbeddingClient.class })
+@EnableConfigurationProperties(CassandraVectorStoreProperties.class)
+public class CassandraVectorStoreAutoConfiguration {
+
+ @Bean
+ @ConditionalOnMissingBean(CassandraConnectionDetails.class)
+ public PropertiesCassandraConnectionDetails cassandraConnectionDetails(CassandraVectorStoreProperties properties) {
+ return new PropertiesCassandraConnectionDetails(properties);
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ public CassandraVectorStore vectorStore(EmbeddingClient embeddingClient, CassandraVectorStoreProperties properties,
+ CassandraConnectionDetails cassandraConnectionDetails) {
+
+ var builder = CassandraVectorStoreConfig.builder();
+ if (cassandraConnectionDetails.hasCassandraContactPoints()) {
+ for (InetSocketAddress contactPoint : cassandraConnectionDetails.getCassandraContactPoints()) {
+ builder = builder.addContactPoint(contactPoint);
+ }
+ }
+ if (cassandraConnectionDetails.hasCassandraLocalDatacenter()) {
+ builder = builder.withLocalDatacenter(cassandraConnectionDetails.getCassandraLocalDatacenter());
+ }
+
+ builder = builder.withKeyspaceName(properties.getKeyspace())
+ .withTableName(properties.getTable())
+ .withContentColumnName(properties.getContentFieldName())
+ .withEmbeddingColumnName(properties.getEmbeddingFieldName())
+ .withIndexName(properties.getIndexName());
+
+ if (properties.getDisallowSchemaCreation()) {
+ builder = builder.disallowSchemaChanges();
+ }
+
+ return new CassandraVectorStore(builder.build(), embeddingClient);
+ }
+
+ private static class PropertiesCassandraConnectionDetails implements CassandraConnectionDetails {
+
+ private final CassandraVectorStoreProperties properties;
+
+ public PropertiesCassandraConnectionDetails(CassandraVectorStoreProperties properties) {
+ this.properties = properties;
+ }
+
+ private String[] getCassandraContactPointHosts() {
+ return this.properties.getCassandraContactPointHosts().split("(,| )");
+ }
+
+ @Override
+ public List getCassandraContactPoints() {
+
+ Preconditions.checkState(hasCassandraContactPoints(), "cassandraContactPointHosts has not been set");
+ final int port = this.properties.getCassandraContactPointPort();
+
+ return Arrays.asList(getCassandraContactPointHosts())
+ .stream()
+ .map((host) -> InetSocketAddress.createUnresolved(host, port))
+ .toList();
+ }
+
+ @Override
+ public String getCassandraLocalDatacenter() {
+ Preconditions.checkState(hasCassandraLocalDatacenter(), "cassandraLocalDatacenter has not been set");
+ return this.properties.getCassandraLocalDatacenter();
+ }
+
+ @Override
+ public boolean hasCassandraContactPoints() {
+ return null != this.properties.getCassandraContactPointHosts();
+ }
+
+ @Override
+ public boolean hasCassandraLocalDatacenter() {
+ return null != this.properties.getCassandraLocalDatacenter();
+ }
+
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/cassandra/CassandraVectorStoreProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/cassandra/CassandraVectorStoreProperties.java
new file mode 100644
index 000000000..73b014ab6
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/cassandra/CassandraVectorStoreProperties.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2024 - 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.cassandra;
+
+import org.springframework.ai.vectorstore.CassandraVectorStoreConfig;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+/**
+ * @author Mick Semb Wever
+ * @since 1.0.0
+ */
+@ConfigurationProperties(CassandraVectorStoreProperties.CONFIG_PREFIX)
+public class CassandraVectorStoreProperties {
+
+ public static final String CONFIG_PREFIX = "spring.ai.vectorstore.cassandra";
+
+ private String cassandraContactPointHosts = null;
+
+ private int cassandraContactPointPort = 9042;
+
+ private String cassandraLocalDatacenter = null;
+
+ private String keyspace = CassandraVectorStoreConfig.DEFAULT_KEYSPACE_NAME;
+
+ private String table = CassandraVectorStoreConfig.DEFAULT_TABLE_NAME;
+
+ private String indexName = CassandraVectorStoreConfig.DEFAULT_INDEX_NAME;
+
+ private String contentColumnName = CassandraVectorStoreConfig.DEFAULT_CONTENT_COLUMN_NAME;
+
+ private String embeddingColumnName = CassandraVectorStoreConfig.DEFAULT_EMBEDDING_COLUMN_NAME;
+
+ private boolean disallowSchemaChanges = false;
+
+ public String getCassandraContactPointHosts() {
+ return this.cassandraContactPointHosts;
+ }
+
+ /** comma or space separated */
+ public void setCassandraContactPointHosts(String cassandraContactPointHosts) {
+ this.cassandraContactPointHosts = cassandraContactPointHosts;
+ }
+
+ public int getCassandraContactPointPort() {
+ return this.cassandraContactPointPort;
+ }
+
+ public void setCassandraContactPointPort(int cassandraContactPointPort) {
+ this.cassandraContactPointPort = cassandraContactPointPort;
+ }
+
+ public String getCassandraLocalDatacenter() {
+ return this.cassandraLocalDatacenter;
+ }
+
+ public void setCassandraLocalDatacenter(String cassandraLocalDatacenter) {
+ this.cassandraLocalDatacenter = cassandraLocalDatacenter;
+ }
+
+ public String getKeyspace() {
+ return this.keyspace;
+ }
+
+ public void setKeyspace(String keyspace) {
+ this.keyspace = keyspace;
+ }
+
+ public String getTable() {
+ return this.table;
+ }
+
+ public void setTable(String table) {
+ this.table = table;
+ }
+
+ public String getIndexName() {
+ return this.indexName;
+ }
+
+ public void setIndexName(String indexName) {
+ this.indexName = indexName;
+ }
+
+ public String getContentFieldName() {
+ return this.contentColumnName;
+ }
+
+ public void setContentFieldName(String contentFieldName) {
+ this.contentColumnName = contentFieldName;
+ }
+
+ public String getEmbeddingFieldName() {
+ return this.embeddingColumnName;
+ }
+
+ public void setEmbeddingFieldName(String embeddingFieldName) {
+ this.embeddingColumnName = embeddingFieldName;
+ }
+
+ public Boolean getDisallowSchemaCreation() {
+ return this.disallowSchemaChanges;
+ }
+
+ public void setDisallowSchemaCreation(boolean disallowSchemaCreation) {
+ this.disallowSchemaChanges = disallowSchemaCreation;
+ }
+
+}
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 3e3fd3574..59f104b68 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
@@ -31,3 +31,4 @@ org.springframework.ai.autoconfigure.vectorstore.mongo.MongoDBAtlasVectorStoreAu
org.springframework.ai.autoconfigure.anthropic.AnthropicAutoConfiguration
org.springframework.ai.autoconfigure.watsonxai.WatsonxAiAutoConfiguration
org.springframework.ai.autoconfigure.vectorstore.elasticsearch.ElasticsearchVectorStoreAutoConfiguration
+org.springframework.ai.autoconfigure.vectorstore.cassandra.CassandraVectorStoreAutoConfiguration
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vectorstore/cassandra/CassandraVectorStoreAutoConfigurationIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vectorstore/cassandra/CassandraVectorStoreAutoConfigurationIT.java
new file mode 100644
index 000000000..15530052f
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vectorstore/cassandra/CassandraVectorStoreAutoConfigurationIT.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright 2024 - 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.cassandra;
+
+import java.util.List;
+import java.util.Map;
+
+import org.junit.jupiter.api.Test;
+import org.testcontainers.junit.jupiter.Container;
+import org.testcontainers.junit.jupiter.Testcontainers;
+import org.testcontainers.containers.CassandraContainer;
+import org.testcontainers.utility.DockerImageName;
+
+import org.springframework.ai.ResourceUtils;
+import org.springframework.ai.document.Document;
+import org.springframework.ai.embedding.EmbeddingClient;
+import org.springframework.ai.transformers.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 static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author Mick Semb Wever
+ * @since 1.0.0
+ */
+@Testcontainers
+class CassandraVectorStoreAutoConfigurationIT {
+
+ static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("cassandra");
+
+ @Container
+ static CassandraContainer cassandraContainer = new CassandraContainer(DEFAULT_IMAGE_NAME.withTag("5.0"));
+
+ 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(CassandraVectorStoreAutoConfiguration.class))
+ .withUserConfiguration(Config.class)
+ .withPropertyValues("spring.ai.vectorstore.cassandra.keyspace=test_autoconfigure")
+ .withPropertyValues("spring.ai.vectorstore.cassandra.contentFieldName=doc_chunk");
+
+ @Test
+ void addAndSearch() {
+ contextRunner
+ .withPropertyValues("spring.ai.vectorstore.cassandra.cassandraContactPointHosts=" + getContactPointHost())
+ .withPropertyValues("spring.ai.vectorstore.cassandra.cassandraContactPointPort=" + getContactPointPort())
+ .withPropertyValues("spring.ai.vectorstore.cassandra.cassandraLocalDatacenter="
+ + cassandraContainer.getLocalDatacenter())
+
+ .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();
+ }
+
+ }
+
+ private String getContactPointHost() {
+ return cassandraContainer.getContactPoint().getHostString();
+ }
+
+ private String getContactPointPort() {
+ return String.valueOf(cassandraContainer.getContactPoint().getPort());
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vectorstore/cassandra/CassandraVectorStorePropertiesTests.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vectorstore/cassandra/CassandraVectorStorePropertiesTests.java
new file mode 100644
index 000000000..c5a3e4d0f
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vectorstore/cassandra/CassandraVectorStorePropertiesTests.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2024 - 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.cassandra;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.ai.vectorstore.CassandraVectorStoreConfig;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author Mick Semb Wever
+ * @since 1.0.0
+ */
+class CassandraVectorStorePropertiesTests {
+
+ @Test
+ void defaultValues() {
+ var props = new CassandraVectorStoreProperties();
+ assertThat(props.getCassandraContactPointHosts()).isNull();
+ assertThat(props.getCassandraContactPointPort()).isEqualTo(9042);
+ assertThat(props.getCassandraLocalDatacenter()).isNull();
+ assertThat(props.getKeyspace()).isEqualTo(CassandraVectorStoreConfig.DEFAULT_KEYSPACE_NAME);
+ assertThat(props.getTable()).isEqualTo(CassandraVectorStoreConfig.DEFAULT_TABLE_NAME);
+ assertThat(props.getContentFieldName()).isEqualTo(CassandraVectorStoreConfig.DEFAULT_CONTENT_COLUMN_NAME);
+ assertThat(props.getEmbeddingFieldName()).isEqualTo(CassandraVectorStoreConfig.DEFAULT_EMBEDDING_COLUMN_NAME);
+ assertThat(props.getIndexName()).isEqualTo(CassandraVectorStoreConfig.DEFAULT_INDEX_NAME);
+ assertThat(props.getDisallowSchemaCreation()).isFalse();
+ }
+
+ @Test
+ void customValues() {
+ var props = new CassandraVectorStoreProperties();
+ props.setCassandraContactPointHosts("127.0.0.1,127.0.0.2");
+ props.setCassandraContactPointPort(9043);
+ props.setCassandraLocalDatacenter("dc1");
+ props.setKeyspace("my_keyspace");
+ props.setTable("my_table");
+ props.setContentFieldName("my_content");
+ props.setEmbeddingFieldName("my_vector");
+ props.setIndexName("my_sai");
+ props.setDisallowSchemaCreation(true);
+
+ assertThat(props.getCassandraContactPointHosts()).isEqualTo("127.0.0.1,127.0.0.2");
+ assertThat(props.getCassandraContactPointPort()).isEqualTo(9043);
+ assertThat(props.getCassandraLocalDatacenter()).isEqualTo("dc1");
+ assertThat(props.getKeyspace()).isEqualTo("my_keyspace");
+ assertThat(props.getTable()).isEqualTo("my_table");
+ assertThat(props.getContentFieldName()).isEqualTo("my_content");
+ assertThat(props.getEmbeddingFieldName()).isEqualTo("my_vector");
+ assertThat(props.getIndexName()).isEqualTo("my_sai");
+ assertThat(props.getDisallowSchemaCreation()).isTrue();
+ }
+
+}
diff --git a/spring-ai-spring-boot-starters/spring-ai-starter-cassandra/pom.xml b/spring-ai-spring-boot-starters/spring-ai-starter-cassandra/pom.xml
new file mode 100644
index 000000000..a40111ee8
--- /dev/null
+++ b/spring-ai-spring-boot-starters/spring-ai-starter-cassandra/pom.xml
@@ -0,0 +1,42 @@
+
+
+ 4.0.0
+
+ org.springframework.ai
+ spring-ai
+ 1.0.0-SNAPSHOT
+ ../../pom.xml
+
+ spring-ai-cassandra-spring-boot-starter
+ jar
+ Spring AI Starter - Apache Cassandra Vector Store
+ Spring AI Apache Cassandra 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-cassandra
+ ${project.parent.version}
+
+
+
+
diff --git a/vector-stores/spring-ai-cassandra/README.md b/vector-stores/spring-ai-cassandra/README.md
new file mode 100644
index 000000000..d35bcbfeb
--- /dev/null
+++ b/vector-stores/spring-ai-cassandra/README.md
@@ -0,0 +1 @@
+[Apache Cassandra Vector Store Documentation](https://docs.spring.io/spring-ai/reference/api/vectordbs/apache-cassandra.html)
\ No newline at end of file
diff --git a/vector-stores/spring-ai-cassandra/pom.xml b/vector-stores/spring-ai-cassandra/pom.xml
new file mode 100644
index 000000000..b01d200f8
--- /dev/null
+++ b/vector-stores/spring-ai-cassandra/pom.xml
@@ -0,0 +1,76 @@
+
+
+ 4.0.0
+
+ org.springframework.ai
+ spring-ai
+ 1.0.0-SNAPSHOT
+ ../../pom.xml
+
+ spring-ai-cassandra
+ jar
+ Spring AI Vector Store – Apache Cassandra
+ Spring AI Vector Store for Apache Cassandra
+ 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
+
+
+
+ 17
+ 17
+
+
+
+
+ org.springframework.ai
+ spring-ai-core
+ ${project.parent.version}
+
+
+
+ org.apache.cassandra
+ java-driver-query-builder
+ ${cassandra.java-driver.version}
+
+
+
+
+ org.springframework.ai
+ spring-ai-transformers
+ ${project.parent.version}
+ test
+
+
+
+ org.springframework.ai
+ spring-ai-test
+ ${project.parent.version}
+ test
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ org.testcontainers
+ junit-jupiter
+ test
+
+
+
+ org.testcontainers
+ cassandra
+ test
+
+
+
+
+
diff --git a/vector-stores/spring-ai-cassandra/src/main/java/org/springframework/ai/vectorstore/CassandraFilterExpressionConverter.java b/vector-stores/spring-ai-cassandra/src/main/java/org/springframework/ai/vectorstore/CassandraFilterExpressionConverter.java
new file mode 100644
index 000000000..4a8b681a5
--- /dev/null
+++ b/vector-stores/spring-ai-cassandra/src/main/java/org/springframework/ai/vectorstore/CassandraFilterExpressionConverter.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright 2024 - 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 java.util.Collection;
+import java.util.Map;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+import com.datastax.oss.driver.api.core.metadata.schema.ColumnMetadata;
+import com.datastax.oss.driver.api.core.type.DataTypes;
+import com.datastax.oss.driver.api.core.type.codec.registry.CodecRegistry;
+import com.datastax.oss.driver.shaded.guava.common.base.Preconditions;
+
+import org.springframework.ai.vectorstore.filter.Filter;
+import org.springframework.ai.vectorstore.filter.Filter.ExpressionType;
+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 CQL where clauses.
+ *
+ * @author Mick Semb Wever
+ * @since 1.0.0
+ */
+final class CassandraFilterExpressionConverter extends AbstractFilterExpressionConverter {
+
+ private final Map columnsByName;
+
+ public CassandraFilterExpressionConverter(Collection columns) {
+
+ this.columnsByName = columns.stream()
+ .collect(Collectors.toMap((c) -> c.getName().asInternal(), Function.identity()));
+ }
+
+ @Override
+ protected void doKey(Key key, StringBuilder context) {
+ String keyName = key.key();
+ Optional column = getColumn(keyName);
+ Preconditions.checkArgument(column.isPresent(), "No metafield %s has been configured", keyName);
+ context.append(column.get().getName().asCql(false));
+ }
+
+ @Override
+ protected void doExpression(Filter.Expression expression, StringBuilder context) {
+ switch (expression.type()) {
+ case AND -> doBinaryOperation(" and ", expression, context);
+ case OR -> doBinaryOperation(" or ", expression, context);
+ case NIN, NOT -> throw new UnsupportedOperationException(
+ String.format("Expression type %s not yet implemented. Patches welcome.", expression.type()));
+ default -> doField(expression, context);
+ }
+ }
+
+ private static void doOperand(ExpressionType type, StringBuilder context) {
+ switch (type) {
+ case EQ -> context.append(" = ");
+ case NE -> context.append(" != ");
+ case GT -> context.append(" > ");
+ case GTE -> context.append(" >= ");
+ case IN -> context.append(" IN ");
+ case LT -> context.append(" < ");
+ case LTE -> context.append(" <= ");
+ // TODO SAI supports collections
+ // reach out to mck@apache.org if you'd like these implemented
+ // case CONTAINS -> context.append(" CONTAINS ");
+ // case CONTAINS_KEY -> context.append(" CONTAINS KEY ");
+ default -> throw new UnsupportedOperationException(
+ String.format("Expression type %s not yet implemented. Patches welcome.", type));
+ }
+ }
+
+ private void doBinaryOperation(String operator, Filter.Expression expression, StringBuilder context) {
+ this.convertOperand(expression.left(), context);
+ context.append(operator);
+ this.convertOperand(expression.right(), context);
+ }
+
+ private void doField(Filter.Expression expression, StringBuilder context) {
+ doKey((Key) expression.left(), context);
+ doOperand(expression.type(), context);
+ ColumnMetadata column = getColumn(((Key) expression.left()).key()).get();
+ var v = ((Value) expression.right()).value();
+ if (ExpressionType.IN.equals(expression.type())) {
+ Preconditions.checkArgument(v instanceof Collection);
+ doListValue(column, v, context);
+ }
+ else {
+ doValue(column, v, context);
+ }
+ }
+
+ private void doListValue(ColumnMetadata column, Object v, StringBuilder context) {
+ context.append('(');
+ for (var e : (Collection) v) {
+ doValue(column, e, context);
+ context.append(',');
+ }
+ context.deleteCharAt(context.length() - 1);
+ context.append(')');
+ }
+
+ private void doValue(ColumnMetadata column, Object v, StringBuilder context) {
+ if (DataTypes.SMALLINT.equals(column.getType())) {
+ v = ((Number) v).shortValue();
+ }
+ context.append(CodecRegistry.DEFAULT.codecFor(column.getType()).format(v));
+ }
+
+ private Optional getColumn(String name) {
+ Optional column = Optional.ofNullable(this.columnsByName.get(name));
+
+ // work around the need to escape filter keys the ANTLR parser doesn't like
+ // e.g. with underscores like chunk_no
+ if (column.isEmpty()) {
+ if (name.startsWith("\"") && name.endsWith("\"")) {
+ name = name.substring(1, name.length() - 1);
+ column = Optional.ofNullable(this.columnsByName.get(name));
+ }
+ }
+ return column;
+ }
+
+}
diff --git a/vector-stores/spring-ai-cassandra/src/main/java/org/springframework/ai/vectorstore/CassandraVectorStore.java b/vector-stores/spring-ai-cassandra/src/main/java/org/springframework/ai/vectorstore/CassandraVectorStore.java
new file mode 100644
index 000000000..6f651c944
--- /dev/null
+++ b/vector-stores/spring-ai-cassandra/src/main/java/org/springframework/ai/vectorstore/CassandraVectorStore.java
@@ -0,0 +1,320 @@
+/*
+ * Copyright 2024 - 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 java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+
+import com.datastax.oss.driver.api.core.cql.BoundStatement;
+import com.datastax.oss.driver.api.core.cql.BoundStatementBuilder;
+import com.datastax.oss.driver.api.core.cql.PreparedStatement;
+import com.datastax.oss.driver.api.core.cql.Row;
+import com.datastax.oss.driver.api.core.data.CqlVector;
+import com.datastax.oss.driver.api.core.metadata.schema.TableMetadata;
+import com.datastax.oss.driver.api.querybuilder.QueryBuilder;
+import com.datastax.oss.driver.api.querybuilder.delete.Delete;
+import com.datastax.oss.driver.api.querybuilder.delete.DeleteSelection;
+import com.datastax.oss.driver.api.querybuilder.insert.InsertInto;
+import com.datastax.oss.driver.api.querybuilder.insert.RegularInsert;
+import com.datastax.oss.driver.shaded.guava.common.base.Preconditions;
+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.CassandraVectorStoreConfig.SchemaColumn;
+import org.springframework.ai.vectorstore.filter.FilterExpressionConverter;
+import org.springframework.beans.factory.InitializingBean;
+
+/**
+ * The CassandraVectorStore is for managing and querying vector data in an Apache
+ * Cassandra db. It offers functionalities like adding, deleting, and performing
+ * similarity searches on documents.
+ *
+ * The store utilizes CQL to index and search vector data. It allows for custom metadata
+ * fields in the documents to be stored alongside the vector and content data.
+ *
+ * This class requires a CassandraVectorStoreConfig configuration object for
+ * initialization, which includes settings like connection details, index name, field
+ * names, etc. It also requires an EmbeddingClient to convert documents into embeddings
+ * before storing them.
+ *
+ * A schema matching the configuration is automatically created if it doesn't exist.
+ * Missing columns and indexes in existing tables will also be automatically created.
+ * Disable this with the disallowSchemaCreation.
+ *
+ * This class is designed to work with brand new tables that it creates for you, or on top
+ * of existing Cassandra tables. The latter is appropriate when wanting to keep data in
+ * place, creating embeddings next to it, and performing vector similarity searches
+ * in-situ.
+ *
+ * Instances of this class are not dynamic against server-side schema changes. If you
+ * change the schema server-side you need a new CassandraVectorStore instance.
+ *
+ * @author Mick Semb Wever
+ * @see VectorStore
+ * @see CassandraVectorStoreConfig
+ * @see EmbeddingClient
+ * @since 1.0.0
+ */
+public final class CassandraVectorStore implements VectorStore, InitializingBean, AutoCloseable {
+
+ /**
+ * Indexes are automatically created with COSINE. This can be changed manually via
+ * cqlsh
+ */
+ public enum Similarity {
+
+ COSINE, DOT_PRODUCT, EUCLIDEAN;
+
+ }
+
+ private static final String QUERY_FORMAT = "select %s,%s,%s%s from %s.%s ? order by %s ann of ? limit ?";
+
+ public static final String SIMILARITY_FIELD_NAME = "similarity_score";
+
+ private static final Logger logger = LoggerFactory.getLogger(CassandraVectorStore.class);
+
+ private final CassandraVectorStoreConfig conf;
+
+ private final EmbeddingClient embeddingClient;
+
+ private final FilterExpressionConverter filterExpressionConverter;
+
+ private final Map, PreparedStatement> addStmts = new HashMap<>();
+
+ private final PreparedStatement deleteStmt;
+
+ private final String similarityStmt;
+
+ private final Similarity similarity;
+
+ public CassandraVectorStore(CassandraVectorStoreConfig conf, EmbeddingClient embeddingClient) {
+
+ Preconditions.checkArgument(null != conf, "Config must not be null");
+ Preconditions.checkArgument(null != embeddingClient, "Embedding client must not be null");
+
+ this.conf = conf;
+ this.embeddingClient = embeddingClient;
+ conf.ensureSchemaExists(embeddingClient.dimensions());
+ prepareAddStatement(Set.of());
+ this.deleteStmt = prepareDeleteStatement();
+
+ TableMetadata cassandraMetadata = conf.session.getMetadata()
+ .getKeyspace(conf.schema.keyspace())
+ .get()
+ .getTable(conf.schema.table())
+ .get();
+
+ this.similarity = getIndexSimilarity(cassandraMetadata);
+ this.similarityStmt = similaritySearchStatement();
+
+ this.filterExpressionConverter = new CassandraFilterExpressionConverter(
+ cassandraMetadata.getColumns().values());
+ }
+
+ @Override
+ public void add(List documents) {
+ CompletableFuture[] futures = new CompletableFuture[documents.size()];
+ short i = 0;
+ for (Document d : documents) {
+ List