Embeddings et métriques de similarité | Guide complet

tl;dr: Les embeddings transforment texte/images en vecteurs. Choisissez entre OpenAI (1536 dim, payant), Sentence-BERT (384-1024 dim, gratuit), ou Cohere (multilingue). Utilisez cosine similarity pour la recherche sémantique.

Les embeddings sont le cœur des bases de données vectorielles. Sans de bons embeddings, impossible d’avoir une bonne recherche sémantique.

Dans cet article, nous allons maîtriser les embeddings, comprendre les métriques de similarité, et apprendre à optimiser la qualité de vos recherches.

Diagramme technique illustrant les embeddings et la recherche par similarité pour les bases de données vectorielles utilisées en IA

Objectifs de l’article

Après avoir lu cet article, vous serez capable de :

  • ✅ Comprendre les différents types d’embeddings
  • ✅ Choisir le bon modèle d’embedding selon votre cas d’usage
  • ✅ Maîtriser les métriques de similarité (cosine, dot product, euclidean)
  • ✅ Optimiser la qualité de vos embeddings (chunking, métadonnées)
  • ✅ Fine-tuner un modèle d’embedding pour votre domaine

Tout part des embeddings

⚠️ Warning
Règle d’or :

Les embeddings sont la traduction de vos données (texte, images, audio) en un format que les ordinateurs peuvent comprendre et comparer : des vecteurs de nombres.


Qu’est-ce qu’un embedding ?

Définition technique

Un embedding est une représentation numérique dense qui capture le sens sémantique d’une information.

Exemple :

# Texte original
text = "Le roi gouverne le royaume"

# Embedding (vecteur de 384 nombres)
embedding = [0.234, -0.512, 0.891, -0.342, 0.621, ...]

Propriétés mathématiques

Les bons embeddings ont des propriétés intéressantes :

1. Similarité sémantique = Proximité vectorielle

distance("roi", "reine") < distance("roi", "voiture")

2. Opérations algébriques

"roi" - "homme" + "femme"  "reine"

3. Clustering naturel

Les mots/concepts similaires se regroupent dans l’espace vectoriel.

💡 Lien utile : Pour une introduction aux vecteurs et embeddings, consultez : - Qu’est-ce qu’un Vecteur ? - Embeddings en IA

Dimensions des embeddings

La dimension est le nombre de nombres dans le vecteur.

Trade-off : précision vs vitesse vs stockage

DimensionPrécisionVitesseStockageUse Case
128-384⭐⭐⭐⭐⭐⭐⭐⭐0.5 KBPrototypes, faible latence
768-1024⭐⭐⭐⭐⭐⭐⭐⭐3-4 KBOptimal (recommandé)
1536⭐⭐⭐⭐⭐⭐⭐6 KBProduction qualité
3072+⭐⭐⭐⭐⭐⭐⭐12+ KBQualité maximale, coût élevé

Calcul de stockage :

# Pour 1 million de vecteurs
dimension = 1536
vectors = 1_000_000

storage_mb = (dimension * 4 bytes * vectors) / (1024 * 1024)
print(f"Stockage : {storage_mb:.0f} MB")  # ~5,860 MB ≈ 6 GB
🔎 Tip
Recommandation : Commencez avec 768 dimensions (Sentence-BERT base). C’est le meilleur compromis.

Modèles d’embedding pour texte

Voici les modèles les plus utilisés en 2025 :

Tableau comparatif complet

ModèleDimensionTypePerformancePrixLanguesUse Case
OpenAI text-embedding-3-small1536API⭐⭐⭐⭐$0.02/1M tokens100+Production, API
OpenAI text-embedding-3-large3072API⭐⭐⭐⭐⭐$0.13/1M tokens100+Qualité max
Sentence-BERT all-MiniLM-L6-v2384Local⭐⭐⭐GratuitENPrototypes, rapide
Sentence-BERT all-mpnet-base-v2768Local⭐⭐⭐⭐GratuitENProduction locale
BGE-base-en-v1.5768Local⭐⭐⭐⭐GratuitENSOTA open source
E5-large-v21024Local⭐⭐⭐⭐Gratuit100+Multilingue
Cohere embed-multilingual-v31024API⭐⭐⭐⭐$0.10/1M tokens100+Multilingue, 100+ langues
Jina embeddings-v2768Local/API⭐⭐⭐⭐Gratuit/API100+Long context (8K tokens)

OpenAI Embeddings (API, payant)

Le plus simple et de très bonne qualité.

from openai import OpenAI

client = OpenAI(api_key="sk-...")

# Générer un embedding
response = client.embeddings.create(
    model="text-embedding-3-small",
    input="Les bases de données vectorielles sont puissantes"
)

embedding = response.data[0].embedding
print(f"Dimension : {len(embedding)}")  # 1536
print(f"Type : {type(embedding)}")      # list

Avantages :

  • ✅ Très bonne qualité out-of-the-box
  • ✅ Multilingue (100+ langues)
  • ✅ Pas de modèle à héberger

Inconvénients :

  • ❌ Coût (mais raisonnable : $0.02/1M tokens)
  • ❌ Dépendance API
  • ❌ Latence réseau

Batch processing (important pour économies) :

# Embedder plusieurs textes en un appel (plus efficace)
texts = [
    "Texte 1",
    "Texte 2",
    # ... jusqu'à 2048 textes par batch
]

response = client.embeddings.create(
    model="text-embedding-3-small",
    input=texts  # Liste de textes
)

embeddings = [item.embedding for item in response.data]
print(f"Nombre d'embeddings : {len(embeddings)}")

Sentence-BERT (local, gratuit)

Open source, exécutable localement sans API.

from sentence_transformers import SentenceTransformer

# Charger le modèle (téléchargé localement)
model = SentenceTransformer('all-mpnet-base-v2')

# Générer embeddings
texts = [
    "Les bases de données vectorielles sont essentielles",
    "La recherche sémantique révolutionne l'IA"
]

embeddings = model.encode(texts)

print(f"Shape : {embeddings.shape}")  # (2, 768)
print(f"Type : {type(embeddings)}")   # numpy.ndarray

Modèles recommandés :

# Rapide et léger (bon pour prototypes)
model_mini = SentenceTransformer('all-MiniLM-L6-v2')  # 384 dim, 80MB

# Optimal (recommandé pour production)
model_base = SentenceTransformer('all-mpnet-base-v2')  # 768 dim, 420MB

