Lesson 16 / 28

Filtering in a Dedicated Engine: Qdrant

See payload filters in an engine built around them.

Payload filters are first-class

Qdrant is an open-source vector database written in Rust. Its model: a collection of points, each with an ID, one or more vectors, and a JSON payload. You create a collection with the vector size and distance metric, upsert points, and query with a vector plus an optional filter built from must, should and must_not conditions on payload fields (match, range, geo, and more). Qdrant's HNSW search is filter-aware: it evaluates the payload conditions while traversing the graph (and you should create payload indexes on fields you filter on), which avoids the empty-result trap of naive post-filtering. The Python client also has a local in-memory mode, used here, that exposes the same API without running a server, which is excellent for tests.

Qdrant: upsert, search, filter, delete, run

I ran this Python in a virtual environment with qdrant-client 1.19.1 (local in-memory mode) and chromadb 1.5.9 (in-memory client). The collection holds four points. A plain query returns points 1, 4 and 2 (4 belongs to another tenant). With a filter for tenant acme and year 2025, only points 1 and 3 qualify. After deleting point 4 the count drops from 4 to 3. Local mode is used, so no server is involved.

from qdrant_client import QdrantClient, models

client = QdrantClient(":memory:")              # local mode: same API as a server, nothing to install or run
client.create_collection("docs", vectors_config=models.VectorParams(size=3, distance=models.Distance.COSINE))
client.upsert("docs", points=[
    models.PointStruct(id=1, vector=[0.9, 0.1, 0.0], payload={"tenant": "acme", "year": 2025, "text": "leave policy 2025"}),
    models.PointStruct(id=2, vector=[0.8, 0.2, 0.1], payload={"tenant": "acme", "year": 2022, "text": "old leave policy"}),
    models.PointStruct(id=3, vector=[0.1, 0.9, 0.1], payload={"tenant": "acme", "year": 2025, "text": "travel and hotels"}),
    models.PointStruct(id=4, vector=[0.9, 0.0, 0.2], payload={"tenant": "beta", "year": 2025, "text": "beta confidential memo"}),
])
print("points:", client.count("docs").count)

hits = client.query_points("docs", query=[1, 0, 0], limit=3).points
print("no filter :", [(h.id, round(h.score, 3)) for h in hits])

only_acme_2025 = models.Filter(must=[
    models.FieldCondition(key="tenant", match=models.MatchValue(value="acme")),
    models.FieldCondition(key="year", match=models.MatchValue(value=2025)),
])
hits = client.query_points("docs", query=[1, 0, 0], query_filter=only_acme_2025, limit=3).points
print("with filter:", [(h.id, h.payload["text"]) for h in hits])

client.delete("docs", points_selector=models.PointIdsList(points=[4]))
print("after delete:", client.count("docs").count)

Output:

points: 4
no filter : [(1, 0.994), (4, 0.976), (2, 0.963)]
with filter: [(1, 'leave policy 2025'), (3, 'travel and hotels')]
after delete: 3

Create payload indexes

In a dedicated engine, index every payload field you filter on, otherwise filtered search may fall back to slower paths.

Quick check: What makes Qdrant's filtered search robust?

  • It only supports exact search
  • It ignores filters
  • Payload conditions are checked during graph traversal, with payload indexes
  • Filters run in the browser
Answer

Payload conditions are checked during graph traversal, with payload indexes — Filter-aware traversal avoids returning too few results.