Hybrid & Sparse Search
|
This section documents the current Qdrant 1.x line (latest 1.19.1), written and verified against the official Qdrant documentation. No specific patch version is pinned. Some capabilities (Qdrant Cloud-only features and the full breadth of client-library APIs beyond Python and Java) are linked, not documented in depth. This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
A single Qdrant point can carry a dense vector, a sparse vector, or both, and the Query API can search
across them in one request and merge the resulting rankings server-side — combining the semantic recall
of dense embeddings with the exact-term precision of a sparse, BM25/SPLADE-style representation.
Sparse vectors
A sparse vector is a pair of arrays: indices (token/dimension ids) and values (their weights), holding
only the non-zero entries of what would otherwise be a very high-dimensional vector — the same shape
produced by a bag-of-words model or a learned-sparse encoder such as SPLADE. Unlike a dense vector, a
sparse vector has no fixed size: its dimensionality is implicit in whatever indices appear across the
collection. A collection declares sparse-vector support separately from dense vectors_config, under its
own named sparse_vectors_config, and each point can then set a value for that name alongside (or instead
of) any named dense vector.
from qdrant_client import QdrantClient
from qdrant_client.models import Distance, VectorParams, SparseVectorParams
client = QdrantClient(url="http://localhost:6333")
client.create_collection(
collection_name="articles",
vectors_config={
"dense": VectorParams(size=384, distance=Distance.COSINE),
},
sparse_vectors_config={
"sparse": SparseVectorParams(),
},
)
Upserting a point sets both the named dense vector and the named sparse vector on the same vector
mapping:
from qdrant_client.models import PointStruct, SparseVector
client.upsert(
collection_name="articles",
points=[
PointStruct(
id=1,
vector={
"dense": [0.05, 0.61, 0.34, "...381 more floats..."],
"sparse": SparseVector(
indices=[1, 42, 1337, 9000],
values=[0.32, 0.11, 0.58, 0.05],
),
},
payload={"title": "renewing an expired TLS certificate"},
),
],
)
Background on the representation, how it compares to dense embeddings, and how a SPLADE-style model
produces the indices/values pairs is covered in
Sparse Vectors.
Hybrid dense + sparse search and fusion
A hybrid query searches the dense vector and the sparse vector in the same request and combines their two
ranked result lists into one. Qdrant’s Query API does this with prefetch: each prefetch entry runs
its own vector search (naming which vector to use via using) and returns its own candidate list, then the
outer query’s fusion step merges those lists. Because a dense cosine score and a sparse dot-product score
live on unrelated scales, Qdrant’s built-in fusion method is Reciprocal Rank Fusion (RRF): it re-ranks by
each point’s rank position in every child list rather than by the raw scores, so the two scales never
need to be normalized against each other.
from qdrant_client.models import FusionQuery, Fusion, Prefetch, SparseVector
query_dense = [0.04, 0.63, 0.31, "...381 more floats..."]
query_sparse = SparseVector(indices=[42, 1337], values=[0.9, 0.4])
results = client.query_points(
collection_name="articles",
prefetch=[
Prefetch(query=query_dense, using="dense", limit=20),
Prefetch(query=query_sparse, using="sparse", limit=20),
],
query=FusionQuery(fusion=Fusion.RRF),
limit=10,
)
Each Prefetch.limit bounds how many candidates that branch contributes before fusion; the outer limit
bounds the final fused result. The prefetch + fusion mechanism, its parameters, and other fusion methods
are documented in Hybrid Queries.
Re-ranking a candidate set
prefetch also composes as a general two-stage pipeline, independent of fusion: run a cheap first-pass
search (dense or sparse) to pull a larger, inexpensive candidate set, then apply a more expensive re-ranking
step — such as scoring each candidate’s full text against the query with a cross-encoder model — over only
the top N. Because the second stage runs on a small, already-narrowed set, the expensive model only ever
scores a handful of candidates instead of the whole collection.
from qdrant_client.models import Prefetch
# Stage 1: cheap dense-vector prefetch narrows the whole collection to 50 candidates.
# Stage 2: an application-side cross-encoder re-scores just those 50 and keeps the top 5.
candidates = client.query_points(
collection_name="articles",
prefetch=Prefetch(query=query_dense, using="dense", limit=50),
query=query_dense,
limit=50,
)
pairs = [(query_text, hit.payload["title"]) for hit in candidates.points]
cross_encoder_scores = cross_encoder.predict(pairs) # e.g. a sentence-transformers CrossEncoder
reranked = sorted(
zip(candidates.points, cross_encoder_scores), key=lambda p: p[1], reverse=True
)[:5]
The Query API also supports re-ranking with a vector stored on the point itself (for example a
late-interaction/multivector re-ranker) as the outer query over a prefetch, without leaving Qdrant — see Hybrid Queries for that variant.
Where to go next
-
Filtering & Payload Indexing — narrowing the candidate set with payload filters before (or alongside) the vector search stages described here.
-
CRUD & Batch Operations — day-to-day point CRUD and batching once a collection’s dense/sparse vector configuration is in place.
-
Vector & semantic search — Elasticsearch composes hybrid search from an
rrfretriever fusing astandardBM25 retriever with aknnretriever, versus Qdrant’s prefetch + fusionQueryAPI shown above. -
Text, wildcard, geospatial & Atlas search — MongoDB Atlas combines Atlas Search (
$search) and Atlas Vector Search ($vectorSearch) via$rankFusion/compound aggregation pipelines, versus Qdrant’s native fusion. -
Hybrid Search & Re-ranking — the vendor-neutral theory of score fusion, Reciprocal Rank Fusion and cross-encoder re-ranking behind this page’s Qdrant-specific hybrid queries.