# Multilingue
model_multi = SentenceTransformer('paraphrase-multilingual-mpnet-base-v2')  # 768 dim

Avantages :

  • ✅ Gratuit et open source
  • ✅ Exécution locale (pas de latence réseau)
  • ✅ Privacy (données restent locales)
  • ✅ Nombreux modèles spécialisés

Inconvénients :

  • ❌ Nécessite GPU pour grande échelle
  • ❌ Qualité légèrement inférieure à OpenAI
  • ❌ Modèle à héberger/maintenir

Cohere embeddings (API, multilingue)

Excellente alternative à OpenAI, spécialisé multilingue.

import cohere

co = cohere.Client(api_key="...")

# Générer embeddings
response = co.embed(
    texts=["Texte en français", "Text in English"],
    model="embed-multilingual-v3.0",
    input_type="search_document"  # ou "search_query"
)

embeddings = response.embeddings
print(f"Dimension : {len(embeddings[0])}")  # 1024

Avantages :

  • ✅ Multilingue (100+ langues) de qualité
  • ✅ Distinction document vs query (important !)
  • ✅ Bon rapport qualité/prix

Inconvénients :

  • ❌ Payant (mais compétitif)
  • ❌ Dépendance API

Batch processing pour performance

⚠️ Warning
Erreur courante : Générer embeddings un par un dans une boucle. python # ❌ LENT : Boucle (100x plus lent) embeddings = [model.encode(text) for text in texts] # ✅ RAPIDE : Batch embeddings = model.encode(texts, batch_size=32)

Benchmark : 1000 textes

import time

texts = ["Texte exemple"] * 1000

# Méthode lente (boucle)
start = time.time()
embeddings_slow = [model.encode(text) for text in texts]
time_slow = time.time() - start

# Méthode rapide (batch)
start = time.time()
embeddings_fast = model.encode(texts, batch_size=32)
time_fast = time.time() - start

print(f"Lent : {time_slow:.2f}s")  # ~45s
print(f"Rapide : {time_fast:.2f}s")  # ~0.5s
print(f"Speedup : {time_slow/time_fast:.0f}x")  # 90x plus rapide !

Métriques de similarité

Comment comparer deux vecteurs pour savoir s’ils sont similaires ?

Cosine Similarity (la plus utilisée)

Mesure l’angle entre deux vecteurs. Valeur entre -1 et 1.

Formule :

$$ \text{similarity} = \frac{A \cdot B}{||A|| \times ||B||} = \cos(\theta) $$

Code :

from sklearn.metrics.pairwise import cosine_similarity
import numpy as np

# Deux vecteurs
vec1 = np.array([[0.5, 0.8, 0.2, 0.1]])
vec2 = np.array([[0.6, 0.7, 0.3, 0.2]])

similarity = cosine_similarity(vec1, vec2)[0][0]
print(f"Similarité : {similarity:.4f}")  # Ex: 0.9823

Interprétation :

  • 1.0 : Vecteurs identiques (même direction)
  • 0.5 : Similaires (angle 60°)
  • 0.0 : Orthogonaux (aucune relation)
  • -1.0 : Opposés (180°)

Avantages :

  • ✅ Invariant à la magnitude (norme)
  • ✅ Très utilisé en NLP
  • ✅ Intuitif (angle)

Inconvénients :

  • ❌ Calcul légèrement plus lent que dot product

Dot Product

Produit scalaire simple.

Formule :

$$ \text{dot} = A \cdot B = \sum_{i=1}^{n} a_i \times b_i $$

Code :

import numpy as np

vec1 = np.array([0.5, 0.8, 0.2])
vec2 = np.array([0.6, 0.7, 0.3])

dot_product = np.dot(vec1, vec2)
print(f"Dot product : {dot_product:.4f}")  # 0.92

Avantages :

  • ✅ Très rapide à calculer
  • ✅ Si vecteurs normalisés : dot product = cosine similarity

Inconvénients :

  • ❌ Sensible à la magnitude
  • ❌ Nécessite normalisation pour être équivalent à cosine

Normalisation :

from sklearn.preprocessing import normalize

# Normaliser (norme L2 = 1)
vec1_norm = normalize([vec1], norm='l2')[0]
vec2_norm = normalize([vec2], norm='l2')[0]

# Maintenant : dot product == cosine similarity
dot_normalized = np.dot(vec1_norm, vec2_norm)
cosine = cosine_similarity([vec1], [vec2])[0][0]

print(f"Dot (normalized) : {dot_normalized:.4f}")
print(f"Cosine : {cosine:.4f}")
# Identiques !

Euclidean distance

Distance géométrique classique.

Formule :

$$ \text{distance} = \sqrt{\sum_{i=1}^{n} (a_i - b_i)^2} $$

Code :

from scipy.spatial.distance import euclidean

vec1 = np.array([0.5, 0.8, 0.2])
vec2 = np.array([0.6, 0.7, 0.3])

distance = euclidean(vec1, vec2)
print(f"Distance : {distance:.4f}")  # 0.1732

Interprétation :

  • 0 : Vecteurs identiques
  • Plus grand : Vecteurs plus différents

Avantages :

  • ✅ Intuitif (distance géométrique)
  • ✅ Utile pour clustering

Inconvénients :

  • ❌ Sensible à la magnitude
  • ❌ Moins utilisé que cosine en NLP

Manhattan Distance (L1)

Somme des différences absolues.

from scipy.spatial.distance import cityblock

distance_l1 = cityblock(vec1, vec2)
print(f"Manhattan distance : {distance_l1:.4f}")

Use case : Rarement utilisée en recherche sémantique.


Choisir la bonne métrique

Use CaseMétrique RecommandéeRaison
Recherche sémantique texteCosineInvariant à longueur
Recherche d’imagesCosine ou DotSelon normalisation
RecommandationCosineCompare préférences
ClusteringEuclideanDistances géométriques
Recherche vocaleCosineInvariant à volume
Performance critiqueDot (avec normalisation)Le plus rapide
🔎 Tip
Recommandation générale : Utilisez cosine similarity pour le texte. C’est le standard de facto.

Optimiser la qualité des embeddings

Chunking Intelligent

La taille des chunks (morceaux de texte) est cruciale.

Problème

Trop petit (50 tokens) :

  • ❌ Perte de contexte
  • ❌ Sens incomplet

Trop grand (1000+ tokens) :

  • ❌ Dilution du sens
  • ❌ Bruit informationnel

Solution : taille optimale

