Embeddings alone are bad at exact identifiers. Ask for InvoiceReconcilerV2 after a rename and vector recall returns “similar billing helpers” while BM25 and a symbol index would have hit the definition in one shot. Production code RAG needs three channels: lexical (BM25), semantic (vectors), and structural (symbols / ctags / SCIP).
⚡ TL;DR: Run BM25 + kNN in parallel, union candidates, then rerank. Always maintain a symbol index keyed by name → file:line. Boost exact identifier matches above semantic neighbors. Evaluate with identifier-heavy queries, not only natural-language ones. See Bedrock Knowledge Bases: Chunking Strategies That Fit Code RAG and RAG Evaluation on AWS.
Three indexes, one query planner
type HybridHit = {
path: string;
startLine: number;
endLine: number;
score: number;
channel: "bm25" | "vector" | "symbol";
};
export async function hybridSearch(q: string, opts: { tenantId: string; k: number }) {
const identifiers = extractIdentifiers(q); // CamelCase, snake_case, dotted paths
const [bm25, vectors, symbols] = await Promise.all([
openSearchBm25(q, opts),
openSearchKnn(embed(q), opts),
symbolLookup(identifiers, opts),
]);
// ✅ Exact symbol hits get a hard boost — not a soft preference
const merged = fuseRRF([
{ hits: symbols.map((h) => ({ ...h, score: h.score + 10 })), weight: 1.0 },
{ hits: bm25, weight: 1.0 },
{ hits: vectors, weight: 0.8 },
]);
return merged.slice(0, opts.k);
}
function extractIdentifiers(q: string): string[] {
return q.match(/\b[A-Z][a-zA-Z0-9]{2,}\b|\b[a-z]+(?:_[a-z0-9]+)+\b/g) ?? [];
}
❌ Single OpenSearch neural query with no lexical branch.
✅ Reciprocal Rank Fusion (RRF) or a learned reranker over the union.
Compare stores in OpenSearch vs Aurora pgvector for Codebase Chat — hybrid works on both if you keep BM25.
OpenSearch query that actually hybridizes
{
"size": 40,
"query": {
"bool": {
"filter": [{ "term": { "tenantId": "t_9f2a" } }],
"should": [
{
"multi_match": {
"query": "InvoiceReconcilerV2 settle ledger",
"fields": ["path^3", "symbol^5", "content"],
"type": "best_fields"
}
},
{
"knn": {
"embedding": {
"vector": ["/* 1024-d */"],
"k": 40
}
}
}
]
}
}
}
Stamp symbol, path, and tenantId at ingest the same way you do for Bedrock Retrieval Filters.
Symbol index that survives renames
# build_symbol_index.py — illustrative SCIP / ctags style
import json, subprocess, pathlib
def export_symbols(repo: pathlib.Path) -> list[dict]:
# Prefer SCIP or LSIF when available; ctags as fallback
raw = subprocess.check_output(
["ctags", "-R", "--fields=+n", "-f", "-", str(repo)],
text=True,
)
out = []
for line in raw.splitlines():
if line.startswith("!"):
continue
name, path, *rest = line.split("\t")
out.append({"name": name, "path": path, "kind": rest[-1] if rest else "unknown"})
return out
def upsert_symbols(tenant: str, repo: str, symbols: list[dict], client):
for s in symbols:
client.index(
index="code-symbols",
id=f"{tenant}:{repo}:{s['path']}:{s['name']}",
body={**s, "tenantId": tenant, "repoId": repo},
)
Refresh on merge alongside embeddings — see Codebase Embeddings: Refresh Pipelines When Git SHAs Keep Moving.
Evaluation: identifier queries are non-negotiable
| Query class | Example | Must hit |
|---|---|---|
| Exact symbol | where is InvoiceReconcilerV2 |
definition file |
| Alias / rename | old SettleInvoice helper |
new name + callers |
| Error string | E_LEDGER_DRIFT |
throw site |
| NL only | “how do we settle invoices” | service module |
If your offline harness only scores NL questions, you will ship a vector-only system that fails daily IDE use. Use the faithfulness/hit-rate harness from RAG Evaluation on AWS.
Closing checklist
- [ ] BM25 and vector recall run in parallel with tenant filters on both
- [ ] Symbol index maps name → path:line and boosts exact matches
- [ ] Fusion via RRF or cross-encoder rerank — not max(score) across incompatible spaces
- [ ] Identifier-heavy eval set with rename and error-code cases
- [ ] Ingest stamps
symbol,path,tenantId; sync on merge - [ ] Latency budget includes the slowest of the three channels + rerank
Related reading
- Bedrock Knowledge Bases: Chunking Strategies That Fit Code RAG
- RAG on AWS: OpenSearch vs Aurora pgvector for Codebase Chat
- Codebase Embeddings: Refresh Pipelines When Git SHAs Keep Moving
- Bedrock Retrieval Filters: Tenant Isolation for Multi-SaaS Code RAG
Last updated on September 11, 2026
Discover more from CheatCoders
Subscribe to get the latest posts sent to your email.
