Skip to main content

Hybrid search

This page explains query.hybrid, which combines graph filtering, vector similarity and BM25 keyword ranking in one request and fuses the rankings, by reciprocal-rank fusion (RRF) or distribution-based score fusion (DBSF). It is for developers building retrieval for RAG or search experiences where neither semantic nor keyword matching alone is good enough.

Before you start, you need a vector index with bound vectors, a text index on the same label, or both.

How a hybrid query runs​

  1. Filter once. labels and predicate are resolved by the query planner into one candidate set of records.
  2. Rank inside the candidates. The vector leg and the text leg each rank only records in that set. Filtering happens before ranking, so a record outside the predicate can never take a top slot.
  3. Fuse. Each leg contributes to every record it returned, by the chosen fusion. A record found by both legs receives both contributions. Raw cosine and BM25 scores are never compared directly.
  4. Page and return. Fused results are sorted by fused score (ties by record), offset results are skipped, and up to limit full records are returned.

Each leg fetches prefetchLimit hits before fusion, by default 4 × (offset + limit), so that records ranked moderately by both legs can still surface and deep pages are not under-filled.

Fusion​

fusionA leg's contribution to a recordUse when
rrf (default)weight / (rrfK + rank + 1), rank from 0The legs' scores are not comparable or their spread does not matter; robust default.
dbsfweight × normalized score: each leg's scores are mapped by that leg's own mean ± 3 standard deviations to [0, 1] (clamped)How far ahead a record is within a leg should count, not just its place.
  • DBSF follows Qdrant's definition. A leg whose returned scores do not spread (one hit, or all equal) maps them all to 0.5. Normalization uses the scores the leg returned, so prefetchLimit shapes the distribution.
  • weight (default 1) sets a leg's share: 0 leaves the other leg's order, 2 doubles a leg's pull. At least one leg's weight must be above 0.
  • With one leg, score is that leg's own score, and fusion and weight do not apply.

Request​

FieldTypeRequiredDefaultDescription
labelsstring[]no[]Labels a candidate must carry; empty matches every label.
predicateobjectno{}Filter applied before either leg ranks (same syntax as query.records).
vectorobjectone of vector/text—Vector leg (below).
textobjectone of vector/text—Lexical leg (below).
offsetintegerno0Fused results to skip.
limitintegeryes—Fused results to return (minimum 1).
fusionstringnorrfrrf or dbsf (Fusion).
rrfKnumberno60RRF smoothing constant (≥ 0).

At least one leg is required; with both set the query is a true hybrid.

Vector leg

FieldRequiredDefaultDescription
propertyyes—Vector property.
vectoryes—Query vector with the index's dimensions.
indexyes¹—Vector index name.
similaritynocosineOnly used to derive the index alias when index is omitted.
modenoautoauto, exact or ann (see Vector search).
efnoresources.vector-default-efANN breadth when the leg runs on the ANN graph.
scoreThresholdno—Drop vector hits scoring below this before fusion.
weightno1This leg's share of the fused score (≥ 0).
prefetchLimitno4 × (offset + limit)Hits this leg ranks before fusion, 1 to 10,000. Below offset + limit, a page can hold fewer results.

¹ Required while resources.require-vector-index-name is on (the default).

Text leg

FieldRequiredDescription
indexyesText index name. The index must be ready.
queryyesQuery string (syntax).
allTermsnoUnmarked terms must all match (AND) instead of any (OR). Default false.
phrasenoMatch the whole query as one phrase. Default false.
weightnoThis leg's share of the fused score (≥ 0). Default 1.
prefetchLimitnoHits this leg ranks before fusion, 1 to 10,000. Default 4 × (offset + limit).

Hybrid search covers records only. For relationship vectors, use query.vector directly.

Example​

The text index docs covers Doc with property: "*" and the vector index doc-embedding holds 2-dimensional vectors:

curl -s http://127.0.0.1:3000/protocol/v1/execute \
-H "Authorization: Bearer $RUSHDB_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"protocolVersion": 1,
"operation": {
"kind": "query",
"query": {
"kind": "hybrid",
"query": {
"labels": ["Doc"],
"predicate": { "tag": "keep" },
"vector": {
"property": "embedding",
"vector": [1.0, 0.0],
"index": "doc-embedding"
},
"text": { "index": "docs", "query": "quick" },
"limit": 10,
"rrfK": 60
}
}
}
}'
{
"protocolVersion": 1,
"response": {
"kind": "records",
"total": 2,
"records": [
{
"id": "0192f0c4-7a1e-7b8a-9c3d-2f6e5a4b3c21",
"label": "Doc",
"properties": { "tag": "keep", "body": "the quick brown fox" },
"propertyTypes": { "tag": "string", "body": "string" },
"score": 0.03278688524590164,
"provenance": ["vector:0", "lexical:0"]
},
{
"id": "0192f0c4-7a1e-7b8a-9c3d-2f6e5a4b3c22",
"label": "Doc",
"properties": { "tag": "keep", "body": "a lazy dog sleeps" },
"propertyTypes": { "tag": "string", "body": "string" },
"score": 0.016129032258064516,
"provenance": ["vector:1"]
}
]
}
}

A third Doc with tag: "drop" and identical text and vector would match both legs perfectly, but is never returned: it is outside the predicate.

Response fields​

FieldMeaning
scoreWith both legs, the fused score: under RRF at most (vector weight + text weight) / (rrfK + 1), under DBSF between 0 and the sum of the weights. With one leg, that leg's own score: BM25 for text, similarity for vector. Only comparable within one response.
vectorScoreThe record's similarity, when the vector leg ranked it.
textScoreThe record's BM25 score, when the text leg ranked it.
provenanceOne "<leg>:<rank>" entry per leg that returned the record: vector:<rank> and/or lexical:<rank>, ranks starting at 0.
totalNumber of records in this page.
vectorSearchHow the vector leg ran (as for query.vector).
fusionWith both legs: the fusion that combined them.
textSearchThe text leg's matches (records matching the query inside the filtered candidates, beyond this page) and pending (changes the index had not applied).

Choosing parameters​

GoalSetting
Default, well-tested fusionfusion: rrf, rrfK: 60
Let a strong lead within a leg count, not only rankfusion: dbsf
Trust one leg moreraise its weight (for example text.weight: 0.5 to halve the keyword pull)
Wider candidate pool per leglarger prefetchLimit (more work per query)
Let top-ranked hits of either leg dominatesmaller rrfK (for example 10–20)
Flatten differences between ranks, favour agreement between legslarger rrfK
Keyword-only search with filters, paging and full recordstext leg only
Similarity search across all labelsvector leg with index, labels: []
Lower latency on large vector indexesmode: auto (the default) switches large cosine indexes to ANN; ef tunes its breadth

Pages are stable: for the same data, offset: 5, limit: 5 returns exactly results 6–10 of offset: 0, limit: 10.

Consistency and security​

  • Both legs and record reconstruction read the same snapshot. With an eventual text index, the lexical leg reflects what the applier has processed; use an immediate index when results must include the caller's own latest writes.
  • Every returned record is reconstructed through the tenant's data-access policy. When a policy is enforced, the query must name only allowed labels (an empty labels list is refused).

See also​