Implement Qdrant vector store
- Implement QdrantVectorStore. Uses a custom parser for converting Spring AI metadata(Map<String, Object>) to Qdrant GRPC payload. - Implement Qdrant Expression Filter support. Uses a custom parser for converting Spring AI filters to Qdrant-compatible GRPC filters. - Add ITs using testcontainers. - Add antora docs adrant.adoc. - Add Qdrant vector store auto-configuraton and boot starter. Additional (review) change: - Fix poms parent to 0.8.1-SNAPSHOT. - Rename ObjectFactory into QdrantObjectFactor. - Rename ValueFactory into QdrantValueFactory. - Move the org.springframework.ai.vectorstore package into org.springframework.ai.vectorstore.qdrant. - Add missing Autoconfigure definition. - Add missing license and JavaDocs. - Minor code style improvmentes. - Move the qdrant version to the main pom - Add QdrantVectorStoreAutoConfigurationIT - Remove guava dependency - Improve gdrant.adoc conent and structure. - Remove the grpc-protobuf dependency Resolves #331
This commit is contained in:
committed by
Christian Tzolov
parent
e1462b86e3
commit
ea0b439dac
@@ -162,7 +162,7 @@ Though the `DocumentWriter` interface isn't exclusively for Vector Database writ
|
||||
|
||||
**Vector Stores:** Vector Databases are instrumental in incorporating your data with AI models.
|
||||
They ascertain which document sections the AI should use for generating responses.
|
||||
Examples of Vector Databases include Chroma, Postgres, Pinecone, Weaviate, Mongo Atlas, and Redis. Spring AI's `VectorStore` abstraction permits effortless transitions between database implementations.
|
||||
Examples of Vector Databases include Chroma, Postgres, Pinecone, Qdrant, Weaviate, Mongo Atlas, and Redis. Spring AI's `VectorStore` abstraction permits effortless transitions between database implementations.
|
||||
|
||||
|
||||
|
||||
|
||||
4
pom.xml
4
pom.xml
@@ -41,6 +41,7 @@
|
||||
<module>spring-ai-spring-boot-starters/spring-ai-starter-redis</module>
|
||||
<module>spring-ai-spring-boot-starters/spring-ai-starter-stability-ai</module>
|
||||
<module>spring-ai-spring-boot-starters/spring-ai-starter-neo4j-store</module>
|
||||
<module>spring-ai-spring-boot-starters/spring-ai-starter-qdrant-store</module>
|
||||
<module>spring-ai-spring-boot-starters/spring-ai-starter-postgresml-embedding</module>
|
||||
<module>spring-ai-docs</module>
|
||||
<module>vector-stores/spring-ai-pgvector-store</module>
|
||||
@@ -55,6 +56,7 @@
|
||||
<module>vector-stores/spring-ai-redis</module>
|
||||
<module>spring-ai-spring-boot-starters/spring-ai-starter-vertex-ai-palm2</module>
|
||||
<module>spring-ai-spring-boot-starters/spring-ai-starter-vertex-ai-gemini</module>
|
||||
<module>vector-stores/spring-ai-qdrant</module>
|
||||
<module>spring-ai-spring-boot-starters/spring-ai-starter-bedrock-ai</module>
|
||||
<module>spring-ai-spring-boot-starters/spring-ai-starter-mistral-ai</module>
|
||||
</modules>
|
||||
@@ -112,6 +114,7 @@
|
||||
<djl.version>0.26.0</djl.version>
|
||||
<onnxruntime.version>1.17.0</onnxruntime.version>
|
||||
<com.google.cloud.version>26.33.0</com.google.cloud.version>
|
||||
<qdrant.version>1.7.1</qdrant.version>
|
||||
|
||||
<!-- Protobuf -->
|
||||
<protobuf-java.version>3.25.2</protobuf-java.version>
|
||||
@@ -125,6 +128,7 @@
|
||||
<fastjson.version>2.0.46</fastjson.version>
|
||||
<azure-search.version>11.6.1</azure-search.version>
|
||||
<weaviate-client.version>4.5.1</weaviate-client.version>
|
||||
<qdrant.version>1.7.1</qdrant.version>
|
||||
|
||||
<!-- testing dependencies -->
|
||||
<testcontainers.version>1.19.6</testcontainers.version>
|
||||
|
||||
@@ -161,6 +161,12 @@
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-qdrant</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Utilities -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
@@ -283,6 +289,11 @@
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-qdrant-store-spring-boot-starter</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
*** xref:api/vectordbs/weaviate.adoc[]
|
||||
*** xref:api/vectordbs/redis.adoc[]
|
||||
*** xref:api/vectordbs/pinecone.adoc[]
|
||||
*** xref:api/vectordbs/qdrant.adoc[]
|
||||
** xref:api/etl-pipeline.adoc[]
|
||||
** xref:api/testing.adoc[]
|
||||
** xref:api/generic-model.adoc[]
|
||||
|
||||
@@ -93,6 +93,7 @@ These are the available implementations of the `VectorStore` interface:
|
||||
* xref:api/vectordbs/neo4j.adoc[Neo4jVectorStore] - 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.
|
||||
* 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.
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
= Qdrant
|
||||
|
||||
This section walks you through setting up the Qdrant `VectorStore` to store document embeddings and perform similarity searches.
|
||||
|
||||
link:https://www.qdrant.tech/[Qdrant] is an open-source, high-performance vector search engine/database.
|
||||
|
||||
== Prerequisites
|
||||
|
||||
* Qdrant Instance: Set up a Qdrant instance by following the link:https://qdrant.tech/documentation/guides/installation/[installation instructions] in the Qdrant documentation.
|
||||
* If required, an API key for the xref:api/embeddings.adoc#available-implementations[EmbeddingClient] to generate the embeddings stored by the `QdrantVectorStore`.
|
||||
|
||||
== Configuration
|
||||
|
||||
To set up `QdrantVectorStore`, you'll need the following information from your Qdrant instance:
|
||||
|
||||
* Qdrant Host
|
||||
* Qdrant GRPC Port
|
||||
* Qdrant Collection Name
|
||||
* Optional Qdrant API Key (not required for local development)
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
A Qdrant collection has to be link:https://qdrant.tech/documentation/concepts/collections/#create-a-collection[created] in advance with the appropriate dimensions and configurations.
|
||||
|
||||
For example if using the OpenAI `text-embedding-ada-002` embedding model, create a collection with a vector size of `1536`.
|
||||
====
|
||||
|
||||
== Dependencies
|
||||
|
||||
* The Vector Store requires an `EmbeddingClient` instance to calculate embeddings for the documents.
|
||||
You can pick one of the available xref:api/embeddings.adoc#available-implementations[EmbeddingClient Implementations]. For example ou can use the OpenAI boot starter:
|
||||
|
||||
[source,xml]
|
||||
----
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
----
|
||||
|
||||
or to your Gradle `build.gradle` build file.
|
||||
|
||||
[source,groovy]
|
||||
----
|
||||
dependencies {
|
||||
implementation 'org.springframework.ai:spring-ai-openai-spring-boot-starter'
|
||||
}
|
||||
----
|
||||
|
||||
TIP: Additionally, you'll need to provide your OpenAI API Key. Set it as an environment variable like so:
|
||||
`export SPRING_AI_OPENAI_API_KEY='Your_OpenAI_API_Key`
|
||||
|
||||
|
||||
* Add the Qdrant Boot Starter dependency to your project:
|
||||
|
||||
[source,xml]
|
||||
----
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-qdrant-store-spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
----
|
||||
|
||||
or to your Gradle `build.gradle` build file.
|
||||
|
||||
[source,groovy]
|
||||
----
|
||||
dependencies {
|
||||
implementation 'org.springframework.ai:spring-ai-qdrant-store-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.
|
||||
|
||||
Please have a look at the list of xref:#qdrant-vectorstore-properties[configuration parameters] for the vector store to learn about the default values and configuration options.
|
||||
|
||||
TIP: Refer to the xref:getting-started.adoc#repositories[Repositories] section to add Milestone and/or Snapshot Repositories to your build file.
|
||||
|
||||
|
||||
Now you can Auto-wire the Qdrant Vector Store in your application and use it
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@Autowired
|
||||
VectorStore vectorStore;
|
||||
|
||||
...
|
||||
List <Document> documents = List.of(
|
||||
new Document("Spring AI rocks!! Spring AI rocks!! Spring AI rocks!! Spring AI rocks!! Spring AI rocks!!", Map.of("meta1", "meta1")),
|
||||
new Document("The World is Big and Salvation Lurks Around the Corner"),
|
||||
new Document("You walk forward facing the past and you turn back toward the future.", Map.of("meta2", "meta2")));
|
||||
|
||||
// Add the documents to Qdrant
|
||||
vectorStore.add(List.of(document));
|
||||
|
||||
// Retrieve documents similar to a query
|
||||
List<Document> results = vectorStore.similaritySearch(SearchRequest.query("Spring").withTopK(5));
|
||||
----
|
||||
|
||||
== Configuration
|
||||
|
||||
To connect to Qdrant and use the `QdrantVectorStore`, you need to provide access details for your instance.
|
||||
A simple configuration can either be provided via Spring Boot's _application.properties_,
|
||||
|
||||
[source,properties]
|
||||
----
|
||||
spring.ai.vectorstore.qdrant.host=<host of your qdrant instance>
|
||||
spring.ai.vectorstore.qdrant.port=<port of your qdrant instance>
|
||||
spring.ai.vectorstore.qdrant.api-key=<your api key>
|
||||
spring.ai.vectorstore.qdrant.collection-name=<The name of the collection to use in Qdrant>
|
||||
|
||||
# API key if needed, e.g. OpenAI
|
||||
spring.ai.openai.api.key=<api-key>
|
||||
----
|
||||
|
||||
|
||||
== Manual Configuration
|
||||
|
||||
Instead of using the Spring Boot auto-configuration, you can manually configure the `QdrantVectorStore`. For this you need to add the `spring-ai-qdrant` dependency to your project:
|
||||
|
||||
[source,xml]
|
||||
----
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-qdrant</artifactId>
|
||||
</dependency>
|
||||
----
|
||||
|
||||
or to your Gradle `build.gradle` build file.
|
||||
|
||||
[source,groovy]
|
||||
----
|
||||
dependencies {
|
||||
implementation 'org.springframework.ai:spring-ai-qdrant'
|
||||
}
|
||||
----
|
||||
|
||||
To configure Qdrant in your application, you can use the following setup:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@Bean
|
||||
public QdrantVectorStoreConfig qdrantVectorStoreConfig() {
|
||||
|
||||
return QdrantVectorStoreConfig.builder()
|
||||
.withHost("<QDRANT_HOSTNAME>")
|
||||
.withPort(<QDRANT_GRPC_PORT>)
|
||||
.withCollectionName("<QDRANT_COLLECTION_NAME>")
|
||||
.withApiKey("<QDRANT_API_KEY>")
|
||||
.build();
|
||||
}
|
||||
----
|
||||
|
||||
Integrate with OpenAI's embeddings by adding the Spring Boot OpenAI starter to your project.
|
||||
This provides you with an implementation of the Embeddings client:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@Bean
|
||||
public VectorStore vectorStore(QdrantVectorStoreConfig config, EmbeddingClient embeddingClient) {
|
||||
return new QdrantVectorStore(config, embeddingClient);
|
||||
}
|
||||
----
|
||||
|
||||
=== 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 Qdrant vector store.
|
||||
|
||||
For example, you can use either the text expression language:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
vectorStore.similaritySearch(
|
||||
SearchRequest.defaults()
|
||||
.withQuery("The World")
|
||||
.withTopK(TOP_K)
|
||||
.withSimilarityThreshold(SIMILARITY_THRESHOLD)
|
||||
.withFilterExpression("author in ['john', 'jill'] && article_type == 'blog'"));
|
||||
----
|
||||
|
||||
or programmatically using the `Filter.Expression` DSL:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
FilterExpressionBuilder b = new FilterExpressionBuilder();
|
||||
|
||||
vectorStore.similaritySearch(SearchRequest.defaults()
|
||||
.withQuery("The World")
|
||||
.withTopK(TOP_K)
|
||||
.withSimilarityThreshold(SIMILARITY_THRESHOLD)
|
||||
.withFilterExpression(b.and(
|
||||
b.in("john", "jill"),
|
||||
b.eq("article_type", "blog")).build()));
|
||||
----
|
||||
|
||||
NOTE: These filter expressions are converted into the equivalent Qdrant link:https://qdrant.tech/documentation/concepts/filtering/[filters].
|
||||
|
||||
|
||||
[[qdrant-vectorstore-properties]]
|
||||
== Qdrant VectorStore properties
|
||||
|
||||
You can use the following properties in your Spring Boot configuration to customize the Qdrant vector store.
|
||||
|
||||
|===
|
||||
|Property| Description | Default value
|
||||
|
||||
|`spring.ai.vectorstore.qdrant.host`| The host of the Qdrant server. | localhost
|
||||
|`spring.ai.vectorstore.qdrant.port`| The port of the Qdrant server. | 6334
|
||||
|`spring.ai.vectorstore.qdrant.api-key`| The API key to use for authentication with the Qdrant server. | -
|
||||
|`spring.ai.vectorstore.qdrant.collection-name`| The name of the collection to use in Qdrant. | -
|
||||
|`spring.ai.vectorstore.qdrant.use-tls`| Whether to use TLS(HTTPS). Defaults to false. | false
|
||||
|===
|
||||
@@ -164,6 +164,7 @@ Each of the following sections in the documentation shows which dependencies you
|
||||
** xref:api/vectordbs/neo4j.adoc[Neo4jVectorStore] - 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.
|
||||
** 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 (in-memory) implementation of persistent vector storage, good for educational purposes.
|
||||
|
||||
@@ -15,7 +15,7 @@ Spring AI provides the following features:
|
||||
* Supported Model types are Chat and Text to Image with more on the way.
|
||||
* Portable API across AI providers for Chat and for Embedding models. Both synchronous and stream API options are supported. Dropping down to access model specific features is also supported.
|
||||
* Mapping of AI Model output to POJOs.
|
||||
* Support for all major Vector Database providers such as Azure Vector Search, Chroma, Milvus, Neo4j, PostgreSQL/PGVector, PineCone, Redis, and Weaviate
|
||||
* Support for all major Vector Database providers such as Azure Vector Search, Chroma, Milvus, Neo4j, PostgreSQL/PGVector, PineCone, Qdrant, Redis, and Weaviate
|
||||
* Portable API across Vector Store providers, including a novel SQL-like metadata filter API that is also portable.
|
||||
* Function calling
|
||||
* Spring Boot Auto Configuration and Starters for AI Models and Vector Stores.
|
||||
|
||||
@@ -215,6 +215,14 @@
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<!-- Qdrant Vector Store-->
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-qdrant</artifactId>
|
||||
<version>${project.parent.version}</version>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<!-- test dependencies -->
|
||||
|
||||
<dependency>
|
||||
@@ -287,6 +295,14 @@
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.testcontainers</groupId>
|
||||
<artifactId>qdrant</artifactId>
|
||||
<version>1.19.6</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* 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.qdrant;
|
||||
|
||||
import org.springframework.ai.embedding.EmbeddingClient;
|
||||
import org.springframework.ai.vectorstore.VectorStore;
|
||||
import org.springframework.ai.vectorstore.qdrant.QdrantVectorStore;
|
||||
import org.springframework.ai.vectorstore.qdrant.QdrantVectorStore.QdrantVectorStoreConfig;
|
||||
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 Anush Shetty
|
||||
* @since 0.8.1
|
||||
*/
|
||||
@AutoConfiguration
|
||||
@ConditionalOnClass({ QdrantVectorStore.class, EmbeddingClient.class })
|
||||
@EnableConfigurationProperties(QdrantVectorStoreProperties.class)
|
||||
public class QdrantVectorStoreAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public VectorStore vectorStore(EmbeddingClient embeddingClient, QdrantVectorStoreProperties properties) {
|
||||
|
||||
var config = QdrantVectorStoreConfig.builder()
|
||||
.withCollectionName(properties.getCollectionName())
|
||||
.withHost(properties.getHost())
|
||||
.withPort(properties.getPort())
|
||||
.withTls(properties.useTls())
|
||||
.withApiKey(properties.getApiKey())
|
||||
.build();
|
||||
|
||||
return new QdrantVectorStore(config, embeddingClient);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* 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.qdrant;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
/**
|
||||
* @author Anush Shetty
|
||||
* @since 0.8.1
|
||||
*/
|
||||
@ConfigurationProperties(QdrantVectorStoreProperties.CONFIG_PREFIX)
|
||||
public class QdrantVectorStoreProperties {
|
||||
|
||||
public static final String CONFIG_PREFIX = "spring.ai.vectorstore.qdrant";
|
||||
|
||||
/**
|
||||
* The name of the collection to use in Qdrant.
|
||||
*/
|
||||
private String collectionName;
|
||||
|
||||
/**
|
||||
* The host of the Qdrant server.
|
||||
*/
|
||||
private String host = "localhost";
|
||||
|
||||
/**
|
||||
* The port of the Qdrant server.
|
||||
*/
|
||||
private int port = 6334;
|
||||
|
||||
/**
|
||||
* Whether to use TLS(HTTPS). Defaults to false.
|
||||
*/
|
||||
private boolean useTls = false;
|
||||
|
||||
/**
|
||||
* The API key to use for authentication with the Qdrant server.
|
||||
*/
|
||||
private String apiKey = null;
|
||||
|
||||
public String getCollectionName() {
|
||||
return this.collectionName;
|
||||
}
|
||||
|
||||
public void setCollectionName(String collectionName) {
|
||||
this.collectionName = collectionName;
|
||||
}
|
||||
|
||||
public String getHost() {
|
||||
return this.host;
|
||||
}
|
||||
|
||||
public void setHost(String host) {
|
||||
this.host = host;
|
||||
}
|
||||
|
||||
public int getPort() {
|
||||
return this.port;
|
||||
}
|
||||
|
||||
public void setPort(int port) {
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
public boolean useTls() {
|
||||
return this.useTls;
|
||||
}
|
||||
|
||||
public void setUseTls(boolean useTls) {
|
||||
this.useTls = useTls;
|
||||
}
|
||||
|
||||
public String getApiKey() {
|
||||
return this.apiKey;
|
||||
}
|
||||
|
||||
public void setApiKey(String apiKey) {
|
||||
this.apiKey = apiKey;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -21,4 +21,4 @@ org.springframework.ai.autoconfigure.vectorstore.chroma.ChromaVectorStoreAutoCon
|
||||
org.springframework.ai.autoconfigure.vectorstore.azure.AzureVectorStoreAutoConfiguration
|
||||
org.springframework.ai.autoconfigure.vectorstore.weaviate.WeaviateVectorStoreAutoConfiguration
|
||||
org.springframework.ai.autoconfigure.vectorstore.neo4j.Neo4jVectorStoreAutoConfiguration
|
||||
|
||||
org.springframework.ai.autoconfigure.vectorstore.qdrant.QdrantVectorStoreAutoConfiguration
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
/*
|
||||
* 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.qdrant;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
import io.qdrant.client.QdrantClient;
|
||||
import io.qdrant.client.QdrantGrpcClient;
|
||||
import io.qdrant.client.grpc.Collections.Distance;
|
||||
import io.qdrant.client.grpc.Collections.VectorParams;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.testcontainers.junit.jupiter.Container;
|
||||
import org.testcontainers.junit.jupiter.Testcontainers;
|
||||
import org.testcontainers.qdrant.QdrantContainer;
|
||||
|
||||
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 org.springframework.core.io.DefaultResourceLoader;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* @author Christian Tzolov
|
||||
* @since 0.8.1
|
||||
*/
|
||||
@Testcontainers
|
||||
public class QdrantVectorStoreAutoConfigurationIT {
|
||||
|
||||
private static final String COLLECTION_NAME = "test_collection";
|
||||
|
||||
private static final int EMBEDDING_DIMENSION = 384;
|
||||
|
||||
private static final int QDRANT_GRPC_PORT = 6334;
|
||||
|
||||
@Container
|
||||
static QdrantContainer qdrantContainer = new QdrantContainer("qdrant/qdrant:v1.7.4");
|
||||
|
||||
List<Document> documents = List.of(
|
||||
new Document(getText("classpath:/test/data/spring.ai.txt"), Map.of("spring", "great")),
|
||||
new Document(getText("classpath:/test/data/time.shelter.txt")),
|
||||
new Document(getText("classpath:/test/data/great.depression.txt"), Map.of("depression", "bad")));
|
||||
|
||||
@BeforeAll
|
||||
static void setup() throws InterruptedException, ExecutionException {
|
||||
|
||||
String host = qdrantContainer.getHost();
|
||||
int port = qdrantContainer.getMappedPort(QDRANT_GRPC_PORT);
|
||||
QdrantClient client = new QdrantClient(QdrantGrpcClient.newBuilder(host, port, false).build());
|
||||
|
||||
client
|
||||
.createCollectionAsync(COLLECTION_NAME,
|
||||
VectorParams.newBuilder().setDistance(Distance.Cosine).setSize(EMBEDDING_DIMENSION).build())
|
||||
.get();
|
||||
|
||||
client.close();
|
||||
}
|
||||
|
||||
public static String getText(String uri) {
|
||||
var resource = new DefaultResourceLoader().getResource(uri);
|
||||
try {
|
||||
return resource.getContentAsString(StandardCharsets.UTF_8);
|
||||
}
|
||||
catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
|
||||
.withConfiguration(AutoConfigurations.of(QdrantVectorStoreAutoConfiguration.class))
|
||||
.withUserConfiguration(Config.class)
|
||||
.withPropertyValues("spring.ai.vectorstore.qdrant.port=" + qdrantContainer.getMappedPort(QDRANT_GRPC_PORT),
|
||||
"spring.ai.vectorstore.qdrant.host=" + qdrantContainer.getHost(),
|
||||
"spring.ai.vectorstore.qdrant.collectionName=" + COLLECTION_NAME);
|
||||
|
||||
@Test
|
||||
public void addAndSearch() {
|
||||
contextRunner.run(context -> {
|
||||
|
||||
VectorStore vectorStore = context.getBean(VectorStore.class);
|
||||
|
||||
vectorStore.add(documents);
|
||||
|
||||
List<Document> results = vectorStore
|
||||
.similaritySearch(SearchRequest.query("What is Great Depression?").withTopK(1));
|
||||
|
||||
assertThat(results).hasSize(1);
|
||||
Document resultDoc = results.get(0);
|
||||
assertThat(resultDoc.getId()).isEqualTo(documents.get(2).getId());
|
||||
assertThat(resultDoc.getMetadata()).containsKeys("depression", "distance");
|
||||
|
||||
// Remove all documents from the store
|
||||
vectorStore.delete(documents.stream().map(doc -> doc.getId()).toList());
|
||||
results = vectorStore.similaritySearch(SearchRequest.query("Great Depression").withTopK(1));
|
||||
assertThat(results).hasSize(0);
|
||||
});
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class Config {
|
||||
|
||||
@Bean
|
||||
public EmbeddingClient embeddingClient() {
|
||||
return new TransformersEmbeddingClient();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai</artifactId>
|
||||
<version>0.8.1-SNAPSHOT</version>
|
||||
<relativePath>../../pom.xml</relativePath>
|
||||
</parent>
|
||||
<artifactId>spring-ai-qdrant-store-spring-boot-starter</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
<name>Spring AI Starter - Qdrant Vector Store</name>
|
||||
<description>Spring AI Qdrant Vector Store Auto Configuration</description>
|
||||
<url>https://github.com/spring-projects/spring-ai</url>
|
||||
|
||||
<scm>
|
||||
<url>https://github.com/spring-projects/spring-ai</url>
|
||||
<connection>git://github.com/spring-projects/spring-ai.git</connection>
|
||||
<developerConnection>git@github.com:spring-projects/spring-ai.git</developerConnection>
|
||||
</scm>
|
||||
|
||||
<dependencies>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-spring-boot-autoconfigure</artifactId>
|
||||
<version>${project.parent.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-qdrant</artifactId>
|
||||
<version>${project.parent.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
1
vector-stores/spring-ai-qdrant/README.md
Normal file
1
vector-stores/spring-ai-qdrant/README.md
Normal file
@@ -0,0 +1 @@
|
||||
Qdrant Vector Store
|
||||
87
vector-stores/spring-ai-qdrant/pom.xml
Normal file
87
vector-stores/spring-ai-qdrant/pom.xml
Normal file
@@ -0,0 +1,87 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai</artifactId>
|
||||
<version>0.8.1-SNAPSHOT</version>
|
||||
<relativePath>../../pom.xml</relativePath>
|
||||
</parent>
|
||||
<artifactId>spring-ai-qdrant</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
<name>spring-ai-qdrant</name>
|
||||
<description>spring-ai-qdrant</description>
|
||||
<url>https://github.com/spring-projects/spring-ai</url>
|
||||
|
||||
<scm>
|
||||
<url>https://github.com/spring-projects/spring-ai</url>
|
||||
<connection>git://github.com/spring-projects/spring-ai.git</connection>
|
||||
<developerConnection>git@github.com:spring-projects/spring-ai.git</developerConnection>
|
||||
</scm>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.target>17</maven.compiler.target>
|
||||
<maven.compiler.source>17</maven.compiler.source>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-core</artifactId>
|
||||
<version>${project.parent.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-web</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- <dependency>
|
||||
<groupId>io.grpc</groupId>
|
||||
<artifactId>grpc-protobuf</artifactId>
|
||||
<version>1.59.0</version>
|
||||
</dependency> -->
|
||||
|
||||
<dependency>
|
||||
<groupId>io.qdrant</groupId>
|
||||
<artifactId>client</artifactId>
|
||||
<version>${qdrant.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.google.protobuf</groupId>
|
||||
<artifactId>protobuf-java-util</artifactId>
|
||||
<version>${protobuf-java.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- TESTING -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-openai</artifactId>
|
||||
<version>${project.parent.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.testcontainers</groupId>
|
||||
<artifactId>qdrant</artifactId>
|
||||
<version>1.19.6</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.testcontainers</groupId>
|
||||
<artifactId>junit-jupiter</artifactId>
|
||||
<version>${testcontainers.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,261 @@
|
||||
/*
|
||||
* 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.qdrant;
|
||||
|
||||
import static io.qdrant.client.ConditionFactory.filter;
|
||||
import static io.qdrant.client.ConditionFactory.match;
|
||||
import static io.qdrant.client.ConditionFactory.matchExceptKeywords;
|
||||
import static io.qdrant.client.ConditionFactory.matchExceptValues;
|
||||
import static io.qdrant.client.ConditionFactory.matchKeyword;
|
||||
import static io.qdrant.client.ConditionFactory.matchKeywords;
|
||||
import static io.qdrant.client.ConditionFactory.matchValues;
|
||||
import static io.qdrant.client.ConditionFactory.range;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
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.Operand;
|
||||
import org.springframework.ai.vectorstore.filter.Filter.Value;
|
||||
|
||||
import io.qdrant.client.grpc.Points.Condition;
|
||||
import io.qdrant.client.grpc.Points.Filter;
|
||||
import io.qdrant.client.grpc.Points.Range;
|
||||
|
||||
/**
|
||||
* @author Anush Shetty
|
||||
* @since 0.8.1
|
||||
*/
|
||||
class QdrantFilterExpressionConverter {
|
||||
|
||||
public Filter convertExpression(Expression expression) {
|
||||
return this.convertOperand(expression);
|
||||
}
|
||||
|
||||
protected Filter convertOperand(Operand operand) {
|
||||
var context = Filter.newBuilder();
|
||||
List<Condition> mustClauses = new ArrayList<Condition>();
|
||||
List<Condition> shouldClauses = new ArrayList<Condition>();
|
||||
List<Condition> mustNotClauses = new ArrayList<Condition>();
|
||||
|
||||
if (operand instanceof Expression expression) {
|
||||
if (expression.type() == ExpressionType.NOT && expression.left() instanceof Group group) {
|
||||
mustNotClauses.add(filter(convertOperand(group.content())));
|
||||
}
|
||||
else if (expression.type() == ExpressionType.AND) {
|
||||
mustClauses.add(filter(convertOperand(expression.left())));
|
||||
mustClauses.add(filter(convertOperand(expression.right())));
|
||||
}
|
||||
else if (expression.type() == ExpressionType.OR) {
|
||||
shouldClauses.add(filter(convertOperand(expression.left())));
|
||||
shouldClauses.add(filter(convertOperand(expression.right())));
|
||||
}
|
||||
else {
|
||||
if (!(expression.right() instanceof Value)) {
|
||||
throw new RuntimeException("Non AND/OR/NOT expression must have Value right argument!");
|
||||
}
|
||||
mustClauses.add(parseComparison((Key) expression.left(), (Value) expression.right(), expression));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return context.addAllMust(mustClauses).addAllShould(shouldClauses).addAllMustNot(mustNotClauses).build();
|
||||
}
|
||||
|
||||
protected Condition parseComparison(Key key, Value value, Expression exp) {
|
||||
|
||||
ExpressionType type = exp.type();
|
||||
switch (type) {
|
||||
case EQ: {
|
||||
return buildEqCondition(key, value);
|
||||
}
|
||||
case NE: {
|
||||
return buildNeCondition(key, value);
|
||||
}
|
||||
case GT: {
|
||||
return buildGtCondition(key, value);
|
||||
}
|
||||
case GTE: {
|
||||
return buildGteCondition(key, value);
|
||||
}
|
||||
case LT: {
|
||||
return buildLtCondition(key, value);
|
||||
}
|
||||
case LTE: {
|
||||
return buildLteCondition(key, value);
|
||||
}
|
||||
case IN: {
|
||||
return buildInCondition(key, value);
|
||||
}
|
||||
case NIN: {
|
||||
return buildNInCondition(key, value);
|
||||
}
|
||||
default: {
|
||||
throw new RuntimeException("Unsupported expression type: " + type);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected Condition buildEqCondition(Key key, Value value) {
|
||||
String identifier = doKey(key);
|
||||
if (value.value() instanceof String valueStr) {
|
||||
return matchKeyword(identifier, valueStr);
|
||||
}
|
||||
else if (value.value() instanceof Number valueNum) {
|
||||
long lValue = Long.parseLong(valueNum.toString());
|
||||
return match(identifier, lValue);
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException("Invalid value type for EQ. Can either be a string or Number");
|
||||
|
||||
}
|
||||
|
||||
protected Condition buildNeCondition(Key key, Value value) {
|
||||
String identifier = doKey(key);
|
||||
if (value.value() instanceof String valueStr) {
|
||||
return filter(Filter.newBuilder().addMustNot(matchKeyword(identifier, valueStr)).build());
|
||||
}
|
||||
else if (value.value() instanceof Number valueNum) {
|
||||
long lValue = Long.parseLong(valueNum.toString());
|
||||
Condition condition = match(identifier, lValue);
|
||||
return filter(Filter.newBuilder().addMustNot(condition).build());
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException("Invalid value type for NEQ. Can either be a string or Number");
|
||||
|
||||
}
|
||||
|
||||
protected Condition buildGtCondition(Key key, Value value) {
|
||||
String identifier = doKey(key);
|
||||
if (value.value() instanceof Number valueNum) {
|
||||
Double dvalue = Double.parseDouble(valueNum.toString());
|
||||
return range(identifier, Range.newBuilder().setGt(dvalue).build());
|
||||
}
|
||||
throw new RuntimeException("Unsupported value type for GT condition. Only supports Number");
|
||||
|
||||
}
|
||||
|
||||
protected Condition buildLtCondition(Key key, Value value) {
|
||||
String identifier = doKey(key);
|
||||
if (value.value() instanceof Number valueNum) {
|
||||
Double dvalue = Double.parseDouble(valueNum.toString());
|
||||
return range(identifier, Range.newBuilder().setLt(dvalue).build());
|
||||
}
|
||||
throw new RuntimeException("Unsupported value type for LT condition. Only supports Number");
|
||||
|
||||
}
|
||||
|
||||
protected Condition buildGteCondition(Key key, Value value) {
|
||||
String identifier = doKey(key);
|
||||
if (value.value() instanceof Number valueNum) {
|
||||
Double dvalue = Double.parseDouble(valueNum.toString());
|
||||
return range(identifier, Range.newBuilder().setGte(dvalue).build());
|
||||
}
|
||||
throw new RuntimeException("Unsupported value type for GTE condition. Only supports Number");
|
||||
|
||||
}
|
||||
|
||||
protected Condition buildLteCondition(Key key, Value value) {
|
||||
String identifier = doKey(key);
|
||||
if (value.value() instanceof Number valueNum) {
|
||||
Double dvalue = Double.parseDouble(valueNum.toString());
|
||||
return range(identifier, Range.newBuilder().setLte(dvalue).build());
|
||||
}
|
||||
throw new RuntimeException("Unsupported value type for LTE condition. Only supports Number");
|
||||
|
||||
}
|
||||
|
||||
protected Condition buildInCondition(Key key, Value value) {
|
||||
if (value.value() instanceof List valueList && !valueList.isEmpty()) {
|
||||
Object firstValue = valueList.get(0);
|
||||
String identifier = doKey(key);
|
||||
|
||||
if (firstValue instanceof String) {
|
||||
// If the first value is a string, then all values should be strings
|
||||
List<String> stringValues = new ArrayList<String>();
|
||||
for (Object valueObj : valueList) {
|
||||
stringValues.add(valueObj.toString());
|
||||
}
|
||||
return matchKeywords(identifier, stringValues);
|
||||
}
|
||||
else if (firstValue instanceof Number) {
|
||||
// If the first value is a number, then all values should be numbers
|
||||
List<Long> longValues = new ArrayList<Long>();
|
||||
for (Object valueObj : valueList) {
|
||||
Long longValue = Long.parseLong(valueObj.toString());
|
||||
longValues.add(longValue);
|
||||
}
|
||||
return matchValues(identifier, longValues);
|
||||
}
|
||||
else {
|
||||
throw new RuntimeException("Unsupported value in IN value list. Only supports String or Number");
|
||||
}
|
||||
}
|
||||
throw new RuntimeException(
|
||||
"Unsupported value type for IN condition. Only supports non-empty List of String or Number");
|
||||
|
||||
}
|
||||
|
||||
protected Condition buildNInCondition(Key key, Value value) {
|
||||
if (value.value() instanceof List valueList && !valueList.isEmpty()) {
|
||||
Object firstValue = valueList.get(0);
|
||||
String identifier = doKey(key);
|
||||
|
||||
if (firstValue instanceof String) {
|
||||
// If the first value is a string, then all values should be strings
|
||||
List<String> stringValues = new ArrayList<String>();
|
||||
for (Object valueObj : valueList) {
|
||||
stringValues.add(valueObj.toString());
|
||||
}
|
||||
return matchExceptKeywords(identifier, stringValues);
|
||||
}
|
||||
else if (firstValue instanceof Number) {
|
||||
// If the first value is a number, then all values should be numbers
|
||||
List<Long> longValues = new ArrayList<Long>();
|
||||
for (Object valueObj : valueList) {
|
||||
Long longValue = Long.parseLong(valueObj.toString());
|
||||
longValues.add(longValue);
|
||||
}
|
||||
return matchExceptValues(identifier, longValues);
|
||||
}
|
||||
else {
|
||||
throw new RuntimeException("Unsupported value in NIN value list. Only supports String or Number");
|
||||
}
|
||||
}
|
||||
throw new RuntimeException(
|
||||
"Unsupported value type for NIN condition. Only supports non-empty List of String or Number");
|
||||
|
||||
}
|
||||
|
||||
protected String doKey(Key key) {
|
||||
var identifier = (hasOuterQuotes(key.key())) ? removeOuterQuotes(key.key()) : key.key();
|
||||
return identifier;
|
||||
}
|
||||
|
||||
protected boolean hasOuterQuotes(String str) {
|
||||
str = str.trim();
|
||||
return (str.startsWith("\"") && str.endsWith("\"")) || (str.startsWith("'") && str.endsWith("'"));
|
||||
}
|
||||
|
||||
protected String removeOuterQuotes(String in) {
|
||||
return in.substring(1, in.length() - 1);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* 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.qdrant;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import io.qdrant.client.grpc.JsonWithInt.ListValue;
|
||||
import io.qdrant.client.grpc.JsonWithInt.Value;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Utility methods for building Java objects from io.qdrant.client.grpc.JsonWithInt.Value.
|
||||
*
|
||||
* @author Anush Shetty
|
||||
* @since 0.8.1
|
||||
*/
|
||||
class QdrantObjectFactory {
|
||||
|
||||
private static final Log logger = LogFactory.getLog(QdrantObjectFactory.class);
|
||||
|
||||
private QdrantObjectFactory() {
|
||||
}
|
||||
|
||||
public static Map<String, Object> toObjectMap(Map<String, Value> payload) {
|
||||
Assert.notNull(payload, "Payload map must not be null");
|
||||
return payload.entrySet().stream().collect(Collectors.toMap(e -> e.getKey(), e -> object(e.getValue())));
|
||||
}
|
||||
|
||||
private static Object object(ListValue listValue) {
|
||||
return listValue.getValuesList().stream().map(QdrantObjectFactory::object).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private static Object object(Value value) {
|
||||
|
||||
switch (value.getKindCase()) {
|
||||
case INTEGER_VALUE:
|
||||
return value.getIntegerValue();
|
||||
case STRING_VALUE:
|
||||
return value.getStringValue();
|
||||
case DOUBLE_VALUE:
|
||||
return value.getDoubleValue();
|
||||
case BOOL_VALUE:
|
||||
return value.hasBoolValue();
|
||||
case LIST_VALUE:
|
||||
return object(value.getListValue());
|
||||
case STRUCT_VALUE:
|
||||
return toObjectMap(value.getStructValue().getFieldsMap());
|
||||
case KIND_NOT_SET:
|
||||
case NULL_VALUE:
|
||||
default:
|
||||
logger.warn("Unsupported value type: " + value.getKindCase());
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* 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.qdrant;
|
||||
|
||||
import java.lang.reflect.Array;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import io.qdrant.client.ValueFactory;
|
||||
import io.qdrant.client.grpc.JsonWithInt.Struct;
|
||||
import io.qdrant.client.grpc.JsonWithInt.Value;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Utility methods for building io.qdrant.client.grpc.JsonWithInt.Value from Java objects.
|
||||
*
|
||||
* @author Anush Shetty
|
||||
* @since 0.8.1
|
||||
*/
|
||||
class QdrantValueFactory {
|
||||
|
||||
private QdrantValueFactory() {
|
||||
}
|
||||
|
||||
public static Map<String, Value> toValueMap(Map<String, Object> inputMap) {
|
||||
Assert.notNull(inputMap, "Input map must not be null");
|
||||
|
||||
return inputMap.entrySet().stream().collect(Collectors.toMap(e -> e.getKey(), e -> value(e.getValue())));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static Value value(Object value) {
|
||||
|
||||
if (value == null) {
|
||||
return ValueFactory.nullValue();
|
||||
}
|
||||
|
||||
if (value.getClass().isArray()) {
|
||||
int length = Array.getLength(value);
|
||||
Object[] objectArray = new Object[length];
|
||||
for (int i = 0; i < length; i++) {
|
||||
objectArray[i] = Array.get(value, i);
|
||||
}
|
||||
return value(objectArray);
|
||||
}
|
||||
|
||||
if (value instanceof Map) {
|
||||
return value((Map<String, Object>) value);
|
||||
}
|
||||
|
||||
switch (value.getClass().getSimpleName()) {
|
||||
case "String":
|
||||
return ValueFactory.value((String) value);
|
||||
case "Integer":
|
||||
return ValueFactory.value((Integer) value);
|
||||
case "Double":
|
||||
return ValueFactory.value((Double) value);
|
||||
case "Float":
|
||||
return ValueFactory.value((Float) value);
|
||||
case "Boolean":
|
||||
return ValueFactory.value((Boolean) value);
|
||||
default:
|
||||
throw new IllegalArgumentException("Unsupported Qdrant value type: " + value.getClass());
|
||||
}
|
||||
}
|
||||
|
||||
private static Value value(Object[] elements) {
|
||||
List<Value> values = new ArrayList<Value>(elements.length);
|
||||
|
||||
for (Object element : elements) {
|
||||
values.add(value(element));
|
||||
}
|
||||
|
||||
return ValueFactory.list(values);
|
||||
}
|
||||
|
||||
private static Value value(Map<String, Object> inputMap) {
|
||||
Struct.Builder structBuilder = Struct.newBuilder();
|
||||
Map<String, Value> map = toValueMap(inputMap);
|
||||
structBuilder.putAllFields(map);
|
||||
return Value.newBuilder().setStructValue(structBuilder).build();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,329 @@
|
||||
/*
|
||||
* 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.qdrant;
|
||||
|
||||
import static io.qdrant.client.PointIdFactory.id;
|
||||
import static io.qdrant.client.ValueFactory.value;
|
||||
import static io.qdrant.client.VectorsFactory.vectors;
|
||||
import static io.qdrant.client.WithPayloadSelectorFactory.enable;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
import org.springframework.ai.document.Document;
|
||||
import org.springframework.ai.embedding.EmbeddingClient;
|
||||
import org.springframework.ai.vectorstore.SearchRequest;
|
||||
import org.springframework.ai.vectorstore.VectorStore;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import io.qdrant.client.QdrantClient;
|
||||
import io.qdrant.client.QdrantGrpcClient;
|
||||
import io.qdrant.client.grpc.JsonWithInt.Value;
|
||||
import io.qdrant.client.grpc.Points.Filter;
|
||||
import io.qdrant.client.grpc.Points.PointId;
|
||||
import io.qdrant.client.grpc.Points.PointStruct;
|
||||
import io.qdrant.client.grpc.Points.ScoredPoint;
|
||||
import io.qdrant.client.grpc.Points.SearchPoints;
|
||||
import io.qdrant.client.grpc.Points.UpdateStatus;
|
||||
|
||||
/**
|
||||
* Qdrant vectorStore implementation. This store supports creating, updating, deleting,
|
||||
* and similarity searching of documents in a Qdrant collection.
|
||||
*
|
||||
* @author Anush Shetty
|
||||
* @since 0.8.1
|
||||
*/
|
||||
public class QdrantVectorStore implements VectorStore {
|
||||
|
||||
private static final String CONTENT_FIELD_NAME = "doc_content";
|
||||
|
||||
private static final String DISTANCE_FIELD_NAME = "distance";
|
||||
|
||||
private final EmbeddingClient embeddingClient;
|
||||
|
||||
private final QdrantClient qdrantClient;
|
||||
|
||||
private final String collectionName;
|
||||
|
||||
private final QdrantFilterExpressionConverter filterExpressionConverter = new QdrantFilterExpressionConverter();
|
||||
|
||||
/**
|
||||
* Configuration class for the QdrantVectorStore.
|
||||
*/
|
||||
public static final class QdrantVectorStoreConfig {
|
||||
|
||||
private final String collectionName;
|
||||
|
||||
private QdrantClient qdrantClient;
|
||||
|
||||
/*
|
||||
* Constructor using the builder.
|
||||
*
|
||||
* @param builder The configuration builder.
|
||||
*/
|
||||
private QdrantVectorStoreConfig(Builder builder) {
|
||||
this.collectionName = builder.collectionName;
|
||||
|
||||
QdrantGrpcClient.Builder grpcClientBuilder = QdrantGrpcClient.newBuilder(builder.host, builder.port,
|
||||
builder.useTls);
|
||||
|
||||
if (builder.apiKey != null) {
|
||||
grpcClientBuilder.withApiKey(builder.apiKey);
|
||||
}
|
||||
|
||||
this.qdrantClient = new QdrantClient(grpcClientBuilder.build());
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 QdrantVectorStoreConfig defaultConfig() {
|
||||
return builder().build();
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
|
||||
private String collectionName;
|
||||
|
||||
private String host = "localhost";
|
||||
|
||||
private int port = 6334;
|
||||
|
||||
private boolean useTls = false;
|
||||
|
||||
private String apiKey = null;
|
||||
|
||||
private Builder() {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param host The host of the Qdrant instance. Defaults to "localhost".
|
||||
*/
|
||||
public Builder withHost(String host) {
|
||||
Assert.notNull(host, "host cannot be null");
|
||||
this.host = host;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param collectionName REQUIRED. The name of the collection.
|
||||
*/
|
||||
public Builder withCollectionName(String collectionName) {
|
||||
this.collectionName = collectionName;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param port The GRPC port of the Qdrant instance. Defaults to 6334.
|
||||
* @return
|
||||
*/
|
||||
public Builder withPort(int port) {
|
||||
this.port = port;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param useTls Whether to use TLS(HTTPS). Defaults to false.
|
||||
* @return
|
||||
*/
|
||||
public Builder withTls(boolean useTls) {
|
||||
this.useTls = useTls;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param apiKey The Qdrant API key to authenticate with. Defaults to null.
|
||||
*/
|
||||
public Builder withApiKey(String apiKey) {
|
||||
this.apiKey = apiKey;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@return the immutable configuration}
|
||||
*/
|
||||
public QdrantVectorStoreConfig build() {
|
||||
Assert.notNull(collectionName, "collectionName cannot be null");
|
||||
return new QdrantVectorStoreConfig(this);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new QdrantVectorStore.
|
||||
* @param config The configuration for the store.
|
||||
* @param embeddingClient The client for embedding operations.
|
||||
*/
|
||||
public QdrantVectorStore(QdrantVectorStoreConfig config, EmbeddingClient embeddingClient) {
|
||||
this(config.qdrantClient, config.collectionName, embeddingClient);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new QdrantVectorStore.
|
||||
* @param qdrantClient A {@link QdrantClient} instance for interfacing with Qdrant.
|
||||
* @param collectionName The name of the collection to use in Qdrant.
|
||||
* @param embeddingClient The client for embedding operations.
|
||||
*/
|
||||
public QdrantVectorStore(QdrantClient qdrantClient, String collectionName, EmbeddingClient embeddingClient) {
|
||||
Assert.notNull(qdrantClient, "QdrantClient must not be null");
|
||||
Assert.notNull(collectionName, "collectionName must not be null");
|
||||
Assert.notNull(embeddingClient, "EmbeddingClient must not be null");
|
||||
|
||||
this.embeddingClient = embeddingClient;
|
||||
this.collectionName = collectionName;
|
||||
this.qdrantClient = qdrantClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a list of documents to the vector store.
|
||||
* @param documents The list of documents to be added.
|
||||
*/
|
||||
@Override
|
||||
public void add(List<Document> documents) {
|
||||
try {
|
||||
List<PointStruct> points = documents.stream().map(document -> {
|
||||
// Compute and assign an embedding to the document.
|
||||
document.setEmbedding(this.embeddingClient.embed(document));
|
||||
|
||||
return PointStruct.newBuilder()
|
||||
.setId(id(UUID.fromString(document.getId())))
|
||||
.setVectors(vectors(toFloatList(document.getEmbedding())))
|
||||
.putAllPayload(toPayload(document))
|
||||
.build();
|
||||
}).toList();
|
||||
|
||||
this.qdrantClient.upsertAsync(this.collectionName, points).get();
|
||||
}
|
||||
catch (InterruptedException | ExecutionException | IllegalArgumentException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a list of documents by their IDs.
|
||||
* @param documentIds The list of document IDs to be deleted.
|
||||
* @return An optional boolean indicating the deletion status.
|
||||
*/
|
||||
@Override
|
||||
public Optional<Boolean> delete(List<String> documentIds) {
|
||||
try {
|
||||
List<PointId> ids = documentIds.stream().map(id -> id(UUID.fromString(id))).toList();
|
||||
var result = this.qdrantClient.deleteAsync(this.collectionName, ids)
|
||||
.get()
|
||||
.getStatus() == UpdateStatus.Completed;
|
||||
return Optional.of(result);
|
||||
}
|
||||
catch (InterruptedException | ExecutionException | IllegalArgumentException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a similarity search on the vector store.
|
||||
* @param request The {@link SearchRequest} object containing the query and other
|
||||
* search parameters.
|
||||
* @return A list of documents that are similar to the query.
|
||||
*/
|
||||
@Override
|
||||
public List<Document> similaritySearch(SearchRequest request) {
|
||||
try {
|
||||
Filter filter = (request.getFilterExpression() != null)
|
||||
? this.filterExpressionConverter.convertExpression(request.getFilterExpression())
|
||||
: Filter.getDefaultInstance();
|
||||
|
||||
List<Double> queryEmbedding = this.embeddingClient.embed(request.getQuery());
|
||||
|
||||
var searchPoints = SearchPoints.newBuilder()
|
||||
.setCollectionName(this.collectionName)
|
||||
.setLimit(request.getTopK())
|
||||
.setWithPayload(enable(true))
|
||||
.addAllVector(toFloatList(queryEmbedding))
|
||||
.setFilter(filter)
|
||||
.setScoreThreshold((float) request.getSimilarityThreshold())
|
||||
.build();
|
||||
|
||||
var queryResponse = this.qdrantClient.searchAsync(searchPoints).get();
|
||||
|
||||
return queryResponse.stream().map(scoredPoint -> {
|
||||
return toDocument(scoredPoint);
|
||||
}).toList();
|
||||
|
||||
}
|
||||
catch (InterruptedException | ExecutionException | IllegalArgumentException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts metadata from a Protobuf Struct.
|
||||
* @param metadataStruct The Protobuf Struct containing metadata.
|
||||
* @return The metadata as a map.
|
||||
*/
|
||||
private Document toDocument(ScoredPoint point) {
|
||||
try {
|
||||
var id = point.getId().getUuid();
|
||||
|
||||
var payload = QdrantObjectFactory.toObjectMap(point.getPayloadMap());
|
||||
payload.put(DISTANCE_FIELD_NAME, 1 - point.getScore());
|
||||
|
||||
var content = (String) payload.remove(CONTENT_FIELD_NAME);
|
||||
|
||||
return new Document(id, content, payload);
|
||||
}
|
||||
catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the document metadata to a Protobuf Struct.
|
||||
* @param document The document containing metadata.
|
||||
* @return The metadata as a Protobuf Struct.
|
||||
*/
|
||||
private Map<String, Value> toPayload(Document document) {
|
||||
try {
|
||||
var payload = QdrantValueFactory.toValueMap(document.getMetadata());
|
||||
payload.put(CONTENT_FIELD_NAME, value(document.getContent()));
|
||||
return payload;
|
||||
}
|
||||
catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a list of doubles to a list of floats.
|
||||
* @param doubleList The list of doubles.
|
||||
* @return The converted list of floats.
|
||||
*/
|
||||
private List<Float> toFloatList(List<Double> doubleList) {
|
||||
return doubleList.stream().map(d -> d.floatValue()).toList();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
/*
|
||||
* 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.qdrant;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
import io.qdrant.client.QdrantClient;
|
||||
import io.qdrant.client.QdrantGrpcClient;
|
||||
import io.qdrant.client.grpc.Collections.Distance;
|
||||
import io.qdrant.client.grpc.Collections.VectorParams;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.testcontainers.junit.jupiter.Container;
|
||||
import org.testcontainers.junit.jupiter.Testcontainers;
|
||||
import org.testcontainers.qdrant.QdrantContainer;
|
||||
|
||||
import org.springframework.ai.document.Document;
|
||||
import org.springframework.ai.embedding.EmbeddingClient;
|
||||
import org.springframework.ai.openai.OpenAiEmbeddingClient;
|
||||
import org.springframework.ai.openai.api.OpenAiApi;
|
||||
import org.springframework.ai.vectorstore.SearchRequest;
|
||||
import org.springframework.ai.vectorstore.VectorStore;
|
||||
import org.springframework.boot.SpringBootConfiguration;
|
||||
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* @author Anush Shetty
|
||||
* @since 0.8.1
|
||||
*/
|
||||
@Testcontainers
|
||||
public class QdrantVectorStoreIT {
|
||||
|
||||
private static final String COLLECTION_NAME = "test_collection";
|
||||
|
||||
private static final int EMBEDDING_DIMENSION = 1536;
|
||||
|
||||
private static final int QDRANT_GRPC_PORT = 6334;
|
||||
|
||||
@Container
|
||||
static QdrantContainer qdrantContainer = new QdrantContainer("qdrant/qdrant:v1.7.4");
|
||||
|
||||
List<Document> documents = List.of(
|
||||
new Document("Spring AI rocks!! Spring AI rocks!! Spring AI rocks!! Spring AI rocks!! Spring AI rocks!!",
|
||||
Collections.singletonMap("meta1", "meta1")),
|
||||
new Document("Hello World Hello World Hello World Hello World Hello World Hello World Hello World"),
|
||||
new Document(
|
||||
"Great Depression Great Depression Great Depression Great Depression Great Depression Great Depression",
|
||||
Collections.singletonMap("meta2", "meta2")));
|
||||
|
||||
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
|
||||
.withUserConfiguration(TestApplication.class)
|
||||
.withPropertyValues("spring.ai.openai.apiKey=" + System.getenv("OPENAI_API_KEY"));
|
||||
|
||||
@BeforeAll
|
||||
static void setup() throws InterruptedException, ExecutionException {
|
||||
|
||||
String host = qdrantContainer.getHost();
|
||||
int port = qdrantContainer.getMappedPort(QDRANT_GRPC_PORT);
|
||||
QdrantClient client = new QdrantClient(QdrantGrpcClient.newBuilder(host, port, false).build());
|
||||
|
||||
client
|
||||
.createCollectionAsync(COLLECTION_NAME,
|
||||
VectorParams.newBuilder().setDistance(Distance.Cosine).setSize(EMBEDDING_DIMENSION).build())
|
||||
.get();
|
||||
|
||||
client.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addAndSearch() {
|
||||
contextRunner.run(context -> {
|
||||
|
||||
VectorStore vectorStore = context.getBean(VectorStore.class);
|
||||
|
||||
vectorStore.add(documents);
|
||||
|
||||
List<Document> results = vectorStore.similaritySearch(SearchRequest.query("Great").withTopK(1));
|
||||
|
||||
assertThat(results).hasSize(1);
|
||||
Document resultDoc = results.get(0);
|
||||
assertThat(resultDoc.getId()).isEqualTo(documents.get(2).getId());
|
||||
assertThat(resultDoc.getContent()).isEqualTo(
|
||||
"Great Depression Great Depression Great Depression Great Depression Great Depression Great Depression");
|
||||
assertThat(resultDoc.getMetadata()).containsKeys("meta2", "distance");
|
||||
|
||||
// Remove all documents from the store
|
||||
vectorStore.delete(documents.stream().map(doc -> doc.getId()).toList());
|
||||
|
||||
List<Document> results2 = vectorStore.similaritySearch(SearchRequest.query("Great").withTopK(1));
|
||||
assertThat(results2).hasSize(0);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addAndSearchWithFilters() {
|
||||
|
||||
contextRunner.run(context -> {
|
||||
|
||||
VectorStore vectorStore = context.getBean(VectorStore.class);
|
||||
|
||||
var bgDocument = new Document("The World is Big and Salvation Lurks Around the Corner",
|
||||
Map.of("country", "Bulgaria", "number", 3));
|
||||
var nlDocument = new Document("The World is Big and Salvation Lurks Around the Corner",
|
||||
Map.of("country", "Netherlands", "number", 90));
|
||||
|
||||
vectorStore.add(List.of(bgDocument, nlDocument));
|
||||
|
||||
var request = SearchRequest.query("The World").withTopK(5);
|
||||
|
||||
List<Document> results = vectorStore.similaritySearch(request);
|
||||
assertThat(results).hasSize(2);
|
||||
|
||||
results = vectorStore
|
||||
.similaritySearch(request.withSimilarityThresholdAll().withFilterExpression("country == 'Bulgaria'"));
|
||||
assertThat(results).hasSize(1);
|
||||
assertThat(results.get(0).getId()).isEqualTo(bgDocument.getId());
|
||||
|
||||
results = vectorStore.similaritySearch(
|
||||
request.withSimilarityThresholdAll().withFilterExpression("country == 'Netherlands'"));
|
||||
assertThat(results).hasSize(1);
|
||||
assertThat(results.get(0).getId()).isEqualTo(nlDocument.getId());
|
||||
|
||||
results = vectorStore.similaritySearch(
|
||||
request.withSimilarityThresholdAll().withFilterExpression("NOT(country == 'Netherlands')"));
|
||||
assertThat(results).hasSize(1);
|
||||
assertThat(results.get(0).getId()).isEqualTo(bgDocument.getId());
|
||||
|
||||
results = vectorStore
|
||||
.similaritySearch(request.withSimilarityThresholdAll().withFilterExpression("number in [3, 5, 12]"));
|
||||
assertThat(results).hasSize(1);
|
||||
assertThat(results.get(0).getId()).isEqualTo(bgDocument.getId());
|
||||
|
||||
results = vectorStore
|
||||
.similaritySearch(request.withSimilarityThresholdAll().withFilterExpression("number nin [3, 5, 12]"));
|
||||
assertThat(results).hasSize(1);
|
||||
assertThat(results.get(0).getId()).isEqualTo(nlDocument.getId());
|
||||
|
||||
// Remove all documents from the store
|
||||
vectorStore.delete(List.of(bgDocument, nlDocument).stream().map(doc -> doc.getId()).toList());
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void documentUpdateTest() {
|
||||
|
||||
contextRunner.run(context -> {
|
||||
|
||||
VectorStore vectorStore = context.getBean(VectorStore.class);
|
||||
|
||||
Document document = new Document(UUID.randomUUID().toString(), "Spring AI rocks!!",
|
||||
Collections.singletonMap("meta1", "meta1"));
|
||||
|
||||
vectorStore.add(List.of(document));
|
||||
|
||||
List<Document> results = vectorStore.similaritySearch(SearchRequest.query("Spring").withTopK(5));
|
||||
|
||||
assertThat(results).hasSize(1);
|
||||
Document resultDoc = results.get(0);
|
||||
assertThat(resultDoc.getId()).isEqualTo(document.getId());
|
||||
assertThat(resultDoc.getContent()).isEqualTo("Spring AI rocks!!");
|
||||
assertThat(resultDoc.getMetadata()).containsKey("meta1");
|
||||
assertThat(resultDoc.getMetadata()).containsKey("distance");
|
||||
|
||||
Document sameIdDocument = new Document(document.getId(),
|
||||
"The World is Big and Salvation Lurks Around the Corner",
|
||||
Collections.singletonMap("meta2", "meta2"));
|
||||
|
||||
vectorStore.add(List.of(sameIdDocument));
|
||||
|
||||
results = vectorStore.similaritySearch(SearchRequest.query("FooBar").withTopK(5));
|
||||
|
||||
assertThat(results).hasSize(1);
|
||||
resultDoc = results.get(0);
|
||||
assertThat(resultDoc.getId()).isEqualTo(document.getId());
|
||||
assertThat(resultDoc.getContent()).isEqualTo("The World is Big and Salvation Lurks Around the Corner");
|
||||
assertThat(resultDoc.getMetadata()).containsKey("meta2");
|
||||
assertThat(resultDoc.getMetadata()).containsKey("distance");
|
||||
|
||||
vectorStore.delete(List.of(document.getId()));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void searchThresholdTest() {
|
||||
|
||||
contextRunner.run(context -> {
|
||||
|
||||
VectorStore vectorStore = context.getBean(VectorStore.class);
|
||||
|
||||
vectorStore.add(documents);
|
||||
|
||||
var request = SearchRequest.query("Great").withTopK(5);
|
||||
List<Document> fullResult = vectorStore.similaritySearch(request.withSimilarityThresholdAll());
|
||||
|
||||
List<Float> distances = fullResult.stream().map(doc -> (Float) doc.getMetadata().get("distance")).toList();
|
||||
|
||||
assertThat(distances).hasSize(3);
|
||||
|
||||
float threshold = (distances.get(0) + distances.get(1)) / 2;
|
||||
|
||||
List<Document> results = vectorStore.similaritySearch(request.withSimilarityThreshold(1 - threshold));
|
||||
|
||||
assertThat(results).hasSize(1);
|
||||
Document resultDoc = results.get(0);
|
||||
assertThat(resultDoc.getId()).isEqualTo(documents.get(2).getId());
|
||||
assertThat(resultDoc.getContent()).isEqualTo(
|
||||
"Great Depression Great Depression Great Depression Great Depression Great Depression Great Depression");
|
||||
assertThat(resultDoc.getMetadata()).containsKey("meta2");
|
||||
assertThat(resultDoc.getMetadata()).containsKey("distance");
|
||||
|
||||
// Remove all documents from the store
|
||||
vectorStore.delete(documents.stream().map(doc -> doc.getId()).toList());
|
||||
});
|
||||
}
|
||||
|
||||
@SpringBootConfiguration
|
||||
public static class TestApplication {
|
||||
|
||||
@Bean
|
||||
public QdrantClient qdrantClient() {
|
||||
String host = qdrantContainer.getHost();
|
||||
int port = qdrantContainer.getMappedPort(QDRANT_GRPC_PORT);
|
||||
QdrantClient qdrantClient = new QdrantClient(QdrantGrpcClient.newBuilder(host, port, false).build());
|
||||
return qdrantClient;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public VectorStore qdrantVectorStore(EmbeddingClient embeddingClient, QdrantClient qdrantClient) {
|
||||
return new QdrantVectorStore(qdrantClient, COLLECTION_NAME, embeddingClient);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public EmbeddingClient embeddingClient() {
|
||||
return new OpenAiEmbeddingClient(new OpenAiApi(System.getenv("OPENAI_API_KEY")));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user