La recherche dans la documentation est le recours des utilisateurs quand la navigation échoue, la mémoire fait défaut ou l’ensemble de docs est simplement volumineux. C’est aussi l’endroit où la crédibilité de votre produit peut mourir quand l’interface saccade, ne renvoie rien ou « recherche » en faisant tourner un loader pendant 900ms comme si elle consultait des feuilles de thé.
Le truc n’est pas une bibliothèque magique. C’est un modèle d’interface : expédier tôt un index de recherche petit et cachable, rechercher d’abord localement, rendre de façon progressive, et seulement ensuite interroger le réseau pour la longue traîne. Bien fait, cela semble instantané parce que c’est instantané pour le cas courant.
Le modèle : « Local-first, network-late »
Vous voulez que la recherche de la doc donne l’impression que le navigateur complète la pensée de l’utilisateur, pas qu’il négocie avec une base distante. Le modèle d’interface qui délivre de manière fiable cette sensation est :
- Précharger un index de recherche compact tôt (ou au moins le chauffer lors de la première interaction).
- Rechercher localement à chaque frappe avec un algorithme rapide et prévisible.
- Rendre les résultats de façon progressive (top N tout de suite ; affiner/étendre ensuite) avec une mise en page stable.
- Utiliser le réseau seulement pour la longue traîne : extraits complets, « vouliez-vous dire », analytics, classement personnalisé, ou résultats inter-sites.
- Mettre en cache agressivement (cache HTTP, Service Worker, IndexedDB) pour que « instantané » le reste lors des visites répétées.
Ce modèle est volontariste sur l’endroit où vous dépensez votre temps. Vous dépensez du calcul une fois (au build) pour créer un index qui rend l’exécution coûteuse peu onéreuse. Vous dépensez la bande passante une fois (mis en cache) pour que les recherches ultérieures soient essentiellement des appels de fonction locaux. Vous évitez de faire des travaux lourds sur le thread principal pendant que l’utilisateur tape.
Pourquoi « local-first » fonctionne spécifiquement pour la doc
Les requêtes dans la doc sont souvent courtes, ambiguës et corrigées en vol (« s3 policy » → « s3 bucket policy deny public »). Les utilisateurs tapent, font une pause, retapent. Si chaque frappe déclenche une requête, votre interface oscille entre « chargement » et « périmé », et votre back-end devient un journal de frappes par défaut.
La recherche locale transforme ce bazar en une boucle déterministe :
- Le contenu de l’entrée change
- La requête locale s’exécute en quelques millisecondes
- L’interface affiche les meilleurs résultats
- Optionnel : raffinage en arrière-plan ou enrichissement réseau
Une chose non négociable : ne prétendez pas être instantané en affichant des résultats faux. Les utilisateurs le sentent. Donnez de vrais résultats locaux rapidement, puis enrichissez-les.
Ce que « instantané » veut dire réellement (budgets de latence, pas des impressions)
« Instantané » est un budget. C’est le délai entre une frappe et des pixels signifiants. En pratique vous jonglez avec :
- Input-to-next-paint (territoire INP) : si vous bloquez le thread principal, le clavier paraît mou.
- Temps jusqu’au premier résultat (TTFR) : quand les premiers résultats plausibles apparaissent.
- Temps jusqu’à des résultats stables : quand la liste cesse de bouger et que l’utilisateur peut cliquer en confiance.
Un objectif réaliste pour la doc sur un ordinateur portable milieu de gamme et un bon téléphone :
- TTFR < 100ms pour un index local mis en cache (chemin rapide)
- TTFR < 300ms pour le chargement initial de l’index avec préchargement (chemin chaud)
- Résultats stables < 500ms même lorsque vous ajoutez des extraits ou un reranking côté serveur (chemin d’enrichissement)
L’interface doit se comporter comme si elle était instantanée : pas de tremblements, pas de clignotements, pas de résultats qui disparaissent. Mais elle doit aussi être honnête : si vous n’avez pas encore chargé l’index, affichez un petit indicateur « recherche en chauffe » et gardez la frappe réactive.
Une citation pour garder la tête froide : L’espoir n’est pas une stratégie.
— James Cameron. Ce n’est pas spécifique à la recherche, mais cela s’applique à la fiabilité et aux performances à chaque fois.
Faits intéressants et un peu d’histoire
Un peu de contexte facilite la défense du modèle lors des revues de conception et des réunions budgétaires.
- Le typeahead prédate la stack frontend moderne du web. Les premiers « incremental search » sont apparus dans les applications desktop il y a des décennies parce que les humains détestent attendre entre une pensée et un retour UI.
- La recherche UX précoce de Google a popularisé la « vitesse comme fonctionnalité ». La leçon centrale n’était pas seulement des serveurs plus rapides ; c’était de supprimer la latence perçue par un retour immédiat.
- Les CDN ont fait des docs statiques une architecture par défaut. Une fois que la doc est devenue statique + cachée, il était naturel d’expédier les index de recherche de la même façon.
- Les Service Workers (mainstream vers 2015) ont rendu le « offline-first » viable, ce qui correspond bien au « local-first search ».
- Les index inversés sont anciens. L’idée de base — terme → liste de postings — existait bien avant votre site de docs, et c’est toujours la colonne vertébrale d’une recherche rapide.
- La compression est une fonctionnalité UX. Des techniques comme Brotli et la compression basée sur dictionnaire ne sont pas seulement des gains de bande passante ; elles réduisent directement le temps jusqu’au premier résultat sur les chargements froids.
- Les CPU mobiles punissent le JS bâclé. Un algorithme de recherche qui semble fluide sur un MacBook peut bloquer sur un Android milieu de gamme, transformant « instantané » en « j’utiliserai Google ».
- La « recherche dans la doc » est devenue incontournable quand les jeux de docs ont explosé. Microservices, SDK et produits cloud ont créé des corpus trop grands pour une découverte uniquement par navigation.
Architecture de référence (la version ennuyeuse qui fonctionne)
Au build : produire un bundle de recherche conçu pour le runtime
Au moment du build, vous avez du temps et du CPU. Servez-vous-en. Créez un artefact de recherche séparé de vos pages HTML :
- Fichier d’index : termes + postings ou structures d’index spécifiques à la librairie.
- Map des documents : id de doc → URL, titre, headings, résumé optionnel.
- Métadonnées de version : hash de build, version de schéma, langue.
Contraintes de conception :
- L’index doit être assez petit pour être préchargé sans remords. S’il est énorme, découpez par section, langue ou version.
- L’index doit être cachable longtemps (noms de fichiers immuables, URLs content-hashées). Cela permet un caching agressif sans risque de pollution du cache.
- L’analyse de l’index doit être rapide et incrémentale. Envisagez un format binaire ou au moins du JSON optimisé pour la performance de parsing.
Au runtime : charger une fois, interroger vite, rendre progressivement
Au runtime, la boucle UX devrait ressembler à ceci :
- Préfetch en temps d’inactivité : après le premier rendu de contenu, préchargez l’index en basse priorité.
- Fallback à la première interaction : si l’utilisateur focalise la recherche avant la fin du préchargement, augmentez la priorité et affichez « recherche en chauffe ».
- Requête locale : exécutez la requête dans un Web Worker quand c’est possible ; sinon, respectez un budget temps strict.
- Rendre les meilleurs hits : affichez titres et fil d’Ariane d’abord (cheap), différer les extraits (coûteux).
- Enrichir : récupérer les extraits ou lancer un reranking de façon asynchrone ; mettre à jour l’UI sans trop remanier la liste.
Flux de données : résultats à deux niveaux
Pensez en paliers :
- Palier 1 (local) : rapide, approximatif, suffisant pour 80% des requêtes.
- Palier 2 (réseau) : plus lent, plus riche, correct pour les cas limites (typos, synonymes, résultats filtrés pour sécurité, personnalisation multi-tenant).
Quand le palier 2 revient, vous fusionnez les résultats avec précaution. Si vous réordonnez tout à chaque réponse réseau, l’UI donne l’impression d’être hantée.
Blague n°1 : La recherche la plus rapide est celle qui n’atteint pas votre backend — votre base de données aimerait aussi arrêter d’entendre parler de chaque faute de frappe.
Détails d’interface qui changent tout
1) Ne bloquez pas la frappe
Si votre handler de recherche fait un travail lourd à chaque pression de touche, l’entrée lagge. Les utilisateurs blâment « la recherche », mais la vraie défaillance est la contention du thread principal.
Utilisez :
- Debounce (p.ex. 50–120ms) pour les opérations coûteuses comme la génération d’extraits.
- Correspondance locale immédiate pour les opérations bon marché comme le préfixe sur les titres.
- Web Worker pour la requête complète si l’index n’est pas trivial.
2) Gardez la mise en page stable
Les résultats qui se réorganisent violemment sont un tueur silencieux de conversion. Si la liste change de hauteur, les utilisateurs se trompent de clic et reprochent ensuite que votre doc est « confuse ». Réparez cela avec :
- Hauteurs de ligne fixes quand possible
- Squelettes seulement quand nécessaire (et jamais en substitution de résultats manquants)
- Réserver l’espace pour les extraits afin qu’ils ne poussent pas tout vers le bas
3) Affichez « aucun résultat » seulement quand vous en êtes sûr
Dans le modèle local-first, « pas de résultats » peut signifier « index pas encore chargé », « index périmé » ou « requête trop courte ». Distinguez :
- Chargement de l’index : afficher « Recherche en chauffe… »
- Requête trop courte : afficher « Tapez 2+ caractères » (ou le seuil choisi)
- Vrai zéro : afficher des conseils (filtres, orthographe) et éventuellement un fallback réseau
4) Priorité au clavier non négociable
Les utilisateurs de docs vivent au clavier. Votre UI de recherche doit supporter :
- Raccourci de focus (p.ex. / ou Cmd+K)
- Navigation aux flèches
- Entrée pour ouvrir
- Échap pour fermer
Et : ne piégez pas le focus comme s’il s’agissait d’une modal hantée. Rendez-le accessible et prévisible.
5) Soyez explicite sur la portée
Les docs ont souvent des versions, produits, langues et permissions. Si le périmètre de recherche est ambigu, les résultats paraissent « faux ». Ajoutez un indicateur de portée : « Recherche : API v2 • Français ». Oui, ça prend de la place. Prenez-la.
Pertinence : votre index est un produit
Une recherche rapide mais incorrecte n’est qu’un moyen rapide de perdre la confiance.
Ce qu’il faut indexer (et ce qu’il faut éviter)
Indexez :
- Titre de la page
- Headings (H2/H3)
- Résumé/description courte (rédigé à la main ou généré au build)
- Jetons de breadcrumb/chemin (produit, section)
- Optionnel : symboles de code (noms de fonctions, flags)
Évitez d’indexer tout le corps de la page pour la recherche locale sauf si vous pouvez le faire efficacement. Les corps en full-text gonflent la taille de l’index, augmentent le coût de parsing et ralentissent les requêtes. Si vous avez besoin de correspondances dans le corps, envisagez une seconde couche : le local trouve des pages candidates, le réseau renvoie des extraits.
Heuristiques de classement qui fonctionnent pour la doc
- Boost par champ : titre > headings > résumé > corps
- Récence : si vos docs changent souvent, les pages plus récentes peuvent obtenir un léger boost (sans enterrer les docs canoniques)
- Boost par section : « référence » vs « blog » vs « guides »
- Correspondance exacte gagnante : une égalité exacte sur le titre doit remonter en tête
- Correspondance préfixe pour les symboles : « kubectl get » devrait se comporter comme une palette de commandes
Fautes de frappe et synonymes : choisissez votre champ de bataille
Tolérance aux fautes est coûteuse. Faites-la localement seulement si vous pouvez la borner (p.ex. distance d’édition limitée, index petit, exécution en worker). Sinon, faites-en une fonctionnalité de palier 2.
Les synonymes sont politiques. « VM » vs « instance » vs « node » dépendent de la taxonomie interne de votre entreprise. Si vous ajoutez des synonymes, faites-le délibérément et mesurez l’impact sur le taux de clic et le taux « retour à la recherche ».
Ingénierie des performances : de la frappe aux pixels
La latence est une propriété de bout en bout
Le composant le plus lent gagne. Coupables typiques :
- Téléchargement de l’index (trop volumineux, mauvais caching)
- Temps de parsing de l’index (JSON énorme + parsing sur le main thread)
- Temps de requête (mauvais algorithme, fuzziness excessive)
- Temps de rendu (listes DOM énormes, reflow, mise en valeur coûteuse)
- Enrichissement réseau (edge lent, latence d’origine)
Rendez le chemin rapide ennuyeux
La sensation « instantanée » vient de la prévisibilité. Une bonne implémentation a un chemin rapide qui est :
- Caché : l’index se charge depuis le cache la plupart du temps
- Basé sur worker : la requête ne bloque pas la frappe
- Coût fixe : top N résultats seulement, travail maximal fixe par frappe
Utilisez un worker ou acceptez votre destin
Si votre index est plus grand qu’un dataset jouet, vous voulez un Web Worker. Ce n’est pas une « optimisation prématurée ». C’est du contrôle du risque. Les blocages du main thread sont difficiles à déboguer et faciles à livrer.
Mise en valeur : la taxe cachée
La mise en valeur des résultats (mettre en gras les sous-chaînes correspondantes) paraît bon marché jusqu’à ce que vous le fassiez pour 30 résultats, chacun avec plusieurs champs, à chaque frappe. Bornez-la :
- Mettre en valeur seulement les lignes visibles
- Mettre en valeur seulement le titre + une ligne d’extrait
- Sauter la mise en valeur pendant que l’utilisateur tape rapidement (un court debounce)
Blague n°2 : La recherche floue est comme le café décaféiné — réconfortante, mais si vous en abusez, rien n’avance.
Observabilité : instrumenter comme un service de production
La recherche dans la doc est une fonctionnalité frontend, mais elle se comporte comme un système distribué : cache, CDN, ordonnancement du navigateur, réseau, origine, et parfois des API de recherche tierces. Traitez-la en conséquence.
Ce qu’il faut mesurer
- Temps de chargement de l’index : téléchargement + parsing + prêt à requêter
- Taux de hit du cache : l’index a-t-il été servi depuis la mémoire/Cache Storage/cache HTTP ?
- TTFR : temps de l’événement d’entrée au premier rendu de résultats
- Durée de la requête : temps d’exécution du worker par requête
- Blocage du thread principal : longues tâches pendant la frappe active
- Taux de clics et taux retour-à-la-recherche (proxy de pertinence)
- Taux de zéro-résultat (et si l’index était chargé)
- Latence d’enrichissement et taux d’erreur
Corréler les métriques client avec les métriques de livraison
Quand la recherche « paraît lente », la cause racine peut être que la requête d’index a été servie depuis un POP distant, ou que les en-têtes de cache sont erronés, ou que le fichier d’index a grossi après une réorganisation des docs. Liez vos mesures frontend à :
- En-têtes d’état de cache CDN
- Taille de l’artefact d’index et ratio de compression par release
- Temps de déploiement et invalidations
Si vous ne pouvez pas répondre à « est-ce que l’utilisateur avait l’index en cache ? », vous vous battrez contre des ombres.
Tâches pratiques avec commandes, sorties et décisions
Voici le genre de vérifications que je fais quand quelqu’un dit « la recherche est lente » et que mon pager commence à me regarder de travers. Chaque tâche inclut : une commande, ce que signifie la sortie, et la décision à en tirer.
Tâche 1 : Confirmer la taille du fichier d’index et la compression sur disque
cr0x@server:~$ ls -lh public/search/index.json public/search/index.json.br
-rw-r--r-- 1 deploy deploy 18M Jan 12 10:14 public/search/index.json
-rw-r--r-- 1 deploy deploy 3.2M Jan 12 10:14 public/search/index.json.br
Signe : Le JSON brut fait 18Mo ; Brotli le réduit à 3,2Mo. C’est préchargeable sur du haut débit, discutable sur mobile si vous le faites trop tôt.
Décision : Si le compressé > ~2–4Mo, envisager de scinder l’index (par section/version) ou passer à un format binaire ; aussi vérifier que Brotli est servi.
Tâche 2 : Vérifier que le serveur sert bien Brotli
cr0x@server:~$ curl -I -H 'Accept-Encoding: br' https://docs.example.test/search/index.json
HTTP/2 200
content-type: application/json
content-encoding: br
cache-control: public, max-age=31536000, immutable
etag: "b3f9a2c4"
Signe : Le serveur respecte Brotli et utilise un cache immutable. Bien : le navigateur peut mettre en cache indéfiniment et réutiliser.
Décision : Si content-encoding manque, corriger les réglages de compression CDN/origin. Si le caching est court, utiliser des noms de fichiers content-hashés et définir immutable.
Tâche 3 : Vérifier le statut du cache CDN (hit vs miss)
cr0x@server:~$ curl -I https://docs.example.test/search/index.json | grep -i -E 'cache|age|cf-cache-status|x-cache'
cache-control: public, max-age=31536000, immutable
age: 86400
x-cache: HIT
Signe : L’index est mis en cache à la périphérie et est servi depuis un jour.
Décision : Si vous voyez souvent des MISS, investiguez les clés de cache, les params de requête ou les invalidations fréquentes. Les misses au edge rendent « instantané » en « éventuellement ».
Tâche 4 : Confirmer des noms de fichiers immuables (artefacts content-hashés)
cr0x@server:~$ ls public/search | head
index.7a9c2f1a.json.br
docs.7a9c2f1a.map.json.br
meta.7a9c2f1a.json
Signe : Les noms de fichiers incluent un hash ; vous pouvez les cacher à long terme sans craindre les mises à jour.
Décision : Si vous servez encore index.json avec un contenu mutable, passez à des noms hashés et mettez à jour le loader pour récupérer le hash courant via un petit fichier meta.
Tâche 5 : Inspecter les en-têtes de cache du fichier meta (ils doivent être à courte durée)
cr0x@server:~$ curl -I https://docs.example.test/search/meta.json | grep -i cache-control
cache-control: public, max-age=300
Signe : Le fichier meta peut se mettre à jour rapidement (nouvelle release) tandis que les artefacts hashés restent immuables.
Décision : Si le meta est mis en cache pour un an, les clients n’apprendront pas les nouveaux hashes ; s’il n’est pas mis en cache, vous ajoutez une latence inutile à chaque démarrage de session.
Tâche 6 : Mesurer le temps de transfert et la taille depuis un hôte typique
cr0x@server:~$ curl -o /dev/null -s -w 'size=%{size_download} time=%{time_total} speed=%{speed_download}\n' https://docs.example.test/search/index.7a9c2f1a.json.br
size=3355443 time=0.142 speed=23629670
Signe : ~3,2Mo téléchargés en 142ms depuis ce point de vue. Pas une garantie pour le mobile.
Décision : Si le temps est élevé, envisager un index plus petit ou précharger plus tôt uniquement sur de bonnes connexions (Network Information API, avec prudence).
Tâche 7 : Confirmer que le JSON de l’index se parse dans le budget (Node comme proxy)
cr0x@server:~$ node -e 'const fs=require("fs"); const t=Date.now(); JSON.parse(fs.readFileSync("public/search/index.json","utf8")); console.log("ms="+(Date.now()-t));'
ms=287
Signe : Le parsing prend ~287ms sur cette machine. Sur mobile, ça peut être pire.
Décision : Si le temps de parsing > ~100ms sur du hardware de dev, déplacer le parsing hors du thread principal (worker), ou changer de format pour un parsing plus rapide.
Tâche 8 : Vérifier que le bundle du worker est bien séparé et cachable
cr0x@server:~$ ls -lh public/assets/search-worker.*.js
-rw-r--r-- 1 deploy deploy 54K Jan 12 10:14 public/assets/search-worker.a19c7c0d.js
Signe : Le script du worker est petit et peut être mis en cache. Bon pour les recherches répétées.
Décision : Si le worker est bundlé dans le JS principal, envisager du code-splitting pour que le chargement initial de la page ne paie pas le prix de la recherche tant que nécessaire.
Tâche 9 : Identifier les longues tâches pendant l’interaction de recherche (trace Chrome exportée, analysée localement)
cr0x@server:~$ node -e 'const fs=require("fs"); const t=JSON.parse(fs.readFileSync("trace.json","utf8")); const long=t.traceEvents.filter(e=>e.name==="Task" && e.dur>50000).length; console.log("long_tasks_over_50ms="+long);'
long_tasks_over_50ms=7
Signe : Il y a 7 longues tâches de plus de 50ms, cause probable de lag à la saisie.
Décision : Déplacer la logique de requête et de mise en valeur dans un worker ; réduire le travail DOM ; virtualiser la liste de résultats si elle est grande.
Tâche 10 : Vérifier que vos logs serveur montrent que les requêtes d’index n’écrasent pas l’origine
cr0x@server:~$ sudo awk '$7 ~ /search\/index\./ {c++} END{print "index_requests=" c}' /var/log/nginx/access.log
index_requests=43
Signe : Seulement 43 requêtes d’index dans ce log d’origine (peut-être parce que le CDN fait son travail).
Décision : Si l’origine voit un flot de requêtes d’index, votre caching CDN est cassé ou vous changez trop fréquemment le nom de l’index.
Tâche 11 : Valider le comportement ETag (304 doit se produire pour le meta ; les assets hashés doivent être des hits)
cr0x@server:~$ curl -I https://docs.example.test/search/meta.json | awk -F': ' 'tolower($1)=="etag"{print $2}'
"9c3a0f11"
Signe : Le meta a un ETag. Les clients peuvent révalider à faible coût.
Décision : Si les ETags manquent, activez-les à l’origine. Pour le meta, cela réduit les octets tout en conservant la fraîcheur.
Tâche 12 : Vérifier que l’index n’est pas accidentellement non compressé en transit à cause d’un mauvais config
cr0x@server:~$ curl -I https://docs.example.test/search/index.7a9c2f1a.json.br | grep -i -E 'content-encoding|content-length'
content-encoding: br
content-length: 3355443
Signe : La charge compressée est servie en Brotli, et la taille est plausible.
Décision : Si vous voyez un content-length énorme sans encodage, vous payez le coût non compressé et perdez probablement « instantané » sur les chargements froids.
Tâche 13 : Détecter des query params qui cassent le cache accidentellement
cr0x@server:~$ sudo grep -R "index.*\?v=" -n public | head
public/assets/app.js:412:fetch("/search/index.json?v="+Date.now())
Signe : Quelqu’un ajoute Date.now() à l’URL de l’index, garantissant des misses de cache.
Décision : Enlevez-le. Utilisez des noms hashés ou la révalidation ETag. Le cache-busting n’est pas un trait de personnalité.
Tâche 14 : Confirmer que le Service Worker met en cache les artefacts de recherche (si vous en utilisez un)
cr0x@server:~$ rg -n 'search/index|CacheStorage|workbox' public/sw.js
42: const SEARCH_ASSETS = ["/search/meta.json", "/search/index.7a9c2f1a.json.br", "/search/docs.7a9c2f1a.map.json.br"];
58: event.waitUntil(caches.open("docs-search-v1").then(c => c.addAll(SEARCH_ASSETS)));
Signe : Les assets de recherche sont explicitement mis en cache, ce qui stabilise la performance répétée.
Décision : Si le SW ne les met pas en cache, ajoutez une stratégie de cache ; s’il les met en cache sans versioning, vous risquez de servir des index périmés après un deploy.
Playbook de diagnostic rapide
Quand la recherche cesse de paraître instantanée, ne brainstormez pas. Triez. Voici le chemin le plus court vers le goulot d’étranglement.
Première étape : est-ce le téléchargement, le parsing, la requête ou le rendu ?
- Vérifiez si l’index est caché (DevTools Application → Cache Storage/cache HTTP ; ou regardez les en-têtes HIT du CDN). S’il n’est pas caché, votre histoire « instantanée » est finie avant de commencer.
- Mesurez le temps prêt de l’index : temps du focus à « index chargé + parsé ». Si c’est élevé, c’est download/parse.
- Mesurez le temps de requête isolément : lancez la même requête 10 fois ; si ça s’améliore drastiquement, le premier hit est parse/initialisation.
- Vérifiez les longues tâches pendant la frappe : si la saisie lagge, vous bloquez le thread principal (rendu/mise en valeur/requête sur le main).
Deuxième étape : validez la sémantique du cache (le méchant habituel)
- Meta court TTL, artefacts hashés immuables
- Pas de query params cache-bust
- Content-Encoding et Content-Type corrects
- Le CDN met réellement l’index en cache (non contourné par cookies ou en-têtes)
Troisième étape : inspecter la croissance de l’index et les changements de schéma
- Taille d’index en hausse ? Probablement un changement de build qui indexe des corps complets ou des champs dupliqués.
- Schéma changé sans incrémenter la version ? Un vieux cache d’index casse le parsing et force des comportements de fallback.
- Nouvelle langue/version ajoutée ? Vous pourriez précharger trop pour tout le monde.
Quatrième étape : confirmer que l’enrichissement ne sabote pas l’UI
- Les requêtes d’enrichissement ne doivent pas bloquer les résultats locaux.
- L’enrichissement ne doit pas réordonner la liste agressivement ; mettez à jour les extraits en place.
- Mettre un timeout rapide sur l’enrichissement ; ne prenez pas l’UI en otage pour du « sympa à avoir ».
Erreurs courantes (symptômes → cause racine → correctif)
1) Symptom : La recherche est rapide en Wi‑Fi, terrible sur mobile
Cause racine : Vous préchargez un index de plusieurs mégaoctets au chargement de la page, et le mobile paye le coût cold-download + parse avant même que l’utilisateur recherche.
Correctif : Précharger en idle avec basse priorité ; filtrer selon la qualité de connexion ; scinder l’index par section ; cacher via SW. Garder le meta petit et actualisable.
2) Symptom : La frappe lagge, les caractères apparaissent en retard
Cause racine : La requête et/ou la mise en valeur s’exécutent sur le main thread ; les mises à jour DOM sont lourdes ; la liste de résultats est rerendue entièrement à chaque frappe.
Correctif : Déplacer la recherche dans un worker ; limiter les résultats ; virtualiser la liste ; debouncer la mise en valeur ; utiliser un rendu keyé et éviter les thrashs de layout.
3) Symptom : Les utilisateurs rapportent « pas de résultats » pour des termes évidents
Cause racine : Le build de l’index manque de headings/titres, ou vous avez mal scoped la recherche (mauvaise version/langue), ou l’index est périmé en cache.
Correctif : Valider la pipeline de build ; ajouter l’UI de portée ; versionner votre schéma d’index et invalider correctement ; ajouter de la télémétrie « mismatch de version d’index ».
4) Symptom : Les résultats bougent pendant la frappe, provoquant des clics erronés
Cause racine : Le classement est instable, et l’enrichissement réordonne les résultats quand les extraits arrivent ; aussi l’UI peut ne pas préserver l’état de sélection.
Correctif : Garder le classement local stable ; ne réordonner qu’après une action explicite (Entrée) ou après une pause ; fusionner l’enrichissement sans reshuffle.
5) Symptom : Les coûts backend grimpent après le déploiement de la « recherche instantanée »
Cause racine : Vous appelez toujours le serveur à chaque frappe pour analytics ou enrichissement, ou votre debounce est trop petit, ou vous manquez le caching côté serveur.
Correctif : Grouper les analytics ; envoyer des événements sur sélection, pas à la frappe ; cacher les réponses d’enrichissement ; ajouter des rate limits ; utiliser local-first pour le cas commun.
6) Symptom : La recherche marche en dev mais casse de façon intermittente en prod
Cause racine : Caching mixte du meta/index entre deploys : clients récupèrent un meta nouveau mais un index ancien (ou l’inverse), provoquant un mismatch de schéma.
Correctif : Faire du meta l’autorité pour un ensemble complet d’URLs d’artefacts ; assurer un déploiement atomique ; inclure la version du schéma dans le meta et dans la télémétrie.
7) Symptom : Bugs d’accessibilité (lecteurs d’écran perdus, focus piégé)
Cause racine : Comportement custom de listbox/dialog sans rôles ARIA corrects ; gestion du focus bricolée avec de l’espoir.
Correctif : Utiliser des modèles ARIA établis pour combobox/listbox ; préserver le focus ; s’assurer que Échap ferme et renvoie le focus ; tester en flux clavier uniquement.
Trois mini-récits d’entreprise du terrain
Récit 1 : L’incident causé par une mauvaise hypothèse
L’équipe docs a livré une super overlay de recherche. En staging, c’était fluide. « Instantané », disaient-ils, et l’orga produit hochait la tête comme si elle comprenait. L’implémentation utilisait un endpoint serveur pour retourner les résultats, parce que « nous avons déjà Elasticsearch ». L’UI débounçait les requêtes à 100ms et ne cachait rien côté client.
La mauvaise hypothèse était simple : « Le trafic de recherche docs est faible. » C’était vrai tant que la navigation marchait et que les utilisateurs étaient patients. Ça a cessé d’être vrai quand une release a introduit un changement CLI cassant et que tout le monde a essayé de trouver le nouveau nom de flag en même temps.
Pendant cette semaine de release, le backend de recherche s’est retrouvé bombardé de requêtes par frappe : courtes, répétitives et non cachées. La latence a monté. L’UI a commencé à afficher des spinners. Les utilisateurs tapaient plus. Le backend a reçu encore plus de charge. Le système a trouvé un nouvel équilibre : la misère.
Le SRE a été appelé parce que le cluster semblait en incident : CPU haut, files d’attente qui grossissent, timeouts. Mais le « fix » n’était pas d’ajouter des nœuds. C’était de déplacer le cas commun en local-first. Ils ont expédié un index compact pour titres/headings, interrogé localement, et réduit les appels serveur à l’enrichissement sur sélection. La charge backend a baissé, non parce que le cluster a grossi, mais parce qu’on l’a ignoré la plupart du temps.
Récit 2 : L’optimisation qui a mal tourné
Une autre entreprise a voulu rendre la recherche « plus intelligente ». Ils ont activé une tolérance floue agressive et indexé le corps entier localement. L’index a explosé. Ça se téléchargeait encore bien sur le Wi‑Fi corporate, donc l’équipe a appelé ça une victoire.
Puis le support a commencé à voir un motif : des utilisateurs mobiles se plaignant que la recherche « bloquait ». L’UI ne bloquait pas ; elle parseait un JSON énorme et faisait un scoring flou sur le main thread. Sur des appareils bas de gamme, le lag clavier rendait la page entière inutilisable.
L’équipe a essayé de réparer en augmentant le debounce. Cela a réduit le nombre de requêtes, mais a aussi rendu l’UI lente et imprévisible : les résultats arrivaient par rafales, déconnectés de la frappe. Les utilisateurs ont perdu confiance et ont commencé à utiliser des moteurs externes, ce qui les conduisait sur des pages obsolètes et ouvrait plus de tickets support. Un cercle parfait de douleur auto-infligée.
La correction finale a été ennuyeuse : réduire l’index local aux champs importants (titre/headings/résumé), déplacer les requêtes dans un worker, et pousser le fuzzy au palier 2 (réseau) derrière un timeout court. Ils ont gardé le comportement « intelligent », mais seulement là où il ne sabote pas la réactivité de la saisie.
Récit 3 : La pratique ennuyeuse mais correcte qui a sauvé la situation
Un site de docs plateforme avait plusieurs versions et langues. L’équipe était disciplinée : chaque artefact de recherche avait une version de schéma, chaque déploiement était atomique, et le fichier meta était le seul point mutable. Ils enregistraient aussi de la télémétrie client : version d’index, temps de chargement, statut du cache et si le worker était utilisé.
Un vendredi, une release a introduit un changement subtil de schéma d’index : un champ renommé dans la map des docs. Dans beaucoup d’organisations, c’est un incident en attente — clients périmés récupérant des artefacts non appairés, plantant le worker et tombant en « pas de résultats ».
Cette fois, la télémétrie l’a détecté en quelques minutes : pic d’événements « mismatch schéma d’index », groupés par un vieux cache Service Worker. Parce que les artefacts étaient versionnés, l’équipe a pu avancer en toute sécurité : ils ont incrémenté la version de schéma, mis à jour le nom du cache SW et déployé. Les clients ont naturellement rafraîchi le meta, vu les nouvelles URLs d’artefacts et obtenu un cache propre.
Pas de rollback d’urgence. Pas de feu de paille le week-end. Juste une correction silencieuse et une petite note postmortem : « Versionnez vos artefacts. Ce n’est pas glamour. C’est ce qui vous fait dormir. »
Listes de contrôle / plan étape par étape
Étape par étape : implémenter le modèle sans drame
- Définir votre périmètre chemin rapide : titres + headings + résumé ; limiter les résultats à 10–20.
- Construire un index compact : générer au build ; produire des artefacts hashés ; générer un petit fichier meta pointant vers le hash courant.
- Servir l’index correctement : Brotli activé, content-type correct, cache immutable pour les fichiers hashés, TTL court pour le meta.
- Précharger intelligemment : préfetch en idle ; prioriser quand l’utilisateur focalise l’input de recherche.
- Déplacer la requête dans un worker : garder le main thread pour la saisie + le rendu ; appliquer des budgets temps.
- Rendre progressivement : afficher titres/breadcrumbs d’abord ; charger les extraits de manière asynchrone.
- Stabiliser le classement : tri déterministe ; éviter les jitter ; gérer l’enrichissement sans reshuffle.
- Instrumenter les métriques : TTFR, temps prêt index, taux de hit cache, durée requête, longues tâches, zéro résultats.
- Ajouter des garde-fous : timeouts, fallbacks et un état « recherche en chauffe » qui ne bloque pas la frappe.
- Tester sur appareils lents : simuler throttling CPU ; valider que la frappe reste réactive.
Checklist de release : éviter les incidents auto-infligés
- Version du schéma d’index incrémentée quand les champs changent
- Cache-control du meta réglé en minutes, pas en jours
- Artefacts hashés réglés immuables avec long max-age
- Pas de cache-busting via query-param
- Chemin du worker testé en build de production
- Dashboards de télémétrie mis à jour pour les nouveaux champs
- Vérification de régression de taille d’index (échouer le build si ça augmente de façon inattendue)
Checklist UX : le rendre instantané sans mentir
- La frappe ne lagge jamais
- Les résultats apparaissent dans un budget prévisible sur le chemin caché
- Liste stable ; pas de gros reflows
- Navigation au clavier fonctionnelle
- Indicateur de portée clair (version/langue/produit)
- L’état zéro-résultat est honnête et actionnable
FAQ
1) Dois-je utiliser une recherche côté client ou un service hébergé ?
Utilisez le côté client pour le chemin rapide (titres/headings). Ajoutez un service hébergé pour l’enrichissement, la tolérance aux fautes et la recherche cross-property. Hybride bat la pureté.
2) Quelle taille peut atteindre l’index avant que le modèle cesse de fonctionner ?
Il n’y a pas de chiffre universel, mais dès que le téléchargement + parsing de l’index compressé dépasse votre budget TTFR, les utilisateurs le ressentent. Scindez par section/version, ou passez à un format binaire/streamable.
3) JSON est-il toujours une mauvaise idée pour l’index ?
Pas toujours. Un petit JSON avec Brotli peut convenir. Ça devient problématique quand le parsing bloque le main thread ou quand la structure est profondément imbriquée et énorme.
4) Pourquoi ne pas interroger simplement le serveur à chaque frappe avec debounce ?
Parce que le debounce ne règle pas la latence tail, le comportement offline ni l’amplification de charge backend lors de pics de release. Local-first rend la performance prévisible.
5) Ai-je vraiment besoin d’un Web Worker ?
Si vous voulez l’« instantané » sur téléphones et que vous avez plus qu’un petit index, oui. Les workers sont une assurance bon marché contre les blocages du main thread.
6) Comment empêcher les jitter quand l’enrichissement arrive ?
Rendez les résultats locaux avec un ordre stable. Quand l’enrichissement revient, mettez à jour les métadonnées/extraits en place. Si vous devez réordonner, ne le faites qu’après une pause ou sur soumission explicite.
7) Quelles métriques mettre sur un dashboard ?
Temps prêt index (p50/p95), TTFR (p50/p95), durée de requête, taux de hit cache, taux zéro-résultat, et nombre de longues tâches pendant les interactions de recherche.
8) Comment gérer plusieurs versions de docs sans tout charger ?
Rendez la portée explicite et chargez des index par périmètre. Utilisez un petit registre meta et récupérez le bon shard pour la version/langue sélectionnée.
9) Et les pages filtrées pour sécurité ou internes ?
Ne pas expédier de contenu restreint dans un index public. Pour les environnements authentifiés, gardez l’index local aux métadonnées publiques et appuyez-vous sur l’application serveur pour les résultats restreints.
10) Puis-je le rendre utilisable hors ligne ?
Oui. Mettez en cache l’index et le worker avec un Service Worker. La recherche hors ligne marche bien pour titres/headings ; l’enrichissement des extraits peut être sauté ou servi depuis des pages mises en cache.
Conclusion : étapes pratiques suivantes
Si la recherche de votre doc ne paraît pas instantanée, ne commencez pas par changer de librairie. Commencez par corriger la forme du système : résultats local-first avec un index compact et cachable, interrogé hors du thread principal, rendu progressivement avec une UI stable.
Étapes suivantes réalisables cette semaine :
- Mesurer TTFR et temps prêt de l’index sur un profil de téléphone milieu de gamme.
- Rendre vos artefacts de recherche content-hashés et immuables ; rendre le meta à courte durée.
- Déplacer l’exécution des requêtes dans un worker et limiter le travail par frappe.
- Scinder l’index si la taille compressée augmente.
- Ajouter un dashboard de diagnostic rapide : taux de hit cache, TTFR p95, longues tâches et raisons des zéro-résultats.
L’objectif n’est pas une démo brillante. C’est une boîte de recherche qui se comporte comme une infrastructure : discrète, rapide et fiable — surtout quand vos utilisateurs sont déjà frustrés.