Livrable 15
Recherche
Objectif
Intention → résultats réels, rapidement, dans une ville.
Exemples à supporter comme parseurs, pas comme promesses de résultats Orléans : « bar techno », « manger après minuit », « concert gratuit », « terrasse romantique », « quelque chose pour ce soir », « rue de bourgogne », « burger ouvert maintenant ».
MVP
PostgreSQL :
tsvectorconfigfrenchsi le dictionnaire est présent ;pg_trgmsur les noms si l'extension charge ;- normalisation accents côté application (NFD + strip) stockée dans
name_fold, pour ne pas dépendre deunaccent.
Boot : détecter les extensions. Si absentes (PGLite probable) : name_fold ILIKE + filtres structurés. Même API. Moins tolérant. Honnête.
Pas d'OpenSearch / Elasticsearch / Algolia au MVP. Réouverture seulement avec une mesure (latence ou pertinence) — aucune mesure n'existe encore.
Synonymes
Table search_synonyms (city_id nullable, term, expands_to[]). Globaux + override ville. Seed minimal linguistique (techno/électronique, burger/hamburger) comme dictionnaire, pas comme contenu local. Le city manager peut ajouter « Bourgogne » → zone rue-de-bourgogne une fois la zone créée.
Intent
Structure cible, remplie seulement pour les slots reconnus :
{
"what": ["bar"],
"music": "techno",
"when": "tonight",
"open_now": false,
"price": "free",
"zone_slug": null,
"attributes": ["terrace"],
"party_size": null,
"budget_per_person": null,
"sequence": []
}MVP : règles (listes de mots + heures), pas un LLM obligatoire. Le LLM, si XAI_API_KEY et appel déclenché par l'utilisateur, peut proposer cette structure. Elle est jetée si elle cite un lieu qui n'est pas un id renvoyé par la base. THIRD-PARTY DEPENDENCY + plafond de tokens. Sinon : règles seules.
Ranking organique
Fonction documentée, poids en table ranking_weights :
text + distance + open_status + freshness + event_match + quality
qualityMVP = complétude (horaire présent, source non UNKNOWN, pas de signalement ouvert). Pas un avis.- Le paiement n'est pas une feature.
- Sponsorisé : requête séparée, slots identifiés, plafond, jamais réinjectés dans le score organique.
Distance : si pas de point utilisateur, le terme distance est omis (pas de faux centre).
Search gaps
À chaque requête 0 résultat : upsert search_gaps sur (city_id, query_normalized, day) compteur +1. Pas d'IP, pas d'user id. Le city manager voit le compte. Exemple d'usage produit : « karaoké » sans fiche — l'exemple n'est pas une statistique.
Décisions
D1 — FTS d'abord
- Alternatives : SaaS search (moins d'ops, lock-in, coût, donnée qui sort de l'UE —
LEGALsi hors UE) ; LLM qui répond en prose (interdit comme source de vérité). - Compromis : pertinence linguistique limitée.
- Multi-ville : filtre
city_idobligatoire dans chaque requête. Test d'isolation.
D2 — Pages de recherche non indexées
Voir SEO. L'URL canonique d'une intention stable et non vide peut être une page catégorie (/bars), pas /recherche?q=.
Flags
- Dictionnaire french de PostgreSQL sur Neon : à vérifier,
EXTERNAL DATA REQUIRED(capacité d'extension). - Coût d'un SaaS search : non chiffré.