Aller au contenu
English

Les plongements pour la recherche

  • Leçon 7
  • 50 min
  • Module 3 : Créer et comparer la récupération

Ce que vous saurez faire

Trouvez un passage qui exprime la même idée que la question, même avec d’autres mots. Assurez-vous ensuite que l’index et la question passent toujours par le même modèle de plongement. Un mélange donne des résultats incorrects, sans erreur.

Avant de commencerCe qu’il faut savoir et avoir sous la mainEnviron 50 minutes

Ce que vous devez savoir

Ce qu’il vous faut sur votre ordinateur

  • Le projet policy-assistant à la fin du module 2, dans ~/projects/policy-assistant, avec .venv activé. python -m pytest affiche 31 passed (31 tests réussis).
  • Si votre projet ne fonctionne plus ou manque, copiez l’instantané end-of-m02 comme vous avez copié start dans Préparer le projet. Créez ensuite un nouveau .venv.
  • Le dépôt des exercices dans ~/projects/nextia-learning-labs. Vous copierez certains fichiers de son instantané end-of-m03.
  • Environ 2 Go d’espace disque libre. Les nouveaux paquets Python occupent environ 0,9 Go, et le modèle local environ 0,5 Go.

Aucun compte ni aucune clé. Le modèle de plongement fonctionne sur votre ordinateur. Testé avec Python 3.12.13 et sentence-transformers 6.1.0 sur macOS (puce Apple). Les commandes Windows et Linux ne sont pas testées.

Les modèles changent vite. Les chiffres de ce module représentent un instantané. C’est la méthode qui reste.

Vous demandez à une bibliothécaire « un livre pour réparer un vélo ». Elle vous apporte Entretien et réparation des bicyclettes. Le titre ne contient aucun de vos mots, mais c’est le bon livre. La bibliothécaire comprend le sens des mots. Vos mots et ceux du livre n’ont donc pas besoin d’être identiques.

Un catalogue de bibliothèque qui cherche seulement les mêmes mots ne trouverait pas ce livre. Il trouverait des livres avec « vélo » ou « réparer » dans le titre, mais manquerait le meilleur.

Votre recherche par mots-clés du module 2 joue le rôle du catalogue. Elle trouve un segment seulement s’il contient les mots de la question. Posez-lui la question de Grace sur un arroseur de jardin :

Fenêtre de terminal
python -m policy_assistant search "How long is the warranty on a garden sprinkler?" --k 20

La réponse se trouve dans le tableau de garantie de warranty-policy, à la ligne « Watering | hoses, hose reels, sprinklers, timers | 1 year » (arrosage | tuyaux, dévidoirs, arroseurs, minuteries | 1 an). Dans la sortie, ce segment arrive 17e. (La prochaine leçon ajoute le filtre de date, qui retire un segment qui n’est pas en vigueur; le tableau arrive alors 16e.) Le premier résultat concerne la garantie des meubles de jardin, car il contient « garden » (jardin) et « warranty » (garantie) :

1. 7.3530 [ce196c57771f] product-care-garden-furniture v2 | Warranty | public
Warranty Your garden furniture has a 2-year warranty against defects. Cushions and fabric covers ha…
...
17. 3.4516 [d9a5bd8ea531] warranty-policy v5 | Warranty periods by product c… | public
Warranty periods by product category Product category | Examples | Warranty period | Exceptions Han…

La question contient « sprinkler » (arroseur), et le tableau contient « sprinklers » (arroseurs). Pour la recherche plein texte de SQLite, ce sont deux mots différents. Une question en français échoue de la même façon. Camille demande « Puis-je retourner une carte-cadeau? », et la réponse se trouve dans le document anglais gift-cards. Les 5 premiers résultats contiennent seulement des segments français sur les retours et les remboursements, sans segment sur les cartes-cadeaux. Les mots de la question et ceux de la réponse ne se rencontrent jamais.

L’idée : une liste de nombres pour représenter le sens

Section intitulée « L’idée : une liste de nombres pour représenter le sens »

