Hybrid Search
Combine BM25 and vector ranking with Reciprocal Rank Fusion.
Hybrid search combines keyword relevance (BM25) and semantic similarity (vector KNN) into a single ranked result list using Reciprocal Rank Fusion (RRF). This is useful when neither keyword matching nor vector similarity alone produces the best ranking.
When to use hybrid search
- Keyword search alone works well when the user knows the exact terminology, but misses semantically related content that uses different words.
- Vector search alone captures semantic similarity, but can rank irrelevant results highly if the embedding space is noisy or the query is ambiguous.
- Hybrid search hedges between both. If a result ranks highly on both signals, it floats to the top.
Use hybrid search when your data has both structured text fields and embedding vectors, and you want queries to benefit from both signals.
Schema setup
Hybrid search requires both a String @index field (for BM25) and a
Vector(N) @index field (for nearest-neighbor):
node Person {
name: String @key
bio: String @index
embedding: Vector(1536) @index
}rrf() syntax
rrf() appears in the order block and takes two ranking expressions as
arguments, plus an optional third argument k (the rank-fusion constant,
default 60). Because rrf() depends on ranked inputs, it requires a
limit clause. The typechecker rejects rrf() ordering without one.
query hybrid_search($term: String, $vec: Vector(1536)) {
match {
$p: Person
}
return { $p.name, $p.bio }
order { rrf(nearest($p.embedding, $vec), bm25($p.bio, $term)) desc }
limit 10
}How RRF score is computed
Reciprocal Rank Fusion does not combine raw scores. It combines ranks. For each result, RRF computes:
score = 1 / (k + rank_a) + 1 / (k + rank_b)Where:
rank_ais the result's rank in the first rankingrank_bis the result's rank in the second rankingkis a constant that controls how much weight is given to top-ranked results
The default value of k is 60. Override it with a third argument to
rrf() when you want top-ranked results to dominate more or less aggressively:
order { rrf(nearest($p.embedding, $vec), bm25($p.bio, $term), 30) desc }Combining with filters and traversal
Hybrid ranking composes with match filters and graph traversal. The filter
narrows the candidate set before ranking:
query experts_at_company($term: String, $vec: Vector(1536), $company: String) {
match {
$p: Person
search($p.bio, $term)
$p worksAt $c: Company { name: $company }
}
return { $p.name, $p.bio, $c.name }
order { rrf(nearest($p.embedding, $vec), bm25($p.bio, $term)) desc }
limit 10
}Graph-constrained reranking is one of Omnigraph's key advantages over standalone vector databases: you can narrow candidates by graph structure before applying ranking.