Aller au contenu principal

Générer des embeddings pour vos données

Mis à jour le 29 juillet 2026

Objectifs

  • Préparer vos données textuelles pour l’embedding
  • Générer des embeddings efficacement à grande échelle
  • Stocker les vecteurs pour une utilisation ultérieure

Préparer vos données

La qualité de vos embeddings dépend directement de la qualité de vos données. Un texte truffé d’espaces parasites, de caractères de contrôle hérités d’un export mal configuré ou de retours à la ligne intempestifs produit un vecteur légèrement décalé, et ce décalage se paie ensuite sur chaque recherche. Le nettoyage n’est pas une coquetterie : c’est la première optimisation de pertinence, et la moins chère.

import re

def nettoyer_texte(texte: str) -> str:
    """Prépare un texte pour l'embedding."""
    # Supprimer les espaces multiples
    texte = re.sub(r"\s+", " ", texte)
    # Supprimer les caractères de contrôle
    texte = re.sub(r"[\x00-\x08\x0b\x0c\x0e-\x1f]", "", texte)
    # Trim
    texte = texte.strip()
    return texte

# Exemple
brut = "  Texte   avec   des   espaces\n\n\nen trop  "
propre = nettoyer_texte(brut)
print(propre)  # "Texte avec des espaces en trop"

Enrichir le contexte

Un extrait de documentation isolé de son titre perd une partie de son sens. Prenez un paragraphe qui commence par « Téléchargez le package depuis le site officiel » : hors contexte, rien n’indique qu’il s’agit de PostgreSQL, et le vecteur produit ne rapprochera jamais ce passage d’une requête portant sur PostgreSQL. En préfixant le contenu par son titre et sa section, vous réinjectez cette information dans le texte que le modèle voit.

def enrichir_document(titre: str, section: str, contenu: str) -> str:
    """Crée un texte enrichi pour un meilleur embedding."""
    return f"Titre : {titre}\nSection : {section}\n\n{contenu}"

texte_enrichi = enrichir_document(
    titre="Installation de PostgreSQL",
    section="Guide de démarrage",
    contenu="Téléchargez le package depuis le site officiel..."
)

Générer des embeddings en lots

Sur un corpus réel de centaines ou de milliers de documents, le traitement doit être découpé et capable de survivre à un incident réseau. La fonction ci-dessous parcourt les textes par tranches, trie systématiquement la réponse par index pour garantir que le vecteur n° 7 correspond bien au document n° 7, et réessaie une fois après une pause en cas d’erreur.

from openai import OpenAI
import time
import json

client = OpenAI()

def generer_embeddings(
    textes: list[str],
    model: str = "text-embedding-3-large",
    batch_size: int = 100
) -> list[list[float]]:
    """Génère des embeddings par lots avec gestion des erreurs."""
    tous_les_embeddings = []

    for i in range(0, len(textes), batch_size):
        batch = textes[i:i + batch_size]
        print(f"Traitement du lot {i // batch_size + 1}/"
              f"{(len(textes) - 1) // batch_size + 1}")

        try:
            response = client.embeddings.create(
                input=batch,
                model=model
            )
            # Trier par index pour garantir l'ordre
            batch_embeddings = sorted(
                response.data, key=lambda x: x.index
            )
            tous_les_embeddings.extend(
                [item.embedding for item in batch_embeddings]
            )
        except Exception as e:
            print(f"Erreur sur le lot {i}: {e}")
            # Retry après une pause
            time.sleep(5)
            response = client.embeddings.create(
                input=batch,
                model=model
            )
            batch_embeddings = sorted(
                response.data, key=lambda x: x.index
            )
            tous_les_embeddings.extend(
                [item.embedding for item in batch_embeddings]
            )

    return tous_les_embeddings

Le tri par index est la précaution à ne jamais négliger. Un décalage silencieux d’un seul rang suffit à associer chaque document au vecteur de son voisin, et le symptôme observé sera « la recherche renvoie n’importe quoi », sans aucune erreur technique pour vous mettre sur la piste.

Stocker les embeddings

Pour un petit corpus de quelques milliers de documents, un simple fichier JSON contenant textes, métadonnées et vecteurs fait parfaitement l’affaire, et il a l’avantage d’être lisible à l’œil nu quand vous cherchez à comprendre un résultat étrange.

import json

