Qdrant Hybrid Search — Named Vectors, Payload Filters, and Fusion

Qdrant ハイブリッド検索 — 名前付きベクトル、ペイロードフィルタ、フュージョン

article technology high #qdrant#vector-database#hybrid-search#rag#payload-filter
Created: 2026-04-18 Updated:

Qdrant supports multiple vector heads per point and structured payload filters in a single query_points call. Hybrid retrieval lives inside Qdrant rather than on top of it, which collapses three network trips into one and preserves score semantics across heads.

Qdrant Hybrid Search — Named Vectors, Payload Filters, and Fusion

Qdrant’s hybrid search reduces three separate retrieval hops — vector search, lexical search, reranking — to one server-side query. The design rests on three features: named vectors, sparse vector support, and payload filtering inside the same query plan.

Named vectors

A Qdrant collection stores multiple vectors per point under distinct names. A typical hybrid layout is dense (1024-dim cosine), sparse (IDF-modified inverted list), and colbert (multi-vector MAX_SIM). All three live on the same point, so a single document upsert populates every retrieval mode. The operational cost is one collection, one backup, one drift check — versus three separate indices in a Solr/Elasticsearch-plus-FAISS setup.

Payload indexes

A payload is Qdrant’s per-point structured metadata (JSON). A payload index lets the query filter category = "finance" or tags contains "rag" before the vector scan instead of after, which in turn dramatically narrows the candidate set. Nine index types are supported — keyword, integer, float, geo, bool, datetime, uuid, text (BM25), and nested. For a knowledge base, the common set is keyword on category/language/type/status, keyword on tags and relation_ids (list-valued), and datetime on date_updated for recency filters.

Hybrid query in one call

query_points accepts a prefetch pipeline, letting the server fuse heads without a client round-trip:

client.query_points(
    collection_name="magi_knowledge",
    prefetch=[
        Prefetch(query=dense_vec, using="dense", limit=50),
        Prefetch(query=sparse_vec, using="sparse", limit=50),
    ],
    query=FusionQuery(fusion=Fusion.RRF),
    query_filter=Filter(must=[FieldCondition(key="language", match=MatchValue(value="en"))]),
    limit=10,
)

The server scores both heads over the same 50-candidate window and returns the fused top-10. The filter is evaluated before scoring, so the result set is always in-language. No application-side merge logic, no triple-paging, no rank stitching.

Score semantics across fusion modes

Fusion choice affects how the final score reads:

  • RRF — the returned score is 1 / (rank + k) summed across heads. Not comparable to cosine similarity. Good for sorting, meaningless for thresholding.
  • Weighted Sum — requires pre-normalisation per head; the final score retains cosine-ish shape. Allows threshold cutoffs.
  • Dense-only — the score is raw cosine similarity in [0, 1]. Cleanest semantics when only one head is populated.

For most personal-scale corpora, RRF is the right default — it degrades gracefully as the sparse head matures and doesn’t require tuning.

Drift detection

Qdrant’s collection schema is mutable at the API level (add a new payload index, delete one) but silently drifting between your provisioner and the live collection is the common operational failure. A good practice is a verify_collection_schema step at startup that compares the live CollectionInfo against a pinned spec and raises on any mismatch in vector dim, distance metric, multivector comparator, sparse modifier, or payload index data type. Warn on extras, fail on deviations — this catches accidental double-provisioning before it corrupts a retrieval run.

Local graph