← Stack

Qdrant

pgvector is the right start. Qdrant earns its place when filtering complexity, corpus scale, or data sovereignty outgrow what a Postgres extension can deliver.

Overview

Qdrant is an Apache 2.0 vector database written in Rust, purpose-built for similarity search with metadata filtering. It is developed by Qdrant Solutions GmbH, headquartered in Berlin, Germany, and has raised $87.8 M across four funding rounds. Each record is a point: a unique id, a dense vector (1-4096 dimensions), and an optional JSON payload. Points live in collections, the equivalent of tables. The database is available as a Docker container for self-hosted deployments and as Qdrant Cloud for managed hosting.

The right question is not whether to use Qdrant or pgvector in the abstract. pgvector is the correct default when your vector corpus lives inside an existing Postgres database, your queries are similarity-only, and SQL joins or transactional writes matter. Qdrant earns its place when three signals appear: metadata filtering complexity (queries combining vector similarity with selective payload conditions degrade pgvector at scale), memory pressure at corpus size (TurboQuant and memory-mapped storage handle large corpora on modest RAM), and a p99 latency SLA that cannot tolerate the spikes pgvector shows under concurrent load past 1-5 M vectors. Start with pgvector, measure your corpus and query patterns, and graduate when you hit a real limit.

The EU data-sovereignty argument is concrete for Qdrant. Qdrant Solutions GmbH is incorporated in Germany, which removes the CLOUD Act exposure that applies to any US-headquartered provider regardless of where data physically sits. Apache 2.0 licensing means no per-query pricing, no vendor lock-in, and no runtime dependency on a commercial cloud. A production Qdrant instance runs in a single Docker container on Hetzner (ISO 27001, GDPR-compliant) or Scaleway fr-par, the same infrastructure suited for self-hosted model inference. For teams building GDPR-sensitive RAG pipelines or agent memory stores, this combination delivers dedicated vector search within EU-only coordinates, with no US-cloud dependency. See the sovereign-ai pillar for the full EU data-jurisdiction decision tree.

Architecture

Qdrant organizes data into collections: named, isolated namespaces for sets of points. Each point carries a unique id, a dense vector, and an optional JSON payload of arbitrary keys. The storage engine is HNSW, with a key modification: payload indexes created before data ingestion inform the HNSW graph with filter-aware edges. A query filtered to 0.5% of the corpus navigates a relevant sub-graph rather than scanning all candidates and discarding the rest. Quantization (scalar, binary, TurboQuant) trades a controlled recall loss for dramatic memory reduction: TurboQuant at 4-bit delivers similar recall to scalar quantization (SQ8) at roughly 2x lower memory footprint. Sharding and replication layer on top for distributed scale and HA.

pgvectorPostgres already in stackSQL joins requiredcorpus < 1M vectorstransactional writes neededgraduate when →Qdrantheavy payload filteringcorpus > 1M vectorsp99 SLA under loadEU self-host, no cloud dep
pgvector vs Qdrant: key signals for staying with pgvector (left) vs graduating to Qdrant (right)

Key concepts

Collection
A named namespace in Qdrant for a set of points, analogous to a table. Each collection has its own vector dimensions, distance metric, and payload index configuration.
Point
The atomic record in Qdrant: a unique id (integer or UUID) + a dense vector (1-4096 floats) + an optional JSON payload. Points are upserted into collections and searched by vector similarity, optionally filtered by payload.
Payload
Arbitrary JSON metadata attached to a point. A payload key is filterable only if a payload index exists for that key. Types supported: keyword, integer, float, text (full-text), geo.
Payload index
An index on a payload key that enables fast pre-filtering during vector search. Create payload indexes BEFORE loading data: Qdrant incorporates filter-aware edges into the HNSW graph at build time. Adding indexes after ingestion triggers a graph rebuild.
HNSW (filter-aware)
Qdrant builds HNSW with edges that are aware of payload index values. A filtered search navigates only the sub-graph matching the filter condition, so latency scales with the result set, not the full corpus. pgvector applies filters post-search, which degrades latency when filters are selective.
TurboQuant
Qdrant's rotation-based quantization algorithm (v1.18, adapted from Google Research). Applies a Hadamard rotation before 4-bit (or lower) compression to redistribute vector values evenly. At 4-bit, delivers similar recall to scalar quantization (SQ8) at roughly 2x less memory.
Shard
A horizontal partition of a collection distributed across nodes for scale-out. Qdrant handles shard routing internally; no external coordinator (etcd, ZooKeeper) is required for single-node or small cluster deployments.

