# language: fr Fonctionnalité: Recherche de contenu En tant qu'utilisateur de RoadWave Je veux rechercher des contenus audio par mots-clés, localisation et filtres Afin de trouver facilement le contenu qui m'intéresse Contexte: Étant donné que l'application RoadWave est démarrée Et que l'utilisateur "jean@example.com" est connecté # 15.3.1 - Recherche par mot-clé Scénario: Recherche full-text basique Étant donné que la base contient les contenus suivants: | titre | description | créateur | | Balade à Paris | Visite du quartier Latin | @paris_stories | | Secrets de Montmartre | Histoire de la butte | @explore_paris | | Voyage en Normandie | Découverte des plages | @voyages_fr | Quand l'utilisateur recherche "paris" Alors 2 résultats sont retournés Et les résultats incluent "Balade à Paris" Et les résultats incluent "Secrets de Montmartre" Scénario: Recherche avec stemming français Étant donné un contenu avec le titre "Voyage en Bretagne" Quand l'utilisateur recherche "voyages" Alors le contenu "Voyage en Bretagne" est trouvé Et le stemming a transformé "voyages" en racine "voyag" Plan du Scénario: Stemming français sur différentes formes Étant donné un contenu avec le mot "" Quand l'utilisateur recherche "" Alors le contenu est trouvé grâce au stemming français Exemples: | mot_original | recherche | | voyage | voyages | | voyager | voyage | | balades | balade | | historique | histoire | Scénario: Recherche avec accents ignorés Étant donné un contenu avec le titre "Découverte de l'Élysée" Quand l'utilisateur recherche "decouverte elysee" Alors le contenu est trouvé Et les accents sont normalisés automatiquement Scénario: Champs indexés avec pondération Étant donné les contenus suivants: | titre | description | créateur | tags | | Voyage Paris | Balade sympa | @user1 | Tourisme | | Balade Lyon | Voyage en ville | @paris_guide | Voyage | Quand l'utilisateur recherche "paris" Alors "Voyage Paris" est en première position Parce que le titre a un poids × 3 Et "@paris_guide" apparaît en second Parce que le créateur a un poids × 2 Scénario: Ranking par pertinence et popularité Étant donné les contenus suivants: | titre | écoutes | rang_texte | | Balade Paris | 50000 | 0.8 | | Paris la nuit | 1000 | 0.9 | Quand l'utilisateur recherche "paris" Alors le score final combine rang_texte × (1 + log(écoutes + 1)) Et "Balade Paris" est mieux classé grâce à sa popularité Scénario: Autocomplete pendant la frappe Étant donné que l'utilisateur commence à taper "par" Quand 3 caractères sont saisis Alors des suggestions apparaissent: | suggestion | | paris | | parc naturel | | parvis notre-dame | Et le top 5 des suggestions est affiché Scénario: Historique des 10 dernières recherches Étant donné que l'utilisateur a effectué les recherches suivantes: | recherche | date | | voyage paris | 2026-01-20 | | audio-guide louvre | 2026-01-19 | | podcast automobile | 2026-01-18 | Quand l'utilisateur ouvre la barre de recherche Alors les 10 dernières recherches sont affichées Et elles sont triées par date décroissante Scénario: Correction automatique si aucun résultat Étant donné que l'utilisateur recherche "ballade paris" (faute d'orthographe) Et qu'aucun résultat n'est trouvé Quand la page de résultats s'affiche Alors une suggestion "Essayez plutôt : balade paris" est affichée Scénario: Recherches populaires suggérées Étant donné qu'aucun résultat n'est trouvé pour une recherche Quand la page s'affiche Alors des suggestions populaires sont affichées: | suggestion | | balade paris | | audio-guide louvre | | visite montmartre | # 15.3.2 - Recherche géographique Scénario: Saisie d'un lieu avec autocomplete Étant donné que l'utilisateur ouvre le filtre "Lieu" Quand il tape "Louv" Alors Nominatim retourne des suggestions: | suggestion | type | | Musée du Louvre, Paris | monument | | Louvres, Val-d'Oise | commune | Scénario: Sélection d'un lieu et définition du rayon Étant donné que l'utilisateur sélectionne "Paris, France" Et que les coordonnées sont (48.8566, 2.3522) Quand il définit un rayon de 50 km Alors la recherche PostGIS utilise ST_DWithin avec 50000 mètres Plan du Scénario: Recherche géographique avec différents rayons Étant donné un contenu à 30 km de Paris Quand l'utilisateur recherche autour de Paris avec un rayon de Alors le contenu est Exemples: | rayon | résultat | | 20 km | non trouvé | | 50 km | trouvé | | 100 km | trouvé | Scénario: Utilisation de "Autour de moi" (GPS actuel) Étant donné que l'utilisateur active le GPS Et que sa position est (48.8566, 2.3522) Quand il sélectionne "Autour de moi" Alors la recherche utilise ses coordonnées GPS actuelles Et un rayon par défaut de 10 km est appliqué Scénario: Curseur de rayon avec limites Étant donné que l'utilisateur ouvre le curseur de rayon Quand il ajuste le curseur Alors les valeurs disponibles vont de 5 km à 500 km Et la valeur s'affiche en temps réel "50 km" Scénario: Affichage de la distance dans les résultats Étant donné une recherche géographique autour de Paris Et un contenu à 2.3 km de distance Quand les résultats sont affichés Alors la distance "À 2.3 km" est indiquée pour chaque résultat Plan du Scénario: Tri par proximité géographique Étant donné des contenus à différentes distances de Paris: | contenu | distance | | Louvre Guide | 0.5 km | | Tour Eiffel | 2.0 km | | Versailles | 20 km | Quand l'utilisateur trie par "Proximité" Alors les résultats sont affichés dans l'ordre: | position | contenu | | 1 | Louvre Guide | | 2 | Tour Eiffel | | 3 | Versailles | Scénario: Géocodage avec Nominatim (MVP) Étant donné que l'application est en phase MVP Quand une requête de géocodage est effectuée Alors l'API publique Nominatim est utilisée Et le rate limit de 1 req/s est respecté Scénario: Géocodage avec fallback Mapbox Étant donné que Nominatim ne retourne aucun résultat Quand l'application tente un fallback Alors l'API Mapbox Geocoding est utilisée Et le coût de 0.50€ / 1000 requêtes est appliqué # 15.3.3 - Filtres avancés Scénario: Ouverture du panneau de filtres Étant donné que l'utilisateur est sur la page de recherche Quand il clique sur "Filtres" Alors un panneau latéral s'ouvre Et 7 catégories de filtres sont affichées: | catégorie | | Type de contenu | | Durée | | Classification âge | | Géo-pertinence | | Tags | | Date de publication | | Abonnement | Scénario: Filtre par type de contenu (multi-sélection) Étant donné que l'utilisateur ouvre les filtres Quand il sélectionne: | type | | Contenu court | | Audio-guide | Alors seuls ces types de contenus sont recherchés Et les podcasts et radios live sont exclus Plan du Scénario: Filtre par durée Étant donné un contenu de minutes Quand l'utilisateur filtre par "" Alors le contenu est Exemples: | durée | tranche | résultat | | 3 | <5 min | trouvé | | 3 | 5-15 min | non trouvé | | 10 | 5-15 min | trouvé | | 20 | 15-30 min | trouvé | | 45 | >30 min | trouvé | Scénario: Filtre par classification âge Étant donné des contenus avec différentes classifications: | contenu | classification | | Conte enfants | Tout public | | Podcast news | 13+ | | Débat politique | 16+ | Quand l'utilisateur filtre "Tout public" Alors seul "Conte enfants" est affiché Scénario: Filtre par géo-pertinence Étant donné des contenus avec différents types géo: | contenu | type_geo | | Guide Louvre | Ancré | | Podcast Paris | Contextuel | | News nationales | Neutre | Quand l'utilisateur filtre "Ancré, Contextuel" Alors "Guide Louvre" et "Podcast Paris" sont affichés Et "News nationales" est exclu Scénario: Filtre par tags (multi-sélection) Étant donné des contenus taggés: | contenu | tags | | Voyage en Italie | Voyage, Gastronomie | | Histoire de Rome | Voyage, Histoire | | Économie italienne | Économie | Quand l'utilisateur sélectionne les tags "Voyage, Histoire" Alors "Histoire de Rome" est en priorité (2 tags correspondants) Et "Voyage en Italie" est affiché (1 tag correspondant) Et "Économie italienne" est exclu Plan du Scénario: Filtre par date de publication Étant donné un contenu publié il y a Quand l'utilisateur filtre par "" Alors le contenu est Exemples: | délai | période | résultat | | 12 heures | Dernières 24h | trouvé | | 3 jours | Cette semaine | trouvé | | 15 jours | Ce mois | trouvé | | 8 mois | Cette année | trouvé | | 2 ans | Toutes dates | trouvé | | 2 ans | Cette année | non trouvé | Scénario: Filtre par type d'abonnement Étant donné des contenus gratuits et Premium: | contenu | type | | Balade Paris | Gratuit | | Visite VIP Louvre | Premium | Quand l'utilisateur filtre "Premium uniquement 👑" Alors seul "Visite VIP Louvre" est affiché Scénario: Combinaison de filtres multiples (AND logic) Étant donné que l'utilisateur applique les filtres: | filtre | valeur | | Type | Audio-guide | | Durée | 5-15 min | | Tags | Voyage | | Classification | Tout public | Quand la recherche est lancée Alors seuls les contenus respectant TOUS les critères sont affichés Scénario: Réinitialisation des filtres Étant donné que l'utilisateur a appliqué 5 filtres différents Quand il clique sur "Réinitialiser" Alors tous les filtres sont désactivés Et la recherche affiche tous les résultats Scénario: Sauvegarde d'une recherche Étant donné que l'utilisateur a appliqué plusieurs filtres Quand il clique sur "💾 Sauvegarder cette recherche" Et qu'il entre le nom "Podcasts voyage Paris" Alors la recherche est sauvegardée Et elle apparaît dans l'onglet "Recherches sauvegardées" Scénario: Limite de 5 recherches sauvegardées Étant donné que l'utilisateur a déjà 5 recherches sauvegardées Quand il tente de sauvegarder une 6ème recherche Alors un message d'erreur s'affiche Et il doit supprimer une recherche existante avant d'en ajouter une nouvelle Scénario: Notifications pour recherches sauvegardées Étant donné une recherche sauvegardée "Podcasts voyage Paris" Et que l'utilisateur a activé les notifications Quand 3 nouveaux contenus correspondants sont publiés Alors une notification "3 nouveaux contenus dans 'Podcasts voyage Paris'" est envoyée Plan du Scénario: Options de tri des résultats Étant donné une recherche avec plusieurs résultats Quand l'utilisateur sélectionne le tri "