La bibliothécaire garde en tête une image du sujet de chaque livre. Un ordinateur peut faire quelque chose de semblable. Un modèle de plongement lit un texte et renvoie une liste de nombres de longueur fixe. Cette liste s’appelle un plongement (embedding) ou un vecteur. Le modèle a été entraîné pour que des textes de sens semblable obtiennent des listes semblables. « Un remboursement pour une bouilloire cassée » et « de l’argent rendu pour un appareil défectueux » obtiennent des vecteurs proches. Pourtant, ils ne partagent aucun mot important.

Le cours sur l’apprentissage profond montre comment un réseau apprend ces nombres, dans Jetons et plongements. Vous n’en avez pas besoin pour cette leçon. Ici, le modèle de plongement est un outil : du texte entre, 384 nombres sortent.

La recherche par plongements comporte deux étapes. À l’ingestion, le modèle produit une fois le plongement de chaque segment, et l’index stocke les vecteurs. À la recherche, le modèle produit le plongement de la question. Le code trouve ensuite les vecteurs de segments dont la direction ressemble le plus à celle de la question. C’est la recherche vectorielle. On l’appelle aussi récupération dense, car presque chacun des 384 nombres est non nul. Un index par mots-clés est « creux » : chaque segment utilise seulement quelques mots du vocabulaire complet.

Comment mesurer une « direction semblable »? Commencez par des vecteurs de deux nombres, pour pouvoir les voir. Ces vecteurs sont inventés pour cet exemple. Imaginez que le premier nombre indique à quel point un texte parle d’argent rendu, et le second, d’un appareil.

Texte Vecteur
Question : « un remboursement pour une bouilloire cassée » q = (4, 3)
A : « de l’argent rendu pour un appareil défectueux » a = (3, 4)
R : « les remboursements utilisent le mode de paiement initial » r = (5, 0)
K : « remplissez la bouilloire d’eau fraîche » k = (0, 5)
Quatre vecteurs inventés de longueur 5. L'angle entre la question q et un texte montre leur similarité : a est le plus proche, r et k sont plus éloignés.
Description textuelle du graphique

Quatre flèches partent de (0, 0), et chacune a une longueur de 5. La question q va jusqu’à (4, 3). Le texte a va jusqu’à (3, 4), juste au-dessus de q : l’angle entre eux est petit. Le texte r va jusqu’à (5, 0), le long de l’axe horizontal « à propos d’argent rendu ». Le texte k va jusqu’à (0, 5), sur l’axe vertical « à propos d’un appareil ». L’angle entre q et r est plus petit que l’angle entre q et k.

Le produit scalaire multiplie les nombres à la même position, puis additionne les résultats. Pour q et a : 4 × 3 + 3 × 4 = 24. Pour q et r : 4 × 5 + 3 × 0 = 20. Pour q et k : 4 × 0 + 3 × 5 = 15. Un texte plus long pourrait avoir des nombres plus grands, donc un produit scalaire plus grand, sans avoir un sens plus proche. Nous divisons donc par les deux longueurs. Chaque vecteur ici a une longueur de 5 (pour q : √(4² + 3²) = 5). Nous divisons donc par 5 × 5 = 25.

Paire Produit scalaire ÷ 25 = similarité cosinus
q et a 24 0,96
q et r 20 0,80
q et k 15 0,60

Le résultat est la similarité cosinus : le cosinus de l’angle entre deux vecteurs. Elle vaut 1 quand les deux flèches pointent dans la même direction, et 0 quand elles forment un angle droit. Le texte a gagne, comme la bibliothécaire s’y attendrait. En général :

cos⁡(q,p)=q⋅p∥q∥ ∥p∥\cos(\mathbf{q}, \mathbf{p}) = \frac{\mathbf{q} \cdot \mathbf{p}}{\lVert \mathbf{q} \rVert \, \lVert \mathbf{p} \rVert}

Lisez cette formule ainsi : le produit scalaire du vecteur de la question et du vecteur du passage, divisé par leurs deux longueurs. Le fichier embed.py du projet ramène d’abord chaque vecteur à une longueur de 1. Le produit scalaire est alors la similarité cosinus. Une seule multiplication matricielle compare la question à chaque segment.

L’assistant de Larkfield utilise intfloat/multilingual-e5-small, un modèle ouvert sous licence MIT. Il renvoie 384 nombres par texte, fonctionne sur le processeur d’un portable et lit le français comme l’anglais. Nous l’appelons e5 à partir d’ici. Amira lui a donné la question et quatre phrases qu’elle a écrites pour cette leçon. Les nombres sont les vraies similarités cosinus d’e5 :