# Règle générale
OPTIMAL_CHUNK_SIZE = 300  # tokens (~200-500 selon cas)
CHUNK_OVERLAP = 50        # 10-20% overlap

Code : Chunking avec Overlap

def chunk_text(text: str, chunk_size: int = 300, overlap: int = 50) -> list[str]:
    """
    Découper texte en chunks avec overlap.

    Args:
        text: Texte à découper
        chunk_size: Taille en mots (approximation de tokens)
        overlap: Overlap en mots

    Returns:
        Liste de chunks
    """
    words = text.split()
    chunks = []

    for i in range(0, len(words), chunk_size - overlap):
        chunk = ' '.join(words[i:i + chunk_size])
        if len(chunk.strip()) > 0:
            chunks.append(chunk)

    return chunks

# Exemple
long_text = "..." * 1000  # Texte long
chunks = chunk_text(long_text, chunk_size=300, overlap=50)

print(f"Nombre de chunks : {len(chunks)}")
print(f"Premier chunk : {chunks[0][:100]}...")

Chunking sémantique (avancé)

Découper par paragraphes ou phrases au lieu de taille fixe :

def semantic_chunk(text: str, max_tokens: int = 500) -> list[str]:
    """Découper par paragraphes avec limite de tokens."""
    import re

    # Découper par paragraphes
    paragraphs = text.split('\n\n')

    chunks = []
    current_chunk = []
    current_size = 0

    for para in paragraphs:
        para_size = len(para.split())  # Approximation tokens

        if current_size + para_size > max_tokens and current_chunk:
            # Sauvegarder chunk actuel
            chunks.append('\n\n'.join(current_chunk))
            current_chunk = [para]
            current_size = para_size
        else:
            current_chunk.append(para)
            current_size += para_size

    # Dernier chunk
    if current_chunk:
        chunks.append('\n\n'.join(current_chunk))

    return chunks

Enrichir avec métadonnées

Ajouter du contexte avant de générer l’embedding.

def enrich_text(text: str, metadata: dict) -> str:
    """
    Enrichir texte avec métadonnées.

    Args:
        text: Texte original
        metadata: Métadonnées (title, category, date, etc.)

    Returns:
        Texte enrichi
    """
    parts = []

    # Ajouter titre
    if 'title' in metadata:
        parts.append(f"Titre: {metadata['title']}")

    # Ajouter catégorie
    if 'category' in metadata:
        parts.append(f"Catégorie: {metadata['category']}")

    # Ajouter date
    if 'date' in metadata:
        parts.append(f"Date: {metadata['date']}")

    # Texte original
    parts.append(text)

    return '\n'.join(parts)

# Exemple
text = "PyTorch est un framework de deep learning."
metadata = {
    'title': "Introduction à PyTorch",
    'category': "Deep Learning",
    'date': "2025-01-15"
}

enriched = enrich_text(text, metadata)
print(enriched)

Output :

Titre: Introduction à PyTorch
Catégorie: Deep Learning
Date: 2025-01-15
PyTorch est un framework de deep learning.

Résultat : Embeddings plus riches et contextualisés !

Normalisation

Normaliser les vecteurs permet d’utiliser dot product au lieu de cosine similarity (plus rapide).

from sklearn.preprocessing import normalize
import numpy as np

# Embeddings originaux
embeddings = model.encode(texts)  # (N, 768)

# Normaliser (norme L2 = 1)
embeddings_normalized = normalize(embeddings, norm='l2')

# Vérification
norms = np.linalg.norm(embeddings_normalized, axis=1)
print(f"Normes après normalisation : {norms[:5]}")
# [1. 1. 1. 1. 1.]

# Maintenant : dot product == cosine similarity !
similarity_cosine = cosine_similarity([embeddings[0]], [embeddings[1]])[0][0]
similarity_dot = np.dot(embeddings_normalized[0], embeddings_normalized[1])

print(f"Cosine : {similarity_cosine:.4f}")
print(f"Dot (normalized) : {similarity_dot:.4f}")
# Identiques !

Fine-tuning de modèles d’embedding

Pour améliorer la qualité sur votre domaine spécifique (médical, juridique, technique).

Fine-Tuner Sentence-BERT

from sentence_transformers import SentenceTransformer, InputExample, losses
from torch.utils.data import DataLoader

# 1. Charger modèle de base
model = SentenceTransformer('all-mpnet-base-v2')

# 2. Préparer données d'entraînement
# Format : paires (query, document positif)
train_examples = [
    InputExample(
        texts=['Symptômes COVID-19', 'Le COVID-19 provoque fièvre, toux et fatigue'],
        label=1.0  # Similaires
    ),
    InputExample(
        texts=['Traitement diabète', 'Le diabète se traite avec insuline'],
        label=1.0
    ),
    # ... plus d'exemples
]

# 3. DataLoader
train_dataloader = DataLoader(train_examples, shuffle=True, batch_size=16)

# 4. Loss function
train_loss = losses.CosineSimilarityLoss(model)

# 5. Fine-tuning
model.fit(
    train_objectives=[(train_dataloader, train_loss)],
    epochs=3,
    warmup_steps=100,
    output_path='./model_finetuned'
)

# 6. Utiliser le modèle fine-tuné
model_custom = SentenceTransformer('./model_finetuned')

Amélioration attendue : +10-30% de pertinence sur votre domaine !


Évaluer la qualité des embeddings

Métriques clés

Recall@K

Pourcentage de documents pertinents trouvés dans les top-K résultats.

def recall_at_k(query_embedding, doc_embeddings, relevant_docs, k=5):
    """
    Calculer Recall@K.

    Args:
        query_embedding: Vecteur de la requête
        doc_embeddings: Vecteurs de tous les documents
        relevant_docs: Liste des indices des documents pertinents
        k: Nombre de résultats à considérer

    Returns:
        Recall@K (0.0 à 1.0)
    """
    # Calculer similarités
    similarities = cosine_similarity([query_embedding], doc_embeddings)[0]

    # Top-K indices
    top_k_indices = np.argsort(similarities)[-k:][::-1]

    # Combien de pertinents dans top-K ?
    relevant_found = sum(1 for idx in top_k_indices if idx in relevant_docs)

    # Recall
    recall = relevant_found / len(relevant_docs)

    return recall

# Exemple
query_emb = embeddings[0]
doc_embs = embeddings[1:]
relevant = [2, 5, 8]  # Indices des docs pertinents

