grepai

Hybrid Search

Merge vector search with keyword matching using Reciprocal Rank Fusion to find exact identifiers in grepai search results.

Hybrid search merges two result lists: vector similarity and a simple keyword match on chunk text. It combines them with Reciprocal Rank Fusion (RRF), so chunks that rank well in both lists come first. It helps when queries contain exact identifiers such as function or variable names.

Hybrid search is disabled by default. Enable it in .grepai/config.yaml:

search:
  hybrid:
    enabled: true
    k: 60   # RRF constant (default: 60)

Then search as usual. No re-indexing is needed:

grepai search "handleAuth token refresh"

How It Works

Query: "handleAuth authentication"
        │
        ├─► Vector Search ──► [chunk_a, chunk_b, chunk_c]
        │                      rank 1    rank 2    rank 3
        │
        └─► Text Search ────► [chunk_b, chunk_d, chunk_a]
                               rank 1    rank 2    rank 3
        │
        ▼
    RRF Fusion: score = Σ 1/(k + rank)
        │
        ▼
    [chunk_b, chunk_a, chunk_c, chunk_d]
     (appears in both lists = higher score)
  1. Vector search: the usual semantic search through the store.
  2. Text search: keyword matching over every indexed chunk.
  3. RRF fusion: both rankings are merged into one list.
  4. Boost and dedup: structural boost and search.dedup are then applied to the fused list, as in a normal search.

Each list holds up to 2x --limit candidates (4x when search.dedup is enabled). The --path filter applies to both lists.

Configuration

KeyDefaultDescription
search.hybrid.enabledfalseTurn hybrid search on
search.hybrid.k60RRF constant. A value of 0 or less falls back to 60

grepai search --workspace ignores this setting: workspace searches always run without hybrid search.

The k parameter

The RRF formula is score(doc) = Σ 1/(k + rank_i), with ranks starting at 1.

  • Higher k (e.g., 100): rank differences matter less, so appearing in both lists counts more than being first in one.
  • Lower k (e.g., 30): the top results of each list weigh more.
  • Default (60): the value commonly used for RRF.

When to Enable

Enable hybrid search when:

  • Queries often include exact function or class names
  • You mix natural language with identifiers (e.g., “handleAuth function”)
  • Vector-only search misses obvious keyword matches

Examples

QueryVector-onlyHybrid
handleUserLoginMay miss if the embedding does not capture the identifierChunks containing handleuserlogin (case-insensitive) rank higher
authentication flowGood semantic matchSimilar results
validateEmail function in user modulePartial matchCombines semantic and keyword signals

Performance Note

On every query, hybrid search loads all chunks from the store (GetAllChunks) and scans their text in memory. On large indexes, or with a remote PostgreSQL or Qdrant store, this adds latency and memory use.

Consider keeping it disabled for:

  • Very large monorepos
  • Purely semantic queries (no identifiers)
  • Latency-sensitive use, such as an agent that runs many searches

Technical Details

Simple keyword matching, no stemming or ranking model:

  • The query is lowercased and split on whitespace. Words shorter than 2 characters are dropped.
  • A chunk matches a word if its lowercased content contains that word as a substring.
  • Score = number of matched words / number of query words. Chunks with no match are excluded.
  • Results are sorted by score.

RRF Fusion

Reciprocal Rank Fusion merges ranked lists:

score(doc) = Σ 1/(k + rank_i) for each list containing doc

Benefits:

  • No need to normalize scores between the two sources
  • Robust to outliers

Because the final score is an RRF value, hybrid results show small scores (at most about 0.033 with k: 60, before boosting) instead of similarity values.