Phrase (écrite pour cette leçon)Partage un mot avec la question?Similarité cosinus
We give your money back for a faulty electrical appliance.no0.834
Refunds are sent to the original payment method."refund"0.816
Fill the kettle with fresh water before each use."kettle"0.813
Garden furniture has a two-year warranty.no0.764

La phrase de même sens, sans mot commun, arrive en premier, comme avec les vecteurs inventés. Deux choses diffèrent du tout petit exemple. D’abord, les quatre nombres sont proches, entre 0,76 et 0,84. Avec e5, même des textes sans rapport obtiennent un score d’environ 0,7 ou plus. Un nombre isolé comme 0,81 nous dit donc peu de choses. C’est l’ordre qui compte. Ensuite, les écarts sont petits : 0,834 pour la meilleure phrase, 0,816 pour la suivante. La recherche vectorielle donne un classement, pas un oui ou un non.

Le modèle e5 a une autre règle. Il a été entraîné avec une étiquette devant chaque texte : query: (question) avant une question, et passage: (passage) avant un texte à chercher. Le projet les ajoute dans embed.py. Sans elles, la recherche fonctionne encore, mais moins bien. Sur les 59 questions ayant un passage pertinent, la recherche dense avec e5 obtient un hit@5 de 0.915 avec les étiquettes et de 0.864 sans elles. Lisez la fiche d’un modèle avant de l’utiliser : ces règles y figurent.

La question de Larkfield sur l’arroseur, avec deux modèles

Section intitulée « La question de Larkfield sur l’arroseur, avec deux modèles »

e5 suffit-il, ou Larkfield devrait-elle payer pour un modèle hébergé? Le cours a aussi enregistré embed-small : le modèle text-embedding-3-small d’OpenAI, déployé sur Azure sous ce nom. Il renvoie 1 536 nombres par texte. Ses vecteurs pour chaque segment et chaque question ont été enregistrés, pour que vous puissiez les utiliser sans compte.

Voici la question sur l’arroseur comparée aux 185 segments, avec les deux modèles. Chaque point représente un segment.

  • Pertinent : le tableau de garantie
  • La garantie des meubles de jardin
  • Autres segments
La question « Quelle est la durée de la garantie d'un arroseur de jardin? » comparée aux 185 segments. Le point vert étiqueté table représente le tableau de garantie, et le point orange juste à sa droite représente la garantie des meubles de jardin. Les deux modèles utilisent des échelles différentes, mais s'accordent sur les segments proches.
Description textuelle du graphique

Chacun des 185 points représente un segment. Sa position horizontale indique sa similarité cosinus avec la question selon e5, de 0,71 à 0,86. Sa position verticale indique sa similarité selon embed-small, de 0,03 à 0,54. Les points forment un nuage ascendant : quand un modèle trouve un segment proche, l’autre le trouve généralement proche aussi. Deux segments se trouvent en haut à droite. La garantie des meubles de jardin obtient 0,864 avec e5 et 0,523 avec embed-small. Le tableau de garantie obtient 0,854 avec e5 et 0,541 avec embed-small. e5 classe donc la garantie des meubles en premier et le tableau en deuxième; embed-small classe le tableau en premier. Le tableau a un second segment : sa dernière ligne sur les produits qui appartiennent à deux catégories. Il est plus bas : au rang 20 pour e5 et au rang 5 pour embed-small.

Cette image nous apprend deux choses. Les échelles diffèrent : les nombres d’e5 vont de 0,71 à 0,86, et ceux d’embed-small de 0,03 à 0,54. Ne comparez donc jamais le score d’un modèle à celui d’un autre. Les deux modèles placent aussi le bon segment près du sommet : e5 le classe deuxième, embed-small premier. Sur cette seule question, embed-small est un peu meilleur.

Une question ne prouve rien. Priya a lancé la recherche dense sur tout le jeu de questions, avec les filtres de la prochaine leçon :

Modèle hit@5 Lieu d’exécution Coût des plongements de la collection Temps pour une question
e5 (local) 0,915 votre ordinateur gratuit, 1,1 s 5,2 ms
embed-small (hébergé) 0,881 Azure 0,0005 $ US pour les 443 textes du cours un appel réseau

