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. Because rrf() depends on ranked inputs, it also requires a
limit clause.
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.
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.