Aller au contenu principal

Créer les Embeddings

Encoder les chunks en vecteurs

Une fois vos documents découpés en chunks, l’étape suivante consiste à les transformer en embeddings via l’API mistral-embed. Ces vecteurs seront ensuite stockés dans un vector store pour la recherche.

Encodage simple

Pour un petit corpus (moins de 100 chunks), un appel direct suffit :

import numpy as np
from mistralai import Mistral

client = Mistral(api_key=api_key)

# Supposons que chunks est la liste de nos chunks préparés
chunk_texts = [c["text"] for c in chunks]

# Encoder en un seul appel
response = client.embeddings.create(
    model="mistral-embed",
    inputs=chunk_texts,
)

embeddings = np.array([d.embedding for d in response.data])
print(f"Embeddings: {embeddings.shape}")
# Embeddings: (87, 1024) par exemple

Encodage par lots (batching)

Pour des corpus plus grands, vous devez découper en lots pour respecter les limites de l’API :

def encode_chunks(chunks, batch_size=25, model="mistral-embed"):
    """Encoder une liste de chunks par lots."""
    texts = [c["text"] for c in chunks]
    all_embeddings = []

    for i in range(0, len(texts), batch_size):
        batch = texts[i:i + batch_size]
        print(f"  Encodage lot {i // batch_size + 1}/{(len(texts) - 1) // batch_size + 1} "
              f"({len(batch)} chunks)...")

        response = client.embeddings.create(
            model=model,
            inputs=batch,
        )
        all_embeddings.extend([d.embedding for d in response.data])

    return np.array(all_embeddings)

# Encoder tous les chunks
embeddings = encode_chunks(chunks, batch_size=25)
print(f"Total: {embeddings.shape[0]} vecteurs de {embeddings.shape[1]} dimensions")

Gestion des erreurs et retry

En production, les appels API peuvent échouer. Ajoutez une logique de retry :

import time

def encode_chunks_robust(chunks, batch_size=25, max_retries=3):
    """Encodage robuste avec gestion d'erreurs."""
    texts = [c["text"] for c in chunks]
    all_embeddings = []

    for i in range(0, len(texts), batch_size):
        batch = texts[i:i + batch_size]

        for attempt in range(max_retries):
            try:
                response = client.embeddings.create(
                    model="mistral-embed",
                    inputs=batch,
                )
                all_embeddings.extend([d.embedding for d in response.data])
                break  # Succès, sortir de la boucle retry

            except Exception as e:
                wait_time = 2 ** attempt  # Backoff exponentiel : 1s, 2s, 4s
                print(f"  Erreur lot {i // batch_size + 1}, "
                      f"tentative {attempt + 1}/{max_retries}: {e}")
                if attempt < max_retries - 1:
                    print(f"  Attente {wait_time}s avant retry...")
                    time.sleep(wait_time)
                else:
                    raise RuntimeError(
                        f"Échec après {max_retries} tentatives pour le lot {i // batch_size + 1}"
                    )

    return np.array(all_embeddings)

Sauvegarde et chargement

Ne recalculez pas les embeddings à chaque démarrage. Sauvegardez-les sur disque :

import json

def save_index(chunks, embeddings, filepath="rag_index"):
    """Sauvegarder les chunks et embeddings."""
    # Sauvegarder les embeddings (NumPy)
    np.save(f"{filepath}_embeddings.npy", embeddings)

    # Sauvegarder les métadonnées (JSON)
    metadata = []
    for c in chunks:
        metadata.append({
            "text": c["text"],
            "source": c.get("source", ""),
            "chunk_index": c.get("chunk_index", 0),
        })
    with open(f"{filepath}_metadata.json", "w", encoding="utf-8") as f:
        json.dump(metadata, f, ensure_ascii=False, indent=2)

    print(f"Index sauvegardé: {len(chunks)} chunks, {embeddings.shape}")


def load_index(filepath="rag_index"):
    """Charger les chunks et embeddings depuis le disque."""
    embeddings = np.load(f"{filepath}_embeddings.npy")

    with open(f"{filepath}_metadata.json", "r", encoding="utf-8") as f:
        metadata = json.load(f)

    print(f"Index chargé: {len(metadata)} chunks, {embeddings.shape}")
    return metadata, embeddings

# Sauvegarder
save_index(chunks, embeddings)

# Charger (au prochain démarrage)
chunks_loaded, embeddings_loaded = load_index()

Vérification de la qualité

Avant de passer à l’étape suivante, vérifiez que vos embeddings sont corrects :

from sklearn.metrics.pairwise import cosine_similarity

def verify_embeddings(chunks, embeddings, n_tests=5):
    """Vérifier la qualité des embeddings avec des tests simples."""
    print("=== Vérification des embeddings ===")
    print(f"Nombre de chunks: {len(chunks)}")
    print(f"Dimensions: {embeddings.shape[1]}")
    print(f"Norme moyenne: {np.linalg.norm(embeddings, axis=1).mean():.4f}")

    # Test : les chunks du même document devraient être plus proches
    for i in range(min(n_tests, len(chunks) - 1)):
        sim = cosine_similarity([embeddings[i]], [embeddings[i + 1]])[0][0]
        print(f"  Similarité chunk {i} <-> {i+1}: {sim:.4f}")

    # Test : un chunk comparé à lui-même = 1.0
    self_sim = cosine_similarity([embeddings[0]], [embeddings[0]])[0][0]
    print(f"  Auto-similarité (doit etre 1.0): {self_sim:.6f}")

verify_embeddings(chunks, embeddings)

Pipeline complet

Voici le résumé du pipeline d’encodage :

# 1. Préparer les chunks (leçon précédente)
chunks = prepare_documents(raw_docs)

# 2. Encoder
embeddings = encode_chunks_robust(chunks, batch_size=25)

# 3. Vérifier
verify_embeddings(chunks, embeddings)

# 4. Sauvegarder
save_index(chunks, embeddings)

print("Index prêt pour la recherche !")

Points clés à retenir

  • Utilisez le batching pour encoder de gros corpus (lots de 25-50 chunks)
  • Ajoutez une logique de retry avec backoff exponentiel pour la robustesse
  • Sauvegardez les embeddings sur disque (NumPy) pour éviter de recalculer
  • Conservez les métadonnées (texte, source, position) en JSON séparé
  • Vérifiez la qualité des embeddings avant de les utiliser en production