Le modèle local est aussi bon que le modèle hébergé sur les questions de Larkfield. Il est meilleur sur celles en français : les 8 questions françaises ont trouvé un passage pertinent avec e5. L’écart de 2 questions sur 59 ne prouve pas qu’e5 est meilleur. Et embed-small a un vrai point fort : il place plus souvent un passage pertinent en premier. Il le fait pour 80 % des questions, contre 68 % pour e5. Nous pouvons simplement conclure que le modèle hébergé n’est pas nécessaire ici. Avec e5, aucun texte de politique ne quitte les ordinateurs de Larkfield. Le cours a aussi essayé deux autres modèles ouverts. all-MiniLM-L6-v2 est 5,4 fois plus petit et deux fois plus rapide. Mais il lit seulement l’anglais, et son hit@5 sur les questions françaises est de 0,625, contre 1,000 pour e5.

Quand utiliser les plongements, et ce qu’ils coûtent

Section intitulée « Quand utiliser les plongements, et ce qu’ils coûtent »

Utilisez les plongements quand les questions emploient d’autres mots que les documents : reformulations, synonymes, pluriel et singulier, autre langue. Les agents de Larkfield posent leurs questions dans leurs propres mots, et Camille les pose en français. L’assistant en a donc besoin.

Ne vous attendez pas à ce qu’ils trouvent des codes exacts. Un modèle de plongement découpe « LB-1 » en petits morceaux de texte, sans lui attribuer de sens particulier. La prochaine leçon le montre avec un véritable échec.

Ce qu’ils coûtent :

  • Téléchargement et mémoire. e5 occupe 470 Mo sur disque, plus environ 0,9 Go de paquets Python (PyTorch et sentence-transformers).
  • Temps. Produire les plongements des 185 segments prend environ 1,1 s sur l’ordinateur de test. Une question prend environ 5,2 ms, et la comparaison avec 185 vecteurs prend 0,09 ms.
  • Une reconstruction à chaque changement de modèle. Les vecteurs de deux modèles ne peuvent pas être comparés. Un nouveau modèle, ou une nouvelle version du même modèle, exige de refaire le plongement de chaque segment.

