Omnigraph
Search

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.

  • 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_a is the result's rank in the first ranking
  • rank_b is the result's rank in the second ranking
  • k is 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.

On this page