n4nAI

Using Qdrant as a Haystack document store

A step-by-step guide to wiring Qdrant as a Haystack document store, from Docker setup through hybrid retrieval with metadata filtering.

n4n Team3 min read738 words

Audio narration

Coming soon — every post will get a voice note here.

This qdrant haystack document store tutorial walks you through the complete integration: spinning up Qdrant, configuring the Haystack document store, indexing documents with embeddings, and running dense, sparse, and hybrid retrieval pipelines. By the end you’ll have a production-ready retrieval backbone you can drop into any RAG system.

Step 1: Spin up Qdrant

Qdrant runs as a single container with no external dependencies. For local development, start it with Docker:

docker run -d \
  --name qdrant \
  -p 6333:6333 \
  -p 6334:6334 \
  -v $(pwd)/qdrant_storage:/qdrant/storage \
  qdrant/qdrant:v1.10.0

The HTTP API listens on 6333; gRPC on 6334. The volume mount persists collections across restarts. Verify the instance is healthy:

curl http://localhost:6333/healthz
# {"status":"ok"}

If you prefer a managed instance, Qdrant Cloud offers a free tier with 1 GB storage — just swap the host and add an API key in the next step.

Step 2: Install the Haystack Qdrant integration

Haystack keeps document store implementations in separate packages. Install the Qdrant backend plus an embedding model:

pip install haystack-ai qdrant-haystack sentence-transformers

haystack-ai (the 2.x line) is the current package name; the legacy farm-haystack is deprecated. Pin versions in your requirements file to avoid surprise upgrades:

haystack-ai==2.5.0
qdrant-haystack==1.4.0
sentence-transformers==3.0.1

Step 3: Configure the document store

Create a document_store.py module to centralize configuration. This keeps connection logic out of your pipeline code and makes testing easier.

# document_store.py
from haystack_integrations.document_stores.qdrant import QdrantDocumentStore

def get_document_store(
    host: str = "localhost",
    port: int = 6333,
    index: str = "documents",
    embedding_dim: int = 384,
    recreate_index: bool = False,
) -> QdrantDocumentStore:
    """
    Returns a configured QdrantDocumentStore.

    Args:
        host: Qdrant host (use your cloud URL for managed).
        port: HTTP port.
        index: Collection name in Qdrant.
        embedding_dim: Must match your embedder output dimension.
        recreate_index: Drop and recreate collection on init (dev only).
    """
    return QdrantDocumentStore(
        host=host,
        port=port,
        index=index,
        embedding_dim=embedding_dim,
        recreate_index=recreate_index,
        # Optional: enable on-disk payload storage for large metadata
        on_disk_payload=True,
        # Optional: hnsw config for larger collections
        hnsw_config={"m": 16, "ef_construct": 100},
    )

Verify: Run a quick smoke test in a REPL:

from document_store import get_document_store
ds = get_document_store(recreate_index=True)
print(ds.count_documents())  # 0

Step 4: Write documents with embeddings

Haystack expects Document objects with content and optional meta. The document store handles embedding storage automatically when you pass an embedder to write_documents.

# index.py
from haystack import Document
from haystack.components.embedders import SentenceTransformersDocumentEmbedder
from document_store import get_document_store

# Sample corpus — replace with your data loader
raw_docs = [
    Document(content="Qdrant is a vector database written in Rust.", meta={"source": "wiki", "topic": "database"}),
    Document(content="Haystack 2.x uses a component-based pipeline architecture.", meta={"source": "docs", "topic": "framework"}),
    Document(content="Sentence Transformers provides state-of-the-art sentence embeddings.", meta={"source": "blog", "topic": "embeddings"}),
]

# Initialize embedder (384-dim all-MiniLM-L6-v2)
embedder = SentenceTransformersDocumentEmbedder(model="sentence-transformers/all-MiniLM-L6-v2")
embedder.warm_up()

# Embed documents
docs_with_embeddings = embedder.run(raw_docs)["documents"]

# Write to Qdrant
ds = get_document_store(recreate_index=True)
ds.write_documents(docs_with_embeddings)

print(f"Indexed {ds.count_documents()} documents")

Verify: Check the Qdrant dashboard at http://localhost:6333/dashboard — you should see a collection named documents with three points, each carrying a 384-dim vector and your metadata.

Step 5: Dense retrieval with the Qdrant retriever

Haystack’s QdrantEmbeddingRetriever wraps the vector search. Wire it into a pipeline for reusable query logic.

# retrieve_dense.py
from haystack import Pipeline
from haystack.components.embedders import SentenceTransformersTextEmbedder
from haystack_integrations.components.retrievers.qdrant import QdrantEmbeddingRetriever
from document_store import get_document_store

ds = get_document_store()

query_embedder = SentenceTransformersTextEmbedder(model="sentence-transformers/all-MiniLM-L6-v2")
retriever = QdrantEmbeddingRetriever(document_store=ds, top_k=3)

