Intégration OCR + RAG
Construire un système documentaire intelligent
Le RAG (Retrieval-Augmented Generation) est la clé pour interroger de grands corpus documentaires. En combinant l’OCR de Mistral, les embeddings et un vector store, vous créez un système capable de répondre à n’importe quelle question sur vos archives.
Pourquoi OCR + RAG ?
Le problème fondamental du RAG documentaire est que les chunks indexés doivent être du texte. Les embeddings multimodaux ne sont pas encore assez performants pour comparer directement des questions textuelles avec des images de documents.
La solution : utilisez l’OCR pour convertir vos documents en texte structuré de haute qualité, puis indexez ce texte dans votre vector store.
Architecture du pipeline
Le pipeline complet se décompose en 5 étapes :
- Ingestion : réception des documents (PDF, images, DOCX)
- OCR + Annotations : extraction du texte et des métadonnées structurées
- Chunking : découpage intelligent du texte en segments
- Embedding + Indexation : vectorisation et stockage dans le vector store
- Requête : recherche sémantique puis génération de réponse
Implémentation complète
Étape 1 : Extraction OCR enrichie
import os
import base64
from mistralai import Mistral
from mistralai.extra import response_format_from_pydantic_model
from pydantic import BaseModel, Field
from typing import Optional
client = Mistral(api_key=os.environ["MISTRAL_API_KEY"])
class DescriptionImage(BaseModel):
type_image: str = Field(..., description="Type: graph, table, photo, logo")
description: str = Field(..., description="Description du contenu")
def extraire_document_enrichi(chemin: str) -> list[dict]:
"""Extrait le contenu enrichi d'un document pour le RAG."""
with open(chemin, "rb") as f:
doc_b64 = base64.b64encode(f.read()).decode("utf-8")
response = client.ocr.process(
model="mistral-ocr-latest",
document={
"type": "document_base64",
"document_base64": doc_b64
},
table_format="markdown",
bbox_annotation_format=response_format_from_pydantic_model(
DescriptionImage
),
include_image_base64=False # Pas besoin des images pour le RAG
)
pages = []
for page in response.pages:
contenu = page.markdown
# Remplacer les références images par leurs descriptions
for image in page.images:
if image.annotation:
desc = DescriptionImage.model_validate_json(
image.annotation
)
placeholder = f""
remplacement = (
f"[{desc.type_image}: {desc.description}]"
)
contenu = contenu.replace(placeholder, remplacement)
pages.append({
"page": page.index + 1,
"contenu": contenu,
"header": page.header or "",
"footer": page.footer or "",
})
return pages
Étape 2 : Chunking intelligent
def chunker_par_sections(
texte: str,
taille_max: int = 1000,
chevauchement: int = 200
) -> list[str]:
"""Découpe le texte en respectant les sections Markdown."""
sections = texte.split("\n## ")
chunks = []
for section in sections:
if not section.strip():
continue
# Reconstituer le titre de section
if not section.startswith("## "):
section = "## " + section
if len(section) <= taille_max:
chunks.append(section)
else:
# Sous-découpage par paragraphes
paragraphes = section.split("\n\n")
chunk_courant = ""
for para in paragraphes:
if len(chunk_courant) + len(para) > taille_max:
if chunk_courant:
chunks.append(chunk_courant.strip())
chunk_courant = para
else:
chunk_courant += "\n\n" + para
if chunk_courant:
chunks.append(chunk_courant.strip())
return chunks
Étape 3 : Embedding et indexation
import numpy as np
def creer_index(documents: dict[str, list[dict]]) -> list[dict]:
"""Crée un index vectoriel à partir des documents extraits."""
index = []
for nom_doc, pages in documents.items():
# Concaténer toutes les pages
texte_complet = "\n\n".join(p["contenu"] for p in pages)
# Chunking
chunks = chunker_par_sections(texte_complet)
for i, chunk in enumerate(chunks):
# Embedding
response = client.embeddings.create(
model="mistral-embed",
inputs=[chunk]
)
index.append({
"document": nom_doc,
"chunk_id": i,
"texte": chunk,
"embedding": np.array(response.data[0].embedding),
"page": pages[0]["page"] if pages else 0,
})
return index
def rechercher_chunks(
question: str,
index: list[dict],
top_k: int = 5
) -> list[dict]:
"""Recherche les chunks les plus pertinents."""
response = client.embeddings.create(
model="mistral-embed",
inputs=[question]
)
q_emb = np.array(response.data[0].embedding)
scores = []
for item in index:
similarite = np.dot(q_emb, item["embedding"]) / (
np.linalg.norm(q_emb) * np.linalg.norm(item["embedding"])
)
scores.append((similarite, item))
scores.sort(key=lambda x: x[0], reverse=True)
return [item for _, item in scores[:top_k]]
Étape 4 : Génération de réponse
def repondre_question(question: str, index: list[dict]) -> str:
"""Répond à une question en utilisant le RAG."""
# Recherche des chunks pertinents
resultats = rechercher_chunks(question, index, top_k=5)
# Construction du contexte
contexte = "\n\n---\n\n".join(
f"[Source: {r['document']}, Chunk {r['chunk_id']}]\n{r['texte']}"
for r in resultats
)
# Génération de la réponse
response = client.chat.complete(
model="mistral-small-latest",
messages=[
{
"role": "system",
"content": (
"Vous êtes un assistant documentaire. Répondez "
"aux questions en vous basant uniquement sur le "
"contexte fourni. Citez les sources."
)
},
{
"role": "user",
"content": (
f"Contexte :\n{contexte}\n\n"
f"Question : {question}"
)
}
]
)
return response.choices[0].message.content
Utilisation en production
Pour un déploiement en production, remplacez le stockage numpy par un vrai vector store (Pinecone, Weaviate, Qdrant, ChromaDB) :
# Exemple avec ChromaDB
import chromadb
chroma_client = chromadb.Client()
collection = chroma_client.create_collection(name="documents")
# Indexation
for item in index:
collection.add(
ids=[f"{item['document']}_{item['chunk_id']}"],
embeddings=[item["embedding"].tolist()],
documents=[item["texte"]],
metadatas=[{"document": item["document"]}]
)
# Recherche
resultats = collection.query(
query_embeddings=[q_emb.tolist()],
n_results=5
)
Points clés à retenir
- Le pipeline OCR + RAG convertit vos documents en une base de connaissances interrogeable
- L’enrichissement par annotations (descriptions d’images) évite la perte d’information
- Le chunking intelligent respecte les sections et paragraphes du document
- Utilisez un vrai vector store en production (ChromaDB, Pinecone, Qdrant)
- La qualité de l’extraction OCR détermine la qualité des réponses RAG