Le projet reçoit deux nouveaux fichiers, dense.py et embed.py, une nouvelle version de store.py et une modification de la commande ingest. Travaillez dans ~/projects/policy-assistant, avec .venv activé.

  1. Copiez le nouveau requirements.txt depuis l’instantané end-of-m03 :

    Fenêtre de terminal
    cp ../nextia-learning-labs/C09/snapshots/end-of-m03/requirements.txt .

    Il ajoute numpy, openai, sentence-transformers, torch, transformers et huggingface-hub, avec des versions exactes.

  2. Sur Linux seulement, installez d’abord la version de PyTorch pour processeur seulement. Sans cette étape, pip télécharge la version beaucoup plus volumineuse pour les cartes graphiques NVIDIA :

    Fenêtre de terminal
    python -m pip install torch==2.14.1 --index-url https://download.pytorch.org/whl/cpu
  3. Installez les dépendances. Cela peut prendre quelques minutes :

    Fenêtre de terminal
    python -m pip install -r requirements.txt
  4. Créez le fichier policy_assistant/dense.py. Il contient la recherche vectorielle de cette leçon, avec NumPy :

    policy_assistant/dense.py
    """Vector search with NumPy: compare the question's vector with every chunk's vector (brute force).
    The vectors are normalised to length 1, so the dot product of two vectors is their cosine similarity:
    1 = same direction (same meaning, for the model), 0 = unrelated. With a few hundred chunks, comparing
    with every chunk takes well under a millisecond; a vector index (for example sqlite-vec, FAISS) only
    pays off with many thousands of chunks.
    """
    from typing import Callable
    import numpy as np
    from .lexical import Hit
    def normalise(vectors: np.ndarray) -> np.ndarray:
    vectors = np.asarray(vectors, dtype=np.float32)
    norms = np.linalg.norm(vectors, axis=-1, keepdims=True)
    return vectors / np.where(norms == 0, 1, norms)
    def cosine(a: np.ndarray, b: np.ndarray) -> float:
    a, b = normalise(a), normalise(b)
    return float(a @ b)
    def search(query_vector: np.ndarray, matrix: np.ndarray, ids: list[str], k: int = 5,
    allowed: Callable[[str], bool] | None = None) -> list[Hit]:
    """The k chunks whose vectors are most similar to the query vector."""
    scores = normalise(matrix) @ normalise(query_vector)
    order = sorted(range(len(ids)), key=lambda i: (-scores[i], ids[i]))
    hits = [(ids[i], float(scores[i])) for i in order if allowed is None or allowed(ids[i])]
    return [Hit(i, round(s, 4), rank) for rank, (i, s) in enumerate(hits[:k], 1)]
    Partie Signification
    normalise Divise chaque vecteur par sa longueur, pour que chaque vecteur ait une longueur de 1.
    matrix @ query_vector Un produit scalaire par segment : 185 similarités cosinus à la fois.
    sorted(..., key=(-score, id)) Le score le plus élevé en premier. Les scores égaux sont triés par identifiant de segment, pour garder un résultat identique.
    allowed Une fonction qui indique si un segment peut être renvoyé. La prochaine leçon l’utilise pour les filtres.
    Hit Le même type de résultat que la recherche par mots-clés, dans lexical.py.
  5. Copiez embed.py, le nouveau store.py et les vecteurs enregistrés d’embed-small depuis l’instantané :

    Fenêtre de terminal
    cp ../nextia-learning-labs/C09/snapshots/end-of-m03/policy_assistant/embed.py policy_assistant/
    cp ../nextia-learning-labs/C09/snapshots/end-of-m03/policy_assistant/store.py policy_assistant/
    mkdir -p recordings
    cp ../nextia-learning-labs/C09/snapshots/end-of-m03/recordings/embeddings_embed-small.npz recordings/

    Ouvrez embed.py et lisez la classe LocalEmbedder. Voici ses lignes les plus importantes :

    policy_assistant/embed.py (part)
    LOCAL_MODEL = "intfloat/multilingual-e5-small"
    LOCAL_REVISION = "614241f622f53c4eeff9890bdc4f31cfecc418b3" # the model's Hugging Face commit, 2026-10-09
    PREFIXES = { # (query prefix, passage prefix) that each model was trained with
    "intfloat/multilingual-e5-small": ("query: ", "passage: "),
    }
    def embed_queries(self, texts: list[str]) -> np.ndarray:
    return self._encode([PREFIXES.get(self.name, ("", ""))[0] + t for t in texts])
    def embed_passages(self, texts: list[str]) -> np.ndarray:
    return self._encode([PREFIXES.get(self.name, ("", ""))[1] + t for t in texts])

    La révision est le commit (enregistrement d’une version) exact du modèle sur Hugging Face, le site qui l’héberge. Un nom de modèle seul peut pointer vers une version plus récente demain. La révision ne le peut pas. RecordedEmbedder("embed-small") donne les vecteurs hébergés enregistrés. Il trouve un vecteur grâce à une empreinte du nom du modèle et du texte. Il fonctionne donc seulement pour les segments et les questions du cours.

    Dans store.py, les nouveautés sont une table vectors, avec une ligne par segment, et deux lignes dans sync() qui enregistrent le modèle :

    policy_assistant/store.py (part)
    CREATE TABLE IF NOT EXISTS vectors (chunk_id TEXT PRIMARY KEY, vector BLOB);
    ...
    if embedder is not None:
    settings |= {"embedding_model": embedder.name, "embedding_revision": embedder.revision}
  6. Ouvrez policy_assistant/__main__.py. Remplacez la fonction cmd_ingest par ces deux fonctions :

    policy_assistant/__main__.py
    def make_embedder(name: str):
    if name == "none":
    return None
    from .embed import make_embedder as make
    return make(name)
    def cmd_ingest(args) -> None:
    from .store import Store
    path = Path(args.index)
    store = Store(path)
    report = store.sync(CORPUS / "documents", args.chunker, make_embedder(args.embedder))
    print(report.line())
    meta = store.meta()
    print(f"index {path.name}: {len(store.documents())} documents, {len(store.chunks())} chunks | chunker {meta.get('chunker')} | "
    f"embeddings {meta.get('embedding_model', 'none')}")
  7. Dans le même fichier, trouvez la ligne qui ajoute --chunker à ingest. Ajoutez l’option --embedder en dessous :

    policy_assistant/__main__.py
    s.add_argument("--chunker", default="structure", choices=["fixed", "overlap", "structure"])
    s.add_argument("--embedder", default="intfloat/multilingual-e5-small",
    help="a sentence-transformers model, embed-small (recorded vectors) or none")
  8. Reconstruisez l’index. La première fois, le modèle télécharge environ 470 Mo dans ~/.cache/huggingface :

    Fenêtre de terminal
    python -m policy_assistant ingest

    La sortie se termine par :

    36 documents, 185 chunks (rebuilt from scratch)
    index index.sqlite: 36 documents, 185 chunks | chunker structure | embeddings intfloat/multilingual-e5-small

    Avant ces lignes, vous voyez aussi une barre de progression, Loading weights (chargement des poids). Un avertissement sur les requêtes non authentifiées au HF Hub peut aussi apparaître. Cet avertissement est sans conséquence : le modèle est public et ne nécessite aucun compte.

  9. Lancez les tests. Ils réussissent encore, car rien d’autre n’a changé :

    Fenêtre de terminal
    python -m pytest

    La dernière ligne est 31 passed.

  1. Lancez Python avec python dans le dossier du projet. L’invite devient >>>.

  2. Produisez les plongements de la question et des quatre phrases de cette leçon, puis affichez chaque similarité :

    Python, in ~/projects/policy-assistant
    from policy_assistant.dense import cosine
    from policy_assistant.embed import LocalEmbedder
    e5 = LocalEmbedder()
    question = e5.embed_queries(["a refund for a broken kettle"])[0]
    print(len(question), question[:4].round(3))
    sentences = [
    "We give your money back for a faulty electrical appliance.",
    "Refunds are sent to the original payment method.",
    "Fill the kettle with fresh water before each use.",
    "Garden furniture has a two-year warranty.",
    ]
    for text, vector in zip(sentences, e5.embed_passages(sentences)):
    print(f"{cosine(question, vector):.3f} {text}")

    Appuyez sur Entrée à la ligne vide après la boucle for. La sortie :

    384 [ 0.06 -0.046 -0.019 -0.097]
    0.834 We give your money back for a faulty electrical appliance.
    0.816 Refunds are sent to the original payment method.
    0.813 Fill the kettle with fresh water before each use.
    0.764 Garden furniture has a two-year warranty.

    La question est maintenant représentée par 384 nombres. Les quatre premiers sont de petits nombres autour de 0, comme dans la plupart des plongements.

  3. Comparez les deux modèles sur la question de l’arroseur. d9a5bd8ea531 est le tableau de garantie, et ce196c57771f est la garantie des meubles de jardin :

    Python (continued)
    from policy_assistant.embed import RecordedEmbedder
    from policy_assistant.store import Store
    store = Store("index.sqlite")
    table = store.chunk("d9a5bd8ea531").text
    furniture = store.chunk("ce196c57771f").text
    question = "How long is the warranty on a garden sprinkler?"
    for model in (e5, RecordedEmbedder("embed-small")):
    q = model.embed_queries([question])[0]
    t, f = model.embed_passages([table, furniture])
    print(f"{model.name}: {len(q)} numbers | table {cosine(q, t):.3f} | furniture {cosine(q, f):.3f}")

    La sortie :

    intfloat/multilingual-e5-small: 384 numbers | table 0.854 | furniture 0.864
    embed-small: 1536 numbers | table 0.541 | furniture 0.523
  4. Regardez ce que l’index conserve :

    Python (continued)
    print(store.meta())

    La sortie (votre heure est différente) :

    {'chunker': 'structure', 'updated_at': '2026-10-09T05:52:44+00:00', 'embedding_model': 'intfloat/multilingual-e5-small', 'embedding_revision': '614241f622f53c4eeff9890bdc4f31cfecc418b3'}

    Restez dans Python pour la prochaine tâche, ou quittez avec exit().

