Retrieval : chunking et ranking
Retrieval : chunking et ranking
Derrière File Search se cache un pipeline de retrieval complet : vos documents sont découpés en chunks, vectorisés, indexés, puis classés par pertinence au moment de la requête. Comprendre ce pipeline vous permet d’optimiser la qualité des réponses.
Le pipeline de retrieval
Quand vous uploadez un fichier dans un vector store, OpenAI exécute automatiquement :
- Parsing : extraction du texte brut depuis le format source
- Chunking : découpage en segments de taille contrôlée
- Embedding : vectorisation de chaque chunk
- Indexation : stockage dans l’index vectoriel
Au moment de la requête :
- Embedding de la requête : la question est vectorisée
- Recherche vectorielle : les chunks les plus proches sont identifiés
- Ranking : les résultats sont re-classés par pertinence
- Injection : les meilleurs chunks sont injectés dans le contexte du modèle
Configurer la stratégie de chunking
Le chunking est le réglage qui décide de tout le reste, et l’arbitrage est facile à énoncer. Des chunks courts donnent une recherche précise mais des extraits amputés de leur contexte — la phrase trouvée est la bonne, on ne sait plus de quoi elle parle. Des chunks longs conservent le contexte mais noient l’information pertinente au milieu de texte indifférent, ce qui affaiblit le score de similarité.
Il n’existe pas de valeur universelle : le bon réglage dépend de la forme de vos documents, et c’est exactement ce que résume le tableau ci-dessous.
from openai import OpenAI
client = OpenAI()
# Stratégie par défaut : auto
vector_store = client.vector_stores.create(
name="base-juridique",
chunking_strategy={"type": "auto"}
)
# Stratégie personnalisée : chunks plus petits pour plus de precision
vector_store = client.vector_stores.create(
name="faq-support",
chunking_strategy={
"type": "static",
"static": {
"max_chunk_size_tokens": 400,
"chunk_overlap_tokens": 100
}
}
)
Choisir la bonne taille de chunk
| Type de contenu | max_chunk_size | overlap | Pourquoi |
|---|---|---|---|
| FAQ, Q&R | 200-400 | 50 | Réponses courtes et autonomes |
| Documentation technique | 600-800 | 200 | Paragraphes avec contexte |
| Contrats, textes juridiques | 800-1200 | 300 | Clauses longues et interconnectées |
| Code source | 400-600 | 100 | Fonctions et classes |
Impact du recouvrement
Le recouvrement répond à un problème précis : une information à cheval sur une frontière de découpage serait sinon coupée en deux, et aucune des deux moitiés ne matcherait la requête. En faisant se chevaucher les chunks, on garantit que toute phrase est intégralement présente dans au moins un segment. Le prix à payer est la redondance — un recouvrement de 300 tokens sur des chunks de 800 stocke près de 40 % de texte en double.
# Texte original (simplifie)
# "La clause 5.1 stipule que le fournisseur doit livrer sous 30 jours.
# En cas de retard, des pénalités de 1% par jour s'appliquent.
# La clause 5.2 précise que les pénalités sont plafonnees a 15%."
# Sans overlap : la clause 5.1 et les pénalités peuvent etre dans des chunks différents
# Avec overlap : le lien entre clause et penalites est preserve
Seuil de pertinence et ranking
Le ranking détermine quels chunks sont retenus. Vous pouvez configurer un filtre :
response = client.responses.create(
model="gpt-5.6-terra",
input="Quelle est la politique de retour ?",
tools=[{
"type": "file_search",
"vector_store_ids": [vector_store.id],
"max_num_results": 10,
"ranking_options": {
"ranker": "auto",
"score_threshold": 0.5
}
}]
)
Le score_threshold filtre les résultats en dessous d’un seuil de pertinence. Une valeur de 0.0 retourne tout, 1.0 serait trop restrictif. En pratique, 0.3 à 0.6 donne de bons résultats.
Optimiser la qualité du retrieval
Préparer les documents
C’est le levier le plus rentable de toute cette leçon, et le seul qui ne coûte rien en réglages : un document bien structuré s’indexe bien. Des titres explicites, une phrase d’introduction par section, pas de tableau converti en bouillie de texte. À l’inverse, aucun paramétrage de chunking ne sauvera un PDF dont l’extraction produit des colonnes entremêlées.
# Mauvais : PDF scanne avec OCR approximatif
# -> Chunks incoherents, embeddings de mauvaise qualite
# Bon : document structure avec titres et sections
# -> Chunks alignes sur les sections logiques
# Pattern : pre-traiter avant upload
def preparer_document(texte_brut: str) -> str:
"""Nettoie et structure le texte avant indexation."""
# Supprimer les en-têtes/pieds de page répétitifs
lignes = texte_brut.split("\n")
lignes = [l for l in lignes if not est_entete_pied(l)]
# Normaliser les espaces
texte = "\n".join(lignes)
texte = re.sub(r"\n{3,}", "\n\n", texte)
return texte
Enrichir avec des métadonnées
Les métadonnées permettent de pré-filtrer avant la recherche vectorielle, ce qui améliore la précision :
# Upload avec métadonnées riches
documents = [
{"fichier": "politique-rh-2026.pdf", "departement": "rh", "type": "politique", "annee": 2026},
{"fichier": "contrat-fournisseur-a.pdf", "departement": "achats", "type": "contrat", "annee": 2026},
{"fichier": "guide-securite-it.pdf", "departement": "dsi", "type": "guide", "annee": 2025},
]
for doc in documents:
with open(doc["fichier"], "rb") as f:
fichier = client.files.create(file=f, purpose="assistants")
client.vector_stores.files.create(
vector_store_id=vector_store.id,
file_id=fichier.id,
attributes={
"departement": doc["departement"],
"type_document": doc["type"],
"annee": doc["annee"]
}
)
Mesurer la qualité du retrieval
Sans mesure, tout réglage de chunking relève de l’intuition. Une vingtaine de questions dont vous connaissez la réponse attendue et le document qui la contient suffisent à objectiver : vous changez un paramètre, vous rejouez le jeu de test, vous comparez. C’est peu de travail au regard des heures passées à discuter d’une taille de chunk sans données.
def evaluer_retrieval(questions_test: list[dict], vs_id: str) -> dict:
"""Évalue la qualite du retrieval sur un jeu de test.
Chaque element de questions_test contient :
- question: la question a poser
- reponse_attendue: mots-cles ou phrases attendus dans la réponse
"""
resultats = {"total": 0, "pertinent": 0, "non_pertinent": 0}
for test in questions_test:
response = client.responses.create(
model="gpt-5.6-terra",
input=test["question"],
tools=[{
"type": "file_search",
"vector_store_ids": [vs_id],
"max_num_results": 5
}]
)
reponse = response.output_text.lower()
mots_cles_trouves = sum(
1 for mot in test["reponse_attendue"]
if mot.lower() in reponse
)
resultats["total"] += 1
if mots_cles_trouves >= len(test["reponse_attendue"]) * 0.5:
resultats["pertinent"] += 1
else:
resultats["non_pertinent"] += 1
resultats["taux_pertinence"] = resultats["pertinent"] / resultats["total"]
return resultats
Stratégies avancées
Multi-vector-store
Interrogez plusieurs bases documentaires en parallèle :
response = client.responses.create(
model="gpt-5.6-terra",
input="Compare notre politique RH avec les obligations légales",
tools=[{
"type": "file_search",
"vector_store_ids": [
vs_politiques_internes,
vs_textes_juridiques
],
"max_num_results": 15
}]
)
Requêtes décomposées
Une question qui contient plusieurs demandes produit un vecteur moyen qui ne ressemble à aucune d’elles, et donc une recherche médiocre sur toutes. La décomposer en sous-questions posées séparément donne des résultats nettement meilleurs — c’est le même principe que pour les requêtes web : une requête précise bat une requête exhaustive.
def recherche_decomposee(question_complexe: str, vs_id: str) -> str:
"""Decompose une question complexe en sous-recherches."""
# Étape 1 : décomposer la question
decomposition = client.responses.create(
model="gpt-5.6-terra",
input=f"Decompose cette question en 2-3 sous-questions simples : {question_complexe}",
)
# Étape 2 : rechercher pour chaque sous-question
sous_resultats = []
for sous_q in decomposition.output_text.split("\n"):
if sous_q.strip():
r = client.responses.create(
model="gpt-5.6-terra",
input=sous_q.strip(),
tools=[{"type": "file_search", "vector_store_ids": [vs_id]}]
)
sous_resultats.append(r.output_text)
# Etape 3 : synthetiser
synthese = client.responses.create(
model="gpt-5.6-terra",
input=f"Question originale : {question_complexe}\n\n"
f"Resultats intermediaires :\n" + "\n---\n".join(sous_resultats) +
"\n\nSynthetise une réponse complete et coherente."
)
return synthese.output_text
Points clés à retenir
- Le chunking
staticavec taille et overlap personnalisés améliore la précision - Petits chunks pour les FAQ, grands chunks pour les documents juridiques
- Le
score_thresholdfiltre les résultats non pertinents - Nettoyez et structurez vos documents avant indexation
- Évaluez la qualité du retrieval avec un jeu de test