recall_5 = recall_at_k(query_emb, doc_embs, relevant, k=5)
print(f"Recall@5 : {recall_5:.2%}")  # Ex: 66.67%

MRR (Mean Reciprocal Rank)

Position du premier résultat pertinent.

def mrr(query_embedding, doc_embeddings, relevant_docs):
    """Calculer MRR."""
    similarities = cosine_similarity([query_embedding], doc_embeddings)[0]
    ranked_indices = np.argsort(similarities)[::-1]

    # Trouver premier pertinent
    for rank, idx in enumerate(ranked_indices, start=1):
        if idx in relevant_docs:
            return 1.0 / rank

    return 0.0  # Aucun pertinent trouvé

Interprétation :

  • 1.0 : Premier résultat est pertinent
  • 0.5 : Premier pertinent est en 2ème position
  • 0.1 : Premier pertinent est en 10ème position

Embeddings multilingues

Pour supporter plusieurs langues dans le même espace vectoriel.

Modèles recommandés

# Sentence-BERT multilingue
model_multi = SentenceTransformer('paraphrase-multilingual-mpnet-base-v2')

# Textes en différentes langues
texts = [
    "Hello, how are you?",  # English
    "Bonjour, comment allez-vous ?",  # Français
    "Hola, ¿cómo estás?",  # Español
]

embeddings = model_multi.encode(texts)

# Calculer similarités cross-lingues
sim_en_fr = cosine_similarity([embeddings[0]], [embeddings[1]])[0][0]
sim_en_es = cosine_similarity([embeddings[0]], [embeddings[2]])[0][0]

print(f"EN-FR : {sim_en_fr:.4f}")  # ~0.85
print(f"EN-ES : {sim_en_es:.4f}")  # ~0.83

Use case : Requête en français → résultats en anglais/espagnol !


Dimensionality Reduction (avancé)

Réduire la dimension pour économiser stockage et accélérer recherche.

PCA (Principal Component Analysis)

from sklearn.decomposition import PCA

# Embeddings originaux (1536 dim)
embeddings_original = model.encode(texts)  # (N, 1536)

# Réduire à 256 dimensions
pca = PCA(n_components=256)
embeddings_reduced = pca.fit_transform(embeddings_original)

print(f"Original : {embeddings_original.shape}")  # (N, 1536)
print(f"Reduced : {embeddings_reduced.shape}")    # (N, 256)

# Variance expliquée
variance_ratio = pca.explained_variance_ratio_.sum()
print(f"Variance conservée : {variance_ratio:.2%}")  # Ex: 92%

Trade-off :

  • ✅ Stockage : 6× moins (1536 → 256)
  • ✅ Vitesse : 6× plus rapide
  • ❌ Qualité : ~8% de perte

Pièges à éviter

❌ Mélanger modèles d’embedding

# ❌ ERREUR : Comparer embeddings de modèles différents
embedding_openai = openai.embed("texte")
embedding_sbert = sbert_model.encode("texte")

similarity = cosine_similarity([embedding_openai], [embedding_sbert])
# ⚠️ Résultat non significatif !

Règle : Tous les embeddings doivent venir du même modèle.

❌ Oublier la normalisation

# ❌ Utiliser dot product sans normalisation
similarity = np.dot(vec1, vec2)  # Sensible à magnitude

# ✅ Normaliser d'abord
vec1_norm = vec1 / np.linalg.norm(vec1)
vec2_norm = vec2 / np.linalg.norm(vec2)
similarity = np.dot(vec1_norm, vec2_norm)  # OK

❌ Chunks trop grands/petits

Trop petits : Perte de contexte Trop grands : Dilution du sens

Optimal : 200-500 tokens avec 10-20% overlap

❌ Ignorer le contexte

# ❌ Texte brut sans contexte
embedding = model.encode("PyTorch est un framework")

# ✅ Enrichir avec métadonnées
enriched = "Titre: Introduction Deep Learning. Catégorie: IA. PyTorch est un framework"
embedding = model.encode(enriched)

Conclusion

Les embeddings sont le cœur des bases vectorielles. Leur qualité détermine directement la pertinence de vos recherches.

Points clés à retenir

Choisir le bon modèle selon votre budget et besoins :

  • OpenAI : Simple, qualité, payant
  • Sentence-BERT : Gratuit, local, très bon
  • Cohere : Multilingue, API, compétitif

Utiliser cosine similarity pour recherche sémantique de texte

Optimiser chunking : 200-500 tokens, 10-20% overlap

Enrichir avec métadonnées pour plus de contexte

Batch processing pour performance (90× plus rapide)

Normaliser pour utiliser dot product (plus rapide)

Dans le prochain article, nous plongerons dans les algorithmes d’indexation (HNSW, IVF) qui rendent la recherche ultra-rapide.

👉 Article 3 : architecture et algorithmes d’indexation


Embeddings pour images

Au-delà du texte, les embeddings peuvent représenter des images.

CLIP (OpenAI)

CLIP (Contrastive Language-Image Pre-training) encode texte ET images dans le même espace vectoriel.

from transformers import CLIPProcessor, CLIPModel
from PIL import Image
import torch

# Charger modèle CLIP
model = CLIPModel.from_pretrained("openai/clip-vit-base-patch32")
processor = CLIPProcessor.from_pretrained("openai/clip-vit-base-patch32")

# Image
image = Image.open("chat.jpg")

# Textes à comparer
texts = ["un chat", "un chien", "une voiture"]

# Encoder image et textes
inputs = processor(text=texts, images=image, return_tensors="pt", padding=True)
outputs = model(**inputs)

# Similarités image-texte
logits_per_image = outputs.logits_per_image
probs = logits_per_image.softmax(dim=1)

print(f"Probabilités : {probs}")
# [[0.85, 0.10, 0.05]]  -> "un chat" le plus probable

Use cases :

  • Recherche d’images par texte
  • Classification zero-shot
  • Recherche visuelle sémantique

ResNet et Vision Transformers

Pour des embeddings purement visuels :

from transformers import ViTFeatureExtractor, ViTModel
from PIL import Image

# Charger Vision Transformer
feature_extractor = ViTFeatureExtractor.from_pretrained('google/vit-base-patch16-224')
model = ViTModel.from_pretrained('google/vit-base-patch16-224')

# Charger image
image = Image.open("photo.jpg")

# Extraire features
inputs = feature_extractor(images=image, return_tensors="pt")
outputs = model(**inputs)

# Embedding de l'image (768 dimensions)
image_embedding = outputs.last_hidden_state[:, 0].detach().numpy()