Remplacez la deuxième phrase de l’étape 2 par : « Nous remboursons un appareil électrique défectueux. » (Nous remboursons un appareil électrique défectueux.) La question reste en anglais. Avant d’exécuter le code, faites une prédiction : quel rang occupera la phrase française parmi les quatre, et pourquoi?

Afficher la réponse
0.834 We give your money back for a faulty electrical appliance.
0.819 Nous remboursons un appareil électrique défectueux.
0.813 Fill the kettle with fresh water before each use.
0.764 Garden furniture has a two-year warranty.

La phrase française arrive en deuxième place, devant la phrase anglaise sur la bouilloire. Le modèle e5 est multilingue : il a appris à partir de textes dans plusieurs langues. Une phrase française et une phrase anglaise de même sens obtiennent donc des vecteurs proches. C’est pourquoi les questions françaises de Camille peuvent trouver des documents anglais. La recherche par mots-clés ne peut pas le faire, car « remboursons » et « refund » sont des mots différents.

Erreur courante

Deux modèles, un index

Symptôme : Tomás veut un modèle plus rapide pour les questions : all-MiniLM-L6-v2 est deux fois plus rapide qu’e5. Il renvoie aussi 384 nombres. Tomás l’utilise pour créer le plongement de la question sur l’arroseur, puis cherche dans l’index e5. Python n’affiche aucune erreur, et cinq résultats sont renvoyés :

