Facing a situation: in a monorepo of 500,000 lines you need to find the payment processing function, but grep returns hundreds of matches. RAG over the codebase solves this, but only if chunking preserves code structure. In such projects we use a combination of Tree-sitter and AST for syntactic parsing and breakdown into logical units: functions, classes, modules. Each chunk is enriched with metadata — name, signature, docstring, imports, and full path in module notation. This allows semantic search to find exactly the code unit you need, not a random piece of text.
Why Preserve Code Structure in Chunking?
Ordinary document RAG cuts text into paragraphs. For code this doesn't work: a break between the signature and body of a function destroys context. Code has a hierarchy — a function inside a class, a class inside a module. We preserve this hierarchy in metadata: module path, start and end lines, list of methods for a class, decorators for a function. This way, when searching for 'how X is implemented', you get the exact unit where X is defined.
How We Implement Code-aware Parsing
We built an indexer based on Tree-sitter. It parses code in 50+ languages and yields a syntax tree. For each node (function, class, method) we extract:
- name and signature,
- docstring (if present),
- function/class body,
- decorators and annotations,
- list of imports (up to 10).
For example, for Python we use ast for precise extraction:
import ast
from tree_sitter import Language, Parser
class CodebaseIndexer:
def __init__(self):
# Tree-sitter for syntax-aware parsing
PY_LANGUAGE = Language('build/languages.so', 'python')
self.parser = Parser()
self.parser.set_language(PY_LANGUAGE)
def extract_python_units(self, file_path: str) -> list[dict]:
"""Extract functions and classes as separate index units"""
with open(file_path, 'r', encoding='utf-8') as f:
source = f.read()
try:
tree = ast.parse(source)
except SyntaxError:
return [{'text': source, 'type': 'file', 'file': file_path}]
units = []
for node in ast.walk(tree):
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
# Get function source code
func_source = ast.get_source_segment(source, node)
docstring = ast.get_docstring(node)
units.append({
'type': 'function',
'name': node.name,
'file': file_path,
'line_start': node.lineno,
'line_end': node.end_lineno,
'text': func_source,
'docstring': docstring or '',
'decorators': [ast.unparse(d) for d in node.decorator_list],
'signature': self._get_signature(node)
})
elif isinstance(node, ast.ClassDef):
class_source = ast.get_source_segment(source, node)
docstring = ast.get_docstring(node)
units.append({
'type': 'class',
'name': node.name,
'file': file_path,
'line_start': node.lineno,
'line_end': node.end_lineno,
'text': class_source,
'docstring': docstring or '',
'methods': [m.name for m in ast.walk(node)
if isinstance(m, ast.FunctionDef)]
})
return units
def _get_signature(self, func_node: ast.FunctionDef) -> str:
args = []
for arg in func_node.args.args:
annotation = f": {ast.unparse(arg.annotation)}" \
if arg.annotation else ""
args.append(f"{arg.arg}{annotation}")
return_type = f" -> {ast.unparse(func_node.returns)}" \
if func_node.returns else ""
return f"def {func_node.name}({', '.join(args)}){return_type}"
Metadata Enrichment: Why It Matters?
Splitting code into chunks is not enough. For quality search, each chunk must be enriched: add name, signature, docstring, imports, and full path in module notation. This turns flat text into a structured object that, when vectorized, gives more accurate embeddings. We create rich_text — a combination of all metadata fed to the embedding model.
class CodeMetadataEnricher:
def enrich(self, unit: dict) -> dict:
unit = unit.copy()
# Create rich text for embedding
# Combine name, signature, docstring and code
rich_text_parts = []
if unit.get('name'):
rich_text_parts.append(f"# {unit['name']}")
if unit.get('signature'):
rich_text_parts.append(f"Signature: {unit['signature']}")
if unit.get('docstring'):
rich_text_parts.append(f"Description: {unit['docstring']}")
rich_text_parts.append(unit['text'])
unit['rich_text'] = '\n\n'.join(rich_text_parts)
# Extract imports for context
imports = re.findall(r'^(?:import|from)\s+\S+', unit['text'], re.MULTILINE)
unit['imports'] = imports[:10]
# Path as breadcrumb
parts = unit['file'].replace('\\', '/').split('/')
unit['module_path'] = '.'.join(
p.replace('.py', '') for p in parts if not p.startswith('.')
)
return unit
Indexing Git History: What Changed?
RAG over code can answer not only structural questions but also about change history. We index the last 100 commits with diffs and metadata: author, date, message, files. This lets you find when and who changed a specific function. For example, "Who edited calculate_total last month?" returns commits with that function in the diff.
import subprocess
class GitHistoryIndexer:
def get_recent_changes(self, repo_path: str, n: int = 100) -> list[dict]:
"""Index last N commits with diffs"""
result = subprocess.run(
['git', 'log', f'-{n}', '--format=%H|%an|%ae|%ad|%s'],
cwd=repo_path, capture_output=True, text=True
)
commits = []
for line in result.stdout.strip().split('\n'):
if not line:
continue
hash_, author, email, date, subject = line.split('|', 4)
# Get diff for this commit
diff_result = subprocess.run(
['git', 'diff', f'{hash_}^', hash_, '--stat'],
cwd=repo_path, capture_output=True, text=True
)
commits.append({
'hash': hash_,
'author': author,
'date': date,
'message': subject,
'changes_summary': diff_result.stdout[:500],
'text': f"Commit: {subject}\nAuthor: {author}\nDate: {date}\n\nChanges: {diff_result.stdout[:500]}"
})
return commits
How to Evaluate Code RAG Quality?
A good metric: when asked "How is X implemented?", the system should return the function or class that implements X, not just a file with a similar name. For evaluation we use a golden set of 50–100 questions with known answers (specific functions). Precision@3 > 0.8 is a good result. Below is a comparison of chunking strategies:
| Chunking Strategy | Precision@3 | Token Cost | Hierarchy Support |
|---|---|---|---|
| File-level (whole file) | 0.45 | Low | No |
| Function-level (AST) | 0.85 | Medium | Yes |
| Mixed (functions+classes) | 0.91 | High | Yes |
Mixed chunking gives a 2x accuracy gain over file-level. We use this approach: each chunk is a function or class, and the file becomes metadata.
Which Embedding Model for Code?
For code it's better to use models trained on source code rather than general text. Below is a comparison of popular options:
| Embedding Model | Dimensionality | Throughput | Average precision@3 |
|---|---|---|---|
text-embedding-3-small |
1536 | 1000 req/min | 0.83 |
code-bert |
768 | 500 req/min | 0.79 |
ada-002 (deprecated) |
1536 | 1000 req/min | 0.74 |
Typical Indexing Mistakes
- Ignoring docstring — without docstring the model doesn't understand the function's purpose, recall drops by 30%.
- Chunking by line count — breaks logical blocks, precision halves.
- No metadata — code without name/signature yields an embedding similar to random text.
- Skipping Git history — losing authorship information and change context.
- Wrong embedding model — a document model performs poorly on code.
What's Included in the Work?
- Codebase audit: assess size, languages, repository structure.
- Pipeline design: choose tools (Tree-sitter, vector DB, embedding model), configure metadata.
- Indexing implementation: write parser, enrich, vectorize, load into vector DB.
- Testing: verify with golden set, iterative improvement of chunking and metadata.
- Integration: set up search API, integrate with IDE, chat bots, or internal tools.
- Deployment and monitoring: deploy, log, track quality metrics (precision, recall, latency p99).
Timelines and Results
Estimated timelines — from 2 to 4 weeks depending on codebase size and integration complexity. Results: fully indexed codebase with code-aware chunking, semantic search API, documentation and team training (1–2 hours), support for one month after delivery.
Our experience — 5 years on the market, over 20 completed RAG projects for fintech, edtech, and e-commerce. We guarantee quality: precision@3 no lower than 0.8 on your golden set. Get in touch with us — we'll assess your project in 1 day and propose the architecture for your code RAG. Get optimization advice on the first call.







