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)
- Vector search: the usual semantic search through the store.
- Text search: keyword matching over every indexed chunk.
- RRF fusion: both rankings are merged into one list.
- Boost and dedup: structural boost and
search.dedupare 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
| Key | Default | Description |
|---|---|---|
search.hybrid.enabled | false | Turn hybrid search on |
search.hybrid.k | 60 | RRF 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
| Query | Vector-only | Hybrid |
|---|---|---|
handleUserLogin | May miss if the embedding does not capture the identifier | Chunks containing handleuserlogin (case-insensitive) rank higher |
authentication flow | Good semantic match | Similar results |
validateEmail function in user module | Partial match | Combines 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
Text Search
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.