Architecture — Guide de réglage de la recherche
Trois leviers principaux contrôlent la pertinence de la recherche. Tous ont une valeur par défaut (variable d'env) et peuvent être surchargés par requête (query param). Le but de cette page : savoir lequel toucher quand un résultat semble faux.
Les trois leviers
| Levier | Env var | Query param | Plage | Défaut |
|---|---|---|---|---|
| Pondération RRF | DEFAULT_RRF_VECTOR_WEIGHT | rrfVectorWeight | [0, 1] | 0.5 |
| Plancher sémantique | DEFAULT_SEMANTIC_FLOOR | semanticFloor | [0, 1] | 0.55 |
| Plancher texte | DEFAULT_TSVECTOR_FLOOR | tsvectorFloor | ≥ 0 | 0.05 |
Compose en dev (
docker-compose.yml) utilise des valeurs un peu plus généreuses :0.4 / 0.5 / 0.0pour exposer plus de résultats pendant l'itération.
Voir architecture/hybrid-search.md pour la mécanique sous-jacente — cette page se concentre sur le « quel bouton tourner ».
Diagnostic rapide
┌────────────────────────┐
│ Plainte utilisateur │
└────────────┬───────────┘
│
┌──────────────────┼──────────────────┐
▼ ▼ ▼
« Pas assez de « Trop de bruit / « Mauvais ordre »
résultats » hors sujet »
│ │ │
▼ ▼ ▼
Baisser les Monter les Ajuster
floors floors rrfVectorWeightCas concrets
Cas 1 — « ski en Autriche » ne renvoie rien alors que le voyage Kitzbühel existe
Diagnostic : le terme « Autriche » ne figure peut-être pas dans name/subtitle/description (juste dans country.name). FTS échoue → text_score < tsvector_floor. La similarité sémantique pourrait sauver le coup, mais elle aussi est sous le seuil.
Solutions :
- Vérifier dans la réponse les champs
semantic_scoreettsvector_score(exposés viaapply_info_densitymême en modemin). - Pousser le poids vers le sémantique :
?rrfVectorWeight=0.8. - Si toujours rien : baisser temporairement le plancher sémantique :
?semanticFloor=0.4.
Cas 2 — « Wengen » renvoie 50 résultats dont seulement 1 est le voyage cherché
Diagnostic : « Wengen » est un nom propre rare. Le sémantique tire des voyages « montagne / ski » alentour. Le texte trouve un match exact pour le voyage de Wengen.
Solutions :
- Pousser vers le texte :
?rrfVectorWeight=0.2. - Monter le plancher sémantique :
?semanticFloor=0.7(ne garde que les voyages très sémantiquement proches). - La combinaison des deux donne typiquement le meilleur résultat sur ces requêtes nominatives.
Cas 3 — La requête « pleine d'expression évocatrice » remonte du bruit
Ex: « vacances ressourcement nature montagne », ?search=… renvoie 80 résultats peu pertinents.
Diagnostic : sémantique large → similarité ≥ 0.55 sur beaucoup de candidats. FTS aussi (chaque mot stem-match plein de descriptions).
Solutions :
- Monter
?semanticFloor=0.7ou0.75pour ne garder que la cible sémantique précise. - Monter
?tsvectorFloor=0.1pour éliminer les matches FTS faiblards (1 mot vague qui matche).
Cas 4 — Le tri par pertinence semble incohérent entre deux pages
Diagnostic : le RRF dépend des rangs. Si vous filtrez (?destination=FR), les rangs sont calculés sur l'ensemble filtré, donc différents qu'en non filtré. Ce n'est pas un bug.
Solution : pas de solution applicative — c'est la nature du RRF. Si l'utilisateur attend un score absolu stable, exposer _semantic_score et _tsvector_score côté front (ils ne dépendent pas du filtre) et trier différemment.
Cas 5 — Le score n'est pas reproductible (les embeddings changent)
Diagnostic : l'API Infomaniak est déterministe sur une même requête, donc le score devrait être stable. Mais :
- Si le record a été ré-embeddé entre deux requêtes (reindex), son vecteur peut avoir changé (texte source modifié côté Horizon).
- Le rang dépend de tous les autres documents en base.
Solution : pour des tests de régression de pertinence, geler le dataset (snapshot) et la requête.
Réglages typiques
| Profil | rrfVectorWeight | semanticFloor | tsvectorFloor |
|---|---|---|---|
| Défaut prod (équilibré) | 0.5 | 0.55 | 0.05 |
Dev (docker-compose.yml) — plus permissif | 0.4 | 0.5 | 0.0 |
| « Mode SEO » (mot-clé exact) | 0.2 | 0.5 | 0.1 |
| « Mode discovery » (requête vague, sémantique) | 0.8 | 0.6 | 0.0 |
| Debug (tout voir) | 0.5 | 0.0 | 0.0 |
Méthodologie pour changer le défaut
Quand vous voulez modifier DEFAULT_* côté prod, ne pas y aller à l'aveugle :
- Reproduire : enregistrer 5-10 requêtes utilisateur réelles depuis les logs (voir
modules/monitoring.md— les recherches non vides sont loggées parserver.run_search). - Tester localement : pour chaque requête, comparer la sortie avec l'ancien et le nouveau défaut. Inspecter
_semantic_score,_tsvector_score,_relevance_score. - A/B sur staging : déployer le nouveau défaut sur
latest-staging, laisser tourner 24-48h, comparer les logs (n_results_in_searchcôté Prometheus). - Rollback simple : revenir à l'ancienne valeur via une nouvelle release, ou (en urgence) modifier la var d'env du service et redémarrer.
Que faire si la pertinence reste insatisfaisante après réglage ?
- Améliorer le texte embeddé.
get_embedding_text()(src/_utils.py) concatènename + subtitle + description + servicesIncluded + highLights. Si une info pertinente n'est dans aucun de ces champs, elle ne fait pas partie du vecteur. Voirmodules/embeddings.md. - Améliorer la pondération FTS. Les poids A/B/C sur
name/subtitle/descriptionsont définis dans la colonne généréesearch_vector(migrations/0001_baseline.sqllignes 49-53). Changer ça = migration + reindex complet. - Changer de modèle d'embedding. Voir
architecture/adr/0003-bge-multilingual-gemma2.mdpour les options. Migration coûteuse (3584D → autre dim, reindex complet). - Index vectoriel
hnsw. Si le catalogue grossit massivement, un indexhnswsurembeddingaccélère leORDER BY embedding <=> %smais introduit une approximation. À ce jour, pas nécessaire.
Pièges fréquents
- Confondre
rrfVectorWeightetsemanticFloor. Le premier décide qui domine dans le classement. Le second décide qui rentre. AugmenterrrfVectorWeightsans touchersemanticFloorpeut quand même faire passer des résultats faibles. - Mettre
semanticFloor=0en prod. Ça désactive complètement le filtre sémantique. Vous afficherez des résultats avec_semantic_score ≈ 0.1qui n'ont rien à voir. Toujours laisser au moins0.3-0.5. - Croire que
tsvectorFloor=0veut dire « pas de filtre texte ». Ça veut dire « accepter même les matches très faibles ». Mais la branche FTS du SQL ajoute déjàAND search_vector @@ websearch_to_tsquery(...), donc un score 0 strict n'est jamais inclus côté texte — sauf via la sémantique. - Ajuster les seuils sans regarder les scores réels.
?infoDensity=cardexposesemantic_scoreettsvector_scoredans chaque record retourné. Toujours les vérifier avant de bouger un seuil.

