customObservationConvention,
BatchingStrategy batchingStrategy) {
- return new QdrantVectorStore(qdrantClient, properties.getCollectionName(), embeddingModel,
- properties.isInitializeSchema(), observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP),
- customObservationConvention.getIfAvailable(() -> null), batchingStrategy);
+ return QdrantVectorStore.builder(qdrantClient)
+ .collectionName(properties.getCollectionName())
+ .embeddingModel(embeddingModel)
+ .initializeSchema(properties.isInitializeSchema())
+ .observationRegistry(observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP))
+ .customObservationConvention(customObservationConvention.getIfAvailable(() -> null))
+ .batchingStrategy(batchingStrategy)
+ .build();
}
static class PropertiesQdrantConnectionDetails implements QdrantConnectionDetails {
diff --git a/vector-stores/spring-ai-qdrant-store/src/main/java/org/springframework/ai/vectorstore/qdrant/QdrantVectorStore.java b/vector-stores/spring-ai-qdrant-store/src/main/java/org/springframework/ai/vectorstore/qdrant/QdrantVectorStore.java
index a5d5ec748..fdc788372 100644
--- a/vector-stores/spring-ai-qdrant-store/src/main/java/org/springframework/ai/vectorstore/qdrant/QdrantVectorStore.java
+++ b/vector-stores/spring-ai-qdrant-store/src/main/java/org/springframework/ai/vectorstore/qdrant/QdrantVectorStore.java
@@ -42,6 +42,7 @@ import org.springframework.ai.embedding.EmbeddingOptionsBuilder;
import org.springframework.ai.embedding.TokenCountBatchingStrategy;
import org.springframework.ai.model.EmbeddingUtils;
import org.springframework.ai.observation.conventions.VectorStoreProvider;
+import org.springframework.ai.vectorstore.AbstractVectorStoreBuilder;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.observation.AbstractObservationVectorStore;
import org.springframework.ai.vectorstore.observation.VectorStoreObservationContext;
@@ -53,6 +54,71 @@ import org.springframework.util.Assert;
* Qdrant vectorStore implementation. This store supports creating, updating, deleting,
* and similarity searching of documents in a Qdrant collection.
*
+ *
+ * The store uses Qdrant's vector search functionality to persist and query vector
+ * embeddings along with their associated document content and metadata. The
+ * implementation leverages Qdrant's HNSW (Hierarchical Navigable Small World) algorithm
+ * for efficient k-NN search operations.
+ *
+ *
+ *
+ * Features:
+ *
+ *
+ * - Automatic schema initialization with configurable collection creation
+ * - Support for cosine similarity distance metric
+ * - Metadata filtering using Qdrant's filter expressions
+ * - Configurable similarity thresholds for search results
+ * - Batch processing support with configurable strategies
+ * - Observation and metrics support through Micrometer
+ *
+ *
+ *
+ * Basic usage example:
+ *
+ * {@code
+ * QdrantVectorStore vectorStore = QdrantVectorStore.builder(qdrantClient)
+ * .embeddingModel(embeddingModel)
+ * .initializeSchema(true)
+ * .build();
+ *
+ * // Add documents
+ * vectorStore.add(List.of(
+ * new Document("content1", Map.of("key1", "value1")),
+ * new Document("content2", Map.of("key2", "value2"))
+ * ));
+ *
+ * // Search with filters
+ * List results = vectorStore.similaritySearch(
+ * SearchRequest.query("search text")
+ * .withTopK(5)
+ * .withSimilarityThreshold(0.7)
+ * .withFilterExpression("key1 == 'value1'")
+ * );
+ * }
+ *
+ *
+ * Advanced configuration example:
+ *
+ * {@code
+ * QdrantVectorStore vectorStore = QdrantVectorStore.builder(qdrantClient)
+ * .embeddingModel(embeddingModel)
+ * .collectionName("custom-collection")
+ * .initializeSchema(true)
+ * .batchingStrategy(new TokenCountBatchingStrategy())
+ * .observationRegistry(observationRegistry)
+ * .customObservationConvention(customConvention)
+ * .build();
+ * }
+ *
+ *
+ * Requirements:
+ *
+ *
+ * - Running Qdrant instance accessible via gRPC
+ * - Collection with vector size matching the embedding model dimensions
+ *
+ *
* @author Anush Shetty
* @author Christian Tzolov
* @author EddĂș MelĂ©ndez
@@ -67,8 +133,6 @@ public class QdrantVectorStore extends AbstractObservationVectorStore implements
private static final String CONTENT_FIELD_NAME = "doc_content";
- private final EmbeddingModel embeddingModel;
-
private final QdrantClient qdrantClient;
private final String collectionName;
@@ -85,7 +149,9 @@ public class QdrantVectorStore extends AbstractObservationVectorStore implements
* @param collectionName The name of the collection to use in Qdrant.
* @param embeddingModel The client for embedding operations.
* @param initializeSchema A boolean indicating whether to initialize the schema.
+ * @deprecated Use {@link #builder(QdrantClient)}
*/
+ @Deprecated(forRemoval = true, since = "1.0.0-M5")
public QdrantVectorStore(QdrantClient qdrantClient, String collectionName, EmbeddingModel embeddingModel,
boolean initializeSchema) {
this(qdrantClient, collectionName, embeddingModel, initializeSchema, ObservationRegistry.NOOP, null,
@@ -100,22 +166,48 @@ public class QdrantVectorStore extends AbstractObservationVectorStore implements
* @param initializeSchema A boolean indicating whether to initialize the schema.
* @param observationRegistry The observation registry to use.
* @param customObservationConvention The custom search observation convention to use.
+ * @deprecated Use {@link #builder(QdrantClient)}
*/
+ @Deprecated(forRemoval = true, since = "1.0.0-M5")
public QdrantVectorStore(QdrantClient qdrantClient, String collectionName, EmbeddingModel embeddingModel,
boolean initializeSchema, ObservationRegistry observationRegistry,
VectorStoreObservationConvention customObservationConvention, BatchingStrategy batchingStrategy) {
- super(observationRegistry, customObservationConvention);
+ this(builder(qdrantClient).embeddingModel(embeddingModel)
+ .collectionName(collectionName)
+ .initializeSchema(initializeSchema)
+ .observationRegistry(observationRegistry)
+ .customObservationConvention(customObservationConvention)
+ .batchingStrategy(batchingStrategy));
+ }
- Assert.notNull(qdrantClient, "QdrantClient must not be null");
- Assert.notNull(collectionName, "collectionName must not be null");
- Assert.notNull(embeddingModel, "EmbeddingModel must not be null");
+ /**
+ * Protected constructor for creating a QdrantVectorStore instance using the builder
+ * pattern.
+ * @param builder the {@link QdrantBuilder} containing all configuration settings
+ * @throws IllegalArgumentException if qdrant client is missing
+ * @see QdrantBuilder
+ * @since 1.0.0
+ */
+ protected QdrantVectorStore(QdrantBuilder builder) {
+ super(builder);
- this.initializeSchema = initializeSchema;
- this.embeddingModel = embeddingModel;
- this.collectionName = collectionName;
- this.qdrantClient = qdrantClient;
- this.batchingStrategy = batchingStrategy;
+ Assert.notNull(builder.qdrantClient, "QdrantClient must not be null");
+
+ this.qdrantClient = builder.qdrantClient;
+ this.collectionName = builder.collectionName;
+ this.initializeSchema = builder.initializeSchema;
+ this.batchingStrategy = builder.batchingStrategy;
+ }
+
+ /**
+ * Creates a new QdrantBuilder instance. This is the recommended way to instantiate a
+ * QdrantVectorStore.
+ * @param qdrantClient the client for interfacing with Qdrant
+ * @return a new QdrantBuilder instance
+ */
+ public static QdrantBuilder builder(QdrantClient qdrantClient) {
+ return new QdrantBuilder(qdrantClient);
}
/**
@@ -272,4 +364,80 @@ public class QdrantVectorStore extends AbstractObservationVectorStore implements
}
+ /**
+ * Builder for creating instances of {@link QdrantVectorStore}. This builder provides
+ * a fluent API for configuring all aspects of the vector store.
+ *
+ * @since 1.0.0
+ */
+ public static final class QdrantBuilder extends AbstractVectorStoreBuilder {
+
+ private final QdrantClient qdrantClient;
+
+ private String collectionName = DEFAULT_COLLECTION_NAME;
+
+ private boolean initializeSchema = false;
+
+ private BatchingStrategy batchingStrategy = new TokenCountBatchingStrategy();
+
+ /**
+ * Creates a new builder instance with the required QdrantClient and
+ * EmbeddingModel.
+ * @param qdrantClient the client for Qdrant operations
+ * @throws IllegalArgumentException if qdrantClient is null
+ */
+ QdrantBuilder(QdrantClient qdrantClient) {
+ Assert.notNull(qdrantClient, "QdrantClient must not be null");
+ this.qdrantClient = qdrantClient;
+ }
+
+ /**
+ * Configures the Qdrant collection name.
+ * @param collectionName the name of the collection to use (defaults to
+ * {@value DEFAULT_COLLECTION_NAME})
+ * @return this builder instance
+ * @throws IllegalArgumentException if collectionName is null or empty
+ */
+ public QdrantBuilder collectionName(String collectionName) {
+ Assert.hasText(collectionName, "collectionName must not be empty");
+ this.collectionName = collectionName;
+ return this;
+ }
+
+ /**
+ * Configures whether to initialize the collection schema.
+ * @param initializeSchema true to initialize schema automatically
+ * @return this builder instance
+ */
+ public QdrantBuilder initializeSchema(boolean initializeSchema) {
+ this.initializeSchema = initializeSchema;
+ return this;
+ }
+
+ /**
+ * Configures the strategy for batching operations.
+ * @param batchingStrategy the batching strategy to use
+ * @return this builder instance
+ * @throws IllegalArgumentException if batchingStrategy is null
+ */
+ public QdrantBuilder batchingStrategy(BatchingStrategy batchingStrategy) {
+ Assert.notNull(batchingStrategy, "BatchingStrategy must not be null");
+ this.batchingStrategy = batchingStrategy;
+ return this;
+ }
+
+ /**
+ * Builds and returns a new QdrantVectorStore instance with the configured
+ * settings.
+ * @return a new QdrantVectorStore instance
+ * @throws IllegalStateException if the builder configuration is invalid
+ */
+ @Override
+ public QdrantVectorStore build() {
+ validate();
+ return new QdrantVectorStore(this);
+ }
+
+ }
+
}
diff --git a/vector-stores/spring-ai-qdrant-store/src/test/java/org/springframework/ai/vectorstore/qdrant/QdrantVectorStoreBuilderTests.java b/vector-stores/spring-ai-qdrant-store/src/test/java/org/springframework/ai/vectorstore/qdrant/QdrantVectorStoreBuilderTests.java
new file mode 100644
index 000000000..2f9f5e364
--- /dev/null
+++ b/vector-stores/spring-ai-qdrant-store/src/test/java/org/springframework/ai/vectorstore/qdrant/QdrantVectorStoreBuilderTests.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.vectorstore.qdrant;
+
+import io.qdrant.client.QdrantClient;
+import io.qdrant.client.QdrantGrpcClient;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.ai.embedding.EmbeddingModel;
+import org.springframework.ai.embedding.TokenCountBatchingStrategy;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.Mockito.mock;
+
+/**
+ * Tests for {@link QdrantVectorStore.QdrantBuilder}.
+ *
+ * @author Mark Pollack
+ */
+class QdrantVectorStoreBuilderTests {
+
+ private QdrantClient qdrantClient;
+
+ private EmbeddingModel embeddingModel;
+
+ @BeforeEach
+ void setUp() {
+ this.qdrantClient = mock(QdrantClient.class);
+ this.embeddingModel = mock(EmbeddingModel.class);
+ }
+
+ @Test
+ void defaultConfiguration() {
+ QdrantVectorStore vectorStore = QdrantVectorStore.builder(qdrantClient).embeddingModel(embeddingModel).build();
+
+ // Verify default values
+ assertThat(vectorStore).hasFieldOrPropertyWithValue("collectionName", "vector_store");
+ assertThat(vectorStore).hasFieldOrPropertyWithValue("initializeSchema", false);
+ assertThat(vectorStore).hasFieldOrPropertyWithValue("batchingStrategy.class", TokenCountBatchingStrategy.class);
+ }
+
+ @Test
+ void customConfiguration() {
+ QdrantVectorStore vectorStore = QdrantVectorStore.builder(qdrantClient)
+ .embeddingModel(embeddingModel)
+ .collectionName("custom_collection")
+ .initializeSchema(true)
+ .batchingStrategy(new TokenCountBatchingStrategy())
+ .build();
+
+ assertThat(vectorStore).hasFieldOrPropertyWithValue("collectionName", "custom_collection");
+ assertThat(vectorStore).hasFieldOrPropertyWithValue("initializeSchema", true);
+ assertThat(vectorStore).hasFieldOrPropertyWithValue("batchingStrategy.class", TokenCountBatchingStrategy.class);
+ }
+
+ @Test
+ void nullQdrantClientInConstructorShouldThrowException() {
+ assertThatThrownBy(() -> QdrantVectorStore.builder(null)).isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("QdrantClient must not be null");
+ }
+
+ @Test
+ void nullEmbeddingModelShouldThrowException() {
+ assertThatThrownBy(() -> QdrantVectorStore.builder(qdrantClient).embeddingModel(null).build())
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("EmbeddingModel must not be null");
+ }
+
+ @Test
+ void emptyCollectionNameShouldThrowException() {
+ assertThatThrownBy(
+ () -> QdrantVectorStore.builder(qdrantClient).embeddingModel(embeddingModel).collectionName("").build())
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("collectionName must not be empty");
+ }
+
+ @Test
+ void nullBatchingStrategyShouldThrowException() {
+ assertThatThrownBy(() -> QdrantVectorStore.builder(qdrantClient)
+ .embeddingModel(embeddingModel)
+ .batchingStrategy(null)
+ .build()).isInstanceOf(IllegalArgumentException.class).hasMessage("BatchingStrategy must not be null");
+ }
+
+}
diff --git a/vector-stores/spring-ai-qdrant-store/src/test/java/org/springframework/ai/vectorstore/qdrant/QdrantVectorStoreIT.java b/vector-stores/spring-ai-qdrant-store/src/test/java/org/springframework/ai/vectorstore/qdrant/QdrantVectorStoreIT.java
index 0cb14fde4..282fb5577 100644
--- a/vector-stores/spring-ai-qdrant-store/src/test/java/org/springframework/ai/vectorstore/qdrant/QdrantVectorStoreIT.java
+++ b/vector-stores/spring-ai-qdrant-store/src/test/java/org/springframework/ai/vectorstore/qdrant/QdrantVectorStoreIT.java
@@ -254,7 +254,11 @@ public class QdrantVectorStoreIT {
@Bean
public VectorStore qdrantVectorStore(EmbeddingModel embeddingModel, QdrantClient qdrantClient) {
- return new QdrantVectorStore(qdrantClient, COLLECTION_NAME, embeddingModel, true);
+ return QdrantVectorStore.builder(qdrantClient)
+ .collectionName(COLLECTION_NAME)
+ .embeddingModel(embeddingModel)
+ .initializeSchema(true)
+ .build();
}
@Bean
diff --git a/vector-stores/spring-ai-qdrant-store/src/test/java/org/springframework/ai/vectorstore/qdrant/QdrantVectorStoreObservationIT.java b/vector-stores/spring-ai-qdrant-store/src/test/java/org/springframework/ai/vectorstore/qdrant/QdrantVectorStoreObservationIT.java
index 42031c3c5..f54a2b480 100644
--- a/vector-stores/spring-ai-qdrant-store/src/test/java/org/springframework/ai/vectorstore/qdrant/QdrantVectorStoreObservationIT.java
+++ b/vector-stores/spring-ai-qdrant-store/src/test/java/org/springframework/ai/vectorstore/qdrant/QdrantVectorStoreObservationIT.java
@@ -195,8 +195,14 @@ public class QdrantVectorStoreObservationIT {
@Bean
public VectorStore qdrantVectorStore(EmbeddingModel embeddingModel, QdrantClient qdrantClient,
ObservationRegistry observationRegistry) {
- return new QdrantVectorStore(qdrantClient, COLLECTION_NAME, embeddingModel, true, observationRegistry, null,
- new TokenCountBatchingStrategy());
+ return QdrantVectorStore.builder(qdrantClient)
+ .collectionName(COLLECTION_NAME)
+ .embeddingModel(embeddingModel)
+ .initializeSchema(true)
+ .observationRegistry(observationRegistry)
+ .customObservationConvention(null)
+ .batchingStrategy(new TokenCountBatchingStrategy())
+ .build();
}
@Bean