Deux recherches, deux angles morts
La recherche hybride lance une recherche par mots et une recherche par sens, puis fusionne les deux listes classées. Elle sert parce que chacune rate ce que l’autre trouve.
Une personne tape « perdu mon identifiant ». Aucune page d’aide ne contient « identifiant », donc les mots ne trouvent rien. Une autre tape « commande numéro 4417 ». La recherche par sens ne sait pas ce qu’est 4417.
À la fin de cette leçon
Remarque
Je peux combiner la recherche par mots et la recherche par sens en une seule liste classée et dire laquelle gagne selon le cas.
Mots clés
| Mot | Ce que ça veut dire |
|---|---|
| BM25 | un score de mots qui récompense les mots rares et les pages courtes |
| Fréquence du terme | le nombre de fois où un mot apparaît dans un document |
| Fréquence inverse | un poids fort pour les mots que peu de documents contiennent |
| Rang | la place d’un résultat dans une liste, à partir de 1 |
| Fusion par rang réciproque | fusionner des listes en ajoutant 1 sur k plus le rang |
| Recherche lexicale | une recherche qui compare les mots eux-mêmes |
Les cellules que les étudiants remplissent restent vides dans cet aperçu.
BM25 note les mots eux-mêmes
Une recherche par mots compte les mots de la requête dans chaque document. BM25 améliore ce simple compte de trois façons. Un mot pèse plus quand peu de documents le contiennent. Le répéter aide de moins en moins. Un document court l’emporte sur un long qui a le même compte.
Le résultat est exact : un mot absent ne rapporte rien. C’est sa force et son angle mort.
La recherche par sens compare des vecteurs
La recherche par sens transforme chaque texte en vecteur, une liste de nombres, et compare leur direction. Ici chaque page d’aide a trois nombres inventés : sa part de compte, de livraison et d’argent.
La requête reçoit ses propres nombres, et les pages les plus proches gagnent. « Perdu mon identifiant » parle surtout de compte, donc les pages de compte arrivent en tête, sans aucun mot commun. Un vrai système obtient ses vecteurs d’un modèle d’embeddings, avec des centaines de nombres.
Fusionnez les rangs, jamais les scores
Les scores BM25 vont de 0 à dix ou plus. Les scores cosinus restent entre 0 et 1. Les additionner laisserait une liste écraser l’autre.
La fusion par rang réciproque ignore les scores et utilise les rangs. Un élément reçoit 1 / (60 + rang) de chaque liste qui le contient, et les points s’additionnent. Un élément que les deux listes aiment bat un élément qu’une seule aime.
Exemple détaillé : commande numéro 4417
| Page | Rang par mots | Rang par sens | Points fusionnés |
|---|---|---|---|
| d4 suivre un colis | 1 | 3 | 0,0323 |
| d3 remboursement | aucun | 1 | 0,0164 |
| d5 payer par carte | aucun | 2 | 0,0161 |
Les cellules que les étudiants remplissent restent vides dans cet aperçu.
Une erreur courante
Attention
Beaucoup pensent que la recherche hybride consiste à additionner les deux scores. Les échelles diffèrent, donc une liste gagne à chaque fois. Additionnez plutôt les rangs, avec 1 / (k + rang), et les deux listes ont le même poids.
Écrire la recherche hybride, partie 1
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const pages = [' ' { id: \"d1\", texte: \"réinitialiser votre mot de passe depuis la page de connexion\", v: [9, 0, 1] },' ' { id: \"d2\", texte: \"changer l’adresse courriel de votre compte\", v: [8, 1, 0] },' ' { id: \"d3\", texte: \"politique de remboursement des colis retournés\", v: [1, 5, 8] },' ' { id: \"d4\", texte: \"suivre un colis avec le numéro de commande\", v: [0, 9, 1] },' ' { id: \"d5\", texte: \"payer par carte ou par virement\", v: [1, 1, 9] },' ' { id: \"d6\", texte: \"supprimer votre compte et vos données\", v: [7, 0, 2] },' '];' 'const K1 = 1.5, B = 0.75;' 'function mots(t) { return t.toLowerCase().split(\" \"); }' 'const moyenne = pages.reduce(function (s, d) { return s + mots(d.texte).length; }, 0) / pages.length;' > hybride1.js"Affichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Écrire la recherche hybride, partie 2
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function bm25(requete, page) {' ' let score = 0;' ' mots(requete).forEach(function (m) {' ' const n = pages.filter(function (d) { return mots(d.texte).includes(m); }).length;' ' if (n === 0) return;' ' const idf = Math.log(1 + (pages.length - n + 0.5) / (n + 0.5));' ' const f = mots(page.texte).filter(function (x) { return x === m; }).length;' ' const long = mots(page.texte).length;' ' score += idf * f * (K1 + 1) / (f + K1 * (1 - B + B * long / moyenne));' ' });' ' return score;' '}' >> hybride1.js"Affichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Écrire la recherche hybride, partie 3
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function cosinus(a, b) {' ' let prod = 0, sa = 0, sb = 0;' ' for (let i = 0; i < a.length; i++) { prod += a[i] * b[i]; sa += a[i] * a[i]; sb += b[i] * b[i]; }' ' return prod / Math.sqrt(sa * sb);' '}' 'function classer(scores) {' ' return pages.map(function (d, i) { return { id: d.id, s: scores[i] }; })' ' .filter(function (x) { return x.s > 0.01; })' ' .sort(function (x, y) { return y.s - x.s; })' ' .map(function (x) { return x.id; });' '}' 'function fusionner(listes) {' ' const total = {};' ' listes.forEach(function (liste) {' ' liste.forEach(function (id, rang) { total[id] = (total[id] || 0) + 1 / (60 + rang + 1); });' ' });' ' return Object.keys(total).sort(function (x, y) { return total[y] - total[x]; });' '}' >> hybride1.js"Affichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Écrire la recherche hybride, partie 4
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const requetes = [' ' { texte: \"perdu mon identifiant\", v: [9, 0, 0] },' ' { texte: \"commande numéro 4417\", v: [2, 3, 3] },' '];' 'requetes.forEach(function (q) {' ' const a = classer(pages.map(function (d) { return bm25(q.texte, d); }));' ' const b = classer(pages.map(function (d) { return cosinus(q.v, d.v); })).slice(0, 3);' ' console.log(q.texte);' ' console.log(\" mots :\", a.join(\" \") || \"rien\");' ' console.log(\" sens :\", b.join(\" \"));' ' console.log(\" fusion :\", fusionner([a, b]).join(\" \"));' '});' >> hybride1.js"Affichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Lancer et comparer les listes
docker exec -w /home/node/project agent node hybride1.jsAffichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Ceci est une maquette
Le Run n’appelle aucun modèle. Les vecteurs sont trois nombres inventés et six pages tiennent lieu d’index. Un vrai système contient des millions de pages et obtient ses vecteurs d’un modèle d’embeddings.
La forme reste la même : deux listes classées, puis une liste fusionnée.
Ce que le Run affiche
| Requête | Mots | Sens | Fusion |
|---|---|---|---|
| perdu mon identifiant | rien | d1 d2 d6 | d1 d2 d6 |
| commande numéro 4417 | d4 | d3 d5 d4 | d4 d3 d5 |
Les cellules que les étudiants remplissent restent vides dans cet aperçu.