Skip to content

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_cd avec websearch_to_tsquery('french', …), pondéré sur name (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 de 0.5–0.8 même sur des résultats moyens.
  • ts_rank_cd[0, +∞[ théorique, en pratique presque toujours < 0.5, et exactement 0 dè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éfaut 0.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.5 donne un équilibre, w=1 pur vecteur, w=0 pur texte. Lisible.
  • Override par requête : ?rrfVectorWeight=0.8 permet de tester en live.
  • Symétrique : si une page front veut « plus de sémantique pour les requêtes vagues », elle peut monter w côté URL sans changement serveur.
  • Planchers indépendants : le OR final laisse passer un voyage avec excellent FTS mais sémantique moyenne (et vice-versa).

Négatives

  • Les rangs dépendent du filtre. Si ?destination=FR retire 80% des voyages, les rank_vector et rank_text se recalculent sur l'ensemble filtré. Conséquence : le _relevance_score d'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.0164 en pur w=1, ou 0.5*1/61 + 0.5*1/61 = 0.0164 en équilibré). Inutile d'exposer en UI comme « score 80% pertinent ». On expose _semantic_score et _tsvector_score séparément pour ça.
  • Constante 60 hardcodé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 → contribution 1/(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 — un ts_rank_cd = 0 met 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.5 figé) : simple, mais perd la possibilité de tuner. La pondération est gratuite à implémenter et utile en pratique (cas Wengen / mode discovery dans architecture/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 / tsvectorFloor selon les symptômes : architecture/search-tuning.md.

Contributors

No contributors

Changelog

No recent changes