Python, in ~/projects/policy-assistant
from policy_assistant.dense import search
minilm = LocalEmbedder("sentence-transformers/all-MiniLM-L6-v2")
ids, matrix = store.vectors()
q = minilm.embed_queries([question])[0]
print(len(q), matrix.shape)
for hit in search(q, matrix, ids):
c = store.chunk(hit.id)
print(hit.rank, hit.score, c.doc_id, "|", c.section)
384 (185, 384)
1 0.0582 refund-approval-procedure | Rules
2 0.0512 damaged-or-wrong-items | After 14 days
3 0.049 damaged-or-wrong-items | Report within 14 days
4 0.0484 trade-accounts | Returns for trade customers
5 0.0469 refunds-and-payments | Double charges

Aucun des cinq ne concerne une garantie ou un arroseur. Sur l’ensemble des questions, ce mélange obtient un hit@5 de 0.068 : 4 questions sur 59, contre 0,915 avec e5 seul. (Ce code télécharge all-MiniLM-L6-v2, environ 90 Mo. Vous n’avez pas besoin de l’exécuter.)

Cause : chaque modèle a sa propre « carte du sens ». Le nombre 17 d’un vecteur e5 et le nombre 17 d’un vecteur MiniLM représentent des choses différentes. Pourtant, les deux listes contiennent 384 nombres. Le code peut les multiplier, donc il le fait, et le résultat est du bruit.

Solution : stockez le nom du modèle et sa révision avec les vecteurs, et vérifiez-les avant chaque recherche. L’index les consigne déjà dans meta. La prochaine leçon ajoute search.py, qui les compare et s’arrête avec une erreur IndexMismatch (incompatibilité d’index). Pour changer de modèle, reconstruisez l’index : python -m policy_assistant ingest --embedder NAME.

À gauche : pendant l'ingestion, e5 lit passage : suivi de chacun des 185 segments. Il écrit 185 vecteurs de 384 nombres, ainsi que le nom du modèle et sa révision, dans meta. À droite : pendant la recherche, un modèle lit query : suivi de la question et crée un vecteur. Une vérification demande : même modèle et même révision? Oui mène à une comparaison avec les 185 vecteurs et aux 5 meilleurs résultats. Non mène à IndexMismatch : la recherche s'arrête. Un chemin en pointillés, sans vérification, mène à des résultats sans erreur, mais avec un hit@5 de 0,068.
L'index conserve le nom du modèle qui a créé ses vecteurs. La recherche compare cette information au modèle de la question avant de comparer les vecteurs.
Description textuelle du diagramme

Pendant l’ingestion, e5 lit chacun des 185 segments précédé de l’étiquette « passage : ». Il écrit 185 vecteurs de 384 nombres chacun dans l’index. Il écrit aussi le nom du modèle et sa révision dans la table meta. Pendant la recherche, un modèle lit la question précédée de l’étiquette « query : » et crée un vecteur. Avant toute comparaison, une vérification demande : est-ce le même modèle, avec la même révision? Si oui, le code compare le vecteur de la question aux 185 vecteurs selon la similarité cosinus. Il renvoie les 5 meilleurs résultats. Sinon, la recherche s’arrête avec l’erreur IndexMismatch. Un chemin en pointillés montre ce qui arrive sans vérification : les résultats sont renvoyés sans erreur, mais le hit@5 est de 0,068.