print(f"Shape : {image_embedding.shape}")  # (1, 768)

Modèles populaires :

  • CLIP : Text + Image (512 dim)
  • ViT (Vision Transformer) : Images (768 dim)
  • ResNet-50 : Images (2048 dim)
  • EfficientNet : Images (variable)

Embeddings pour audio

Whisper Embeddings

Utiliser Whisper (OpenAI) pour encoder de l’audio :

import whisper
import numpy as np

# Charger modèle Whisper
model = whisper.load_model("base")

# Charger audio
audio = whisper.load_audio("audio.mp3")
audio = whisper.pad_or_trim(audio)

# Générer mel spectrogram
mel = whisper.log_mel_spectrogram(audio).to(model.device)

# Extraire embeddings
with torch.no_grad():
    embeddings = model.embed_audio(mel)

print(f"Audio embedding shape : {embeddings.shape}")

Use cases :

  • Recherche sémantique dans podcasts
  • Détection de similarité audio
  • Classification de sons

Audio Embeddings avec CLAP

CLAP (Contrastive Language-Audio Pretraining) : équivalent de CLIP pour l’audio.

from transformers import ClapModel, ClapProcessor

model = ClapModel.from_pretrained("laion/clap-htsat-unfused")
processor = ClapProcessor.from_pretrained("laion/clap-htsat-unfused")

# Texte décrivant l'audio
texts = ["bruit de pluie", "chien qui aboie", "guitare acoustique"]

# Audio
audio = load_audio("sample.wav")

# Encoder
inputs = processor(text=texts, audios=audio, return_tensors="pt", padding=True)
outputs = model(**inputs)

# Similarités audio-texte
logits_per_audio = outputs.logits_per_audio
probs = logits_per_audio.softmax(dim=1)

Caching et réutilisation des embeddings

Générer des embeddings est coûteux (temps + argent). Il faut les cacher !

Sauvegarder les embeddings

import numpy as np
import json

# Générer embeddings
texts = ["texte 1", "texte 2", "texte 3"]
embeddings = model.encode(texts)

# Sauvegarder en NumPy
np.save('embeddings.npy', embeddings)

# Sauvegarder métadonnées
metadata = {
    'model': 'all-mpnet-base-v2',
    'dimension': 768,
    'count': len(texts),
    'texts': texts
}
with open('metadata.json', 'w') as f:
    json.dump(metadata, f)

print("✅ Embeddings sauvegardés")

Charger les embeddings

# Charger embeddings
embeddings = np.load('embeddings.npy')

# Charger métadonnées
with open('metadata.json', 'r') as f:
    metadata = json.load(f)

print(f"Loaded {metadata['count']} embeddings")
print(f"Model : {metadata['model']}")
print(f"Dimension : {metadata['dimension']}")

Cache avec hash

Pour éviter de générer plusieurs fois le même embedding :

import hashlib
import pickle
from pathlib import Path

class EmbeddingCache:
    def __init__(self, cache_dir='./embedding_cache'):
        self.cache_dir = Path(cache_dir)
        self.cache_dir.mkdir(exist_ok=True)

    def _hash_text(self, text: str) -> str:
        """Générer hash MD5 du texte."""
        return hashlib.md5(text.encode()).hexdigest()

    def get(self, text: str):
        """Récupérer embedding du cache."""
        hash_key = self._hash_text(text)
        cache_file = self.cache_dir / f"{hash_key}.pkl"

        if cache_file.exists():
            with open(cache_file, 'rb') as f:
                return pickle.load(f)
        return None

    def set(self, text: str, embedding):
        """Sauvegarder embedding dans cache."""
        hash_key = self._hash_text(text)
        cache_file = self.cache_dir / f"{hash_key}.pkl"

        with open(cache_file, 'wb') as f:
            pickle.dump(embedding, f)

    def get_or_compute(self, text: str, model):
        """Récupérer du cache ou générer."""
        embedding = self.get(text)

        if embedding is None:
            # Générer
            embedding = model.encode(text)
            # Cacher
            self.set(text, embedding)
            print(f"✨ Generated new embedding")
        else:
            print(f"✅ Loaded from cache")

        return embedding

# Utilisation
cache = EmbeddingCache()
model = SentenceTransformer('all-mpnet-base-v2')

# Premier appel : génère
emb1 = cache.get_or_compute("Hello world", model)  # ✨ Generated

# Deuxième appel : cache !
emb2 = cache.get_or_compute("Hello world", model)  # ✅ Loaded from cache

Gains : 100× plus rapide pour textes déjà encodés !


Embeddings sparse vs dense

Il existe deux types d’embeddings : dense et sparse.

Dense Embeddings (ce qu’on a vu)

Vecteurs denses : tous les éléments ont une valeur.

# Dense embedding (768 dimensions)
dense = [0.234, -0.512, 0.891, -0.342, 0.621, ...]
# 768 valeurs, toutes non-nulles

Caractéristiques :

  • ✅ Capture le sens sémantique
  • ✅ Performant pour similarité
  • ❌ Coûteux en stockage
  • ❌ Nécessite modèle ML

Use case : Recherche sémantique, RAG, recommandation

Sparse embeddings

Vecteurs sparse : la plupart des éléments sont 0.

# Sparse embedding (ex: 30,000 dimensions)
sparse = {
    145: 0.52,   # "paris"
    892: 0.31,   # "france"
    1203: 0.19,  # "capitale"
    # ... le reste est 0
}

Méthode classique : BM25 (Bag-of-Words amélioré)

Caractéristiques :

  • ✅ Très efficace en stockage (format sparse)
  • ✅ Pas besoin de modèle ML
  • ✅ Recherche de mots-clés exacts
  • ❌ Pas de compréhension sémantique

Use case : Recherche par mots-clés, filtrage lexical

Hybrid Search : Le meilleur des deux mondes

Combiner dense (sémantique) + sparse (mots-clés).

from rank_bm25 import BM25Okapi
from sentence_transformers import SentenceTransformer
import numpy as np

# Documents
docs = [
    "Paris est la capitale de la France",
    "Tokyo est la capitale du Japon",
    "Berlin est la capitale de l'Allemagne"
]

# 1. Dense embeddings
model_dense = SentenceTransformer('all-mpnet-base-v2')
dense_embeddings = model_dense.encode(docs)

# 2. Sparse embeddings (BM25)
tokenized_docs = [doc.lower().split() for doc in docs]
bm25 = BM25Okapi(tokenized_docs)

