reference/moteur-rag.md

Moteur RAG — référence du composant

Nature

Le moteur RAG est un bundle Symfony headless fournissant le volet retrieval d'une chaîne RAG (retrieval-augmented generation). Il ne génère pas de texte (pas d'appel LLM, pas d'UI de chat) : il transforme une arborescence de fichiers Markdown en un index vectoriel interrogeable par similarité sémantique, chaque résultat conservant sa provenance (fichier, section, ancre, position) pour permettre la citation des sources.

Exposition : une CLI et des services autowirables. Aucune interface graphique.

Hors périmètre du composant : génération LLM, UI, outils MCP, sources externes / veille.

Pipeline

Indexation (batch)
  racine ─▶ Walk (parcours + filtres) ─▶ Chunking (sections → chunks)
        ─▶ Client embeddings (POST /embed, type=passage) ─▶ Store (upsert par document)

Requête (à la demande)
  question ─▶ Client embeddings (POST /embed, type=query)
          ─▶ Store.search (kNN cosinus) ─▶ Hit[]

Architecture — couches et responsabilités

Trois contrats publics interchangeables (points de substitution), plus des services concrets d'orchestration.

Couche Interface Rôle
Découpage ChunkerInterface document → Chunk[]
Embeddings EmbeddingClientInterface texte(s) → vecteurs (via HTTP)
Store VectorStoreInterface persistance + recherche kNN
Ingestion (service concret) walk → chunk → embed → upsert
Récupération (service concret) embed(query) → search
Parcours (service concret) fichiers → Document
Estimation tokens (service concret) comptage approché de tokens

Le bundle n'embarque pas le microservice d'embeddings (dépendance réseau externe) ni l'extension vectorielle SQLite (dépendance système). Pas d'ORM : le schéma SQL est géré à la main, sans migrations.

Chaîne d'exceptions

Interface marqueur RagException (extends \Throwable) que l'appelant peut attraper de façon générique. Exceptions concrètes : EmbeddingException (invalidInput / unavailable / malformedResponse) et VectorStoreException (dont extensionUnavailable).

Ingestion et gouvernance

Parcours récursif d'une racine source. Un fichier est retenu si toutes les conditions sont réunies : extension md/markdown (insensible à la casse) ; fichier régulier ; contenu lisible et non vide après trim ; passe les filtres de gouvernance.

