Construire un pipeline RAG hybride avec Reciprocal Rank Fusion
Comment la similarité vectorielle et la recherche plein texte PostgreSQL tournent en parallèle et fusionnent via Reciprocal Rank Fusion en réponses avec citations et scores de pertinence.
Pourquoi une seule stratégie de retrieval ne suffit jamais
Le retrieval-augmented generation vaut ce que vaut ce que le retriever passe au modèle. Interrogez une base de connaissances, et la réponse n'est bonne que si les chunks qui la précèdent le sont - et les questions enterprise se présentent sous deux formes qu'aucune stratégie de recherche unique ne gère bien.
Les questions sémantiques - "comment la facturation gère-t-elle la prorata ?", "qu'avons-nous décidé sur le vendor lock-in ?" - concernent le sens. La recherche vectorielle excelle ici : elle plonge la requête et les chunks dans le même espace et récupère les voisins les plus proches, si bien qu'un chunk écrit avec des mots complètement différents peut quand même remonter. Mais la recherche purement vectorielle est peu fiable pour l'autre forme de question : les identifiants exacts. Codes produit, chaînes d'erreur, numéros de règlement, noms de personnes - les embeddings les brouillent, et le voisin le plus proche d'un code d'erreur obscur est souvent un autre code, sans rapport.
Les questions par mots-clés sont l'image miroir. La recherche plein texte de PostgreSQL excelle sur les tokens exacts et les termes rares, mais rate entièrement les paraphrases : demandez "staff augmentation" et elle ne trouvera pas le document qui dit "contractor resourcing".
Les documents d'entreprise réels attirent les deux types de questions, souvent dans la même session. Simmetric Chat refuse donc de choisir. Chaque retrieval lance deux jambes en parallèle - similarité vectorielle et recherche plein texte PostgreSQL - et fusionne les résultats avec Reciprocal Rank Fusion (RRF) avant que quoi que ce soit n'atteigne le modèle. Cet article parcourt ce pipeline : les deux jambes, les maths de la fusion, le chemin d'ingestion qui les alimente, et la couche de citations qui rend les réponses auditable.
Deux jambes, une requête
Quand un chat a besoin de connaissances, l'agent ReAct invoque sa skill rag_search, et la recherche hybride lance deux retrievals indépendants en même temps :
- La jambe vectorielle quitte le serveur et appelle le collector, le service qui possède le magasin vectoriel. Le collector plonge la requête avec le même modèle d'embedding qu'à l'ingestion et renvoie les chunks les plus proches de la collection du workspace. Le magasin vectoriel lui-même est pluggable - LanceDB par défaut (fichier, local, compatible air-gap), ou Qdrant, pgvector, ou Chroma pour les déploiements qui préfèrent un index côté serveur.
- La jambe plein texte ne quitte jamais le serveur. Elle interroge directement PostgreSQL, en confrontant la requête à une colonne de vecteur plein texte concaténée sur la table des chunks, soutenue par un index GIN et classée par pertinence lexicale. La tokenisation est sensible à la langue sur sept langues, donc les documents italiens et allemands sont recherchables avec leurs propres règles de mots plutôt qu'avec des approximations anglaises.
Les collections sont par workspace, ce qui garde les connaissances cloisonnées : un chat dans le workspace A récupère les chunks du workspace A et rien d'autre. Faire tourner les deux jambes sur chaque requête coûte un aller-retour supplémentaire et achète une couverture qu'aucune jambe n'a seule.
Le mode de défaillance est délibérément redondant aussi. Un garde compare le modèle d'embedding configuré aujourd'hui avec celui avec lequel les documents du workspace ont été plongés. En cas de désaccord - après un changement de modèle, par exemple - la jambe vectorielle est entièrement sautée et le retrieval dégrade au plein texte seul, plutôt que de servir des scores de similarité issus d'espaces d'embedding incomparables. Le chat continue de fonctionner, et l'opérateur obtient un chemin de ré-indexation explicite dans le panneau d'administration au lieu d'une régression de qualité silencieuse.
Reciprocal Rank Fusion, concrètement
Les deux jambes renvoient des listes classées mesurées sur des échelles incommensurables : similarité cosinus d'un côté, pertinence lexicale de l'autre. Moyenner les scores bruts reviendrait à calibrer ces échelles l'une contre l'autre - fragile, et différent pour chaque modèle d'embedding et chaque corpus. Simmetric Chat fusionne les rangs plutôt que les scores, en utilisant Reciprocal Rank Fusion :
fused(chunk) = sum over legs of 1 / (k + rank), with k = 60Chaque chunk accumule une contribution par jambe où il apparaît, donc un chunk classé premier sur les deux jambes marque environ deux fois un chunk classé premier sur une seule. La constante k amortit l'influence des tout premiers rangs - avec k = 60, la différence entre le rang un et le rang cinq sur une jambe est un petit delta, et c'est le consensus entre jambes qui fait vraiment monter un chunk dans la liste fusionnée.
Deux détails d'ingénierie rendent la fusion digne de la production plutôt que du manuel :
- Un ensemble de sources par chunk enregistre s'il a été trouvé par la jambe vectorielle, la jambe plein texte, ou les deux. Un chunk récupéré par les deux jambes est la corroboration la plus forte que le pipeline puisse produire, et le formatage aval préserve ce signal au lieu de l'aplatir.
- Un départage déterministe - score fusionné décroissant, puis identifiant de document, puis position du chunk - garantit que la même requête renvoie le même ordre sur chaque réplique. Quand quelqu'un demande "pourquoi a-t-il cité ce paragraphe ?", la réponse doit être reproductible ; une fusion non déterministe rendrait la question sans réponse.
Des documents aux chunks recherchables
La fusion n'est que la moitié du pipeline ; l'autre moitié se produit à l'ingestion, quand un document téléversé devient des chunks que les deux jambes peuvent chercher. Le collector analyse les fichiers PDF, Word, Excel, PowerPoint, Markdown, CSV et les transcriptions YouTube, découpe le texte avec un découpeur récursif par caractères (1 000 caractères par chunk, 200 caractères de chevauchement pour ne pas perdre les phrases qui traversent une frontière), plonge chaque chunk, et écrit les vecteurs dans la collection du workspace.
Crucialement, le texte des chunks atterrit aussi dans PostgreSQL, où le serveur maintient le vecteur de recherche plein texte que la jambe FTS interroge. Le collector ne touche jamais à la base relationnelle - une frontière de service dure qui garde le pipeline analyse-et-embedding remplaçable et laisse l'index de recherche possédé par le serveur qui l'interroge.
La version 0.22 a rendu le retrieval précis dans une seconde dimension : le filtrage par métadonnées. L'agent peut désormais circonscrire un appel rag_search aux types de documents (PDF, Markdown, TXT, CSV, DOCX, XLSX) et aux fenêtres de date d'ingestion, ce qui corrige le classique "des documents de 2023 qui répondent aux questions de 2025". Les filtres traversent les deux jambes - comme pré-filtres sur les métadonnées d'ingestion estampillées dans les magasins vectoriels, et comme prédicats SQL paramétrés côté plein texte. Un garde-fou de métadonnées côté serveur revérifie ensuite chaque résultat fusionné contre la table des documents faisant autorité, si bien que la correction des filtres ne dépend pas de l'implémentation correcte des prédicats par chaque fournisseur vectoriel. Les vecteurs hérités ingérés avant l'estampillage des métadonnées sortent simplement du jeu quand un filtre est actif, et retrouvent la filtrabilité via un re-embed déclenché par l'admin.
Des réponses qui montrent leur travail
La qualité du retrieval n'est que la moitié de l'histoire ; l'autre moitié est l'honnêteté sur ce sur quoi la réponse s'appuie. Les citations traversent le même canal de streaming que les tokens : les badges de citation s'affichent en ligne dans la réponse, chacun portant le document source, le chunk et un score de pertinence, et chacun lié à la source pour que le lecteur puisse vérifier l'affirmation en un clic.
La discipline de fraîcheur vit ici aussi. Après déduplication des résultats fusionnés, un filtre d'ancrage plafonne les citations par document aux deux premières par score de pertinence et écarte les citations de chunks dont le texte ne partage aucun chevauchement lexical significatif avec la réponse finale. La porte est délibérément conservatrice - les pages wiki, les résultats d'archive et les réponses très courtes sont toujours gardés - mais elle élimine le mode de défaillance où une réponse dit "aucune information supplémentaire" tout en affichant "Sources (5)" tirées d'un seul document marginalement lié. L'avertissement "aucune preuve à l'appui" est jugé contre le même ensemble filtré, donc l'avertissement et la liste des sources décrivent toujours la même réalité.
Choisir et régler la pile
Le pipeline est délibérément piloté par la configuration, et les valeurs par défaut sont celles qui privilégient la confidentialité :
- Magasin vectoriel : LanceDB par défaut - sur fichier, stocké localement, aucun service externe requis, entièrement compatible air-gap. Qdrant, pgvector et Chroma sont à un changement d'environnement quand un déploiement le dépasse ; le fournisseur est sélectionné au boot.
- Embeddings : transformeurs locaux (384 dimensions, entièrement hors ligne) par défaut, ou embeddings OpenAI quand un déploiement accepte l'appel sortant. Changer le modèle d'embedding exige une ré-indexation, pilotée par le panneau d'administration ; le garde de désaccord maintient le système sûr dans l'intervalle.
- Re-ranking : optionnel. Un reranker peut sur-récupérer et réordonner les résultats fusionnés quand la qualité de la réponse importe plus que la latence.
- Constante de fusion : k = 60 est livré par défaut ; c'est une constante unique dans le service de recherche hybride si vos distributions de classement appellent un amortissement différent.
La présentation complète du flux de données - la frontière serveur-collector, la disposition de stockage derrière chaque fournisseur vectoriel, et les événements de citation en streaming - est documentée dans le guide d'architecture.


