RAG over a code repo can't find the exact function name a developer searches for: best fix?
Exact identifiers like parseUserToken carry little semantic signal, so dense embeddings blur them. Add a sparse/BM25 retriever for literal-token matching and fuse it with dense retrieval as hybrid search.
Imagine looking someone up in a phone book versus describing them to a friend. If you know the exact spelling of their name, the phone book finds them instantly — letter for letter. But if you only remember "the tall doctor on Oak Street," describing them works better. Searching code is the same. A function name like parseUserToken is an exact spelling: you want the phone-book lookup. A question like "how do we check logins?" is a description: you want the friend who understands meaning. The fix is to keep both helpers and combine what each one finds, instead of forcing one to do the other's job.
Concept explanation~2 min read
Everything you need to truly understand this topic: intuition, mechanics, step by step explanation, code, formulas, and worked example. Click to expand.
Concept explanation~2 min read
Everything you need to truly understand this topic: intuition, mechanics, step by step explanation, code, formulas, and worked example. Click to expand.
Code search is where pure dense retrieval most visibly breaks, and it is a favorite interview scenario because the wrong fixes are so tempting. A developer types an exact function name, the RAG system returns vaguely related code, and the instinct is to reach for a bigger embedding model or a wider result set. Both instincts are wrong, and understanding why is the difference between a candidate who has tuned a RAG demo and one who understands what embeddings actually represent.
This deep dive unpacks why an exact identifier is hostile to dense retrieval, why a sparse retriever is the natural complement, how fusion combines the two without comparing incomparable scores, and the code-specific details — tokenization, chunking, symbol tables — that separate a working code-search system from a brittle one.
Why an identifier is hostile to dense retrieval
A dense embedding model is trained to map text into a space where semantic similarity equals geometric closeness. Paraphrases land near each other; "validate the login" sits close to "check the credentials." That training objective is the whole value of dense retrieval, and it is precisely what makes it bad at exact identifiers.
Consider parseUserToken. As a piece of meaning, it is thin — it is a name a programmer coined, often rare or unique in the corpus, and possibly never seen during the embedding model's pretraining. The model has little distributional signal to encode, so it falls back on surface cues: it is camel-case, it contains "parse," "user," "token." The resulting vector lands in a fuzzy neighborhood of other camel-case names that share those sub-words. The exact symbol the developer wants is not guaranteed to rank first; a parseUserSession or validateUserToken can easily outrank it.
The key realization is that this is a representation problem, not a capacity problem. A larger embedding model with more dimensions is even better at semantics — which means it is, if anything, even more committed to collapsing surface form into meaning. You cannot scale your way out of a mismatch between what the representation encodes and what the query needs. The query needs the literal string; the representation threw the literal string away.
Situations where this technique stops working.
2–4 min · Everything important, quickly.
def reciprocal_rank_fusion(dense_hits, sparse_hits, k=60, top_n=5):
# dense_hits, sparse_hits: ordered lists of chunk ids (best first)
scores = {}
for ranked in (dense_hits, sparse_hits):
for rank, chunk_id in enumerate(ranked, start=1):
# combine ranks, not raw scores — the two scales are incomparable
scores[chunk_id] = scores.get(chunk_id, 0.0) + 1.0 / (k + rank)
fused = sorted(scores, key=scores.get, reverse=True)
return fused[:top_n]
# dense recovers intent queries; sparse (BM25/SPLADE) recovers exact symbols
results = reciprocal_rank_fusion(dense.search(q), bm25.search(q))Real products, models, and research that use this idea.
- GitHub and Sourcegraph code search lean on exact-symbol and lexical matching, layering semantic search on top rather than relying on embeddings alone.
- Qdrant and Weaviate ship native hybrid search that fuses a sparse BM25 vector with the dense vector per query.
What an interviewer would ask next. Try answering before peeking at the approach.
QHow would you actually fuse the dense and sparse result lists, given their scores are not on the same scale?
Reciprocal rank fusion is the common answer because it combines ranks, not raw scores, sidestepping the incomparable-scale problem. Each retriever contributes 1/(k + rank) per document and you sum. Alternatives are score normalization (min-max or z-score per channel) then weighted sum, but RRF is more robust when score distributions differ a lot.
Red flags & common mistakes
The phrases that signal junior thinking. Click to expand.
Red flags & common mistakes
The phrases that signal junior thinking. Click to expand.
Reaching for a bigger embedding model to fix exact-symbol search — a larger model still smooths literal tokens into a fuzzy neighborhood, so the exact identifier stays lost.
60 second bullets to scan on the way to the call.
Explain why dense embeddings struggle with exact identifiers
Primary sources. Browse if you want the original framing.
Same topic, related formats. Practice these next.