Aller au contenu principal

Similarité Sémantique

Mis à jour le 29 juillet 2026

Mesurer la proximité entre textes

Une fois vos textes transformés en vecteurs par mistral-embed, vous pouvez quantifier à quel point deux textes sont sémantiquement proches. C’est le coeur de la recherche sémantique : comparer des significations, pas des mots. Là où un LIKE '%facture%' en base de données répond par oui ou par non, une métrique de similarité vous rend un nombre, donc un classement — et c’est ce classement qui fait tout l’intérêt du dispositif.

Dans cette leçon, vous allez implémenter les principales métriques de similarité et voir comment elles fonctionnent en pratique avec des exemples Python concrets.

Distance euclidienne

La distance euclidienne mesure la longueur du segment entre deux points dans l’espace vectoriel. Plus la distance est petite, plus les textes sont similaires. L’exemple ci-dessous compare trois phrases : les deux premières disent la même chose avec un vocabulaire différent, la troisième parle de tarte aux pommes. Aucun mot commun n’unit les phrases A et B, et pourtant leur distance sera nettement inférieure à celle qui sépare A de C.

import numpy as np
from mistralai import Mistral

client = Mistral(api_key=api_key)

def get_embedding(text):
    """Obtenir l'embedding d'un texte via mistral-embed."""
    response = client.embeddings.create(
        model="mistral-embed",
        inputs=[text],
    )
    return np.array(response.data[0].embedding)

# Trois phrases à comparer
phrase_a = "Le machine learning transforme les données en prédictions."
phrase_b = "L'apprentissage automatique convertit les données en résultats."
phrase_c = "La recette de la tarte aux pommes est simple."

emb_a = get_embedding(phrase_a)
emb_b = get_embedding(phrase_b)
emb_c = get_embedding(phrase_c)

# Distances euclidiennes
dist_ab = np.linalg.norm(emb_a - emb_b)
dist_ac = np.linalg.norm(emb_a - emb_c)

print(f"Distance A-B (sémantiquement proches): {dist_ab:.4f}")
print(f"Distance A-C (sémantiquement éloignés): {dist_ac:.4f}")
# A-B sera nettement inférieur à A-C

Similarité cosinus

La similarité cosinus est la métrique la plus utilisée pour les embeddings. Elle mesure l’angle entre deux vecteurs, indépendamment de leur magnitude. Le résultat est compris entre -1 et 1, ce qui donne une échelle immédiatement lisible : on peut fixer un seuil et le communiquer à une équipe métier sans avoir à expliquer ce qu’est une norme vectorielle.

from sklearn.metrics.pairwise import cosine_similarity

# Avec scikit-learn
sim_ab = cosine_similarity([emb_a], [emb_b])[0][0]
sim_ac = cosine_similarity([emb_a], [emb_c])[0][0]

print(f"Similarité cosinus A-B: {sim_ab:.4f}")  # ~0.92 (très proches)
print(f"Similarité cosinus A-C: {sim_ac:.4f}")  # ~0.35 (éloignés)

L’écart entre 0,92 et 0,35 est l’information utile : ce n’est pas la valeur absolue qui compte, mais la marge qui sépare le pertinent du hors sujet sur vos propres données. Vous pouvez aussi implémenter la métrique manuellement, ne serait-ce que pour vérifier ce que fait scikit-learn ou pour vous en passer dans un environnement contraint :

def cosine_sim(vec_a, vec_b):
    """Similarité cosinus manuelle."""
    dot = np.dot(vec_a, vec_b)
    norm_a = np.linalg.norm(vec_a)
    norm_b = np.linalg.norm(vec_b)
    return dot / (norm_a * norm_b)

print(f"Vérification manuelle A-B: {cosine_sim(emb_a, emb_b):.4f}")

Matrice de similarité

Pour comparer plusieurs documents entre eux, vous pouvez construire une matrice de similarité. Le corpus ci-dessous mêle volontairement deux thèmes — l’IA et la gastronomie — pour que la structure saute aux yeux : les documents 0, 1 et 3 se répondent, les documents 2 et 4 forment un îlot séparé. C’est exactement ce signal qu’exploitent les algorithmes de clustering.