When to use

Good fit

  • Heavy metadata filtering: queries combine vector similarity with selective payload conditions (department, date range, tenant id, status). Qdrant's filter-aware HNSW navigates a sub-graph matching the filter; pgvector post-filters and must over-fetch candidates when the filter is selective.
  • Large corpus with a p99 latency SLA: at 1-5M+ vectors under concurrent load, pgvector p95 latency climbs past 100ms in many configurations. Qdrant's dedicated storage engine and quantization keep tail latency more stable.
  • Memory-constrained infrastructure: TurboQuant and memory-mapped storage handle corpora too large for RAM at full float32 precision. 1M 1536-dim vectors in TurboQuant 4-bit requires roughly 1.2 GB vs 6 GB uncompressed.
  • EU self-hosted stack with no US-cloud dependency: Qdrant Solutions GmbH is a German company, Apache 2.0, and ships as a single Docker container. Self-hosted on Hetzner or Scaleway fr-par, it has zero CLOUD Act exposure and no per-query pricing.
  • Agent memory with high write/delete churn: Qdrant supports efficient upsert and delete cycles, enabling an agent to maintain a rolling memory window without the scan degradation that comes from accumulating tombstoned vectors in append-only stores.
  • Multi-tenant RAG: a payload filter on a tenant_id keyword index isolates tenant data within a shared collection, without separate physical indexes per tenant. Add the tenant_id payload index before ingestion for filter-aware graph edges.

Anti-patterns

  • SQL joins and relational queries: Qdrant is a vector store, not a relational database. If you need to join vector search results with relational tables in a single query, pgvector running inside Postgres handles this natively. Qdrant requires fetching ids and joining in application code.
  • Transactional consistency across vector + relational writes: if an upsert to the vector store must succeed or fail atomically with a relational write, pgvector in Postgres is the correct choice. Qdrant has no ACID transaction across collections or external systems.
  • Small corpus with simple similarity-only queries: corpus under 200K vectors with no compound filtering is well within pgvector's comfort zone. Adding Qdrant as a separate service incurs operational overhead for no measurable benefit at this scale.
  • No production signal yet: start with pgvector, instrument p95/p99 query latency and filter selectivity, and graduate when you observe degradation on real queries. Adding Qdrant before you have measured limits adds operational surface for uncertain gain.

Code examples

Collection setup: create indexes before data ingestion, then upsert and search

# requires qdrant-client>=1.10
import numpy as np
from qdrant_client import QdrantClient
from qdrant_client.models import (
    Distance, VectorParams, PointStruct, PayloadSchemaType,
)

VECTOR_SIZE = 1536  # OpenAI text-embedding-3-small / Cohere embed

# Connect to a local Qdrant instance (Docker: qdrant/qdrant on port 6333)
client = QdrantClient(url="http://localhost:6333")

# Create a collection for 1536-dim embeddings, cosine distance
client.create_collection(
    collection_name="documents",
    vectors_config=VectorParams(size=VECTOR_SIZE, distance=Distance.COSINE),
)

# Create payload indexes BEFORE loading data.
# Qdrant builds filter-aware HNSW edges from existing payload indexes at
# build time. Filtering still works if you add an index later, but the
# filter-aware graph is then rebuilt (expensive on a large corpus).
client.create_payload_index(
    collection_name="documents",
    field_name="department",
    field_schema=PayloadSchemaType.KEYWORD,
)
client.create_payload_index(
    collection_name="documents",
    field_name="year",
    field_schema=PayloadSchemaType.INTEGER,
)

