Qdrant improvements

- Update docs
 - Allow colleciton auto-creation if missing
 - Add cloud IT : QdrantVectorStoreCloudAutoConfigurationIT
 - Add auto-configuration properties tests: PgVectorStorePropertiesTests
This commit is contained in:
Christian Tzolov
2024-02-29 09:27:19 +01:00
parent 41a256cb60
commit a8110bb3da
8 changed files with 312 additions and 98 deletions

View File

@@ -9,49 +9,14 @@ link:https://www.qdrant.tech/[Qdrant] is an open-source, high-performance vector
* 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: `Host`, `GRPC Port`, `Collection Name`, and `API Key` (if required).
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`.
====
NOTE: It is recommended that the Qdrant collection is link:https://qdrant.tech/documentation/concepts/collections/#create-a-collection[created] in advance with the appropriate dimensions and configurations.
If the collection is not created, the `QdrantVectorStore` will attempt to create one using the `Cosine` similarity and the dimension of the configured `EmbeddingClient`.
== 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:
Then add the Qdrant boot starter dependency to your project:
[source,xml]
----
@@ -70,21 +35,55 @@ dependencies {
}
----
The Vector Store, also 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 to use the xref:api/embeddings/openai-embeddings.adoc[OpenAI EmbeddingClient] add the following dependency to your project:
[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: Refer to the xref:getting-started.adoc#dependency-management[Dependency Management] section to add the Spring AI BOM to your build file.
Refer to the xref:getting-started.adoc#repositories[Repositories] section to add Milestone and/or Snapshot Repositories 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.
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_,
TIP: Refer to the xref:getting-started.adoc#repositories[Repositories] section to add Milestone and/or Snapshot Repositories to your build file.
[source,properties]
----
spring.ai.vectorstore.qdrant.host=<host of your qdrant instance>
spring.ai.vectorstore.qdrant.port=<the GRPC 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>
----
TIP: Check the list of xref:#qdrant-vectorstore-properties[configuration parameters] to learn about the default values and configuration options.
Now you can Auto-wire the Qdrant Vector Store in your application and use it
[source,java]
----
@Autowired
VectorStore vectorStore;
@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"),
@@ -97,24 +96,7 @@ vectorStore.add(List.of(document));
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
=== 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:
@@ -162,7 +144,7 @@ public VectorStore vectorStore(QdrantVectorStoreConfig config, EmbeddingClient e
}
----
=== Metadata filtering
== 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.
@@ -195,18 +177,18 @@ vectorStore.similaritySearch(SearchRequest.defaults()
NOTE: These filter expressions are converted into the equivalent Qdrant link:https://qdrant.tech/documentation/concepts/filtering/[filters].
[[qdrant-vectorstore-properties]]
== Qdrant VectorStore properties
== Configuration properties
You can use the following properties in your Spring Boot configuration to customize the Qdrant vector store.
[cols="3,5,1"]
|===
|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.port`| The gRPC 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
|`spring.ai.vectorstore.qdrant.use-tls`| Whether to use TLS(HTTPS). | false
|===

View File