# Requête
query = "capitale française"

# 3. Dense search
query_dense = model_dense.encode(query)
dense_scores = cosine_similarity([query_dense], dense_embeddings)[0]

# 4. Sparse search (BM25)
query_tokens = query.lower().split()
sparse_scores = bm25.get_scores(query_tokens)

# 5. Combiner les scores (weighted)
alpha = 0.7  # Poids pour dense
final_scores = alpha * dense_scores + (1 - alpha) * sparse_scores

# 6. Résultats
best_idx = np.argmax(final_scores)
print(f"Meilleur résultat : {docs[best_idx]}")
# "Paris est la capitale de la France"

Avantages Hybrid Search :

  • Dense trouve “capitale française” → “Paris capitale France”
  • Sparse trouve correspondances exactes de mots
  • ✅ Meilleure précision globale

Implémentation dans bases vectorielles :

  • Weaviate : Hybrid search natif
  • Qdrant : Supporte sparse + dense
  • Pinecone : Via metadata filtering + semantic

Comparaison détaillée des fournisseurs d’API

Tableau complet 2025

FournisseurModèleDimensionPrix (/1M tokens)LanguesContextPerformanceLatence
OpenAItext-embedding-3-small1536$0.020100+8191⭐⭐⭐⭐~200ms
OpenAItext-embedding-3-large3072$0.130100+8191⭐⭐⭐⭐⭐~300ms
Cohereembed-multilingual-v31024$0.100100+512⭐⭐⭐⭐~150ms
Cohereembed-english-v31024$0.100EN512⭐⭐⭐⭐~150ms
Voyage AIvoyage-21024$0.100100+4000⭐⭐⭐⭐~180ms
Jina AIjina-embeddings-v2768$0.020100+8192⭐⭐⭐⭐~120ms
Mistralmistral-embed1024$0.100FR/EN8000⭐⭐⭐⭐~250ms
Googletextembedding-gecko768$0.025100+3072⭐⭐⭐⭐~200ms

Recommandations par cas d’usage

Budget serré, bon volume : → OpenAI text-embedding-3-small ($0.02/1M)

Qualité maximale, budget flexible : → OpenAI text-embedding-3-large ($0.13/1M)

Multilingue, contexte long : → Jina AI jina-embeddings-v2 (8K context)

Français prioritaire : → Mistral mistral-embed (optimisé FR)

Gratuit, local, privacy : → Sentence-BERT all-mpnet-base-v2

Calcul du coût réel

def estimate_embedding_cost(
    num_documents: int,
    avg_tokens_per_doc: int,
    model_price_per_1m: float
):
    """
    Estimer le coût d'embedding.

    Args:
        num_documents: Nombre de documents
        avg_tokens_per_doc: Tokens moyens par document
        model_price_per_1m: Prix par 1M tokens

    Returns:
        Coût estimé en dollars
    """
    total_tokens = num_documents * avg_tokens_per_doc
    cost = (total_tokens / 1_000_000) * model_price_per_1m
    return cost

# Exemple : 100K documents, 500 tokens/doc
cost_openai_small = estimate_embedding_cost(
    num_documents=100_000,
    avg_tokens_per_doc=500,
    model_price_per_1m=0.02  # OpenAI small
)

cost_openai_large = estimate_embedding_cost(
    num_documents=100_000,
    avg_tokens_per_doc=500,
    model_price_per_1m=0.13  # OpenAI large
)

print(f"OpenAI small : ${cost_openai_small:.2f}")  # $1.00
print(f"OpenAI large : ${cost_openai_large:.2f}")  # $6.50
print(f"Sentence-BERT : $0.00 (local)")  # Gratuit !

Cas pratique complet : Système de recherche documentaire

Voici un exemple end-to-end complet.

Scénario

Créer un moteur de recherche pour une base de connaissance de 1000 articles.

Code complet

from sentence_transformers import SentenceTransformer
from sklearn.metrics.pairwise import cosine_similarity
import numpy as np
import json
from pathlib import Path

class DocumentSearchEngine:
    def __init__(self, model_name='all-mpnet-base-v2'):
        """Initialiser le moteur de recherche."""
        self.model = SentenceTransformer(model_name)
        self.documents = []
        self.embeddings = None

    def add_documents(self, documents: list[dict]):
        """
        Ajouter documents à l'index.

        Args:
            documents: Liste de dicts avec 'id', 'title', 'content', 'metadata'
        """
        self.documents = documents

        # Préparer textes pour embedding
        texts_to_embed = []
        for doc in documents:
            # Enrichir avec métadonnées
            text = f"Titre: {doc['title']}\n\n{doc['content']}"
            texts_to_embed.append(text)

        # Générer embeddings
        print(f"Génération de {len(texts_to_embed)} embeddings...")
        self.embeddings = self.model.encode(
            texts_to_embed,
            batch_size=32,
            show_progress_bar=True
        )

        # Normaliser pour utiliser dot product
        from sklearn.preprocessing import normalize
        self.embeddings = normalize(self.embeddings, norm='l2')

        print(f"✅ {len(self.embeddings)} embeddings générés")

    def search(self, query: str, top_k: int = 5) -> list[dict]:
        """
        Rechercher documents pertinents.

        Args:
            query: Requête de recherche
            top_k: Nombre de résultats

        Returns:
            Liste de résultats avec scores
        """
        # Encoder la requête
        query_embedding = self.model.encode(query)

        # Normaliser
        from sklearn.preprocessing import normalize
        query_embedding = normalize([query_embedding], norm='l2')[0]

        # Calculer similarités (dot product car normalisé)
        similarities = np.dot(self.embeddings, query_embedding)

        # Top-K indices
        top_indices = np.argsort(similarities)[-top_k:][::-1]

        # Préparer résultats
        results = []
        for idx in top_indices:
            results.append({
                'document': self.documents[idx],
                'score': float(similarities[idx]),
                'rank': len(results) + 1
            })

        return results

    def save(self, path: str):
        """Sauvegarder l'index."""
        save_path = Path(path)
        save_path.mkdir(exist_ok=True)

        # Sauvegarder embeddings
        np.save(save_path / 'embeddings.npy', self.embeddings)

        # Sauvegarder documents
        with open(save_path / 'documents.json', 'w', encoding='utf-8') as f:
            json.dump(self.documents, f, ensure_ascii=False, indent=2)

        print(f"✅ Index sauvegardé dans {path}")

    def load(self, path: str):
        """Charger l'index."""
        load_path = Path(path)

        # Charger embeddings
        self.embeddings = np.load(load_path / 'embeddings.npy')

        # Charger documents
        with open(load_path / 'documents.json', 'r', encoding='utf-8') as f:
            self.documents = json.load(f)

        print(f"✅ Index chargé : {len(self.documents)} documents")

