Skip to content

Qdrant

Qdrant is an open-source vector database written in Rust. It offers the most expressive metadata filtering language of any vector database, runs locally in-memory or via Docker, and has a managed cloud option. It's the go-to choice for teams that need advanced filtering or self-hosted control.

Learning objectives

  • Create Qdrant collections with custom HNSW configuration
  • Upsert points with structured payloads
  • Write complex metadata filters using Qdrant's filter DSL
  • Use Qdrant's named vectors for multi-vector collections

Setup

from qdrant_client import QdrantClient
from qdrant_client.models import (
    Distance, VectorParams, PointStruct,
    Filter, FieldCondition, MatchValue, Range, MatchAny
)

# In-memory (no Docker needed, great for testing)
client = QdrantClient(":memory:")

# Local persistent storage
# client = QdrantClient(path="./qdrant_storage")

# Remote (Docker or cloud)
# client = QdrantClient(url="http://localhost:6333")
# client = QdrantClient(url="https://your-cluster.qdrant.io", api_key=os.getenv("QDRANT_API_KEY"))

Creating collections

from qdrant_client.models import HnswConfigDiff

# Basic collection
client.create_collection(
    collection_name="documents",
    vectors_config=VectorParams(
        size=1536,           # dimension of your embeddings
        distance=Distance.COSINE
    )
)

# Collection with custom HNSW settings
client.recreate_collection(
    collection_name="documents_hnsw",
    vectors_config=VectorParams(
        size=1536,
        distance=Distance.COSINE,
        hnsw_config=HnswConfigDiff(
            m=16,                    # connections per node
            ef_construct=200,        # construction quality
            full_scan_threshold=10_000  # use brute force below this size
        )
    )
)

# Check collection info
info = client.get_collection("documents")
print(f"Collection: {info.config.params.vectors}")
print(f"Points: {info.points_count}")

Upserting points

In Qdrant, stored items are called points. Each point has an ID, a vector, and an optional payload (metadata).

import os
from openai import OpenAI
from qdrant_client.models import PointStruct

openai_client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))

def embed(texts: list[str]) -> list[list[float]]:
    resp = openai_client.embeddings.create(model="text-embedding-3-small", input=texts)
    return [item.embedding for item in resp.data]

documents = [
    {"text": "Python is a high-level programming language", "source": "wiki.txt", "category": "tech", "year": 2024, "rating": 4.8},
    {"text": "FastAPI builds APIs with Python and type hints", "source": "docs.txt", "category": "tech", "year": 2024, "rating": 4.5},
    {"text": "The Eiffel Tower is 330m tall in Paris", "source": "travel.txt", "category": "travel", "year": 2023, "rating": 4.9},
    {"text": "RAG combines retrieval with generation", "source": "ai-guide.txt", "category": "ai", "year": 2024, "rating": 4.7},
    {"text": "Machine learning enables statistical modeling", "source": "ml-book.txt", "category": "ai", "year": 2023, "rating": 4.3},
]

embeddings = embed([d["text"] for d in documents])

# Upsert points (integer IDs required in Qdrant, or use UUIDs)
points = [
    PointStruct(
        id=i,
        vector=emb,
        payload={k: v for k, v in doc.items() if k != "text"}
        # payload can contain any JSON-serializable data
    )
    for i, (doc, emb) in enumerate(zip(documents, embeddings))
]

client.upsert(collection_name="documents", points=points)
print(f"Upserted {len(points)} points")

Querying

def search_qdrant(
    query: str,
    collection: str = "documents",
    k: int = 5,
    query_filter: Filter | None = None,
    score_threshold: float | None = None
) -> list[dict]:
    query_emb = embed([query])[0]

    results = client.search(
        collection_name=collection,
        query_vector=query_emb,
        limit=k,
        query_filter=query_filter,
        score_threshold=score_threshold,
        with_payload=True
    )

    return [
        {"id": r.id, "score": r.score, **r.payload}
        for r in results
    ]

# Basic search
results = search_qdrant("Python programming", k=3)
for r in results:
    print(f"[{r['score']:.3f}] id={r['id']}, source={r['source']}")

# Search with score threshold (no results below this)
high_quality = search_qdrant(
    "machine learning AI",
    score_threshold=0.7
)

Qdrant filter language

Qdrant's filter DSL is the most expressive of any vector database.

from qdrant_client.models import (
    Filter, FieldCondition, MatchValue, MatchAny,
    Range, IsNullCondition, IsEmptyCondition
)

# Simple equality
tech_filter = Filter(
    must=[FieldCondition(key="category", match=MatchValue(value="tech"))]
)