@@ -43,7 +43,7 @@ public class QdrantVectorStoreAutoConfiguration {
.withCollectionName(properties.getCollectionName())
.withHost(properties.getHost())
.withPort(properties.getPort())
.withTls(properties.useTls())
.withTls(properties.isUseTls())
.withApiKey(properties.getApiKey())
.build();

View File

@@ -76,7 +76,7 @@ public class QdrantVectorStoreProperties {
this.port = port;
}
public boolean useTls() {
public boolean isUseTls() {
return this.useTls;
}

View File

@@ -0,0 +1,56 @@
/*
* Copyright 2023-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.autoconfigure.vectorstore.qdrant;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Christian Tzolov
*/
public class PgVectorStorePropertiesTests {
@Test
public void defaultValues() {
var props = new QdrantVectorStoreProperties();
assertThat(props.getCollectionName()).isNull();
assertThat(props.getHost()).isEqualTo("localhost");
assertThat(props.getPort()).isEqualTo(6334);
assertThat(props.isUseTls()).isFalse();
assertThat(props.getApiKey()).isNull();
}
@Test
public void customValues() {
var props = new QdrantVectorStoreProperties();
props.setCollectionName("MY_COLLECTION");
props.setHost("MY_HOST");
props.setPort(999);
props.setUseTls(true);
props.setApiKey("MY_API_KEY");
assertThat(props.getCollectionName()).isEqualTo("MY_COLLECTION");
assertThat(props.getHost()).isEqualTo("MY_HOST");
assertThat(props.getPort()).isEqualTo(999);
assertThat(props.isUseTls()).isTrue();
assertThat(props.getApiKey()).isEqualTo("MY_API_KEY");
}
}

View File

@@ -54,8 +54,6 @@ 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
@@ -66,31 +64,6 @@ public class QdrantVectorStoreAutoConfigurationIT {
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)
@@ -121,6 +94,16 @@ public class QdrantVectorStoreAutoConfigurationIT {
});
}
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);
}
}
@Configuration(proxyBeanMethods = false)
static class Config {

View File

@@ -0,0 +1,147 @@
/*
* 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.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
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;
/**
* Test using a free tier Qdrant Cloud instance: https://cloud.qdrant.io
*
* @author Christian Tzolov
* @since 0.8.1
*/
// NOTE: The free Qdrant Cluster and the QDRANT_API_KEY expire after 4 weeks of
// inactivity.
@EnabledIfEnvironmentVariable(named = "QDRANT_API_KEY", matches = ".+")
@EnabledIfEnvironmentVariable(named = "QDRANT_HOST", matches = ".+")
public class QdrantVectorStoreCloudAutoConfigurationIT {
private static final String COLLECTION_NAME = "test_collection";
// Because we pre-create the collection.
private static final int EMBEDDING_DIMENSION = 384;
private static final String CLOUD_API_KEY = System.getenv("QDRANT_API_KEY");
private static final String CLOUD_HOST = System.getenv("QDRANT_HOST");
// NOTE: The GRPC port (usually 6334) is different from the HTTP port (usually 6333)!
private static final int CLOUD_GRPC_PORT = 6334;
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 {
// Create a new test collection
try (QdrantClient client = new QdrantClient(
QdrantGrpcClient.newBuilder(CLOUD_HOST, CLOUD_GRPC_PORT, true).withApiKey(CLOUD_API_KEY).build())) {
if (client.listCollectionsAsync().get().stream().anyMatch(c -> c.equals(COLLECTION_NAME))) {
client.deleteCollectionAsync(COLLECTION_NAME).get();
}
var vectorParams = VectorParams.newBuilder()
.setDistance(Distance.Cosine)
.setSize(EMBEDDING_DIMENSION)
.build();
client.createCollectionAsync(COLLECTION_NAME, vectorParams).get();
}
}
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(QdrantVectorStoreAutoConfiguration.class))
.withUserConfiguration(Config.class)
.withPropertyValues("spring.ai.vectorstore.qdrant.port=" + CLOUD_GRPC_PORT,
"spring.ai.vectorstore.qdrant.host=" + CLOUD_HOST,
"spring.ai.vectorstore.qdrant.api-key=" + CLOUD_API_KEY,
"spring.ai.vectorstore.qdrant.collection-name=" + COLLECTION_NAME,
"spring.ai.vectorstore.qdrant.use-tls=true");
@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);
});
}
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);
}
}
@Configuration(proxyBeanMethods = false)
static class Config {
@Bean
public EmbeddingClient embeddingClient() {
return new TransformersEmbeddingClient();
}
}
}

View File

@@ -1 +1,22 @@
Qdrant Vector Store
# Qdrant Vector Store
[Reference Documentation](https://docs.spring.io/spring-ai/reference/0.8-SNAPSHOT/api/vectordbs/qdrant.html#qdrant-vectorstore-properties)
## Run locally
### Accessing the Web UI
First, run the Docker container:
```
docker run -p 6333:6333 -p 6334:6334 \
-v $(pwd)/qdrant_storage:/qdrant/storage:z \
qdrant/qdrant
```
The GUI is available at http://localhost:6333/dashboard
## Qdrant references
- https://qdrant.tech/documentation/interfaces/
- https://github.com/qdrant/java-client

View File

@@ -30,10 +30,13 @@ 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.beans.factory.InitializingBean;
import org.springframework.util.Assert;
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 io.qdrant.client.grpc.JsonWithInt.Value;
import io.qdrant.client.grpc.Points.Filter;
import io.qdrant.client.grpc.Points.PointId;
@@ -47,9 +50,10 @@ import io.qdrant.client.grpc.Points.UpdateStatus;
* and similarity searching of documents in a Qdrant collection.
*
* @author Anush Shetty
* @author Christian Tzolov
* @since 0.8.1
*/
public class QdrantVectorStore implements VectorStore {
public class QdrantVectorStore implements VectorStore, InitializingBean {
private static final String CONTENT_FIELD_NAME = "doc_content";
@@ -326,4 +330,25 @@ public class QdrantVectorStore implements VectorStore {
return doubleList.stream().map(d -> d.floatValue()).toList();
}
@Override
public void afterPropertiesSet() throws Exception {
// Create the collection if it does not exist.
if (!isCollectionExists()) {
var vectorParams = VectorParams.newBuilder()
.setDistance(Distance.Cosine)
.setSize(this.embeddingClient.dimensions())
.build();
this.qdrantClient.createCollectionAsync(this.collectionName, vectorParams).get();
}
}
private boolean isCollectionExists() {
try {
return this.qdrantClient.listCollectionsAsync().get().stream().anyMatch(c -> c.equals(this.collectionName));
}
catch (Exception e) {
throw new RuntimeException(e);
}
}
}