def fake_embedding() -> list[float]:
    # Placeholder for your model call (OpenAI, Cohere, local): text -> vector.
    # Returns a normalized 1536-dim vector so the snippet runs as-is.
    v = np.random.rand(VECTOR_SIZE)
    return (v / np.linalg.norm(v)).tolist()

# Upsert points: id + vector + arbitrary JSON payload
client.upsert(
    collection_name="documents",
    points=[
        PointStruct(
            id=1,
            vector=fake_embedding(),
            payload={"department": "finance", "year": 2025, "title": "Q4 report"},
        ),
        PointStruct(
            id=2,
            vector=fake_embedding(),
            payload={"department": "legal", "year": 2024, "title": "Contract template"},
        ),
    ],
)

# Plain similarity search (query_points replaces the removed search method)
results = client.query_points(
    collection_name="documents",
    query=fake_embedding(),
    limit=10,
).points
for hit in results:
    print(hit.id, hit.score, hit.payload)

The order matters: create payload indexes before upserting points. Qdrant builds filter-aware HNSW edges at index-build time from existing payload indexes. You can still add an index later and filtering will work, but the filter-aware graph is then rebuilt, which is expensive on a large collection.

Filtered search: the key differentiator from pgvector

# continues from the setup snippet (same client and fake_embedding helper)
from qdrant_client.models import Filter, FieldCondition, MatchValue, Range

# Filter-aware HNSW: Qdrant navigates only the sub-graph matching the filter.
# pgvector post-filters (fetch N candidates, discard non-matching ones).
# When the filter is selective (<10% of corpus matches), pgvector must
# over-fetch to find enough results: latency spikes under concurrent load.
# Qdrant keeps latency proportional to the result set, not the corpus.

results = client.query_points(
    collection_name="documents",
    query=fake_embedding(),  # your real query embedding here
    query_filter=Filter(
        must=[
            FieldCondition(
                key="department",
                match=MatchValue(value="finance"),
            ),
            FieldCondition(
                key="year",
                range=Range(gte=2024, lte=2025),
            ),
        ]
    ),
    limit=10,
).points

# The same Filter works in scroll (batch retrieval by payload condition)
# and in count (cardinality estimate before the search).
count = client.count(
    collection_name="documents",
    count_filter=Filter(
        must=[FieldCondition(key="department", match=MatchValue(value="finance"))]
    ),
    exact=True,
)
print(f"finance docs: {count.count}")

The Filter navigates a sub-graph of points matching the conditions. Without payload indexes on department and year, Qdrant falls back to post-filtering and matches pgvector behavior. The gain is largest when the filter is selective: a filter matching 0.5% of the corpus is where the post-filter approach hurts most.

Comparison

vs pgvector

Postgres extension vs dedicated vector database, filtered search, joins, transactions

pgvector is the right default: it runs inside Postgres, costs no extra infra, and handles SQL joins and transactional writes alongside vector search. Its HNSW index post-filters: it searches the full graph, then applies the WHERE condition. When the filter is selective (matching 0.5% of the corpus), the index must over-fetch before discarding, which spikes latency under load. At 5M vectors, pgvector p95 latency can climb to 80-140ms depending on ef_search settings. pgvector 0.8.0+ added an iterative index scan that partially addresses this, but multi-key compound filters still show degradation. Graduate to Qdrant when filtering is selective, the corpus is large, or a p99 SLA is non-negotiable. Sources: qdrant.tech/documentation, markaicode.com.

vs Weaviate

Language runtime, memory footprint, multi-modal support

Weaviate is written in Go, which introduces GC pauses under heavy concurrent write workloads. Qdrant's Rust runtime has no GC. Benchmarks (2025) show Qdrant using 2-3x less memory than Go-based vector databases for equivalent dataset sizes, which matters on resource-constrained self-hosted nodes. Weaviate offers a rich GraphQL API and first-class multi-modal support (text + image in the same collection) out of the box, making it a better fit when the pipeline mixes modalities. For a lean EU-hosted deployment focused on text RAG or agent memory, Qdrant's resource profile wins. Sources: blog.elest.io, zilliz.com/comparison.