pipe = Pipeline()
pipe.add_component("embedder", query_embedder)
pipe.add_component("retriever", retriever)
pipe.connect("embedder.embedding", "retriever.query_embedding")

question = "What vector database is written in Rust?"
result = pipe.run({"embedder": {"text": question}})

for doc in result["retriever"]["documents"]:
    print(f"score={doc.score:.3f} | {doc.content[:80]}... | meta={doc.meta}")

Verify: The top result should be the Qdrant document with a score > 0.7. Adjust top_k and hnsw_config.ef (search-time ef) for recall/latency tradeoffs.

Step 6: Sparse retrieval with BM25

Qdrant supports sparse vectors natively. Haystack exposes this through QdrantSparseEmbeddingRetriever paired with a sparse embedder like FastEmbedSparseDocumentEmbedder (requires fastembed).

pip install fastembed
# index_sparse.py
from haystack import Document
from haystack_integrations.components.embedders.fastembed import FastEmbedSparseDocumentEmbedder
from document_store import get_document_store

raw_docs = [
    Document(content="Qdrant vector database Rust performance", meta={"id": 1}),
    Document(content="Haystack pipeline components retriever generator", meta={"id": 2}),
]

sparse_embedder = FastEmbedSparseDocumentEmbedder(model="Qdrant/bm25")
sparse_embedder.warm_up()
docs_sparse = sparse_embedder.run(raw_docs)["documents"]

ds = get_document_store(index="sparse_docs", embedding_dim=0)  # dim=0 for sparse-only
ds.write_documents(docs_sparse)

Then retrieve:

# retrieve_sparse.py
from haystack import Pipeline
from haystack_integrations.components.embedders.fastembed import FastEmbedSparseTextEmbedder
from haystack_integrations.components.retrievers.qdrant import QdrantSparseEmbeddingRetriever
from document_store import get_document_store

ds = get_document_store(index="sparse_docs", embedding_dim=0)

pipe = Pipeline()
pipe.add_component("embedder", FastEmbedSparseTextEmbedder(model="Qdrant/bm25"))
pipe.add_component("retriever", QdrantSparseEmbeddingRetriever(document_store=ds, top_k=3))
pipe.connect("embedder.sparse_embedding", "retriever.query_sparse_embedding")

result = pipe.run({"embedder": {"text": "vector database Rust"}})
for doc in result["retriever"]["documents"]:
    print(f"score={doc.score:.3f} | {doc.content}")

Step 7: Hybrid retrieval (dense + sparse)

Production RAG systems almost always benefit from hybrid search. Qdrant supports fused scoring via prefetch — Haystack exposes this through QdrantHybridRetriever.

# retrieve_hybrid.py
from haystack import Pipeline
from haystack.components.embedders import SentenceTransformersTextEmbedder
from haystack_integrations.components.embedders.fastembed import FastEmbedSparseTextEmbedder
from haystack_integrations.components.retrievers.qdrant import QdrantHybridRetriever
from document_store import get_document_store

# Collection must have both dense and sparse vectors
ds = get_document_store(index="hybrid_docs", embedding_dim=384)

pipe = Pipeline()
pipe.add_component("dense_embedder", SentenceTransformersTextEmbedder(model="sentence-transformers/all-MiniLM-L6-v2"))
pipe.add_component("sparse_embedder", FastEmbedSparseTextEmbedder(model="Qdrant/bm25"))
pipe.add_component("retriever", QdrantHybridRetriever(document_store=ds, top_k=5))

pipe.connect("dense_embedder.embedding", "retriever.query_embedding")
pipe.connect("sparse_embedder.sparse_embedding", "retriever.query_sparse_embedding")

result = pipe.run({
    "dense_embedder": {"text": "Rust vector database"},
    "sparse_embedder": {"text": "Rust vector database"},
})

for doc in result["retriever"]["documents"]:
    print(f"score={doc.score:.3f} | {doc.content[:100]}")

The hybrid retriever runs a prefetch for each vector type and fuses results using Reciprocal Rank Fusion (RRF) by default. You can tune fusion_type (“rrf” | “dbsf”) and fusion_params in the retriever constructor.

Step 8: Metadata filtering

Qdrant’s payload filtering is a major advantage over pure ANN indexes. Haystack passes filters dictionaries directly to Qdrant’s Filter DSL.

# filter_example.py
from haystack import Pipeline
from haystack.components.embedders import SentenceTransformersTextEmbedder
from haystack_integrations.components.retrievers.qdrant import QdrantEmbeddingRetriever
from document_store import get_document_store

ds = get_document_store()

pipe = Pipeline()
pipe.add_component("embedder", SentenceTransformersTextEmbedder(model="sentence-transformers/all-MiniLM-L6-v2"))
pipe.add_component("retriever", QdrantEmbeddingRetriever(document_store=ds, top_k=3))
pipe.connect("embedder.embedding", "retriever.query_embedding")