documents = [
    {"id": 1, "texte": "Premier document...", "metadata": {"source": "faq"}},
    {"id": 2, "texte": "Deuxième document...", "metadata": {"source": "docs"}},
]

# Générer les embeddings
textes = [d["texte"] for d in documents]
embeddings = generer_embeddings(textes)

# Ajouter les vecteurs aux documents
for doc, emb in zip(documents, embeddings):
    doc["embedding"] = emb

# Sauvegarder
with open("corpus_embeddings.json", "w") as f:
    json.dump(documents, f)

Au-delà, le JSON devient pénalisant : chaque nombre y est écrit en toutes lettres, ce qui gonfle le fichier et impose un parsing coûteux à chaque chargement. NumPy stocke les mêmes valeurs en binaire sur 4 octets et se recharge instantanément.

import numpy as np

# Convertir en matrice NumPy
matrice = np.array(embeddings, dtype=np.float32)
print(f"Shape : {matrice.shape}")  # (n_docs, 3072)

# Sauvegarder au format binaire
np.save("embeddings.npy", matrice)

# Charger
matrice_chargee = np.load("embeddings.npy")

Pipeline complet : de fichiers texte à embeddings

Rassemblons ces éléments dans une fonction qui lit un dossier, nettoie chaque fichier, génère les vecteurs et sépare le stockage en deux : les métadonnées en JSON, lisibles et modifiables, et la matrice en NumPy, compacte et rapide.

from openai import OpenAI
from pathlib import Path
import json
import numpy as np

client = OpenAI()

def pipeline_embeddings(dossier: str, model: str = "text-embedding-3-large"):
    """Pipeline complet : fichiers → embeddings → stockage."""
    dossier_path = Path(dossier)

    # 1. Lire les fichiers
    documents = []
    for fichier in sorted(dossier_path.glob("*.txt")):
        contenu = fichier.read_text(encoding="utf-8")
        contenu = nettoyer_texte(contenu)
        if contenu:
            documents.append({
                "id": fichier.stem,
                "fichier": fichier.name,
                "texte": contenu
            })

    print(f"{len(documents)} documents trouvés")

    # 2. Générer les embeddings
    textes = [d["texte"] for d in documents]
    embeddings = generer_embeddings(textes, model=model)

    # 3. Sauvegarder
    # Métadonnées en JSON
    meta = [{"id": d["id"], "fichier": d["fichier"]} for d in documents]
    with open("metadata.json", "w") as f:
        json.dump(meta, f, ensure_ascii=False, indent=2)

    # Vecteurs en NumPy
    np.save("embeddings.npy", np.array(embeddings, dtype=np.float32))

    print(f"Embeddings sauvegardés : {len(embeddings)} vecteurs "
          f"de {len(embeddings[0])} dimensions")

    return documents, embeddings

# Utilisation
documents, embeddings = pipeline_embeddings("./mes_documents/")

Notez le sorted() sur le glob : sans lui, l’ordre des fichiers dépend du système de fichiers, et vos deux fichiers de sortie risquent de ne plus correspondre l’un à l’autre après un simple changement de machine.

Estimer les coûts

Avant de lancer un traitement massif, comptez les tokens localement avec tiktoken. L’opération prend quelques secondes et vous évite de découvrir après coup qu’un corpus mal filtré contenait dix fois le volume prévu.

import tiktoken

encoder = tiktoken.get_encoding("cl100k_base")

def estimer_cout(textes: list[str], model: str = "text-embedding-3-large"):
    """Estime le coût d'embedding pour une liste de textes."""
    total_tokens = sum(len(encoder.encode(t)) for t in textes)

    # Prix approximatifs par million de tokens (vérifiez les tarifs actuels)
    prix = {
        "text-embedding-3-large": 0.13,
        "text-embedding-3-small": 0.02,
    }

    cout = total_tokens / 1_000_000 * prix.get(model, 0.13)
    print(f"Tokens estimés : {total_tokens:,}")
    print(f"Coût estimé : ${cout:.4f}")
    return total_tokens, cout

Les prix inscrits dans le dictionnaire sont indicatifs et vieillissent : vérifiez toujours les tarifs en vigueur avant de présenter un chiffrage à un client.

Résumé

  • Nettoyez et enrichissez vos textes avant de générer des embeddings
  • Utilisez le traitement par lots (batch) pour les gros corpus
  • Stockez les métadonnées en JSON et les vecteurs en NumPy
  • Estimez vos coûts avant de lancer un traitement massif