ADR 0002 — Recherche hybride par Reciprocal Rank Fusion
Date : initial Statut : Accepté
Contexte
Le service combine deux moteurs de pertinence sur la même requête :
- Vecteur :
pgvector+ similarité cosinus sur des embeddings BGE Multilingual Gemma2 (3584D). - Texte :
tsvector+ts_rank_cdavecwebsearch_to_tsquery('french', …), pondéré surname (A) / subtitle (B) / description (C).
Il faut une stratégie pour fusionner leurs sorties en un classement final unique, robuste aux distributions de scores hétérogènes :
cosine similarity∈[0, 1], distribution dense autour de0.5–0.8même sur des résultats moyens.ts_rank_cd∈[0, +∞[théorique, en pratique presque toujours< 0.5, et exactement0dès qu'il n'y a aucun match texte.
Décision
Utiliser Reciprocal Rank Fusion (RRF) pondéré, avec deux planchers indépendants (semanticFloor, tsvectorFloor) appliqués après fusion :
RRF(doc) = w · 1/(60 + rank_vector(doc)) + (1-w) · 1/(60 + rank_text(doc))60: constante de lissage standard (cf. Cormack et al. 2009).w∈[0, 1]:DEFAULT_RRF_VECTOR_WEIGHT(défaut0.5), overridable par requête.- Filtre final :
WHERE vector_score >= semantic_floor OR text_score >= tsvector_floor(OR, pas AND).
Détails de l'implémentation SQL : architecture/hybrid-search.md.
Conséquences
Positives
- ✅ Robuste : RRF dépend des rangs, pas des valeurs de scores. Pas besoin de normaliser les distributions.
- ✅ Pondération expressive :
w=0.5donne un équilibre,w=1pur vecteur,w=0pur texte. Lisible. - ✅ Override par requête :
?rrfVectorWeight=0.8permet de tester en live. - ✅ Symétrique : si une page front veut « plus de sémantique pour les requêtes vagues », elle peut monter
wcôté URL sans changement serveur. - ✅ Planchers indépendants : le
ORfinal laisse passer un voyage avec excellent FTS mais sémantique moyenne (et vice-versa).
Négatives
- ❌ Les rangs dépendent du filtre. Si
?destination=FRretire 80% des voyages, lesrank_vectoretrank_textse recalculent sur l'ensemble filtré. Conséquence : le_relevance_scored'un même voyage diffère avec/sans filtre. Pas un bug, mais source de confusion utilisateur. - ❌ Pas de score absolu interprétable. RRF score ∈
[0, ~0.033](max =1/61 + 0/61 = 0.0164en pur w=1, ou0.5*1/61 + 0.5*1/61 = 0.0164en équilibré). Inutile d'exposer en UI comme « score 80% pertinent ». On expose_semantic_scoreet_tsvector_scoreséparément pour ça. - ❌ Constante
60hardcodée. Pas exposée comme paramètre. Reconnue comme un bon défaut universel, mais peu de raison de la toucher. - ❌
COALESCE(rank, 1000)plafonne arbitrairement. Un voyage absent d'un classement reçoit rank=1000 → contribution1/(60+1000) ≈ 0.0009. Pratique mais arbitraire ; négligeable face aux top-10.
Alternatives écartées
Score blending pondéré :
final = w · cosine + (1-w) · ts_rank_cd. Échoue à cause des échelles différentes — unts_rank_cd = 0met tout le poids sur la sémantique. Normaliser via min-max est fragile (un outlier casse tout). Normaliser via softmax change la sémantique du score.Re-ranking à 2 étages (Retrieve + Cross-encoder) : retrieve via vecteur, puis re-rank top-50 via un cross-encoder (BGE Reranker). Bonne qualité, mais (a) coût d'inférence ×50 par requête, (b) latence ajoutée, (c) dépendance Infomaniak supplémentaire. Pas justifié pour le volume actuel.
Reciprocal Rank Fusion sans pondération (
w=0.5figé) : simple, mais perd la possibilité de tuner. La pondération est gratuite à implémenter et utile en pratique (cas Wengen / mode discovery dansarchitecture/search-tuning.md).Élasticsearch / OpenSearch : leurs intégrations FTS + vector + RRF sont mûres, mais (a) un service de plus, (b) on perd la simplicité d'une seule DB, (c) coût opérationnel.
Mécanique fine documentée ailleurs
- Forme SQL exacte du WITH CTE :
architecture/hybrid-search.md. - Guide de réglage
w/semanticFloor/tsvectorFloorselon les symptômes :architecture/search-tuning.md.