Filtres de gouvernance (ordre d'application)

  1. Exclusions dures, non configurables — un fichier de type bac-à-sable (SCRATCH.md) et tout segment de dossier de type legacy sont toujours exclus, à n'importe quel niveau.
  2. excluded_paths (configurable) — préfixes relatifs exclus.
  3. included_paths (configurable) — si non vide, allowlist stricte ; sinon tout est autorisé.
  4. Fichier d'ignore (.aiignore) à la racine du parcours — motifs glob simples (un par ligne, # = commentaire), testés sur le chemin relatif.

Modèle d'inclusion par défaut : default-allow (included_paths vide → tout Markdown de la racine est candidat). Pour un modèle default-deny, renseigner explicitement une allowlist via included_paths. Point de vigilance pour l'intégrateur : sans allowlist, tout .md déposé sous la racine est indexé.

Extraction front-matter et titre

  • Front-matter YAML scalaire uniquement (paires clé: valeur ; pas de listes/blocs/imbrication). Le corps indexé est le document sans le bloc front-matter.
  • Titre résolu par priorité : title du front-matter → premier # H1 du corps → basename du fichier.
  • content_hash = sha256 du fichier brut entier (front-matter inclus) — clé de l'incrémental.

Découpage (chunking)

  • Sections délimitées par les titres H1–H3 (H4+ non traités comme titres). Chaque section porte un title et une anchor (slug UTF-8 du titre, accents conservés). Le préambule avant le premier titre porte le titre du document.
  • Paragraphes séparés par les lignes vides.
  • Empaquetage : accumulation de paragraphes jusqu'à la taille cible en tokens (chunk.size), puis émission d'un chunk et amorçage du suivant par une queue de chevauchement (chunk.overlap tokens). Un paragraphe plus grand que la cible forme un chunk isolé (pas de coupe intra-phrase).
  • Estimation de tokens : heuristique déterministe ceil(nb_mots × 1.33) — approximation FR/EN, pas le compte exact du tokenizer du modèle.
  • Bornes : size > 0 ; overlap ∈ [0, size[ (sinon exception).

Embeddings — contrat client

Le bundle n'embarque aucun modèle : il appelle un microservice HTTP externe. La distinction query / passage (préfixage attendu par les modèles de type e5) est gérée côté microservice.

Interface (EmbeddingClientInterface) :

const TYPE_QUERY = 'query';
const TYPE_PASSAGE = 'passage';
public function embed(string $type, array $texts): array; // float[][], @throws EmbeddingException
public function health(): array;                          // {status, model, dim}

Contrat HTTP consommé :

GET  /health → { "status": "ok", "model": "...", "dim": <int> }
POST /embed  → body { "type": "query"|"passage", "texts": [...] }
             → { "model": "...", "dim": <int>, "vectors": [[...], ...] }

Règles de validation et d'erreur :

  • Validation locale avant tout appel réseau : type inconnu ou texts vide → invalidInput (aucune requête émise).
  • Réponse HTTP 422 → invalidInput (le detail du corps JSON est extrait) — jamais de repli silencieux.
  • Autre code ≠ 200, ou transport/timeout → unavailable.
  • Clé vectors absente / non-tableau, ou count(vectors) ≠ count(texts) → malformedResponse.
  • Les vecteurs sont renvoyés dans le même ordre que les textes en entrée.

Store vectoriel — modèle de données

Store SQLite avec extension vectorielle (table virtuelle de type vec0, recherche kNN, métrique cosinus). Schéma créé à la main, idempotent (CREATE TABLE IF NOT EXISTS).

Table document

Colonne Type Contraintes
id INTEGER PK AUTOINCREMENT
path TEXT NOT NULL UNIQUE
title TEXT NOT NULL
content_hash TEXT NOT NULL
indexed_at TEXT NOT NULL (ISO 8601)

Table chunk

Colonne Type Contraintes
id INTEGER PK AUTOINCREMENT
document_id INTEGER NOT NULL, FK → document(id) ON DELETE CASCADE
section TEXT NOT NULL
anchor TEXT NOT NULL
position INTEGER NOT NULL
content TEXT NOT NULL
token_count INTEGER NOT NULL
content_hash TEXT NOT NULL
— — UNIQUE(document_id, position)

Table vectorielle chunk_vector

Table virtuelle vec0, colonne embedding float[<dimension>], distance_metric=cosine. Le rowid référence chunk.id (lien 1-1 par convention). La dimension provient de la configuration.

Une table ingest_run (traçabilité des runs) est prévue au schéma.

Écriture — upsertDocument (transactionnelle)

Remplacement complet par document (pas de diff par chunk) : à la réindexation d'un path existant, ses vecteurs puis son document sont supprimés (CASCADE purge les chunks), puis le document, ses chunks et vecteurs sont réinsérés (position repart de 0). Rollback sur toute exception. Garde-fou : count(chunks) === count(vectors).

Recherche — search (kNN)

Jointure chunk_vector × chunk × document, WHERE embedding MATCH ? AND k=?, tri par distance croissante. k ≤ 0 court-circuite à []. Score = 1.0 - distance (distance cosinus ∈ [0,2] → score typiquement dans [0,1]) : plus haut = plus proche.

Récupération (retrieval)

RetrievalService::retrieve(string $query, ?int $k = null): Hit[]. Requête vide (après trim) → [] sans appel réseau ni store. Sinon : vectorise la requête en type=query, exécute search, renvoie des Hit ({ chunk, score }). k par défaut = retrieval.k.

Incrémentalité

  • Clé = content_hash (sha256 du fichier brut complet).
  • Mode incrémental (défaut) : document sauté si le hash stocké est identique au hash courant ; sinon remplacement complet.
  • Mode complet : tous les documents sont retraités.
  • Pas de détection de suppression : un fichier retiré de la source reste dans l'index (seul un path réingéré est remplacé).
  • Embeddings d'ingestion émis par lots (embedding.batch_size), type passage.

Contrats publics (points d'intégration)

ChunkerInterface

public function chunk(Document $document): array; // Chunk[]

VectorStoreInterface

public function initialize(): void;
public function upsertDocument(Document $d, array $chunks, array $vectors): void;
public function documentHash(string $documentPath): ?string;
public function search(array $vector, int $k): array;    // Hit[]
public function stats(): array;                          // {documents, chunks, last_indexed_at}

Un intégrateur peut substituer chacune des trois implémentations (chunker, client d'embeddings, store) en redéfinissant le service correspondant.

Services concrets appelables

  • IngestionService::ingest(?string $sourceRoot = null, bool $incremental = true): IngestReport
  • RetrievalService::retrieve(string $query, ?int $k = null): Hit[]
  • SourceWalker::walk(string $sourceRoot): \Generator<Document>
  • TokenEstimator::estimate(string $text): int

Modèles de données (DTO immuables)

DTO final, propriétés public readonly, sans logique.

  • Document : path, title, content, contentHash, frontMatter.
  • Chunk : documentPath, documentTitle, section, anchor, position, content, tokenCount, contentHash.
  • Hit : chunk, score.
  • IngestReport : sourceRoot, documentsSeen/Indexed/Skipped, chunksUpserted, chunksUnchanged, durationSeconds.

Interface CLI

Commande Rôle Options
ingest Indexe une racine Markdown --source=PATH, --full
reindex Réindexation toujours incrémentale --source=PATH
search Teste une requête argument query, --k=N
stats État de l'index + santé du microservice —

stats dégrade proprement si l'index ou le microservice est indisponible (avertissement, pas d'exception).

Configuration (racine telaria_rag)

Clé Type Rôle
source_root scalar Racine des documents à indexer.
included_paths list Allowlist de préfixes relatifs (vide = tout).
excluded_paths list Préfixes relatifs exclus.
embedding.service_url scalar URL du microservice d'embeddings.
embedding.model scalar Identifiant du modèle (informatif côté bundle).
embedding.dimension int ≥ 1 Dimension des vecteurs (contrainte du store).
embedding.timeout float Timeout HTTP.
embedding.batch_size int ≥ 1 Taille des lots d'embedding.
chunk.size int ≥ 1 Taille cible d'un chunk (tokens estimés).
chunk.overlap int ≥ 0 Chevauchement (tokens).
retrieval.k int ≥ 1 Nombre de passages récupérés par défaut.
store.database_path scalar Fichier SQLite de l'index.
store.extension_path scalar Chemin du binaire d'extension vectorielle.

Contraintes vérifiées ailleurs qu'au niveau config : overlap < size (à la construction du chunker) et cohérence dimension ↔ dimension réelle du modèle (à l'encodage dans le store).

Prérequis runtime

  • Runtime PHP moderne (API objet PDO/SQLite avec chargement d'extension).
  • Service HTTP client du framework activé.
  • Extension vectorielle SQLite installée et chargeable (dépendance système, hors gestionnaire de paquets PHP).
  • Microservice d'embeddings joignable à l'URL configurée (dépendance réseau externe, non fournie par le bundle).

Assistant documentaire

Posez une question sur la documentation. Les réponses citent leurs sources — un clic ouvre le document à gauche.