from sklearn.metrics.pairwise import cosine_similarity

documents = [
    "Mistral AI développe des modèles de langage.",
    "Les LLMs génèrent du texte à partir de prompts.",
    "La cuisine française est réputée dans le monde.",
    "Le NLP est une branche de l'intelligence artificielle.",
    "Les croissants sont meilleurs le matin.",
]

# Encoder tous les documents
response = client.embeddings.create(
    model="mistral-embed",
    inputs=documents,
)
embeddings = np.array([d.embedding for d in response.data])

# Matrice de similarité (5x5)
sim_matrix = cosine_similarity(embeddings)

print("Matrice de similarité:")
for i, doc in enumerate(documents):
    for j, doc2 in enumerate(documents):
        if j > i:
            print(f"  [{i}]-[{j}]: {sim_matrix[i][j]:.3f}")

Recherche sémantique simple

Tout ce qui précède se combine en un mini moteur de recherche. La requête est encodée avec le même modèle que le corpus — condition non négociable, deux modèles différents produisent des espaces incomparables — puis argsort renverse le classement pour ramener les meilleurs scores en tête.

def recherche_semantique(query, documents, embeddings, top_k=3):
    """Trouve les documents les plus pertinents pour une requête."""
    # Encoder la requête
    query_response = client.embeddings.create(
        model="mistral-embed",
        inputs=[query],
    )
    query_embedding = np.array(query_response.data[0].embedding)

    # Calculer les similarités
    similarities = cosine_similarity(
        [query_embedding], embeddings
    )[0]

    # Trier par similarité décroissante
    ranked_indices = np.argsort(similarities)[::-1][:top_k]

    results = []
    for idx in ranked_indices:
        results.append({
            "document": documents[idx],
            "score": similarities[idx],
            "index": int(idx),
        })
    return results

# Exemple d'utilisation
query = "Comment fonctionne l'IA de Mistral ?"
results = recherche_semantique(query, documents, embeddings)

for r in results:
    print(f"  Score: {r['score']:.3f} -- {r['document']}")

Seuil de pertinence

Ce moteur a un défaut que vous découvrirez vite en production : il retourne toujours trois résultats, même quand la question n’a aucun rapport avec le corpus. Demandez-lui la météo et il vous rendra les trois documents les moins éloignés, avec des scores dérisoires mais un classement d’apparence normale. En pratique, vous devez donc définir un seuil de similarité en dessous duquel un document est considéré comme non pertinent :

SEUIL_PERTINENCE = 0.7

def recherche_avec_seuil(query, documents, embeddings, seuil=SEUIL_PERTINENCE):
    """Recherche avec filtrage par seuil de pertinence."""
    results = recherche_semantique(query, documents, embeddings, top_k=len(documents))
    return [r for r in results if r["score"] >= seuil]

# Un seuil de 0.7 est un bon point de départ
# Ajustez selon votre cas d'usage :
# - 0.8+ : très strict (peu de résultats, haute pertinence)
# - 0.6-0.7 : équilibré
# - 0.5 : permissif (beaucoup de résultats, pertinence variable)

Prenez le temps de calibrer ce seuil sur une trentaine de requêtes réelles avant d’ouvrir le service : trop haut, votre moteur répond « je ne sais pas » à des questions légitimes ; trop bas, il alimente le LLM en contexte hors sujet et vous fabriquez vous-même les hallucinations que le RAG était censé éviter.

Points clés à retenir

  • La similarité cosinus est la métrique standard pour comparer des embeddings
  • Utilisez sklearn.metrics.pairwise.cosine_similarity pour des calculs rapides
  • Une matrice de similarité permet de comparer tous les documents entre eux
  • Définissez un seuil de pertinence adapté à votre cas d’usage (0.7 est un bon défaut)
  • La recherche sémantique trouve des résultats par signification, pas par correspondance exacte de mots