# Filter: only docs where meta.topic == "database"
filters = {
    "operator": "AND",
    "conditions": [
        {"field": "meta.topic", "operator": "==", "value": "database"}
    ]
}

result = pipe.run({
    "embedder": {"text": "vector search"},
    "retriever": {"filters": filters}
})

for doc in result["retriever"]["documents"]:
    print(f"score={doc.score:.3f} | topic={doc.meta.get('topic')} | {doc.content[:80]}")

Verify: Only the Qdrant document (topic=database) should return, even if other documents have higher vector similarity.

Filter syntax cheat sheet

Operator Haystack dict Qdrant equivalent
Equality {"field": "meta.tag", "operator": "==", "value": "prod"} FieldCondition(key="meta.tag", match=MatchValue(value="prod"))
In {"field": "meta.ids", "operator": "in", "value": [1,2,3]} MatchAny(any=[1,2,3])
Range {"field": "meta.date", "operator": ">=", "value": "2024-01-01"} Range(gte="2024-01-01")
Geo {"field": "meta.loc", "operator": "geo_radius", "value": {"lat": 40.7, "lon": -74.0, "radius": 1000}} GeoRadius(center=..., radius=...)

Nested metadata works automatically — use dot notation: meta.author.name.

Step 9: Update and delete documents

Qdrant supports upsert by point ID. Haystack’s write_documents with policy=DuplicatePolicy.OVERWRITE handles this, but you need stable IDs.

# update_delete.py
from haystack import Document
from haystack_integrations.document_stores.qdrant import DuplicatePolicy
from document_store import get_document_store

ds = get_document_store()

# Assign explicit IDs
docs = [
    Document(id="doc-1", content="Updated content", meta={"version": 2}),
    Document(id="doc-2", content="Another doc", meta={"version": 1}),
]

# Upsert
ds.write_documents(docs, policy=DuplicatePolicy.OVERWRITE)

# Delete by ID
ds.delete_documents(ids=["doc-2"])

print(f"Remaining: {ds.count_documents()}")  # 1

Verify: ds.get_documents_by_id(["doc-1"]) returns the updated document; doc-2 raises DocumentNotFoundError.

Step 10: Production hardening

Connection pooling and timeouts

The underlying qdrant-client uses httpx. Configure a shared client for connection reuse:

# document_store_prod.py
import httpx
from haystack_integrations.document_stores.qdrant import QdrantDocumentStore

http_client = httpx.Client(
    limits=httpx.Limits(max_connections=50, max_keepalive_connections=10),
    timeout=httpx.Timeout(connect=5.0, read=30.0, write=10.0, pool=5.0),
)

def get_prod_document_store(**kwargs) -> QdrantDocumentStore:
    return QdrantDocumentStore(
        http_client=http_client,
        prefer_grpc=True,  # lower latency for high throughput
        **kwargs,
    )

Quantization for large collections

For collections > 1M vectors, enable scalar or binary quantization to cut memory and latency:

ds = QdrantDocumentStore(
    index="large_corpus",
    embedding_dim=768,
    quantization_config={
        "scalar": {"type": "int8", "quantile": 0.99, "always_ram": True}
    },
    # Or binary for 32x compression (requires normalized vectors)
    # quantization_config={"binary": {"always_ram": True}},
)

Monitoring

Expose Qdrant metrics (/metrics) to Prometheus. Key series:

  • qdrant_collection_points_total — index size
  • qdrant_request_duration_seconds_bucket — p50/p99 latency
  • qdrant_optimization_status — background indexing health

Set alerts on p99 > 500ms or optimization stuck > 10min.

Verification checklist

Run through this list before calling the integration done:

  • Qdrant health endpoint returns {"status":"ok"}
  • document_store.count_documents() matches expected corpus size
  • Dense retriever returns relevant results for known queries
  • Sparse retriever surfaces keyword matches dense misses
  • Hybrid retriever outperforms either alone on your eval set
  • Metadata filters correctly include/exclude documents
  • Upsert by ID updates without creating duplicates
  • Delete by ID removes documents cleanly
  • Connection pool handles concurrent pipeline runs without socket exhaustion
  • Quantization (if enabled) maintains recall > 0.95 on your test set

Where to go next

  • Reranking: Add a TransformersSimilarityRanker or CohereRanker after retrieval for precision gains.
  • Generative pipeline: Wire the retriever into a PromptBuilderGenerator loop for full RAG.
  • Multi-tenancy: Use meta.tenant_id filters with a single collection, or separate collections per tenant for strict isolation.
  • Streaming updates: Pair Qdrant’s write-ahead log with a CDC pipeline (Debezium → Kafka → Qdrant) for near-realtime indexing.

The Qdrant-Haystack integration is mature enough for production workloads. The main operational lever is collection sizing — monitor memory, tune HNSW m/ef_construct, and enable quantization before you hit resource limits.

Tagshaystackqdrantdocument-storevector-search

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All haystack document stores & retrievers posts →