vs Milvus

Scale ceiling, operational complexity, deployment model

Milvus (Go + C++) is designed for hundreds of millions to billions of vectors. Its distributed architecture requires a Kubernetes cluster with multiple stateful services (etcd, MinIO, query nodes, data nodes): significantly higher operational overhead than a single Qdrant Docker container. Milvus makes sense when the corpus reaches hundreds of millions of vectors and its specific indexing levers (DISKANN, IVF_FLAT, IVF_PQ) and compaction pipeline are needed. For the 1M-100M range that covers most RAG and agent-memory workloads, Qdrant handles the load at far lower operational cost. Sources: kunalganglani.com, milvus.io.

Resources

FAQ

When exactly should I replace pgvector with Qdrant?
The clearest signal is metadata filtering selectivity: if your queries combine vector similarity with a condition that matches less than 10-20% of your corpus (a tenant filter, a date range, a document type), pgvector's post-filtering approach must over-fetch candidates to find enough that pass. The second signal is corpus size under concurrent load: above 1-5M vectors, pgvector p95 latency climbs in many production configurations. A practical approach: deploy pgvector first, instrument p95/p99 latency and observe whether selective filters spike latency, and graduate to Qdrant when you see degradation on real queries, not projected ones.
Is self-hosted Qdrant GDPR-compliant for EU deployments?
Qdrant Solutions GmbH is incorporated in Germany. Running Qdrant on EU-only infrastructure (Hetzner, Scaleway) means your vector data never leaves EU jurisdiction and avoids the CLOUD Act exposure that applies to US-headquartered providers regardless of physical data location. GDPR compliance remains your responsibility as the data controller: you must ensure a lawful basis for processing, data-processing agreements with any sub-processors, and appropriate retention controls. Self-hosted Qdrant gives you the infrastructure layer; the compliance controls are yours to implement. See the sovereign-ai pillar for the full EU data-jurisdiction framework.
How does Qdrant filtering differ from pgvector's WHERE clause?
pgvector applies WHERE filters after the HNSW traversal: it retrieves N candidates from the graph, then discards the ones that do not match. When the filter is selective, the database must fetch many more than N candidates to find enough matching ones, inflating both latency and CPU use. Qdrant builds filter-aware edges into the HNSW graph when payload indexes exist at build time: a filtered query navigates a sub-graph already constrained to matching points, so the number of graph nodes visited scales with the result set, not the corpus. The practical implication: create payload indexes before loading data, because Qdrant only incorporates filter awareness into HNSW at index-build time. Adding indexes after ingestion triggers a rebuild.
What is TurboQuant and should I use it?
TurboQuant (Qdrant 1.18, adapted from Google Research) is a rotation-based quantization method. It applies a Hadamard rotation to vector values before 4-bit (or lower) compression, which distributes energy more evenly across coordinates and reduces the recall penalty of quantization. At 4-bit, TurboQuant delivers similar recall to scalar quantization (SQ8) at roughly 2x less memory. Use it when your corpus is too large for RAM at full float32 precision and you want to avoid the recall degradation of binary quantization. Start with bits4 (the default) and benchmark recall on your specific corpus before moving to bits2 or bits1.
Can I run Qdrant on the same server as my agent orchestration stack?
Yes. Qdrant runs as a single Docker container, listening on port 6333 (REST) or 6334 (gRPC). For a corpus under 5M vectors, an 8 GB RAM instance handles both the vector store and an orchestration process without resource contention. At larger scales, dedicate a separate node to Qdrant so latency stays predictable under concurrent search load. With quantization, memory requirements drop substantially: 5M 1536-dim vectors in TurboQuant 4-bit fits in roughly 6 GB, making co-location viable on a modest dedicated node.