Pour un vrai service d’IA : un index de plongements est lié à une version de modèle. Consignez cette version avec l’index. Ainsi, une mise à jour du modèle, une erreur de configuration ou un deuxième service utilisant un autre modèle provoque une erreur claire. Le système ne renvoie pas de mauvais passages qui semblent normaux.

Vérifiez votre compréhension

Dans vos mots : que fait un modèle de plongement, et comment la recherche vectorielle l'utilise-t-elle pour trouver un passage?

Afficher la réponse

Un modèle de plongement lit un texte et renvoie une liste de nombres de longueur fixe, appelée vecteur. Il a été entraîné pour que les textes de sens semblable obtiennent des vecteurs orientés dans une direction semblable. Pendant l’ingestion, il crée un plongement pour chaque segment une seule fois, et l’index stocke les vecteurs. Pendant la recherche, il crée le plongement de la question. Le code calcule ensuite la similarité cosinus entre le vecteur de la question et celui de chaque segment. Il renvoie les segments ayant les valeurs les plus élevées. On peut donc trouver un passage par son sens, même s’il utilise d’autres mots que la question.

Vérifiez votre compréhension

Faites une prédiction : deux vecteurs sont (1, 0) et (0, 1). Quelle est leur similarité cosinus? Et pour (2, 2) et (1, 1)?

Afficher la réponse

Pour (1, 0) et (0, 1), le produit scalaire est 1 × 0 + 0 × 1 = 0. La similarité cosinus est donc 0 : les flèches forment un angle droit. Pour (2, 2) et (1, 1), le produit scalaire est 4. Les longueurs sont √8 et √2, et √8 × √2 = 4. La similarité cosinus est donc 4 ÷ 4 = 1 : les flèches pointent dans la même direction. Pourtant, l’une est deux fois plus longue. La similarité cosinus tient compte seulement de la direction.

Vérifiez votre compréhension

Omar demande : « Le score e5 de la garantie des meubles est de 0,864. Le score embed-small de la table est de 0,541. Donc e5 est plus sûr, non? » Que répondez-vous?

Afficher la réponse

Non. Les deux modèles utilisent des échelles différentes. Avec e5, presque tous les segments obtiennent un score entre 0,7 et 0,9. Avec embed-small, la plupart obtiennent des scores beaucoup plus bas. On ne peut pas comparer un nombre d’un modèle à un nombre de l’autre. Seul l’ordre au sein d’un modèle a un sens. e5 place la garantie des meubles en premier et la table en deuxième; embed-small place la table en premier. Pour comparer les modèles, mesurez les deux sur les mêmes questions, par exemple avec hit@5.

Récapitulatif

  • Un modèle de plongement transforme un texte en vecteur, une liste de nombres de longueur fixe. Les textes de sens semblable obtiennent des vecteurs orientés dans une direction semblable. La recherche vectorielle, ou récupération dense, compare le vecteur de la question à celui de chaque segment.
  • La similarité cosinus mesure la direction : le produit scalaire divisé par les deux longueurs. Avec des vecteurs de longueur 1, elle correspond au produit scalaire. Seul l’ordre des scores compte; leur valeur dépend du modèle.
  • Le modèle local e5 a besoin des étiquettes query: (requête) et passage: (passage). Sur les questions de Larkfield, il obtient un hit@5 de 0,915, contre 0,881 pour le modèle hébergé embed-small. Il est gratuit, et aucun texte ne quitte l’ordinateur.
  • L’index stocke le nom de son modèle et sa révision. On ne peut pas comparer les vecteurs de deux modèles : un mélange a donné un hit@5 de 0,068, sans erreur.

Ensuite, comparez la recherche par mots-clés et la recherche vectorielle sur quatre types de questions. Combinez-les, puis ajoutez les filtres de dates et d’accès.

Leçon suivante : Recherche lexicale, dense et hybride