# === UTILISATION ===

# 1. Créer des documents
documents = [
    {
        'id': 1,
        'title': 'Introduction à PyTorch',
        'content': 'PyTorch est un framework de deep learning développé par Meta...',
        'metadata': {'category': 'Deep Learning', 'date': '2025-01-15'}
    },
    {
        'id': 2,
        'title': 'RAG avec LangChain',
        'content': 'Le RAG (Retrieval-Augmented Generation) combine recherche et génération...',
        'metadata': {'category': 'LLM', 'date': '2025-01-20'}
    },
    # ... plus de documents
]

# 2. Créer et indexer
engine = DocumentSearchEngine()
engine.add_documents(documents)

# 3. Rechercher
results = engine.search("comment utiliser pytorch pour deep learning", top_k=3)

# 4. Afficher résultats
for result in results:
    print(f"\n📄 Rang {result['rank']} - Score: {result['score']:.4f}")
    print(f"   Titre: {result['document']['title']}")
    print(f"   Extrait: {result['document']['content'][:100]}...")

# 5. Sauvegarder pour réutilisation
engine.save('./search_index')

# 6. Charger plus tard
engine2 = DocumentSearchEngine()
engine2.load('./search_index')

Output exemple :

📄 Rang 1 - Score: 0.8721
   Titre: Introduction à PyTorch
   Extrait: PyTorch est un framework de deep learning développé par Meta...

📄 Rang 2 - Score: 0.6543
   Titre: Guide TensorFlow
   Extrait: TensorFlow est une alternative populaire pour le deep learning...

Embeddings contextuels vs statiques

Embeddings statiques (Word2Vec, GloVe)

Chaque mot a un seul vecteur, quel que soit le contexte.

# "bank" a toujours le même embedding
# → Problème : "river bank" vs "money bank"

Limites :

  • ❌ Pas de gestion de polysémie
  • ❌ Pas de contexte

Embeddings contextuels (BERT, GPT, modernes)

Chaque mot a un embedding différent selon le contexte.

from transformers import AutoTokenizer, AutoModel
import torch

model_name = "bert-base-uncased"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModel.from_pretrained(model_name)

# Deux phrases avec "bank"
sentence1 = "I walked along the river bank"
sentence2 = "I went to the bank to withdraw money"

# Encoder
inputs1 = tokenizer(sentence1, return_tensors="pt")
inputs2 = tokenizer(sentence2, return_tensors="pt")

# Embeddings contextuels
with torch.no_grad():
    outputs1 = model(**inputs1)
    outputs2 = model(**inputs2)

# "bank" aura des embeddings DIFFÉRENTS !
# Car le contexte est différent

Avantages :

  • ✅ Gestion de la polysémie
  • ✅ Compréhension du contexte
  • ✅ Plus précis

Tous les modèles modernes (Sentence-BERT, OpenAI, Cohere) sont contextuels.


Exercices pratiques

Comparer 3 modèles

Générez des embeddings pour le même texte avec 3 modèles différents et comparez les dimensions et performances.

Mesurer Recall@K

Créez un mini-dataset avec 100 documents et 10 requêtes. Mesurez Recall@5 et Recall@10.

Chunking intelligent

Implémentez une fonction de chunking qui :

  • Découpe par paragraphes
  • Respecte une limite de tokens
  • Ajoute 15% d’overlap

FAQ : Questions fréquentes

Quelle est la différence entre un embedding et un vecteur ?

Un embedding est un type spécifique de vecteur : c’est une représentation numérique dense qui capture le sens sémantique d’une donnée (texte, image, audio).

Tous les embeddings sont des vecteurs, mais tous les vecteurs ne sont pas des embeddings sémantiques.

Puis-je utiliser des embeddings générés avec différents modèles ensemble ?

❌ NON ! C’est une erreur fréquente.

Les embeddings doivent toujours provenir du même modèle pour être comparables.

# ❌ ERREUR
emb_openai = openai.embed("texte")
emb_sbert = sbert.encode("texte")
similarity = cosine_similarity([emb_openai], [emb_sbert])  # Non significatif !

# ✅ CORRECT
emb1 = sbert.encode("texte 1")
emb2 = sbert.encode("texte 2")
similarity = cosine_similarity([emb1], [emb2])  # OK !

Quelle métrique de similarité choisir ?

Pour 99% des cas : Cosine Similarity !

Elle est invariante à la magnitude et parfaite pour le texte.

Combien coûte la génération d’embeddings ?

Exemples de coûts (100K documents, 500 tokens/doc) :

  • OpenAI small : ~$1.00
  • OpenAI large : ~$6.50
  • Cohere : ~$5.00
  • Sentence-BERT local : $0 (gratuit, mais coût serveur/GPU)

Quelle taille de chunk optimale ?

Recommandation générale : 300 tokens (environ 200-400 mots)

Avec 15-20% d’overlap entre chunks.

CHUNK_SIZE = 300  # tokens
OVERLAP = 50      # tokens (15-20%)

Dois-je normaliser mes embeddings ?

Ça dépend :

  • Si vous utilisez cosine similarity : pas besoin
  • Si vous utilisez dot product : OUI, normalisez (L2)
  • Si vous utilisez une base vectorielle : vérifiez la documentation

Comment améliorer la qualité de mes embeddings ?

5 techniques :

  1. Chunking intelligent : 300 tokens, overlap 15%
  2. Enrichir avec métadonnées : ajouter titre, catégorie, date
  3. Fine-tuner le modèle sur votre domaine
  4. Utiliser un modèle plus puissant (ex: large vs small)
  5. Hybrid search : combiner dense + sparse (BM25)

Les embeddings sont-ils sensibles à la langue ?

Modèles monolingues : OUI

  • Sentence-BERT all-MiniLM-L6-v2 : Anglais uniquement

Modèles multilingues : NON

  • OpenAI text-embedding-3-* : 100+ langues
  • Sentence-BERT paraphrase-multilingual-* : 50+ langues

Vérifiez toujours la documentation du modèle !

Comment gérer les textes plus longs que la limite du modèle ?

