De la démonstration à la production
Un prototype de RAG tient en quelques lignes : découper un texte tous les 200 mots, calculer des vecteurs, chercher les plus proches, les coller dans le prompt. En production, les difficultés sont ailleurs : des centaines de documents hétérogènes, des mises à jour quotidiennes, des droits d’accès différents selon les équipes, et des utilisateurs qui veulent vérifier la source d’une réponse. Ce cours traite ces questions, en partant de la base : un pipeline d’indexation fiable.
Le pipeline d’indexation
L’indexation est un traitement séparé de l’interrogation, exécuté en tâche de fond à chaque ajout ou modification de document :
- Extraire le texte (PDF, pages web, outil de documentation, tickets). La qualité de l’extraction conditionne tout le reste : en-têtes de page répétés, tableaux aplatis, colonnes mélangées dégradent la recherche.
- Nettoyer : supprimer les éléments répétitifs, normaliser les espaces, conserver les titres.
- Découper en morceaux (chunks).
- Encoder chaque morceau en vecteur (leçon 2).
- Stocker texte, vecteur et métadonnées dans l’index.
Le pipeline doit être reproductible : relancé sur les mêmes documents, il produit les mêmes morceaux avec les mêmes identifiants.
Découper selon la structure
Un découpage à taille fixe coupe au milieu des phrases et mélange deux sections dans un même morceau. Les documents d’entreprise ont presque toujours une structure (titres Markdown ou HTML, styles de titres dans un traitement de texte) : on s’en sert.
def split_sections(markdown, doc_id):
sections, path, buffer = [], [], []
for line in markdown.splitlines():
match = re.match(r"^(#{1,6})\s+(.+)$", line)
if match:
# on enregistre la section précédente, puis on met à jour le chemin
...
level = len(match.group(1))
path[:] = path[:level - 1] + [match.group(2).strip()]
else:
buffer.append(line)
Le chemin de titres est conservé : un paragraphe « Le report est limité à 5 jours » n’a de sens qu’avec son contexte « Congés payés > Report ».
Puis borner la taille
Une section peut être très longue. On regroupe alors ses paragraphes dans des morceaux qui ne dépassent pas une taille maximale (en mots ici, en tokens en production), sans jamais couper un paragraphe s’il tient :
- on ajoute les paragraphes un à un tant que la limite n’est pas dépassée ;
- quand le suivant ne tient plus, on ferme le morceau ;
- le nouveau morceau reprend le dernier paragraphe du précédent (recouvrement), si cela tient dans la limite.
Le recouvrement évite qu’une information à cheval sur deux morceaux soit introuvable. Il a un coût : du texte dupliqué dans l’index et parfois dans le prompt. Un paragraphe seul plus long que la limite forme son propre morceau (ou est redécoupé par phrases).
Identifiants et métadonnées
Chaque morceau reçoit un identifiant stable, par exemple politique-conges#3 (document et rang). Il sert aux citations, à l’évaluation et aux mises à jour. On y attache des métadonnées :
{
"chunk_id": "politique-conges#3",
"doc_id": "politique-conges",
"path": "Congés payés > Report",
"text": "Le report est limité à 5 jours...",
"embed_text": "Congés payés > Report\n\nLe report est limité à 5 jours...",
"organization_id": 12, "visibility": "rh", "version": "2026-09",
}
embed_text (chemin + texte) est ce que l’on encode ; text est ce que l’on affiche et cite. Les champs organization_id et visibility serviront au filtrage par droits (leçon 5).
Régler et vérifier
Il n’existe pas de taille idéale universelle. Des morceaux trop petits perdent leur contexte ; trop grands, ils diluent l’information et remplissent le prompt. On part d’une valeur raisonnable (quelques centaines de tokens), puis on mesure sur un jeu de questions de référence (leçon 4) en comparant plusieurs réglages. Inspectez aussi des morceaux au hasard : c’est le moyen le plus rapide de repérer une extraction ratée.
À retenir
- L’indexation est un pipeline reproductible, séparé de l’interrogation.
- Découpez par la structure, bornez la taille, ajoutez un recouvrement.
- Identifiants stables et métadonnées rendent possibles citations, droits et mises à jour.
Exercices
Les solutions et les tests restent sur le serveur. Dans l'espace étudiant, ton code est exécuté et corrigé, et le tuteur donne des indices avant la solution.
Découper un document Markdown par sections
Écrivez
split_sections(markdown, doc_id)qui renvoie la liste des sections non vides, chacune sous la forme{"doc_id": ..., "path": ..., "text": ...}:- une ligne de titre commence par 1 à 6
#suivis d’un espace ; son niveau est le nombre de#; pathest le chemin des titres en cours, joints par" > ": un titre de niveau n remplace le titre de niveau n et supprime les niveaux plus profonds ;textest le contenu entre deux titres, lignes jointes par\npuis débarrassé des espaces et sauts de ligne de bord ; une section sans texte est ignorée ;- un texte placé avant tout titre a un
pathvide ("").
import re def split_sections(markdown, doc_id): ...- une ligne de titre commence par 1 à 6
Regrouper les paragraphes avec recouvrement
Écrivez :
pack_paragraphs(paragraphs, max_words, overlap=1)qui regroupe des paragraphes consécutifs en morceaux d’au plusmax_wordsmots (mots =split()) : on ajoute les paragraphes tant que la limite n’est pas dépassée ; quand le suivant ne tient plus, on ferme le morceau, puis le nouveau morceau commence par lesoverlapderniers paragraphes du précédent seulement si eux et le nouveau paragraphe tiennent dans la limite (sinon il commence vide). Un paragraphe trop long à lui seul forme son propre morceau. Chaque morceau est renvoyé sous forme de texte, paragraphes joints par\n\n;build_chunks(sections, max_words, overlap=1)qui, pour chaque section (format de l’exercice précédent), découpetexten paragraphes (séparés par\n\n, vides ignorés), les regroupe avecpack_paragraphset renvoie des dictionnaireschunk_id("<doc_id>#<n>", n compté à partir de 0 par document),doc_id,path,textetembed_text(path + "\n\n" + text, outextseul si le chemin est vide).
def pack_paragraphs(paragraphs, max_words, overlap=1): ... def build_chunks(sections, max_words, overlap=1): ...Pourquoi encoder le chemin de titres ?
Un morceau contient seulement « Le report est limité à 5 jours. ». Pourquoi encoder « Congés payés > Report » avec ce texte ?
- Pour réduire la taille du vecteur
- Parce que, sans contexte, le morceau ne mentionne pas les congés et sera mal retrouvé par une question sur le report de congés
- Parce que les modèles d'embedding refusent les textes courts
- Pour accélérer l'index
Quiz de fin de leçon
Que doit produire un pipeline d’indexation relancé sur les mêmes documents ?
- Des morceaux différents pour varier les résultats
- Les mêmes morceaux avec les mêmes identifiants
- Un index vide
À quoi sert le recouvrement entre deux morceaux ?
- À éviter qu'une information à cheval sur deux morceaux soit introuvable
- À réduire la taille de l'index
- À chiffrer le texte
Quelle métadonnée est indispensable pour filtrer les résultats selon l’utilisateur ?
- La longueur du morceau
- L'organisation et le niveau de visibilité du document
- La couleur du titre
La suite du cours
- Embeddings et pgvector : stocker et chercher des vecteurs20 min
- Recherche hybride : mots-clés, vecteurs et fusion20 min
- Évaluer un RAG : recherche, citations et réponses20 min
- Mises à jour et droits d'accès20 min