import "github.com/Aleph-Alpha/std/v1/qdrant"Package qdrant provides a modular, dependency-injected client for the Qdrant vector database.
The qdrant package is designed to simplify interaction with Qdrant in Go applications, offering a clean, testable abstraction layer for common vector database operations such as collection management, embedding insertion, similarity search, and deletion. It integrates seamlessly with the fx dependency injection framework and supports builder-style configuration.
- Managed Qdrant client lifecycle with Fx integration
- Config struct supporting environment and YAML loading
- Automatic health checks on client initialization
- Safe, batched insertion of embeddings with configurable batch size
- Database-agnostic interface via vectordb.Service
- Type-safe collection creation and existence checks
- Support for payload metadata and optional vector retrieval
- Extensible abstraction layer for alternate vector stores (e.g. pgVector)
This package includes Adapter which implements the database-agnostic [vectordb.Service] interface. Use this for new projects to enable easy switching between vector databases:
import (
"github.com/Aleph-Alpha/std/v1/vectordb"
"github.com/Aleph-Alpha/std/v1/qdrant"
)
// Create your existing QdrantClient
qc, _ := qdrant.NewQdrantClient(qdrant.QdrantParams{
Config: &qdrant.Config{
Endpoint: "localhost",
Port: 6334,
},
})
// Create adapter for DB-agnostic usage
var db vectordb.Service = qdrant.NewAdapter(qc.Client())
This allows switching between vector databases (Qdrant, pgVector) without changing application code.
import (
"github.com/Aleph-Alpha/std/v1/qdrant"
"github.com/Aleph-Alpha/std/v1/vectordb"
)
// Create a new client
client, err := qdrant.NewQdrantClient(qdrant.QdrantParams{
Config: &qdrant.Config{
Endpoint: "localhost",
Port: 6334,
},
})
if err != nil {
log.Fatal(err)
}
// Create adapter
adapter := qdrant.NewAdapter(client.Client())
collectionName := "documents"
// Ensure collection exists
if err := adapter.EnsureCollection(ctx, collectionName, 1536); err != nil {
log.Fatal(err)
}
// Insert embeddings
inputs := []vectordb.EmbeddingInput{
{
ID: "doc_1",
Vector: []float32{0.12, 0.43, 0.85, ...},
Payload: map[string]any{"title": "My Document"},
},
}
if err := adapter.Insert(ctx, collectionName, inputs); err != nil {
log.Fatal(err)
}
// Perform similarity search
results, err := adapter.Search(ctx, vectordb.SearchRequest{
CollectionName: collectionName,
Vector: queryVector,
TopK: 5,
})
for _, res := range results[0] {
fmt.Printf("ID=%s Score=%.4f\n", res.ID, res.Score)
}
The package exposes an Fx module for automatic dependency injection:
app := fx.New(
qdrant.FXModule,
// other modules...
)
app.Run()
Search results are returned as [vectordb.SearchResult] structs with public fields:
type SearchResult struct {
ID string // Unique identifier of the matched point
Score float32 // Similarity score
Payload map[string]any // Metadata stored with the vector
Vector []float32 // Stored embedding (if requested)
CollectionName string // Source collection name
}
Access fields directly (no getter methods needed):
for _, result := range results[0] {
fmt.Println(result.ID, result.Score, result.Payload["title"])
}
Filters are defined in the [vectordb] package and support boolean logic (AND, OR, NOT). The qdrant adapter converts these to native Qdrant filters automatically.
Filter Structure:
type FilterSet struct {
Must *ConditionSet // AND - all conditions must match
Should *ConditionSet // OR - at least one condition must match
MustNot *ConditionSet // NOT - none of the conditions should match
}
Condition Types (all in vectordb package):
- MatchCondition: Exact match (string, bool, int64)
- MatchAnyCondition: IN operator (match any of values)
- MatchExceptCondition: NOT IN operator
- NumericRangeCondition: Numeric range filter (gt, gte, lt, lte)
- TimeRangeCondition: DateTime range filter
- IsNullCondition: Check if field is null
- IsEmptyCondition: Check if field is empty, null, or missing
Field Types (Internal vs User):
The package distinguishes between system-managed and user-defined metadata:
const (
InternalField FieldType = iota // Top-level: "status"
UserField // Prefixed: "custom.document_id"
)
User fields are automatically prefixed with "custom." when querying Qdrant.
The vectordb package provides convenience constructors for clean filter creation:
Basic Filter (Must - AND logic):
// Filter: city = "London" AND active = true
results, err := adapter.Search(ctx, vectordb.SearchRequest{
CollectionName: "documents",
Vector: queryVector,
TopK: 10,
Filters: []*vectordb.FilterSet{
vectordb.NewFilterSet(
vectordb.Must(
vectordb.NewMatch("city", "London"),
vectordb.NewMatch("active", true),
),
),
},
})
OR Conditions (Should):
// Filter: city = "London" OR city = "Berlin"
filters := []*vectordb.FilterSet{
vectordb.NewFilterSet(
vectordb.Should(
vectordb.NewMatch("city", "London"),
vectordb.NewMatch("city", "Berlin"),
),
),
}
IN Operator (MatchAny):
// Filter: city IN ["London", "Berlin", "Paris"]
filters := []*vectordb.FilterSet{
vectordb.NewFilterSet(
vectordb.Must(
vectordb.NewMatchAny("city", "London", "Berlin", "Paris"),
),
),
}
Numeric Range Filter:
// Filter: price >= 100 AND price < 500
min, max := float64(100), float64(500)
filters := []*vectordb.FilterSet{
vectordb.NewFilterSet(
vectordb.Must(
vectordb.NewNumericRange("price", vectordb.NumericRange{
Gte: &min,
Lt: &max,
}),
),
),
}
Time Range Filter:
// Filter: created_at >= yesterday AND created_at < now
now := time.Now()
yesterday := now.Add(-24 * time.Hour)
filters := []*vectordb.FilterSet{
vectordb.NewFilterSet(
vectordb.Must(
vectordb.NewTimeRange("created_at", vectordb.TimeRange{
Gte: &yesterday,
Lt: &now,
}),
),
),
}
Complex Filter (Combined Clauses):
// Filter: status = "published" AND (tag = "ml" OR tag = "ai") AND NOT deleted = true
filters := []*vectordb.FilterSet{
vectordb.NewFilterSet(
vectordb.Must(vectordb.NewMatch("status", "published")),
vectordb.Should(
vectordb.NewMatch("tag", "ml"),
vectordb.NewMatch("tag", "ai"),
),
vectordb.MustNot(vectordb.NewMatch("deleted", true)),
),
}
User-Defined Fields:
For fields stored under a custom prefix, use the User* constructors:
// Filter on user-defined field: custom.document_id = "doc-123"
filters := []*vectordb.FilterSet{
vectordb.NewFilterSet(
vectordb.Must(
vectordb.NewUserMatch("document_id", "doc-123"),
),
),
}
Multiple FilterSets (AND between sets):
When you provide multiple FilterSets, they are combined with AND logic:
// Filter: (color = "red") AND (size < 20)
lt := float64(20)
filters := []*vectordb.FilterSet{
vectordb.NewFilterSet(vectordb.Must(vectordb.NewMatch("color", "red"))),
vectordb.NewFilterSet(vectordb.Must(vectordb.NewNumericRange("size", vectordb.NumericRange{Lt: <}))),
}
Nested Filters (Complex Boolean Logic):
For complex expressions like (A OR B) AND (C OR D), use NestedFilterCondition:
// Filter: (status = "active" OR status = "pending") AND (region = "US" OR region = "EU")
filters := []*vectordb.FilterSet{
vectordb.NewFilterSet(
vectordb.Must(
&vectordb.NestedFilterCondition{
Filter: &vectordb.FilterSet{
Should: &vectordb.ConditionSet{
Conditions: []vectordb.FilterCondition{
vectordb.NewMatch("status", "active"),
vectordb.NewMatch("status", "pending"),
},
},
},
},
&vectordb.NestedFilterCondition{
Filter: &vectordb.FilterSet{
Should: &vectordb.ConditionSet{
Conditions: []vectordb.FilterCondition{
vectordb.NewMatch("region", "US"),
vectordb.NewMatch("region", "EU"),
},
},
},
},
),
),
}
Qdrant can be configured via environment variables or YAML:
QDRANT_ENDPOINT=localhost
QDRANT_PORT=6334
QDRANT_API_KEY=your-api-key
The Insert method automatically splits large embedding batches into smaller upserts (default batch size = 100). This minimizes memory usage and avoids timeouts when ingesting large datasets.
All exported methods on the Adapter are safe for concurrent use by multiple goroutines.
For testing and mocking, depend on the [vectordb.Service] interface:
type MockVectorDB struct{}
func (m *MockVectorDB) Search(ctx context.Context, requests ...vectordb.SearchRequest) ([][]vectordb.SearchResult, error) {
return [][]vectordb.SearchResult{
{{ID: "doc-1", Score: 0.95, Payload: map[string]any{"title": "Test"}}},
}, nil
}
// Use in tests:
var db vectordb.Service = &MockVectorDB{}
qdrant/
├── client.go // Qdrant client wrapper and lifecycle
├── operations.go // Service implementation (Adapter)
├── converter.go // vectordb ↔ Qdrant type conversion
├── utils.go // Qdrant-specific helper functions
├── configs.go // Configuration struct
└── fx_module.go // Fx dependency injection module
- [vectordb]: Database-agnostic types and interfaces
- [vectordb.FilterSet]: Filter structures for search queries
- [vectordb.SearchResult]: Search result type
- [vectordb.EmbeddingInput]: Input type for inserting vectors
- Constants
- Variables
- func BuildPayload(internal map[string]any, user map[string]any) map[string]any
- func RegisterQdrantLifecycle(lc fx.Lifecycle, client *QdrantClient)
- type Adapter
- func NewAdapter(client *qdrant.Client) *Adapter
- func (a *Adapter) Delete(ctx context.Context, collectionName string, ids []string) error
- func (a *Adapter) EnsureCollection(ctx context.Context, name string, vectorSize uint64) error
- func (a *Adapter) GetCollection(ctx context.Context, name string) (*vectordb.Collection, error)
- func (a *Adapter) Insert(ctx context.Context, collectionName string, inputs []vectordb.EmbeddingInput) error
- func (a *Adapter) ListCollections(ctx context.Context) ([]string, error)
- func (a *Adapter) Search(ctx context.Context, requests ...vectordb.SearchRequest) ([][]vectordb.SearchResult, []error, error)
- type Config
- func DefaultConfig() *Config
- func FromEndpoint(url string) *Config
- func (c *Config) WithApiKey(key string) *Config
- func (c *Config) WithCompatibilityCheck(enabled bool) *Config
- func (c *Config) WithCompression(enabled bool) *Config
- func (c *Config) WithConnectTimeout(d time.Duration) *Config
- func (c *Config) WithKeepAlive(enabled bool) *Config
- func (c *Config) WithTimeout(d time.Duration) *Config
- type QdrantClient
- type QdrantParams
UserPayloadPrefix is the prefix for user-defined metadata fields. User fields are stored under "custom." in the Qdrant payload.
const UserPayloadPrefix = "custom"FXModule defines the Fx module for the Qdrant client.
This module integrates the Qdrant client into an Fx-based application by providing the client factory and registering its lifecycle hooks.
The module:
- Provides the NewQdrantClient factory function to the dependency injection container, making the client available to other components.
- Provides the NewEmbeddingsStore function, which wraps the client into a higher-level abstraction.
- Invokes RegisterQdrantLifecycle to handle startup/shutdown of the client.
Usage:
app := fx.New(
qdrant.FXModule,
// other modules...
)
Dependencies required by this module: - A qdrant.Config instance must be available in the dependency injection container.
var FXModule = fx.Module("qdrant",
fx.Provide(
NewQdrantClient,
),
fx.Invoke(RegisterQdrantLifecycle),
)func BuildPayload
func BuildPayload(internal map[string]any, user map[string]any) map[string]anyBuildPayload creates a Qdrant payload with separated internal and user fields. Internal fields are stored at the top level, while user fields are stored under the "custom" prefix.
Example:
payload := BuildPayload(
map[string]any{"search_store_id": "store123"},
map[string]any{"document_id": "doc456"},
)
// Result: {"search_store_id": "store123", "custom": {"document_id": "doc456"}}
func RegisterQdrantLifecycle(lc fx.Lifecycle, client *QdrantClient)RegisterQdrantLifecycle handles startup/shutdown of the Qdrant client. It ensures proper resource cleanup and logging.
OnStart:
- Performs a Qdrant health check to verify connectivity.
- Logs a success message once the client is ready.
OnStop:
- Ensures the Qdrant client is closed exactly once.
- Logs a shutdown message.
type Adapter
Adapter implements vectordb.Service for Qdrant. It wraps a Qdrant client and converts between generic vectordb types and Qdrant-specific protobuf types.
This is the recommended way to use Qdrant - it provides a database-agnostic interface that allows switching between different vector databases.
type Adapter struct {
// contains filtered or unexported fields
}func NewAdapter
func NewAdapter(client *qdrant.Client) *AdapterNewAdapter creates a new Qdrant adapter for the vectordb interface. Pass the underlying SDK client via QdrantClient.Client().
Example:
qc, _ := qdrant.NewQdrantClient(params)
adapter := qdrant.NewAdapter(qc.Client())
var db vectordb.Service = adapter
func (*Adapter) Delete
func (a *Adapter) Delete(ctx context.Context, collectionName string, ids []string) errorDelete removes points by their IDs from a collection.
func (*Adapter) EnsureCollection
func (a *Adapter) EnsureCollection(ctx context.Context, name string, vectorSize uint64) errorEnsureCollection creates a collection if it doesn't exist.
func (*Adapter) GetCollection
func (a *Adapter) GetCollection(ctx context.Context, name string) (*vectordb.Collection, error)GetCollection retrieves metadata about a collection.
func (*Adapter) Insert
func (a *Adapter) Insert(ctx context.Context, collectionName string, inputs []vectordb.EmbeddingInput) errorInsert adds embeddings to a collection using batch processing.
func (*Adapter) ListCollections
func (a *Adapter) ListCollections(ctx context.Context) ([]string, error)ListCollections returns names of all collections.
func (*Adapter) Search
func (a *Adapter) Search(ctx context.Context, requests ...vectordb.SearchRequest) ([][]vectordb.SearchResult, []error, error)Search performs similarity search across one or more requests.
type Config
Config holds connection and behavior settings for the Qdrant client.
It is intentionally minimal, readable, and easy to override from environment variables, YAML, or programmatically via helper methods.
Example (programmatic):
cfg := qdrant.DefaultConfig()
cfg.Endpoint = "http://localhost:6334"
cfg.ApiKey = os.Getenv("QDRANT_API_KEY")
cfg.Timeout = 10 * time.Second
Example (builder style):
cfg := qdrant.FromEndpoint("http://localhost:6334").
WithApiKey(os.Getenv("QDRANT_API_KEY")).
WithTimeout(10 * time.Second)
type Config struct {
// Hostname of the Qdrant server, e.g. "localhost".
Endpoint string `yaml:"endpoint" env:"QDRANT_ENDPOINT"`
// gRPC port of the Qdrant server. Defaults to 6334.
Port int `yaml:"port" env:"QDRANT_PORT"`
// Optional authentication token for secured deployments.
ApiKey string `yaml:"api_key" env:"QDRANT_API_KEY"`
// Maximum request duration before timing out.
Timeout time.Duration `yaml:"timeout" env:"QDRANT_TIMEOUT"`
// Connection establishment timeout.
ConnectTimeout time.Duration `yaml:"connect_timeout" env:"QDRANT_CONNECT_TIMEOUT"`
// Whether to keep idle connections open for reuse.
KeepAlive bool `yaml:"keep_alive" env:"QDRANT_KEEP_ALIVE"`
// Enable gzip compression for requests.
Compression bool `yaml:"compression" env:"QDRANT_COMPRESSION"`
// Whether to perform version compatibility checks between client and server.
CheckCompatibility bool `yaml:"check_compatibility" env:"QDRANT_CHECK_COMPATIBILITY"`
}func DefaultConfig
func DefaultConfig() *ConfigDefaultConfig provides sensible defaults for most use cases.
func FromEndpoint
func FromEndpoint(url string) *ConfigFromEndpoint returns a default config pre-filled with a specific endpoint.
func (*Config) WithApiKey
func (c *Config) WithApiKey(key string) *ConfigBuilder-style helpers (optional, ergonomic)
func (*Config) WithCompatibilityCheck
func (c *Config) WithCompatibilityCheck(enabled bool) *Configfunc (*Config) WithCompression
func (c *Config) WithCompression(enabled bool) *Configfunc (*Config) WithConnectTimeout
func (c *Config) WithConnectTimeout(d time.Duration) *Configfunc (*Config) WithKeepAlive
func (c *Config) WithKeepAlive(enabled bool) *Configfunc (*Config) WithTimeout
func (c *Config) WithTimeout(d time.Duration) *Configtype QdrantClient
type QdrantClient struct {
// contains filtered or unexported fields
}func NewQdrantClient
func NewQdrantClient(p QdrantParams) (*QdrantClient, error)NewQdrantClient ────────────────────────────────────────────────────────────── NewQdrantClient ──────────────────────────────────────────────────────────────
NewQdrantClient constructs a new instance of QdrantClient and validates connectivity via a health check.
The Qdrant Go SDK creates lightweight gRPC connections, so this method performs an immediate health check to fail fast if the service is unreachable.
Example:
client, _ := qdrant.NewQdrantClient(qdrant.QdrantParams{Config: cfg})
func (*QdrantClient) Client
func (c *QdrantClient) Client() *qdrant.ClientClient returns the underlying Qdrant SDK client. This is useful for direct access to low-level operations.
func (*QdrantClient) Close
func (c *QdrantClient) Close() errorClose ────────────────────────────────────────────────────────────── Close ──────────────────────────────────────────────────────────────
Close gracefully shuts down the Qdrant client.
Since the official Qdrant Go SDK doesn't maintain persistent connections, this is currently a no-op. It exists for lifecycle symmetry and future safety.
type QdrantParams
QdrantParams defines dependencies needed to construct the Qdrant client.
type QdrantParams struct {
fx.In
Config *Config
}Generated by gomarkdoc