# Multiple conditions (AND)
filtered_results = search_qdrant(
    "Python tutorial",
    query_filter=Filter(
        must=[
            FieldCondition(key="category", match=MatchValue(value="tech")),
            FieldCondition(key="year", range=Range(gte=2024))
        ]
    )
)

# OR conditions
multi_category = Filter(
    should=[
        FieldCondition(key="category", match=MatchValue(value="tech")),
        FieldCondition(key="category", match=MatchValue(value="ai"))
    ],
    minimum_should_match=1
)

# Match any in list
tag_filter = Filter(
    must=[
        FieldCondition(
            key="category",
            match=MatchAny(any=["tech", "ai"])
        )
    ]
)

# Range filter
high_rated = Filter(
    must=[
        FieldCondition(key="rating", range=Range(gte=4.5, lt=5.0))
    ]
)

# Combined example: category="ai" AND year>=2024 AND rating>=4.5
combined = Filter(
    must=[
        FieldCondition(key="category", match=MatchValue(value="ai")),
        FieldCondition(key="year", range=Range(gte=2024)),
        FieldCondition(key="rating", range=Range(gte=4.5))
    ]
)

results = search_qdrant("retrieval augmented generation", query_filter=combined)
print(f"Combined filter results: {len(results)}")

CRUD operations

from qdrant_client.models import PointIdsList, PayloadSelector, SetPayload

# Fetch points by ID
fetched = client.retrieve(
    collection_name="documents",
    ids=[0, 1, 2],
    with_payload=True,
    with_vectors=False
)
for point in fetched:
    print(f"id={point.id}: {point.payload}")

# Update payload only (without re-embedding)
client.set_payload(
    collection_name="documents",
    payload={"status": "verified", "updated_at": "2025-05-23"},
    points=[0, 1]
)

# Delete specific points
client.delete(
    collection_name="documents",
    points_selector=PointIdsList(points=[4])
)

# Delete by filter
client.delete(
    collection_name="documents",
    points_selector=Filter(
        must=[FieldCondition(key="category", match=MatchValue(value="travel"))]
    )
)

print(f"Remaining: {client.count('documents').count}")

# Scroll through all points (useful for data export)
all_points, offset = [], None
while True:
    batch, offset = client.scroll(
        collection_name="documents",
        limit=100,
        offset=offset,
        with_payload=True
    )
    all_points.extend(batch)
    if offset is None:
        break

print(f"All points: {len(all_points)}")

Payload indexing for fast filtering

By default, Qdrant scans all points for metadata filters. For large collections (>100K points), create payload indexes for frequently filtered fields.

from qdrant_client.models import PayloadSchemaType

# Index a keyword field for fast exact matching
client.create_payload_index(
    collection_name="documents",
    field_name="category",
    field_schema=PayloadSchemaType.KEYWORD
)

# Index a numeric field for fast range queries
client.create_payload_index(
    collection_name="documents",
    field_name="year",
    field_schema=PayloadSchemaType.INTEGER
)

# Index a float field for rating queries
client.create_payload_index(
    collection_name="documents",
    field_name="rating",
    field_schema=PayloadSchemaType.FLOAT
)

# List indexes
info = client.get_collection("documents")
print(f"Payload schema: {info.payload_schema}")

Always index high-cardinality filter fields

For a collection of 500K documents with category filter used on 90% of queries, a keyword index reduces filter time from O(N) to O(log N). The index is built once and maintained incrementally.


Qdrant supports multiple vectors per point — useful when you want to embed different representations of the same document (e.g., title + body separately).

from qdrant_client.models import VectorParams, Distance

# Create collection with multiple named vector fields
client.recreate_collection(
    collection_name="multi_vec_docs",
    vectors_config={
        "title": VectorParams(size=1536, distance=Distance.COSINE),
        "body": VectorParams(size=1536, distance=Distance.COSINE),
    }
)

# Upsert with named vectors
title_embs = embed(["Python language overview"])
body_embs = embed(["Python is a high-level language. It supports OOP, functional, and procedural paradigms..."])

client.upsert(
    collection_name="multi_vec_docs",
    points=[
        PointStruct(
            id=1,
            vector={"title": title_embs[0], "body": body_embs[0]},
            payload={"source": "python-wiki.txt"}
        )
    ]
)

# Search by a specific named vector
results = client.search(
    collection_name="multi_vec_docs",
    query_vector=("body", embed(["programming paradigms"])[0]),
    limit=3
)

03-pinecone | 05-indexing-and-filtering