# 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

  1. Comprendre l'architecture actuelle (ce document)
  2. → Identifier le bottleneck principal (chunking ? modèle ? overlap ?)
  3. → Implémenter l'amélioration prioritaire
  4. → Mesurer l'impact