Aller au contenu principal

Retrieval : chunking et ranking

Retrieval : chunking et ranking

Derriere File Search se cache un pipeline de retrieval complet : vos documents sont decoupes en chunks, vectorises, indexes, puis classes par pertinence au moment de la requete. Comprendre ce pipeline vous permet d’optimiser la qualite des reponses.

Le pipeline de retrieval

Quand vous uploadez un fichier dans un vector store, OpenAI execute automatiquement :

  1. Parsing : extraction du texte brut depuis le format source
  2. Chunking : decoupage en segments de taille controlee
  3. Embedding : vectorisation de chaque chunk
  4. Indexation : stockage dans l’index vectoriel

Au moment de la requete :

  1. Embedding de la requete : la question est vectorisee
  2. Recherche vectorielle : les chunks les plus proches sont identifies
  3. Ranking : les resultats sont re-classes par pertinence
  4. Injection : les meilleurs chunks sont injectes dans le contexte du modele

Configurer la strategie de chunking

Vous pouvez controler la taille et le recouvrement des chunks :

from openai import OpenAI

client = OpenAI()

# Strategie par defaut : auto
vector_store = client.vector_stores.create(
    name="base-juridique",
    chunking_strategy={"type": "auto"}
)

# Strategie personnalisee : 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-40050Reponses courtes et autonomes
Documentation technique600-800200Paragraphes avec contexte
Contrats, textes juridiques800-1200300Clauses longues et interconnectees
Code source400-600100Fonctions et classes

Impact du recouvrement

Le chunk_overlap_tokens determine combien de tokens sont partages entre deux chunks consecutifs :

# Texte original (simplifie)
# "La clause 5.1 stipule que le fournisseur doit livrer sous 30 jours.
#  En cas de retard, des penalites de 1% par jour s'appliquent.
#  La clause 5.2 precise que les penalites sont plafonnees a 15%."

# Sans overlap : la clause 5.1 et les penalites peuvent etre dans des chunks differents
# Avec overlap : le lien entre clause et penalites est preserve

Seuil de pertinence et ranking

Le ranking determine quels chunks sont retenus. Vous pouvez configurer un filtre :

response = client.responses.create(
    model="gpt-5.3",
    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 resultats en dessous d’un seuil de pertinence. Une valeur de 0.0 retourne tout, 1.0 serait trop restrictif. En pratique, 0.3 a 0.6 donne de bons resultats.

Optimiser la qualite du retrieval

Preparer les documents

La qualite du retrieval depend enormement de la preparation des documents :

# 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-tetes/pieds de page repetitifs
    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 metadonnees

Les metadonnees permettent de pre-filtrer avant la recherche vectorielle, ce qui ameliore la precision :

# Upload avec metadonnees 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 qualite du retrieval

Pour evaluer si vos chunks retournent les bonnes informations :

def evaluer_retrieval(questions_test: list[dict], vs_id: str) -> dict:
    """Evalue 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 reponse
    """
    resultats = {"total": 0, "pertinent": 0, "non_pertinent": 0}

    for test in questions_test:
        response = client.responses.create(
            model="gpt-5.3",
            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

Strategies avancees

Multi-vector-store

Interrogez plusieurs bases documentaires en parallele :

response = client.responses.create(
    model="gpt-5.3",
    input="Compare notre politique RH avec les obligations legales",
    tools=[{
        "type": "file_search",
        "vector_store_ids": [
            vs_politiques_internes,
            vs_textes_juridiques
        ],
        "max_num_results": 15
    }]
)

Requetes decomposees

Pour des questions complexes, decomposez en sous-requetes :

def recherche_decomposee(question_complexe: str, vs_id: str) -> str:
    """Decompose une question complexe en sous-recherches."""
    # Etape 1 : decomposer la question
    decomposition = client.responses.create(
        model="gpt-5.3",
        input=f"Decompose cette question en 2-3 sous-questions simples : {question_complexe}",
    )

    # Etape 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.3",
                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.3",
        input=f"Question originale : {question_complexe}\n\n"
              f"Resultats intermediaires :\n" + "\n---\n".join(sous_resultats) +
              "\n\nSynthetise une reponse complete et coherente."
    )
    return synthese.output_text

Points cles a retenir

  • Le chunking static avec taille et overlap personnalises ameliore la precision
  • Petits chunks pour les FAQ, grands chunks pour les documents juridiques
  • Le score_threshold filtre les resultats non pertinents
  • Nettoyez et structurez vos documents avant indexation
  • Evaluez la qualite du retrieval avec un jeu de test