#
Architecture globale de mgrep
#
Vue d'ensemble
mgrep est un système de recherche sémantique de code qui combine :
- 🌳 Tree-sitter : parsing syntaxique du code
- 🤖 Sentence-transformers : embeddings sémantiques
- 🔍 Elasticsearch : stockage et recherche vectorielle
- 👁️ Watchdog : surveillance temps réel des fichiers
#
Schéma d'architecture
┌─────────────────┐
│ Codebase │
│ (Python, JS) │
└────────┬────────┘
│
▼
┌─────────────────────────────────────┐
│ watcher.py │
│ ┌──────────────────────────────┐ │
│ │ 1. Watchdog │ │
│ │ Détecte changements │ │
│ └──────────┬───────────────────┘ │
│ ▼ │
│ ┌──────────────────────────────┐ │
│ │ 2. Tree-sitter │ │
│ │ Parse le code │ │
│ │ Extrait fonctions/classes│ │
│ └──────────┬───────────────────┘ │
│ ▼ │
│ ┌──────────────────────────────┐ │
│ │ 3. Chunking │ │
│ │ Découpe en 20 lignes │ │
│ │ Crée sous-chunks │ │
│ └──────────┬───────────────────┘ │
│ ▼ │
│ ┌──────────────────────────────┐ │
│ │ 4. Sentence-transformers │ │
│ │ Génère embeddings (768d) │ │
│ └──────────┬───────────────────┘ │
│ ▼ │
│ ┌──────────────────────────────┐ │
│ │ 5. Elasticsearch │ │
│ │ Index code + embeddings │ │
│ └──────────────────────────────┘ │
└─────────────────────────────────────┘
│
▼
┌───────────────┐
│ Index prêt │
└───────┬───────┘
│
┌───────────┴───────────┐
▼ ▼
┌─────────────┐ ┌──────────────┐
│ sgrep.py │ │ es_inspect.py│
│ Recherche │ │ Inspection │
└─────────────┘ └──────────────┘
#
Composants détaillés
#
1. watcher.py - Indexeur principal
Responsabilités :
- Surveiller un répertoire
- Détecter changements (création/modification/suppression)
- Parser, chunker, embedder, indexer
Flow complet :
# 1. Scan initial
for file in walk(directory):
if file.endswith(EXTENSIONS):
index_file(file)
# 2. Surveillance continue
observer = Observer()
observer.start()
while True:
# Watchdog détecte changements
on_modified(file) → index_file(file)
Code simplifié :
class CodeIndexer:
def __init__(self, es_client, model_name):
self.es = es_client
self.model = SentenceTransformer(model_name)
self.parser = Parser(Language(tspython.language()))
def index_file(self, file_path):
# 1. Lire le fichier
content = read_file(file_path)
# 2. Parser avec tree-sitter
tree = self.parser.parse(content)
# 3. Extraire fonctions/méthodes
chunks = self.chunk_code_by_function(tree, content)
# 4. Pour chaque chunk
for chunk in chunks:
# 4a. Générer embedding
embedding = self.model.encode(chunk['content'])
# 4b. Indexer dans Elasticsearch
self.es.index(
index='code-index',
document={
'code_content': chunk['content'],
'code_embedding': embedding,
'file_path': file_path,
'line_start': chunk['start_line'],
'line_end': chunk['end_line'],
'function_name': chunk['name'],
...
}
)
#
2. Tree-sitter - Parser syntaxique
Rôle : Comprendre la structure du code sans l'exécuter
Exemple de parsing :
Code source :
"""
class UserService:
def authenticate(self, user, pwd):
if not user:
return None
return check_password(pwd)
def logout(self, user):
clear_session(user)
"""
Tree-sitter produit :
Module
└─ ClassDefinition "UserService"
└─ Block
├─ FunctionDefinition "authenticate"
│ └─ Block
│ ├─ IfStatement
│ └─ ReturnStatement
└─ FunctionDefinition "logout"
└─ Block
└─ ExpressionStatement
Ce qu'on extrait :
Chunks produits :
1. {
type: 'method_definition',
name: 'authenticate',
class_name: 'UserService',
start_line: 2,
end_line: 6,
content: "def authenticate(self, user, pwd):\n..."
}
2. {
type: 'method_definition',
name: 'logout',
class_name: 'UserService',
start_line: 8,
end_line: 9,
content: "def logout(self, user):\n..."
}
Pourquoi c'est important :
- ✅ Extraction précise des fonctions/méthodes
- ✅ Évite de couper au milieu d'une fonction
- ✅ Capture le contexte (nom classe + méthode)
Limitation actuelle :
- ❌ Seulement Python parsé finement
- ❌ JS/TS/Java tombent en fallback (chunking par lignes)
#
3. Chunking - Découpage intelligent
Stratégie actuelle :
def chunk_code_by_function(file_path, content):
chunks = []
MAX_CHUNK_LINES = 20
# 1. Parser le code
tree = parser.parse(content)
# 2. Pour chaque fonction/classe
for node in tree.root_node.children:
if node.type == 'function_definition':
chunk = extract_function(node)
# 3. Si trop long, diviser
if len(chunk) > MAX_CHUNK_LINES:
sub_chunks = split_chunk(chunk, MAX_CHUNK_LINES)
chunks.extend(sub_chunks)
else:
chunks.append(chunk)
elif node.type == 'class_definition':
# Extraire chaque méthode individuellement
methods = extract_class_methods(node)
for method in methods:
if len(method) > MAX_CHUNK_LINES:
chunks.extend(split_chunk(method, MAX_CHUNK_LINES))
else:
chunks.append(method)
return chunks
Exemple concret :
# Fonction de 45 lignes
def complex_processing(data):
# ... 45 lignes de code ...
Devient :
Chunk 1 (part 1) : lignes 1-20
Chunk 2 (part 2) : lignes 21-40
Chunk 3 (part 3) : lignes 41-45
#
4. Sentence-transformers - Embeddings
Processus :
chunk_code = """
def authenticate_user(username, password):
user = db.query(User).filter_by(username=username).first()
if not user:
return None
return check_password(user.password, password)
"""
# Tokenization
tokens = tokenizer.encode(chunk_code)
# → [101, 2933, 23657, 1041, 2102, ...] (IDs de tokens)
# Modèle BERT
hidden_states = bert_model(tokens)
# → Tensor de shape [sequence_length, 768]
# Pooling (extraction d'UN vecteur)
embedding = pooling_layer(hidden_states)
# → [0.23, -0.56, 0.12, ..., 0.89] (768 valeurs)
Stockage :
- Chaque chunk → 1 vecteur de 768 floats
- 768 × 4 bytes = 3 KB par chunk
- 1000 chunks = 3 MB d'embeddings
#
5. Elasticsearch - Index et recherche
Schema de l'index :
{
"mappings": {
"properties": {
"file_path": { "type": "text", "fields": { "keyword": "keyword" } },
"file_name": { "type": "text", "fields": { "keyword": "keyword" } },
"language": { "type": "keyword" },
"line_start": { "type": "integer" },
"line_end": { "type": "integer" },
"code_content": { "type": "text", "analyzer": "code_analyzer" },
"code_embedding": {
"type": "dense_vector",
"dims": 768,
"index": true,
"similarity": "cosine"
},
"function_name": { "type": "keyword" },
"class_name": { "type": "keyword" },
"is_partial": { "type": "boolean" },
"part_num": { "type": "integer" }
}
}
}
Recherche hybride :
{
"size": 5,
"query": {
"bool": {
"should": [
{
"multi_match": {
"query": "jwt middleware",
"fields": ["code_content^2", "function_name^3", "class_name^3"],
"fuzziness": "AUTO"
}
}
]
}
},
"knn": {
"field": "code_embedding",
"query_vector": [0.23, -0.56, ...],
"k": 5,
"num_candidates": 50,
"boost": 1.5
}
}
Scoring :
Score final = (kNN similarity × 1.5) + (text match score)
#
6. sgrep.py - Interface de recherche
Flow simplifié :
def search_code(query):
# 1. Connecter à Elasticsearch
es = Elasticsearch([ES_HOST], auth=...)
# 2. Charger le même modèle que l'indexation
model = SentenceTransformer(EMBEDDING_MODEL)
# 3. Encoder la requête
query_embedding = model.encode(query)
# 4. Recherche hybride
results = es.search(
index='code-index',
body={
"query": {"multi_match": {"query": query, ...}},
"knn": {"query_vector": query_embedding, ...}
}
)
# 5. Afficher avec Rich
display_results(results)
#
Flow de données complet
#
Indexation (watcher.py)
1. Fichier Python (user.py)
↓
2. Tree-sitter parse
↓
3. Extraire classe User avec 3 méthodes
↓
4. Chunking :
- User.__init__ (12 lignes) → 1 chunk
- User.authenticate (35 lignes) → 2 chunks (part 1, part 2)
- User.logout (8 lignes) → 1 chunk
= 4 chunks au total
↓
5. Pour chaque chunk :
- Encoder en embedding (768 floats)
- Indexer dans Elasticsearch
↓
6. Index mis à jour (4 nouveaux documents)
#
Recherche (sgrep.py)
1. Requête : "authenticate user"
↓
2. Encoder requête → [0.23, -0.56, ...]
↓
3. Elasticsearch recherche :
- kNN : trouve vecteurs similaires
- Text : trouve "authenticate" dans function_name
↓
4. Résultats fusionnés et scorés
↓
5. Top 5 retournés :
1. User.authenticate (part 1) score: 28.5
2. verify_credentials() score: 22.1
3. User.authenticate (part 2) score: 18.3
...
↓
6. Affichage avec Rich (tableau + syntaxe)
#
Points d'optimisation possibles
#
1. Chunking
Actuel :
Découpage fixe de 20 lignes
Amélioration possible :
# Sliding window avec overlap
Chunk 1 : lignes 1-20
Chunk 2 : lignes 11-30 (overlap 10)
Chunk 3 : lignes 21-40 (overlap 10)
#
2. Tree-sitter multi-langages
Actuel :
Seulement Python parsé finement
JS/TS → fallback chunking
Amélioration possible :
# Détecter le langage et charger le bon parser
if file.endswith('.js'):
parser = Parser(Language(tsjavascript.language()))
elif file.endswith('.py'):
parser = Parser(Language(tspython.language()))
#
3. Caching des embeddings
Actuel :
Chaque modification → réindexation complète du fichier
Amélioration possible :
# Cache basé sur hash du chunk
chunk_hash = md5(chunk_content)
if chunk_hash in cache:
embedding = cache[chunk_hash]
else:
embedding = model.encode(chunk_content)
cache[chunk_hash] = embedding
#
4. Indexation incrémentale
Actuel :
Modification fichier → supprimer tous chunks → réindexer tous
Amélioration possible :
# Diff et update seulement chunks modifiés
old_chunks = get_existing_chunks(file_path)
new_chunks = parse_file(file_path)
to_delete = old_chunks - new_chunks
to_add = new_chunks - old_chunks
to_update = chunks_changed(old_chunks, new_chunks)
#
5. Filtres de recherche
Actuel :
Recherche sur tout l'index
Amélioration possible :
# Filtrer par langage, dossier, etc.
python sgrep.py "query" --language python --path "src/api/*"
#
Architecture future possible
#
mgrep v2 (avec toutes les optimisations)
┌─────────────────────────────────────────┐
│ Watcher + Parser multi-langages │
│ ├─ Python (tree-sitter) │
│ ├─ JavaScript (tree-sitter) │
│ ├─ TypeScript (tree-sitter) │
│ └─ Java (tree-sitter) │
└──────────────┬──────────────────────────┘
▼
┌─────────────────────────────────────────┐
│ Chunking intelligent │
│ ├─ Sliding window (overlap 50%) │
│ ├─ AST-aware splitting │
│ └─ Respect des blocs logiques │
└──────────────┬──────────────────────────┘
▼
┌─────────────────────────────────────────┐
│ Embedding avec cache │
│ ├─ Jina Code (768 dims) │
│ ├─ Cache Redis pour embeddings │
│ └─ Batch processing │
└──────────────┬──────────────────────────┘
▼
┌─────────────────────────────────────────┐
│ Elasticsearch avec filters │
│ ├─ Index par langage │
│ ├─ Filtres par dossier │
│ └─ Scoring personnalisé │
└─────────────────────────────────────────┘
#
Prochaines étapes
- ✅ Comprendre l'architecture actuelle (ce document)
- → Identifier le bottleneck principal (chunking ? modèle ? overlap ?)
- → Implémenter l'amélioration prioritaire
- → Mesurer l'impact