3 stratégies :

  1. Chunking : découper en morceaux (recommandé)
  2. Truncation : couper à la limite (perte d’info)
  3. Summarization : résumer d’abord avec LLM, puis embedder
# Chunking recommandé
chunks = chunk_text(long_text, chunk_size=300, overlap=50)
embeddings = model.encode(chunks)

Puis-je réduire la dimension des embeddings ?

OUI, avec PCA ou UMAP :

from sklearn.decomposition import PCA

# Réduire 1536 → 256 dimensions
pca = PCA(n_components=256)
embeddings_reduced = pca.fit_transform(embeddings_original)

# Trade-off : ~85-95% de variance conservée

Trade-off :

  • ✅ 6× moins de stockage
  • ✅ 6× plus rapide
  • ❌ ~5-15% de perte de qualité

Comment savoir si mes embeddings sont de bonne qualité ?

Métriques d’évaluation :

  1. Recall@K : % de documents pertinents dans top-K
  2. MRR (Mean Reciprocal Rank) : position du 1er pertinent
  3. NDCG : qualité du ranking

Test manuel :

  • Cherchez des requêtes typiques
  • Vérifiez les résultats
  • Ajustez chunking/enrichissement

Est-ce que je dois régénérer les embeddings si je change de modèle ?

OUI ! Absolument.

Les embeddings sont spécifiques au modèle. Si vous changez de modèle, vous devez tout régénérer.

C’est pourquoi le choix du modèle est crucial dès le début.


Troubleshooting : Erreurs fréquentes

Erreur : “OutOfMemoryError” lors du batch encoding

Symptôme :

torch.cuda.OutOfMemoryError: CUDA out of memory

Cause : Batch trop grand pour la mémoire GPU

Solution : Réduire le batch size

# ❌ Batch trop grand
embeddings = model.encode(texts, batch_size=128)  # OOM !

# ✅ Batch réduit
embeddings = model.encode(texts, batch_size=16)   # OK

Erreur : Résultats de recherche non pertinents

Cause possible 1 : Chunks trop grands/petits

Solution :

# Ajuster la taille des chunks
CHUNK_SIZE = 300  # Essayer 200, 300, 500

Cause possible 2 : Pas assez de contexte

Solution : Enrichir avec métadonnées

enriched = f"Titre: {title}\nCatégorie: {category}\n\n{content}"

Cause possible 3 : Modèle inadapté

Solution : Tester un autre modèle (ex: all-mpnet-base-v2 → OpenAI)

Erreur : Similarités toujours très élevées (>0.9)

Cause : Vecteurs non normalisés + confusion métrique

Solution :

from sklearn.preprocessing import normalize

# Normaliser
embeddings = normalize(embeddings, norm='l2')

# Utiliser cosine similarity
similarity = cosine_similarity([emb1], [emb2])

Erreur : Performance lente lors de la recherche

Cause : Recherche linéaire sur grand dataset

Solutions :

  1. Normaliser et utiliser dot product (plus rapide)
embeddings_norm = normalize(embeddings, norm='l2')
scores = np.dot(embeddings_norm, query_norm)  # Rapide !
  1. Utiliser une vraie base vectorielle (Pinecone, Weaviate, Qdrant)
  • Algorithmes optimisés (HNSW, IVF)
  • 100× - 1000× plus rapide
  1. Réduire les dimensions (PCA)

Erreur : Embeddings ne capturent pas le sens spécifique à mon domaine

Cause : Modèle généraliste pas adapté à votre domaine (médical, juridique, etc.)

Solution : Fine-tuner sur votre domaine

from sentence_transformers import SentenceTransformer, InputExample, losses

# Charger modèle base
model = SentenceTransformer('all-mpnet-base-v2')

# Préparer exemples de votre domaine
train_examples = [
    InputExample(texts=['query domaine', 'document pertinent'], label=1.0),
    # ... plus d'exemples
]

# Fine-tuner
model.fit(train_objectives=[(train_dataloader, train_loss)], epochs=3)

Erreur : Coûts API trop élevés

Solutions :

  1. Cache agressif : ne jamais régénérer le même embedding
  2. Batch processing : grouper les requêtes
  3. Modèle local : passer à Sentence-BERT (gratuit)
  4. Modèle plus petit : OpenAI small au lieu de large
# Cache pour économiser
cache = EmbeddingCache()
embedding = cache.get_or_compute(text, model)  # Réutilise si existe

Erreur : Latence API trop élevée

Solutions :

  1. Async requests : paralléliser les appels API
  2. Batch API : 1 appel pour N textes
  3. Pre-compute : générer embeddings à l’avance (pas en temps réel)
  4. Modèle local : zéro latence réseau
import asyncio
from openai import AsyncOpenAI

async def embed_async(texts):
    client = AsyncOpenAI()
    tasks = [
        client.embeddings.create(model="text-embedding-3-small", input=text)
        for text in texts
    ]
    return await asyncio.gather(*tasks)

Erreur : Recherche multilingue ne fonctionne pas

Cause : Modèle monolingue

Solution : Utiliser un modèle multilingue

# ❌ Monolingue (EN only)
model = SentenceTransformer('all-MiniLM-L6-v2')

# ✅ Multilingue
model = SentenceTransformer('paraphrase-multilingual-mpnet-base-v2')
# OU
# OpenAI text-embedding-3-* (100+ langues)

Checklist de démarrage

Avant de déployer votre système d’embeddings en production :

Choix du modèle

  • Modèle choisi selon budget et besoins
  • Testé sur échantillon représentatif
  • Comparé 2-3 modèles différents
  • Vérifié le support multilingue si nécessaire

Chunking et préparation

  • Taille de chunk optimisée (300 tokens)
  • Overlap configuré (15-20%)
  • Métadonnées ajoutées (titre, catégorie)
  • Textes nettoyés (HTML, caractères spéciaux)

Performance

  • Batch processing implémenté (batch_size=32)
  • Cache mis en place pour réutilisation
  • Normalisation appliquée si nécessaire
  • Métriques de qualité mesurées (Recall@K)

Stockage

  • Embeddings sauvegardés (pas régénérés à chaque fois)
  • Métadonnées stockées avec embeddings
  • Backup en place
  • Version du modèle documentée

Monitoring

  • Logs de qualité (pertinence résultats)
  • Logs de performance (latence)
  • Logs de coûts (si API)
  • Alertes configurées

Ressources complémentaires

Articles liés :

Documentation :

Benchmarks :

Outils pratiques :