Aller au contenu principal

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 :

  1. Parsing : extraction du texte brut depuis le format source
  2. Chunking : découpage en segments de taille contrôlée
  3. Embedding : vectorisation de chaque chunk
  4. Indexation : stockage dans l’index vectoriel

Au moment de la requête :

  1. Embedding de la requête : la question est vectorisée
  2. Recherche vectorielle : les chunks les plus proches sont identifiés
  3. Ranking : les résultats sont re-classés par pertinence
  4. 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 contenumax_chunk_sizeoverlapPourquoi
FAQ, Q&R200-40050Réponses courtes et autonomes
Documentation technique600-800200Paragraphes avec contexte
Contrats, textes juridiques800-1200300Clauses longues et interconnectées
Code source400-600100Fonctions 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 static avec taille et overlap personnalisés améliore la précision
  • Petits chunks pour les FAQ, grands chunks pour les documents juridiques
  • Le score_threshold filtre les résultats non pertinents
  • Nettoyez et structurez vos documents avant indexation
  • Évaluez la qualité du retrieval avec un jeu de test