{"id":505,"date":"2026-10-07T17:21:49","date_gmt":"2026-10-07T15:21:49","guid":{"rendered":"https:\/\/www.ranksignal.fr\/blog\/?p=505"},"modified":"2026-10-14T17:37:05","modified_gmt":"2026-10-14T15:37:05","slug":"qu-est-ce-qu-une-recherche-hybride","status":"publish","type":"post","link":"https:\/\/www.ranksignal.fr\/blog\/rag\/qu-est-ce-qu-une-recherche-hybride\/","title":{"rendered":"Recherche hybride : mots exacts et sens, comment on les fusionne"},"content":{"rendered":"<p><\/p>\n<p>Sur une boutique en ligne, une cliente tape \u00ab iPhone 16 \u00bb et veut exactement ce produit, pas un cousin approximatif. Un autre client \u00e9crit \u00ab t\u00e9l\u00e9phone de gamme moyenne avec un bon appareil photo \u00bb et n&rsquo;a aucun nom de mod\u00e8le en t\u00eate. Un moteur purement lexical peine sur la seconde requ\u00eate, un moteur purement s\u00e9mantique peut manquer la premi\u00e8re. Voici ce que combine une recherche hybride, comment elle m\u00e9lange deux listes de scores qui ne se comparent pas, et ce qu&rsquo;elle laisse encore de c\u00f4t\u00e9.<\/p>\n<p><strong>Sommaire<\/strong><\/p>\n<ul>\n<li><a href=\"#l-essentiel-en-30-secondes\">L&rsquo;essentiel en 30 secondes<\/a><\/li>\n<li><a href=\"#trois-recherches-une-meme-boutique\">Trois recherches, une m\u00eame boutique<\/a><\/li>\n<li><a href=\"#mots-exacts-ou-sens-deux-moteurs-deux-angles-morts\">Mots exacts ou sens : deux moteurs, deux angles morts<\/a><\/li>\n<li><a href=\"#la-recherche-hybride-deux-recherches-en-parallele-une-fusion\">La recherche hybride : deux recherches en parall\u00e8le, une fusion<\/a><\/li>\n<li><a href=\"#fusionner-deux-scores-qui-ne-parlent-pas-la-meme-langue\">Fusionner deux scores qui ne parlent pas la m\u00eame langue<\/a><\/li>\n<li><a href=\"#la-fusion-en-code-deux-manieres-de-faire\">La fusion en code : deux mani\u00e8res de faire<\/a><\/li>\n<li><a href=\"#ou-se-loge-la-recherche-hybride-dans-un-pipeline\">O\u00f9 se loge la recherche hybride dans un pipeline<\/a><\/li>\n<li><a href=\"#ce-qu-elle-ne-fait-pas-quatre-idees-recues\">Ce qu&rsquo;elle ne fait pas : quatre id\u00e9es re\u00e7ues<\/a><\/li>\n<\/ul>\n<h2 id=\"l-essentiel-en-30-secondes\">L&rsquo;essentiel en 30 secondes<\/h2>\n<ul>\n<li>La <strong>recherche hybride lance deux recherches en parall\u00e8le<\/strong>, l&rsquo;une par mots-cl\u00e9s et l&rsquo;autre par sens, puis fusionne leurs r\u00e9sultats.<\/li>\n<li>Chaque moteur a un <strong>angle mort oppos\u00e9<\/strong> : les mots exacts ratent les paraphrases, les vecteurs peuvent rater le terme pr\u00e9cis.<\/li>\n<li>Les scores n&rsquo;ont pas la m\u00eame \u00e9chelle : on les <strong>divise par leur maximum<\/strong>, puis on les m\u00e9lange avec un poids \u03b1.<\/li>\n<li>Le poids \u03b1 se r\u00e8gle <strong>autour de 0,3 \u00e0 0,5<\/strong>, et suffit \u00e0 faire basculer un classement.<\/li>\n<li>Elle <strong>compl\u00e8te le s\u00e9mantique sans le remplacer<\/strong>, et ne r\u00e8gle pas tout : certaines limites tiennent \u00e0 la structure des donn\u00e9es.<\/li>\n<\/ul>\n<h2 id=\"trois-recherches-une-meme-boutique\">Trois recherches, une m\u00eame boutique<\/h2>\n<p>Imagine un catalogue de plusieurs millions d&rsquo;articles : \u00e9lectronique, mode, \u00e9lectrom\u00e9nager. Les clients y cherchent de trois fa\u00e7ons, et chacune demande un talent diff\u00e9rent.<\/p>\n<ul>\n<li>Le nom pr\u00e9cis : \u00ab iPhone 16 \u00bb. Il faut retrouver ce mot, tel quel.<\/li>\n<li>La description vague : \u00ab t\u00e9l\u00e9phone de gamme moyenne avec un bon appareil photo \u00bb. Aucun mot du catalogue n&rsquo;est garanti d&rsquo;y figurer.<\/li>\n<li>La requ\u00eate mixte : \u00ab iPhone \u00e0 moins de 500 dollars \u00bb. Un nom pr\u00e9cis et une contrainte \u00e0 comprendre.<\/li>\n<\/ul>\n<p>Une recherche lexicale comme BM25 a du mal avec les descriptions vagues. Une recherche s\u00e9mantique peut au contraire <strong>passer \u00e0 c\u00f4t\u00e9 du produit exact<\/strong>. La recherche hybride r\u00e9unit les deux talents : BM25 privil\u00e9gie les correspondances exactes comme \u00ab iPhone \u00bb, la recherche s\u00e9mantique capte le sens d&rsquo;une phrase comme \u00ab t\u00e9l\u00e9phone avec un bon appareil photo \u00bb. Elle sert alors les trois cas \u00e0 la fois.<\/p>\n<h2 id=\"mots-exacts-ou-sens-deux-moteurs-deux-angles-morts\">Mots exacts ou sens : deux moteurs, deux angles morts<\/h2>\n<h3>Le moteur qui lit les lettres<\/h3>\n<p>La <strong>recherche par mots-cl\u00e9s<\/strong> cherche une correspondance exacte avec certains termes. Elle brille sur les termes pr\u00e9cis (noms de produits ou d&rsquo;entreprises, jargon sectoriel), mais elle est sensible aux fautes de frappe et aux synonymes, et ne saisit pas le contexte.<\/p>\n<p>Sous le capot, elle repose sur un index invers\u00e9. Le texte est pr\u00e9trait\u00e9 (d\u00e9coup\u00e9 en mots, pass\u00e9 en minuscules, ramen\u00e9 \u00e0 la racine des mots, d\u00e9barrass\u00e9 des mots vides), puis chaque terme est reli\u00e9 \u00e0 la liste des passages o\u00f9 il appara\u00eet. Plut\u00f4t que de relire toute la base, on suit la liste du terme recherch\u00e9. Pour classer, BM25 p\u00e8se la fr\u00e9quence du terme dans le passage, sa raret\u00e9 dans l&rsquo;ensemble du corpus et la longueur du passage. On dit qu&rsquo;il produit une repr\u00e9sentation <strong>creuse<\/strong>, par opposition aux vecteurs denses des mod\u00e8les de type <a href=\"\/blog\/deep-learning\/qu-est-ce-qu-un-transformer\/\" title=\"Transformer\">Transformer<\/a> que nous verrons plus bas.<\/p>\n<p>Que renvoie un tel moteur quand on lui pose une vraie question ? Voici un index BM25 sur trois r\u00e9pliques, interrog\u00e9 par \u00ab Who is a recruit? \u00bb. <code>corpus<\/code> contient les trois phrases d\u00e9coup\u00e9es en mots, <code>bm25<\/code> est l&rsquo;index construit dessus, et <code>get_top_n<\/code> renvoie les deux meilleures lignes.<\/p>\n<pre><code># pip install rank_bm25\n\nfrom rank_bm25 import BM25Okapi\nfrom typing import List\n\ncorpus: List[List[str]] = [\n    &quot;Agent J is the fresh recruit with attitude&quot;.split(),\n    &quot;Agent K has years of MIB experience and a cool neuralyzer&quot;.split(),\n    &quot;The galaxy is saved by two Agents in black suits&quot;.split(),\n]\n# 2. Build the BM25 index\nbm25 = BM25Okapi(corpus)\n\n# 3. Perform retrieval for a fun query\nquery = &quot;Who is a recruit?&quot;.split()\ntop_n = bm25.get_top_n(query, corpus, n=2)\n\nprint(&quot;Query:&quot;, &quot; &quot;.join(query))\nprint(&quot;Top matching lines:&quot;)\nfor line in top_n:\n    print(&quot; \u2022&quot;, &quot; &quot;.join(line))\n<\/code><\/pre>\n<p>Cette approche est excellente pour rep\u00e9rer des termes exacts ou tr\u00e8s sp\u00e9cifiques. Elle peut en revanche <strong>manquer les paraphrases<\/strong> et les liens conceptuels qui ne sont pas \u00e9crits dans le texte d&rsquo;origine.<\/p>\n<h3>Le moteur qui lit le sens<\/h3>\n<p>La <strong>recherche s\u00e9mantique<\/strong> cherche \u00e0 comprendre le contexte et l&rsquo;intention d&rsquo;une requ\u00eate : elle renvoie des r\u00e9sultats pertinents m\u00eame s&rsquo;ils ne contiennent pas les termes exacts. Son socle, ce sont les embeddings, des repr\u00e9sentations vectorielles qui rangent les mots de sens voisin \u00e0 faible distance les uns des autres.<\/p>\n<p>Ces vecteurs sont petits, faits de nombres r\u00e9els (denses) et gardent l&rsquo;information de contexte. Mais le revers est sym\u00e9trique : la <a href=\"\/blog\/rag\/qu-est-ce-qu-une-recherche-vectorielle\/\" title=\"recherche vectorielle\">recherche vectorielle<\/a> trouve le sens d&rsquo;une requ\u00eate <strong>sans retrouver les termes exacts<\/strong>, parfois essentiels, par exemple en marketing.<\/p>\n<table>\n<thead>\n<tr>\n<th><\/th>\n<th>Par mots-cl\u00e9s (BM25)<\/th>\n<th>Par sens (vecteurs)<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Ce qu&rsquo;elle compare<\/td>\n<td>Les termes de la requ\u00eate, \u00e0 l&rsquo;identique<\/td>\n<td>La proximit\u00e9 de sens<\/td>\n<\/tr>\n<tr>\n<td>Repr\u00e9sentation<\/td>\n<td>Creuse<\/td>\n<td>Dense<\/td>\n<\/tr>\n<tr>\n<td>Forte sur<\/td>\n<td>Noms de produits ou d&rsquo;entreprises, jargon<\/td>\n<td>Synonymes, paraphrases, intention<\/td>\n<\/tr>\n<tr>\n<td>Faible sur<\/td>\n<td>Fautes de frappe, synonymes, contexte<\/td>\n<td>Termes exacts<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<h2 id=\"la-recherche-hybride-deux-recherches-en-parallele-une-fusion\">La recherche hybride : deux recherches en parall\u00e8le, une fusion<\/h2>\n<p>La recherche vectorielle seule est d\u00e9j\u00e0 un net progr\u00e8s par rapport \u00e0 la simple recherche plein texte, mais elle ne suffit g\u00e9n\u00e9ralement pas \u00e0 garantir la qualit\u00e9, la pr\u00e9cision et la performance attendues en production. La <strong>recherche hybride<\/strong> ex\u00e9cute donc en parall\u00e8le une recherche vectorielle et une recherche plein texte, puis fusionne leurs r\u00e9sultats.<\/p>\n\n\n<figure data-wp-context=\"{&quot;imageId&quot;:&quot;6adef4722779f&quot;}\" data-wp-interactive=\"core\/image\" data-wp-key=\"6adef4722779f\" class=\"wp-block-image size-large wp-lightbox-container\"><img loading=\"lazy\" decoding=\"async\" width=\"2560\" height=\"1132\" data-wp-class--hide=\"state.isContentHidden\" data-wp-class--show=\"state.isContentVisible\" data-wp-init=\"callbacks.setButtonStyles\" data-wp-on--click=\"actions.showLightbox\" data-wp-on--load=\"callbacks.setButtonStyles\" data-wp-on--pointerdown=\"actions.preloadImage\" data-wp-on--pointerenter=\"actions.preloadImageWithDelay\" data-wp-on--pointerleave=\"actions.cancelPreload\" data-wp-on-window--resize=\"callbacks.setButtonStyles\" src=\"https:\/\/www.ranksignal.fr\/wp-content\/uploads\/2026\/09\/b1_ch02_recherche_hybride-scaled.png\" alt=\"La question se divise en deux branches, une recherche vectorielle par cosinus et une recherche plein texte, puis les deux r\u00e9sultats convergent vers un bloc de scores normalis\u00e9s et fusionn\u00e9s\" class=\"wp-image-123\" srcset=\"https:\/\/www.ranksignal.fr\/wp-content\/uploads\/2026\/09\/b1_ch02_recherche_hybride-scaled.png 2560w, https:\/\/www.ranksignal.fr\/wp-content\/uploads\/2026\/09\/b1_ch02_recherche_hybride-300x133.png 300w, https:\/\/www.ranksignal.fr\/wp-content\/uploads\/2026\/09\/b1_ch02_recherche_hybride-1024x453.png 1024w, https:\/\/www.ranksignal.fr\/wp-content\/uploads\/2026\/09\/b1_ch02_recherche_hybride-768x340.png 768w, https:\/\/www.ranksignal.fr\/wp-content\/uploads\/2026\/09\/b1_ch02_recherche_hybride-1536x679.png 1536w, https:\/\/www.ranksignal.fr\/wp-content\/uploads\/2026\/09\/b1_ch02_recherche_hybride-2048x905.png 2048w\" sizes=\"auto, (max-width: 2560px) 100vw, 2560px\" \/><button\n\t\t\tclass=\"lightbox-trigger\"\n\t\t\ttype=\"button\"\n\t\t\taria-haspopup=\"dialog\"\n\t\t\tdata-wp-bind--aria-label=\"state.thisImage.triggerButtonAriaLabel\"\n\t\t\tdata-wp-init=\"callbacks.initTriggerButton\"\n\t\t\tdata-wp-on--click=\"actions.showLightbox\"\n\t\t\tdata-wp-style--right=\"state.thisImage.buttonRight\"\n\t\t\tdata-wp-style--top=\"state.thisImage.buttonTop\"\n\t\t>\n\t\t\t<svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"12\" height=\"12\" fill=\"none\" viewBox=\"0 0 12 12\">\n\t\t\t\t<path fill=\"#fff\" d=\"M2 0a2 2 0 0 0-2 2v2h1.5V2a.5.5 0 0 1 .5-.5h2V0H2Zm2 10.5H2a.5.5 0 0 1-.5-.5V8H0v2a2 2 0 0 0 2 2h2v-1.5ZM8 12v-1.5h2a.5.5 0 0 0 .5-.5V8H12v2a2 2 0 0 1-2 2H8Zm2-12a2 2 0 0 1 2 2v2h-1.5V2a.5.5 0 0 0-.5-.5H8V0h2Z\" \/>\n\t\t\t<\/svg>\n\t\t<\/button><figcaption class=\"wp-element-caption\">Suis les fl\u00e8ches : le point cl\u00e9 n&rsquo;est pas la bifurcation, mais le bloc de droite. C&rsquo;est l\u00e0, apr\u00e8s les deux recherches, que se joue la fusion, et c&rsquo;est elle qui fait la diff\u00e9rence entre une simple juxtaposition et un vrai classement commun.<\/figcaption><\/figure>\n\n\n<p>Le m\u00e9lange ne se limite pas \u00e0 deux m\u00e9thodes. On peut aussi combiner des embeddings creux, des embeddings denses et une recherche par graphe, en fusionnant les r\u00e9sultats selon une pond\u00e9ration adapt\u00e9e aux besoins de pr\u00e9cision et de rappel.<\/p>\n<h2 id=\"fusionner-deux-scores-qui-ne-parlent-pas-la-meme-langue\">Fusionner deux scores qui ne parlent pas la m\u00eame langue<\/h2>\n<p>Une difficult\u00e9 saute aux yeux : les deux recherches ne notent pas sur la m\u00eame \u00e9chelle.<\/p>\n<blockquote>\n<p><strong>\ud83d\udca1 Intuition<\/strong> Deux juges notent le m\u00eame plat, l&rsquo;un sur 10 et l&rsquo;autre sur 1 000. Additionner leurs notes brutes revient \u00e0 n&rsquo;\u00e9couter que le second. Pour que leurs voix comptent autant, on ram\u00e8ne d&rsquo;abord les deux notes \u00e0 une r\u00e8gle commune.<\/p>\n<\/blockquote>\n<p>La premi\u00e8re r\u00e9ponse consiste \u00e0 <strong>diviser chaque score par le maximum<\/strong> de sa propre recherche. Le meilleur r\u00e9sultat de chaque liste re\u00e7oit alors 1, et tous les autres se situent entre 0 et 1.<\/p>\n<p>La seconde est de pond\u00e9rer. Le score final est une moyenne pond\u00e9r\u00e9e des deux scores, avec un hyperparam\u00e8tre \u03b1 :<\/p>\n<p><p class=\"ql-center-displayed-equation\" style=\"line-height: 20px;\"><span class=\"ql-right-eqno\"> &nbsp; <\/span><span class=\"ql-left-eqno\"> &nbsp; <\/span><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/www.ranksignal.fr\/wp-content\/ql-cache\/quicklatex.com-2d3f37e1aa3609cfcd9d5e63727d105c_l3.png\" height=\"20\" width=\"375\" class=\"ql-img-displayed-equation quicklatex-auto-format\" alt=\"&#92;&#91; &#92;&#109;&#97;&#116;&#104;&#114;&#109;&#123;&#115;&#99;&#111;&#114;&#101;&#125;&#95;&#123;&#104;&#121;&#98;&#114;&#105;&#100;&#101;&#125;&#61;&#40;&#49;&#45;&#92;&#97;&#108;&#112;&#104;&#97;&#41;&#92;&#99;&#100;&#111;&#116;&#92;&#109;&#97;&#116;&#104;&#114;&#109;&#123;&#115;&#99;&#111;&#114;&#101;&#125;&#95;&#123;&#99;&#114;&#101;&#117;&#120;&#125;&#43;&#92;&#97;&#108;&#112;&#104;&#97;&#92;&#99;&#100;&#111;&#116;&#92;&#109;&#97;&#116;&#104;&#114;&#109;&#123;&#115;&#99;&#111;&#114;&#101;&#125;&#95;&#123;&#100;&#101;&#110;&#115;&#101;&#125;&#46;&#92;&#93;\" title=\"Rendered by QuickLaTeX.com\"\/><\/p><\/p>\n<p>Ici <img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/www.ranksignal.fr\/wp-content\/ql-cache\/quicklatex.com-747a60d98f43e204b76d8d88ff02ac99_l3.png\" class=\"ql-img-inline-formula quicklatex-auto-format\" alt=\"&#92;&#109;&#97;&#116;&#104;&#114;&#109;&#123;&#115;&#99;&#111;&#114;&#101;&#125;&#95;&#123;&#99;&#114;&#101;&#117;&#120;&#125;\" title=\"Rendered by QuickLaTeX.com\" height=\"11\" width=\"73\" style=\"vertical-align: -3px;\"\/> est le score de la recherche par mots-cl\u00e9s (BM25), <img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/www.ranksignal.fr\/wp-content\/ql-cache\/quicklatex.com-5e731d21982ff89b1b716a36eaeeea37_l3.png\" class=\"ql-img-inline-formula quicklatex-auto-format\" alt=\"&#92;&#109;&#97;&#116;&#104;&#114;&#109;&#123;&#115;&#99;&#111;&#114;&#101;&#125;&#95;&#123;&#100;&#101;&#110;&#115;&#101;&#125;\" title=\"Rendered by QuickLaTeX.com\" height=\"11\" width=\"72\" style=\"vertical-align: -3px;\"\/> celui de la recherche vectorielle, et \u03b1, compris entre 0 et 1, r\u00e8gle l&rsquo;importance relative du c\u00f4t\u00e9 dense. Avec \u03b1 = 0, seule la recherche par mots-cl\u00e9s compte ; avec \u03b1 = 1, seule la recherche vectorielle.<\/p>\n<p>En pratique, on rencontre le plus souvent des valeurs de <strong>0,4 ou 0,5<\/strong>, parfois 0,3. Ce n&rsquo;est pas anodin : le poids suffit \u00e0 renverser un classement.<\/p>\n\n\n<figure data-wp-context=\"{&quot;imageId&quot;:&quot;6adef47227e67&quot;}\" data-wp-interactive=\"core\/image\" data-wp-key=\"6adef47227e67\" class=\"wp-block-image size-large wp-lightbox-container\"><img loading=\"lazy\" decoding=\"async\" width=\"1563\" height=\"1126\" data-wp-class--hide=\"state.isContentHidden\" data-wp-class--show=\"state.isContentVisible\" data-wp-init=\"callbacks.setButtonStyles\" data-wp-on--click=\"actions.showLightbox\" data-wp-on--load=\"callbacks.setButtonStyles\" data-wp-on--pointerdown=\"actions.preloadImage\" data-wp-on--pointerenter=\"actions.preloadImageWithDelay\" data-wp-on--pointerleave=\"actions.cancelPreload\" data-wp-on-window--resize=\"callbacks.setButtonStyles\" src=\"https:\/\/www.ranksignal.fr\/wp-content\/uploads\/2026\/10\/b2_ch06_recherche_hybride.png\" alt=\"Deux droites en fonction de \u03b1 : un passage fort en mots-cl\u00e9s dont le score baisse quand \u03b1 augmente, un passage fort en s\u00e9mantique dont le score monte, crois\u00e9es autour de 0,6, avec une zone gris\u00e9e entre 0,4 et 0,5\" class=\"wp-image-503\" srcset=\"https:\/\/www.ranksignal.fr\/wp-content\/uploads\/2026\/10\/b2_ch06_recherche_hybride.png 1563w, https:\/\/www.ranksignal.fr\/wp-content\/uploads\/2026\/10\/b2_ch06_recherche_hybride-300x216.png 300w, https:\/\/www.ranksignal.fr\/wp-content\/uploads\/2026\/10\/b2_ch06_recherche_hybride-1024x738.png 1024w, https:\/\/www.ranksignal.fr\/wp-content\/uploads\/2026\/10\/b2_ch06_recherche_hybride-768x553.png 768w, https:\/\/www.ranksignal.fr\/wp-content\/uploads\/2026\/10\/b2_ch06_recherche_hybride-1536x1107.png 1536w\" sizes=\"auto, (max-width: 1563px) 100vw, 1563px\" \/><button\n\t\t\tclass=\"lightbox-trigger\"\n\t\t\ttype=\"button\"\n\t\t\taria-haspopup=\"dialog\"\n\t\t\tdata-wp-bind--aria-label=\"state.thisImage.triggerButtonAriaLabel\"\n\t\t\tdata-wp-init=\"callbacks.initTriggerButton\"\n\t\t\tdata-wp-on--click=\"actions.showLightbox\"\n\t\t\tdata-wp-style--right=\"state.thisImage.buttonRight\"\n\t\t\tdata-wp-style--top=\"state.thisImage.buttonTop\"\n\t\t>\n\t\t\t<svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"12\" height=\"12\" fill=\"none\" viewBox=\"0 0 12 12\">\n\t\t\t\t<path fill=\"#fff\" d=\"M2 0a2 2 0 0 0-2 2v2h1.5V2a.5.5 0 0 1 .5-.5h2V0H2Zm2 10.5H2a.5.5 0 0 1-.5-.5V8H0v2a2 2 0 0 0 2 2h2v-1.5ZM8 12v-1.5h2a.5.5 0 0 0 .5-.5V8H12v2a2 2 0 0 1-2 2H8Zm2-12a2 2 0 0 1 2 2v2h-1.5V2a.5.5 0 0 0-.5-.5H8V0h2Z\" \/>\n\t\t\t<\/svg>\n\t\t<\/button><figcaption class=\"wp-element-caption\">Regarde le croisement des deux droites, sur ce sch\u00e9ma aux valeurs illustratives : \u00e0 gauche de lui, le passage fort en mots-cl\u00e9s domine ; \u00e0 droite, c&rsquo;est le passage s\u00e9mantique qui prend la t\u00eate. <strong>Le poids \u03b1 fait basculer le classement<\/strong> de deux passages qui ont chacun un atout diff\u00e9rent, et la plage usuelle se situe en de\u00e7\u00e0 du croisement.<\/figcaption><\/figure>\n\n\n<h2 id=\"la-fusion-en-code-deux-manieres-de-faire\">La fusion en code : deux mani\u00e8res de faire<\/h2>\n<p>Comment obtenir cela dans une vraie base ? Voici deux fa\u00e7ons, qui ne fusionnent pas de la m\u00eame mani\u00e8re.<\/p>\n<h3>Noter puis trier ensemble (Neo4j)<\/h3>\n<p>Pour qu&rsquo;une base de graphe fasse de la recherche par mots-cl\u00e9s sur le texte de ses fragments, il faut d&rsquo;abord cr\u00e9er un index plein texte sur leur propri\u00e9t\u00e9 <code>text<\/code>.<\/p>\n<pre><code>driver.execute_query(&quot;CREATE FULLTEXT INDEX PdfChunkFulltext FOR (c:Chunk)\nON EACH [c.text]&quot;)\n<\/code><\/pre>\n<p>La requ\u00eate de fusion lance ensuite les deux recherches. La premi\u00e8re interroge l&rsquo;index vectoriel nomm\u00e9 <code>pdf<\/code> avec l&#8217;embedding de la question ; la seconde, l&rsquo;index plein texte avec le texte de la question. Dans chacune, les scores sont divis\u00e9s par leur maximum. <code><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/www.ranksignal.fr\/wp-content\/ql-cache\/quicklatex.com-8450cdc4cddce5cf26f24cc4359cd13c_l3.png\" class=\"ql-img-inline-formula quicklatex-auto-format\" alt=\"&#107;&#96;&#44;&#32;&#96;\" title=\"Rendered by QuickLaTeX.com\" height=\"16\" width=\"27\" style=\"vertical-align: -4px;\"\/>question_embedding<\/code> et <code>&#36;question<\/code> sont les param\u00e8tres que l&rsquo;application transmet \u00e0 la requ\u00eate.<\/p>\n<pre><code>CALL {\n    \/\/ vector index\n    CALL db.index.vector.queryNodes('pdf', &#36;k, &#36;question_embedding)\nYIELD node, score\n    WITH collect({node:node, score:score}) AS nodes, max(score) AS max\n    UNWIND nodes AS n\n    \/\/ Normalize scores\n    RETURN n.node AS node, (n.score \/ max) AS score\n    UNION\n    \/\/ keyword index\n    CALL db.index.fulltext.queryNodes('ftPdfChunk', &#36;question, {limit: &#36;k})\n    YIELD node, score\n    WITH collect({node:node, score:score}) AS nodes, max(score) AS max\n    UNWIND nodes AS n\n    \/\/ Normalize scores\n    RETURN n.node AS node, (n.score \/ max) AS score\n}\n\/\/ deduplicate nodes\nWITH node, max(score) AS score ORDER BY score DESC LIMIT &#36;k\nRETURN node, score\n<\/code><\/pre>\n<p><code>UNION<\/code> met les deux listes bout \u00e0 bout, puis la derni\u00e8re ligne <strong>garde le meilleur score<\/strong> de chaque fragment trouv\u00e9 par les deux recherches, trie et coupe aux <code>k<\/code> premiers. Un fragment rep\u00e9r\u00e9 par les deux moteurs n&rsquo;appara\u00eet donc qu&rsquo;une fois, avec sa meilleure note.<\/p>\n<blockquote>\n<p><strong>\u26a0\ufe0f Pi\u00e8ge<\/strong> Dans cette requ\u00eate, l&rsquo;index plein texte s&rsquo;appelle <code>ftPdfChunk<\/code>, alors que la commande pr\u00e9c\u00e9dente en cr\u00e9e un nomm\u00e9 <code>PdfChunkFulltext<\/code>. Garde le nom de l&rsquo;index que tu as r\u00e9ellement cr\u00e9\u00e9, sinon la seconde recherche \u00e9choue.<\/p>\n<\/blockquote>\n<h3>Rassembler d&rsquo;abord (Elasticsearch et ChromaDB)<\/h3>\n<p>Seconde fa\u00e7on, avec deux syst\u00e8mes distincts : une base vectorielle ChromaDB d&rsquo;un c\u00f4t\u00e9, Elasticsearch pour le plein texte de l&rsquo;autre. La fonction ci-dessous prend une requ\u00eate et un <code>top_k<\/code>. Elle s&rsquo;appuie sur des objets pr\u00e9par\u00e9s en amont : <code>sentence_model<\/code> (l&rsquo;encodeur), <code>collection<\/code> (la collection ChromaDB), <code>documents<\/code> (les textes index\u00e9s), <code>es<\/code> (le client Elasticsearch) et <code>index_name<\/code> (son index).<\/p>\n<pre><code>def fusion_retrieval(query, top_k=5):\n    &quot;&quot;&quot;\n    Retrieves the top_k most relevant documents using a combination of\nvector-based\n    and textual retrieval methods.\n    Args:\n        query (str): The search query.\n        top_k (int): The number of top documents to retrieve.\n    Returns:\n        list: A list of combined results from both vector and textual\nretrieval methods.\n    &quot;&quot;&quot;\n    query_embedding = sentence_model.encode(query).tolist()\n    vector_results = collection.query(query_embeddings=[query_\nembedding], n_results=min(top_k, len(documents)))\n    es_body = {\n        &quot;size&quot;: top_k,  # Move size into body\n        &quot;query&quot;: {\n            &quot;match&quot;: {\n                &quot;content&quot;: query\n            }\n        }\n    }\n    es_results = es.search(index=index_name, body=es_body)\n    es_documents = [hit[&quot;_source&quot;][&quot;content&quot;] for hit in es_\nresults['hits']['hits']]\n    combined_results = vector_results['documents'][0] + es_documents\n    return combined_results\n<\/code><\/pre>\n<p>Compare avec la requ\u00eate pr\u00e9c\u00e9dente. Ici, la derni\u00e8re ligne ne calcule aucun score commun : elle ajoute simplement la liste textuelle \u00e0 la liste vectorielle. La fusion est une mise bout \u00e0 bout, sans normalisation, et le classement final reste \u00e0 faire ailleurs.<\/p>\n<h2 id=\"ou-se-loge-la-recherche-hybride-dans-un-pipeline\">O\u00f9 se loge la recherche hybride dans un pipeline<\/h2>\n<p>Dans un pipeline de recherche avanc\u00e9, elle occupe l&rsquo;\u00e9tage \u00ab r\u00e9cup\u00e9ration et post-traitement \u00bb, apr\u00e8s la transformation et le routage de la requ\u00eate, et avant le reranking et la compression du contexte.<\/p>\n\n\n<figure data-wp-context=\"{&quot;imageId&quot;:&quot;6adef4722837c&quot;}\" data-wp-interactive=\"core\/image\" data-wp-key=\"6adef4722837c\" class=\"wp-block-image size-large wp-lightbox-container\"><img loading=\"lazy\" decoding=\"async\" width=\"2340\" height=\"1500\" data-wp-class--hide=\"state.isContentHidden\" data-wp-class--show=\"state.isContentVisible\" data-wp-init=\"callbacks.setButtonStyles\" data-wp-on--click=\"actions.showLightbox\" data-wp-on--load=\"callbacks.setButtonStyles\" data-wp-on--pointerdown=\"actions.preloadImage\" data-wp-on--pointerenter=\"actions.preloadImageWithDelay\" data-wp-on--pointerleave=\"actions.cancelPreload\" data-wp-on-window--resize=\"callbacks.setButtonStyles\" src=\"https:\/\/www.ranksignal.fr\/wp-content\/uploads\/2026\/10\/b2_ch06_pipeline_avance.png\" alt=\"Pipeline en trois \u00e9tages : avant la r\u00e9cup\u00e9ration (transformation de la requ\u00eate, routage), r\u00e9cup\u00e9ration et post-traitement (recherche hybride, reranking, compression), puis g\u00e9n\u00e9ration avec un prompt strict\" class=\"wp-image-496\" srcset=\"https:\/\/www.ranksignal.fr\/wp-content\/uploads\/2026\/10\/b2_ch06_pipeline_avance.png 2340w, https:\/\/www.ranksignal.fr\/wp-content\/uploads\/2026\/10\/b2_ch06_pipeline_avance-300x192.png 300w, https:\/\/www.ranksignal.fr\/wp-content\/uploads\/2026\/10\/b2_ch06_pipeline_avance-1024x656.png 1024w, https:\/\/www.ranksignal.fr\/wp-content\/uploads\/2026\/10\/b2_ch06_pipeline_avance-768x492.png 768w, https:\/\/www.ranksignal.fr\/wp-content\/uploads\/2026\/10\/b2_ch06_pipeline_avance-1536x985.png 1536w, https:\/\/www.ranksignal.fr\/wp-content\/uploads\/2026\/10\/b2_ch06_pipeline_avance-2048x1313.png 2048w\" sizes=\"auto, (max-width: 2340px) 100vw, 2340px\" \/><button\n\t\t\tclass=\"lightbox-trigger\"\n\t\t\ttype=\"button\"\n\t\t\taria-haspopup=\"dialog\"\n\t\t\tdata-wp-bind--aria-label=\"state.thisImage.triggerButtonAriaLabel\"\n\t\t\tdata-wp-init=\"callbacks.initTriggerButton\"\n\t\t\tdata-wp-on--click=\"actions.showLightbox\"\n\t\t\tdata-wp-style--right=\"state.thisImage.buttonRight\"\n\t\t\tdata-wp-style--top=\"state.thisImage.buttonTop\"\n\t\t>\n\t\t\t<svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"12\" height=\"12\" fill=\"none\" viewBox=\"0 0 12 12\">\n\t\t\t\t<path fill=\"#fff\" d=\"M2 0a2 2 0 0 0-2 2v2h1.5V2a.5.5 0 0 1 .5-.5h2V0H2Zm2 10.5H2a.5.5 0 0 1-.5-.5V8H0v2a2 2 0 0 0 2 2h2v-1.5ZM8 12v-1.5h2a.5.5 0 0 0 .5-.5V8H12v2a2 2 0 0 1-2 2H8Zm2-12a2 2 0 0 1 2 2v2h-1.5V2a.5.5 0 0 0-.5-.5H8V0h2Z\" \/>\n\t\t\t<\/svg>\n\t\t<\/button><figcaption class=\"wp-element-caption\">Lis la bande du milieu : la recherche hybride y pr\u00e9c\u00e8de le reranking. Le reranker r\u00e9ordonne une liste d\u00e9j\u00e0 large de candidats pour ne transmettre que les meilleurs au mod\u00e8le de langage.<\/figcaption><\/figure>\n\n\n<p>C\u00f4t\u00e9 outils, deux familles se rejoignent. Les plateformes de recherche textuelle historiques (Solr, Elasticsearch, OpenSearch, Apache Lucene) ont ajout\u00e9 la prise en charge des vecteurs ; des bases vectorielles comme Weaviate ou Milvus proposent de combiner dense et creux ; Neo4j le permet avec deux index, comme on vient de le voir. <strong>Toutes les bases n&rsquo;acceptent pas<\/strong> la recherche hybride ou un reranker : mieux vaut le v\u00e9rifier avant de choisir.<\/p>\n<p>Reste \u00e0 ne pas la confondre avec deux autres fa\u00e7ons de marier mots et sens :<\/p>\n<table>\n<thead>\n<tr>\n<th>Approche<\/th>\n<th>Ce qu&rsquo;elle fait de la requ\u00eate<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Routage<\/td>\n<td>Choisit une seule voie par requ\u00eate, par exemple une recherche par mots-cl\u00e9s si elle contient un terme pr\u00e9cis<\/td>\n<\/tr>\n<tr>\n<td>Filtrer puis affiner<\/td>\n<td>Garde d&rsquo;abord les documents qui contiennent certains mots-cl\u00e9s, puis affine par la recherche s\u00e9mantique<\/td>\n<\/tr>\n<tr>\n<td>Hybride<\/td>\n<td>Lance les deux recherches, puis fusionne les r\u00e9sultats<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<blockquote>\n<p><strong>\ud83d\udccc \u00c0 retenir<\/strong> Le routage choisit, le filtre encha\u00eene, l&rsquo;hybride m\u00e9lange. Seule la recherche hybride garde en m\u00eame temps le meilleur des deux listes de r\u00e9sultats.<\/p>\n<\/blockquote>\n<h2 id=\"ce-qu-elle-ne-fait-pas-quatre-idees-recues\">Ce qu&rsquo;elle ne fait pas : quatre id\u00e9es re\u00e7ues<\/h2>\n<h3>\u00ab Le s\u00e9mantique a rendu les mots-cl\u00e9s inutiles \u00bb<\/h3>\n<p>La recherche s\u00e9mantique donne des r\u00e9sultats plus exacts et plus significatifs que le mot \u00e0 mot, c&rsquo;est vrai. Mais elle ne retrouve pas toujours les termes pr\u00e9cis, et le moteur par mots-cl\u00e9s, lui, ne retrouve pas les paraphrases. <strong>Les deux se compl\u00e8tent<\/strong>, ce qui est justement la raison d&rsquo;\u00eatre du m\u00e9lange.<\/p>\n<h3>\u00ab Il existe un bon r\u00e9glage universel \u00bb<\/h3>\n<p>Les valeurs de 0,4 ou 0,5 sont des plages usuelles, pas une r\u00e8gle. La pond\u00e9ration s&rsquo;adapte aux besoins de pr\u00e9cision et de rappel du cas d&rsquo;usage. Le sch\u00e9ma de tout \u00e0 l&rsquo;heure le montre : un poids diff\u00e9rent inverse l&rsquo;ordre de deux passages.<\/p>\n<h3>\u00ab Hybride veut dire n&rsquo;importe quel m\u00e9lange \u00bb<\/h3>\n<p>Le mot recouvre plusieurs choses. La fusion de requ\u00eates, par exemple, \u00e9tend la question en plusieurs perspectives avec un mod\u00e8le de langage, lance des recherches en parall\u00e8le, puis fusionne les r\u00e9sultats par re-classement : elle multiplie les formulations d&rsquo;une m\u00eame question, elle ne combine pas deux m\u00e9thodes de recherche. Et on parle aussi de recherche hybride pour m\u00ealer les donn\u00e9es prot\u00e9g\u00e9es d&rsquo;une organisation et le contexte trouv\u00e9 par une recherche sur internet, ce qui soul\u00e8ve d&rsquo;autres questions : faut-il mettre \u00e0 jour la base, comment filtrer les r\u00e9sultats hors sujet, quels probl\u00e8mes de s\u00e9curit\u00e9 ?<\/p>\n<h3>\u00ab Une fois hybride, c&rsquo;est r\u00e9gl\u00e9 \u00bb<\/h3>\n<p>Combiner recherche vectorielle et recherche plein texte donne de meilleurs r\u00e9sultats que l&rsquo;une ou l&rsquo;autre seule. Mais cette approche garde des limites tant qu&rsquo;elle repose uniquement sur des donn\u00e9es non structur\u00e9es : les r\u00e9f\u00e9rences internes au texte ne sont pas toujours bien capt\u00e9es, et le contexte environnant ne suffit pas toujours \u00e0 donner du sens au mod\u00e8le de langage. La suite consiste \u00e0 tirer parti d&rsquo;une structure dans les donn\u00e9es quand elle existe.<\/p>","protected":false},"excerpt":{"rendered":"<p>La recherche hybride lance une recherche par mots-cl\u00e9s et une recherche s\u00e9mantique, puis fusionne leurs scores. Voici comment, avec du code, et ce qu&rsquo;elle ne fait pas.<\/p>\n","protected":false},"author":1,"featured_media":504,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[5],"tags":[],"class_list":["post-505","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-rag"],"_links":{"self":[{"href":"https:\/\/www.ranksignal.fr\/blog\/wp-json\/wp\/v2\/posts\/505","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.ranksignal.fr\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.ranksignal.fr\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.ranksignal.fr\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.ranksignal.fr\/blog\/wp-json\/wp\/v2\/comments?post=505"}],"version-history":[{"count":3,"href":"https:\/\/www.ranksignal.fr\/blog\/wp-json\/wp\/v2\/posts\/505\/revisions"}],"predecessor-version":[{"id":808,"href":"https:\/\/www.ranksignal.fr\/blog\/wp-json\/wp\/v2\/posts\/505\/revisions\/808"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.ranksignal.fr\/blog\/wp-json\/wp\/v2\/media\/504"}],"wp:attachment":[{"href":"https:\/\/www.ranksignal.fr\/blog\/wp-json\/wp\/v2\/media?parent=505"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.ranksignal.fr\/blog\/wp-json\/wp\/v2\/categories?post=505"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.ranksignal.fr\/blog\/wp-json\/wp\/v2\/tags?post=505"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}