Freedam
IngénieriePartie 5 sur 7 · 31 min de lectureMis à jour le

Mesurer une recherche que personne ne croyait défaillante

Une recherche peut perdre une grande part de ses résultats alors que les pages se chargent, que les tests restent verts et que les premiers résultats paraissent plausibles. Les utilisateurs ne voient pas ce qui a été omis : les signaux produit habituels sont donc de faibles indices du rappel (recall).

Cet article est le quatrième d’une série sur le fonctionnement de la recherche dans freedam. Le premier traitait de la récupération : fabriquer un document interrogeable, fusionner BM25 et pgvector, garantir le rappel sous plafond. Le deuxième écartait le texte du problème pour étudier le hash perceptuel. Le troisième le remettait et demandait dans quelle langue se trouve l’index. Tous trois citent des chiffres. Celui-ci explique d’où viennent ces chiffres.

Nous avons construit un panel de soixante cas de pertinence et l’avons exécuté à la main sur une réplique de la bibliothèque d’un client, qui contient 22 944 médias. La première exécution sérieuse a révélé quinze échecs bloquants. Au cours des huit exécutions suivantes, le panel a corrigé un diagnostic plausible, mesuré le coût d’un correctif et mis au jour un trou dans son propre jeu de cas.

Remplacer « plutôt bien » par une mesure

Aucun problème de recherche n’avait été signalé, et les gens trouvaient assez de médias pour que la bibliothèque paraisse raisonnablement bonne à l’usage courant.

La première exécution sérieuse du panel a renvoyé le code de sortie 1 : quarante-cinq réussites, quinze échecs bloquants, zéro cas en erreur, sur soixante cas, avec une réplique de 22 944 médias d’un vrai catalogue de commerce de détail.

Exécution 1 du panel de pertinence : soixante cas par catégorie, quarante-cinq réussis et quinze en échec EXÉCUTION 1, SOIXANTE CAS, UN CORPUS CLIENT RÉEL autocomplete color compatibility did_you_mean gtin material misspelling model negative part_number phrase scoped semantic suggestion LES IDENTIFIANTS EXACTS ÉTAIENT DE NIVEAU PRODUCTION La recherche par GTIN obtient recall@10 1,000, recall@50 1,000 et MRR 1,000. Numéros de référence : 0,833 sur six cas. Phrases entre guillemets : 1,000. Rien à corriger. Toute catégorie où la requête est une chaîne que le catalogue stocke aussi, telle quelle, était déjà correcte. TOUT CE QUI EST SAISI EN MOTS S’EST DÉGRADÉ matière recall@10 0,340, couleur 0,520, modèle 0,640, compatibilité appareil 0,537 avec cinq échecs bloquants. Les deux cas limités à une collection ont échoué. Les deux cas d’autocomplétion ont échoué. Vert : réussi, rouge : échec bloquant. 45 / 15 / 0 en erreur, sur une recherche dont personne ne s’était plaint.
La première exécution du panel, par catégorie. La forme du résultat est la conclusion : la recherche était parfaite pour les requêtes qui sont aussi des chaînes stockées, et s’effondrait pour tout ce qu’un distributeur saisirait réellement.

On juge d’ordinaire une recherche à ses premiers résultats. Savoir si 178 autres médias correspondants étaient accessibles est difficile à déduire de l’interface, et peu d’utilisateurs examinent des positions assez profondes pour révéler un problème de limite de candidats. La qualité perçue en dit donc peu sur le rappel au-delà du haut du classement.

Par exemple, le panel a montré que galaxy a52 case renvoyait douze résultats alors que 178 médias portaient cette valeur d’appareil. L’écart est devenu visible une fois l’ensemble attendu écrit et comparé au chemin de production.

Ce que contient un cas de pertinence utile

Un cas de pertinence associe une affirmation sur le corpus à une attente sur le comportement de la recherche : la vérité terrain (ground truth) doit donc survivre aux changements normaux de la réplique.

Chaque cas porte un identifiant, une catégorie, la requête telle qu’un distributeur la saisirait, une vérité terrain, et des assertions réparties en bloquantes et souples. Les assertions bloquantes font échouer l’exécution ; les objectifs souples sont signalés sans changer le verdict, une limite discutée dans la section sur le test de défaillance de l’instrument. Le champ intéressant est la vérité terrain, et surtout le fait que ce n’est presque jamais une liste d’identifiants de médias.

Anatomie d’un cas de pertinence, avec une vérité terrain résolue depuis la base de données et non depuis la recherche UN CAS id: compat-galaxya52-01 category: compatibility query: galaxy a52 case ground_truth: derived metadata_equals device = Galaxy A52/A52S hard find_all, min_results 1 soft first_relevant_in_top_k 10 LE SYSTÈME TESTÉ le même point d’entrée qu’appelle le champ de recherche de la galerie, paginé jusqu’à épuisement VÉRITÉ TERRAIN résolue dans la base de données du tenant à l’exécution, jamais via la recherche ÉVALUATEUR pass ou hard_fail recall@10, recall@50 rang réciproque, notes LA RÈGLE UNIQUE La vérité terrain doit être indépendante de ce qui est mesuré. 51 des 63 cas du panel ont une vérité terrain dérivée, aucun une liste d’identifiants manuscrite. Douze vérifient autre chose que l’appartenance.
Les deux voies ne se touchent jamais. Le chemin de recherche résout la requête ; le résolveur résout la spécification directement dans la base du tenant, avec le query builder du modèle de média, si bien que les lignes supprimées (soft delete) et archivées sont hors des deux.

Nous dérivons la plupart des vérités terrain parce que la réplique est remplacée périodiquement à partir de la production et que les identifiants de médias ne survivent pas à ce processus. Un cas lié à 178 identifiants serait en erreur après une nouvelle capture, ce qui rendrait le panel coûteux à maintenir et moins souvent exécuté.

Un cas dérivé déclare donc ce qu’il veut dire plutôt que ce qu’il a trouvé la dernière fois. Quatre types de spécification couvrent le panel : un champ de métadonnées égal à une valeur, un champ correspondant à un préfixe ou à une sous-chaîne, l’appartenance à une collection nommée, et une correspondance de titre au niveau du token. Le résolveur exécute la spécification dans la base au début de chaque exécution et remet à l’évaluateur un nouvel ensemble d’identifiants.

Le schéma autorise aussi une vérité terrain curated, une liste explicite d’identifiants, pour les cas où la pertinence est un jugement plutôt qu’une propriété de la base. Le panel actuel n’en utilise aucune, parce que ces cas demandent de la maintenance après une nouvelle capture, et qu’exprimer une attente comme une spécification révèle souvent une règle qu’on peut dériver à la place.

Les noms de collection ne sont pas uniques, et les clés de métadonnées ne le sont qu’au sein d’une classe de médias : les deux résolveurs peuvent donc légitimement trouver plusieurs enregistrements. Plutôt que de prendre le premier et de réduire silencieusement l’ensemble attendu, le panel marque le cas comme une erreur avec le code de sortie 2. Une erreur explicite se corrige plus facilement qu’un cas réussi sur la base d’une mauvaise vérité terrain.

Douze cas n’ont pas de vérité terrain par identifiants, parce qu’ils couvrent les suggestions « did you mean », les suggestions, les chaînes d’autocomplétion et les requêtes négatives. L’utilité étant en partie subjective, chaque cas d’aide à la saisie consigne une justification fondée sur le menu déroulant réel capturé sur la réplique et relu par une personne.

Consigner les conditions de chaque exécution

Deux choses déterminent si un chiffre de pertinence signifie quelque chose, et toutes deux sont des propriétés de l’instrument et non de la recherche : la configuration qui l’a produit, et ce que le plafond de candidats lui a fait avant que quiconque regarde.

Chaque exécution imprime un instantané des réglages avant le premier cas : limites de candidats, hybrid.rrf_k, poids de fusion, et activation ou non des replis sur les fautes de frappe et les préfixes. Un résultat sur une faute d’orthographe dépend de la correction de frappe, comme le rappel dépend de la limite de candidats : consigner ces valeurs permet de distinguer un changement de code d’un changement de réglages. Les exécutions 1 à 8 ont toutes utilisé hybrid.limit 1000, hybrid.rrf_k 60, des poids de 1,0 et 1,0, avec la correction de frappe activée.

L’instantané montrait aussi prefix.enabled à null sur le tenant utilisé pour les trois premières exécutions, ce qui signifie qu’aucune ligne de réglages n’existait. La valeur par défaut du code était true, de sorte que la recherche par préfixe s’exécutait quand même, mais le rapport indiquait clairement que le comportement venait d’une valeur par défaut et non d’un choix explicite du tenant. Nous consignons désormais les valeurs effectives et les valeurs stockées là où cette distinction compte.

Le plafond de candidats limite aussi ce qu’un cas peut affirmer. Sur ce tenant, une requête qui a plus de 1 000 correspondances plausibles en annonce 1 000 à cause de la fenêtre de candidats, et non parce que le corpus en contient exactement ce nombre. Un ensemble de vérité terrain de 2 189 médias ne peut donc pas exiger l’appartenance complète par ce chemin ; le faire créerait un échec permanent qui rendrait les nouvelles régressions plus difficiles à remarquer.

Le garde-fou de saturation : comment find_all passe à un rappel échantillonné sur les grands ensembles de vérité terrain taille de l’ensemble de vérité terrain ? seuil : 200 ≤ 200 : APPARTENANCE COMPLÈTE chaque média attendu doit figurer dans les résultats collectés, sinon le cas échoue et liste les absents 200+ : RAPPEL ÉCHANTILLONNÉ un échantillon déterministe de 50, trié par hash de l’id du cas et de l’id du média, doit obtenir au moins 0,80 ; le rapport note la dégradation SATURATION : SIGNALÉE, JAMAIS AFFIRMÉE COMME UN NOMBRE Si le résultat peut être tronqué, son total est une borne basse. max_results est ignoré, avec une note à la place. MAIS UN ABSENT RESTE UN ÉCHEC Un média correspondant directement écarté par le plafond est signalé : "not findable within candidate window" et échoue quand même (bloquant). Tirage par hash de l’id du cas et de l’id du média : pas de graine globale, et le même cas tire toujours les mêmes cinquante.
Dégrader l’assertion est une concession au plafond. Excuser l’échec n’en est pas une. Le garde-fou change la façon de poser la question et refuse de changer la réponse.

Une saturation n’excuse pas toute correspondance manquante. Le produit garantit qu’un média dont le titre contient la requête reste retrouvable quels que soient le rang ou le plafond : une correspondance directe de titre manquante reste donc un échec. Le rapport l’étiquette not findable within candidate window, ce qui préserve à la fois le verdict et sa cause probable.

Un détail d’échantillonnage mérite d’être repris. L’échantillon est tiré par hash plutôt que par un tirage aléatoire à graine, si bien qu’un même cas tire les mêmes cinquante médias à chaque exécution, et qu’un rappel échantillonné de 0,76 dans l’exécution 1 contre 0,76 dans l’exécution 2 est une vraie comparaison, et non deux tirages de la même urne.

Regrouper les échecs par mécanisme

Les quinze échecs couvraient sept catégories du panel, mais se réduisaient à six mécanismes sous-jacents.

Les quinze échecs de l’exécution 1 classés en six mécanismes, et le nombre de cas déplacés par chaque correctif QUINZE ÉCHECS, SIX MÉCANISMES Libellés d’appareil avec « / » en un seul token galaxy a52 case, iphone 12 case, iphone x case 3 Portée appliquée après le plafond, pas avant black dans une collection de 28 : 1 résultat ; tan dans une de 24 : 10 2 Vrai effet de plafond sur les grandes plages lynge 0,76, copenhagen 0,60, iphone 16 case 0,52 (rappel éch.) 3 Vocabulaire absent du corpus vegan leather case : 0 résultat ; airtag holder : 28 sur 149 2 Correction des typos iphon, kickstnd 2 Parasites du menu SKU bruts, fichiers, copii 3 CORRIGER UNE CAUSE Un changement de tokenisation a déplacé trois cas à la fois et n’a rien changé d’autre de mesuré. Un changement de portée en a déplacé deux, et aucun autre cas n’a bougé sur aucune métrique. CORRIGER UN SYMPTÔME Les cinq échecs de compatibilité sont trois problèmes, pas un : tokenisation, un vrai plafond, et un mot absent. Un seul concerne les limites de récupération. Trois correctifs distincts, un symptôme.
Classer les échecs par catégorie dit où ils apparaissent. Les classer par mécanisme dit de combien de correctifs on a besoin, un nombre différent et bien plus petit.

Le premier mécanisme illustre bien pourquoi les termes d’index demandent une inspection directe.

Les valeurs de compatibilité d’appareil de ce catalogue ressemblent à Galaxy A52/A52S et iPhone 12/12 Pro. L’analyseur anglais de PostgreSQL classe une valeur jointe par une barre oblique comme un seul token de type file : Galaxy A52/A52S est donc indexé comme le lexème a52/a52s, et une recherche de a52 ne peut pas le trouver. Le texte était dans le document ; une recherche de sous-chaîne le trouvait ; le tsvector ne le contenait pas. Dans l’ensemble du corpus, 178 documents contenaient le texte brut A52 et douze contenaient le lexème, ces douze arrivant par des titres séparés par des espaces.

Le symptôme suggérait d’abord des métadonnées manquantes ou un index périmé, mais les métadonnées étaient présentes et la reconstruction produisait les mêmes lexèmes. Nous avons corrigé la normalisation du vocabulaire pour que les libellés joints par une barre oblique émettent leurs segments en plus de la valeur d’origine.

Le dictionnaire de fautes de frappe utilisait un autre tokenizer, qui découpait déjà sur les barres obliques : il signalait donc a52 dans 178 documents alors que BM25 n’en atteignait que douze. Cet écart faisait du dictionnaire un mauvais substitut des termes stockés dans l’index BM25 ; le panel se compare donc désormais au chemin de récupération lui-même.

L’exécution 2 a mesuré séparément le correctif de tokenisation et le changement de portée, dans un plan à quatre points. galaxy a52 case est passé de 12 résultats à 178, avec zéro média introuvable, iphone 12 case d’un rappel échantillonné de 0,06 à 1,00, et les deux cas limités à une collection de 1 sur 28 et 10 sur 24 à l’appartenance complète. Entre le bras sans portée et le bras avec portée, rien d’autre dans le panel n’a bougé : ni un verdict, ni un total, ni un rappel, ni un MRR. La contrainte de conception selon laquelle les recherches sans portée ne paient aucun coût supplémentaire a cessé d’être une affirmation pour devenir une mesure.

Tester un diagnostic plausible

Le correctif de tokenisation a eu un effet de bord. Il a d’abord été livré en appliquant sa règle à tout le document normalisé et non aux seuls libellés de vocabulaire : il découpait donc tout token joint par une barre oblique, quelle qu’en soit la source : vitesses d’obturation EXIF comme 1/100, tensions, URL, références de directives européennes comme 2009/125/EC, conjonctions ordinaires. Environ la moitié du corpus a grossi, et les documents contenant une barre oblique faisaient en moyenne 3 758 caractères contre 917 pour les autres.

BM25 normalise par la longueur du document : tout document qui a grossi a perdu un peu de score sur chaque terme qu’il contenait, et tout document qui n’a pas grossi a gagné du rang gratuitement. Comme prévu, cinq cas ont régressé en recall@10 entre l’exécution 1 et l’exécution 2, dont deux d’un score parfait à zéro. L’analyse allait de soi : l’expansion avait gonflé la longueur des documents, la longueur gonflée avait rétrogradé la vérité terrain, et restreindre l’expansion aux libellés de vocabulaire inverserait cela tout en gardant le gain de rappel.

Nous avons consigné cette prédiction avant l’exécution 3, puis restreint l’expansion, reconstruit le corpus et mesuré les cas concernés.

Cinq régressions de rappel attribuées à la tokenisation, dont quatre n’ont pas bougé du tout une fois celle-ci corrigée RECALL@10 : LES CINQ CAS QUE LE RESSERREMENT DEVAIT INVERSER cas exéc. 1 exéc. 3a exéc. 3b model-greenland-01 0,70 0,00 0,00 misspelling-grenland-01 1,00 0,00 0,00 misspelling-iphon-01 0,70 0,00 0,00 color-clear-01 1,00 0,80 0,80 material-biodegradable 0,70 0,60 0,70 Quatre des cinq sont identiques à quinze décimales près. Mêmes recall@10, recall@50 et rang réciproque. Un cas a récupéré. CE QUI DÉCIDE DE LA REQUÊTE GREENLAND rangs 1 à 11 : documents Product Video, 155 à 493 caractères rang 12 et suivants : images produit, 746 à 1 280 caractères le document à la frontière ne contient aucun slash L’ATTRIBUTION L’expansion des slashs a gonflé les longueurs. BM25 pénalise la longueur. La vérité terrain a été rétrogradée. Cohérent, précis, au niveau du mécanisme, et consigné avant que quiconque ne le mesure. LA MESURE 2 077 des 2 189 documents de vérité terrain portaient bien un slash et avaient été gonflés. Retirer le gonflement a déplacé le classement de zéro position. La cause : la longueur du document.
L’attribution n’était pas paresseuse. Elle nommait un vrai mécanisme, dans le bon sous-système, et elle s’est trompée quatre fois sur cinq. Quelques dizaines de caractères ajoutés à un document de plusieurs centaines sont une erreur d’arrondi face à un rapport de longueur de trois à huit entre le document d’un clip vidéo et celui d’une image produit.

Le resserrement a fait exactement ce pour quoi il était conçu au niveau du corpus. Le lexème a52 correspondait toujours à 178 documents ; le lexème 2009, qui fuyait depuis 2009/125/EC, correspondait à zéro. Et quatre des cinq inversions prédites n’ont pas bougé d’un cheveu.

Le vrai mécanisme est apparu quand quelqu’un a cessé de regarder les tokens pour regarder les longueurs. La requête greenland était gagnée par onze documents Product Video de 155 à 493 caractères, chacun essentiellement le titre répété selon le poids de son champ, plus un peu de métadonnées de codec. La première image produit, dont se compose la vérité terrain du cas, arrivait au rang 12 avec 751 caractères contre une moyenne de 1 280 pour la vérité terrain. Un rapport de longueur de trois à huit suffit à décider d’un classement à lui seul, et le document à la frontière ne contenait aucune barre oblique : aucune des deux versions de l’expansion ne l’avait jamais touché.

Le diagnostic était cohérent et assez précis pour passer une relecture, mais la prédiction par cas l’a rendu testable. La mesure a montré que quatre des cinq inversions attendues ne se sont pas produites, ce qui a réorienté l’enquête vers la longueur des documents plutôt que vers l’expansion des tokens.

Le resserrement a été livré malgré tout, car c’était toujours le bon changement. Il a aussi coûté un cas. compat-iphonexxs-01, la requête iphone x case, est passé d’un rappel échantillonné de 1,00 à 0,74 et d’une réussite à un échec bloquant, parce que son ensemble de résultats sature au plafond à chaque exécution et que l’expansion trop large avait allongé ses concurrents juste assez pour admettre plus de médias iPhone X et Xs. On retire la pollution, les concurrents raccourcissent, obtiennent un meilleur score et reviennent se presser. Le panel est passé de 48 réussites à 47, et le compromis a été consigné avec le correctif.

Le classement dans une fenêtre plafonnée est proche d’un jeu à somme nulle : une amélioration peut donc en déplacer un autre résultat. Quand un changement ne montre que des gains, nous vérifions si le jeu de cas couvre ce qui a reculé. Les régressions connues restent dans le rapport avec leur justification, pour qu’on les distingue des nouveaux échecs.

L’habitude utile est de consigner des prédictions par cas avant l’exécution, y compris pour les cas qui ne doivent pas bouger. L’exécution 2 a confirmé dix prédictions, l’exécution 3 une sur cinq ; les deux étaient instructives, car les contrôles négatifs ont montré si le mécanisme proposé était le bon.

Un panel ne voit que ce que ses cas couvrent

L’exécution 4 était un canari pour une autre fonctionnalité : la garantie de retrouvabilité par le titre, qui promet qu’un média dont le titre contient la requête est retrouvable par cette requête quels que soient le rang ou le plafond. Bras témoin désactivé, bras traité activé, même corpus, même build, mêmes réglages. Une réserve accompagne désormais chaque chiffre : l’exécution 4 a mesuré une réplique fraîche de la production, volontairement non réindexée depuis la livraison du correctif de tokenisation, et son corpus est donc celui d’avant le correctif. Les deux bras le partagent, ce qui suffit à un A/B, mais les verdicts de l’exécution 4 ne sont pas comparables cas par cas à ceux de l’exécution 3, même quand les totaux coïncident.

Le résultat était plat sur les verdicts, 47 réussites dans les deux bras, et sans ambiguïté sur les métriques. Le recall@10 moyen est tombé de 0,6667 à 0,6333, le recall@50 moyen de 0,7446 à 0,7338, et trois cas ont régressé. Aucun cas ne s’est amélioré sur aucune métrique de pertinence.

Pris seul, le résultat suggérait que la fonctionnalité réduisait la pertinence sans rien apporter. Pourtant, aucun des soixante cas ne demandait si un média au-delà du plafond restait retrouvable par son titre, ce qui était le contrat de la fonctionnalité. Les cas existants utilisaient une vérité terrain dérivée des métadonnées : le panel pouvait donc mesurer le coût, mais pas le bénéfice visé.

L’exécution 4 a donc identifié un trou de couverture, et l’exécution 5 a ajouté trois cas de retrouvabilité par le titre.

Un panel aveugle et un panel voyant : la même fonctionnalité mesurée avant et après l’ajout des cas de titre LA MÊME FONCTIONNALITÉ, LE MÊME CORPUS, DEUX INSTRUMENTS DIFFÉRENTS EXÉC. 4 : SOIXANTE CAS, SANS TITRE verdicts 47 / 13, garantie désactivée verdicts 47 / 13, garantie activée 0 cas meilleur, 3 cas pires Se lit : la fonctionnalité coûte de la pertinence et n’apporte rien. EXÉC. 5 : SOIXANTE-TROIS CAS verdicts 48 / 15, garantie désactivée verdicts 50 / 13, garantie activée 2 échecs bloquants corrigés, 0 introduit Les trois mêmes cas coûtent encore du recall@10. Il y a maintenant quelque chose en face. LES TROIS CAS, ET POURQUOI L’UN D’EUX EST UN TÉMOIN title-mainimage-01 bande de 3 339 titres, bien au-delà du plafond de 1 000 candidats rappel échantillonné de 0,32 à 1,00 title-product-01 bande de 2 156 contre 13 516 documents concurrents rappel échantillonné de 0,30 à 1,00 title-lifestyle...-01 bande de 188, sous le plafond et sous le seuil d’échantillonnage passe dans les DEUX bras. Le témoin.
Deux cas que la fonctionnalité doit corriger, et un qu’elle doit laisser intact. Sans le troisième, un sans-faute montrerait que les cas ont été écrits en fonction de la réponse, et non que la garantie fonctionne.

Le cas témoin est celui qui ressemble à du remplissage. Sa bande de titres compte 188 médias : sous le plafond de 1 000 candidats, et sous le seuil de 200 médias à partir duquel les assertions d’appartenance se dégradent en échantillonnage. Rien n’est jamais coupé : il réussit avec la garantie désactivée comme activée, à l’identique dans les deux bras.

Un cas utilise une bande de titres sous le plafond de candidats et doit réussir avec la fonctionnalité désactivée comme activée. Ce témoin distingue la garantie voulue d’un changement plus large qui réordonne des résultats déjà accessibles sans elle. Il est resté vert et inchangé pendant que les deux cas au-delà du plafond mesuraient le nouveau comportement.

L’ajout a aussi permis au panel de voir ce que les soixante cas ne pouvaient jamais voir. title-product-01 a renvoyé 2 587 résultats pour une vérité terrain de 2 156, soit 431 correspondances hors titre qui ont survécu à côté d’une bande plus de deux fois supérieure au plafond. Une version antérieure de la garantie avait numéroté ensemble la bande et le groupe classé : la bande prenait toutes les positions sous le plafond, était réadmise de toute façon par sa propre clause, et supprimait silencieusement les correspondances classées qu’elle déplaçait. Ces 431 médias sont ce correctif, rendu visible sur un vrai corpus.

Le panel a aussi montré que la première tentative de rétrogradation des lignes de la bande de titres n’avait aucun effet : l’exécution 6 était identique octet pour octet à l’exécution 5b. L’ordre avait été modifié dans une sous-requête, mais les deux consommateurs qui déterminent l’ordre visible par l’utilisateur ne le préservaient pas. Appliquer la rétrogradation dans les deux consommateurs a supprimé les régressions mesurées sur les soixante-trois cas, tandis que les deux cas de titre au-delà du plafond continuaient de réussir.

Le rejeu de journal de requêtes mesure le changement, pas la justesse

Tout ce qui précède vient d’un seul instrument, avec un angle mort fixe. Ses cas sondent des modes de défaillance que nous soupçonnions déjà et sa vérité terrain est dérivée des métadonnées : il répond précisément à « le bon élément est-il retrouvable », mais pas à « un vrai utilisateur le remarquerait-il ». L’expérience sur la retrouvabilité par le titre montrait plus haut pourquoi cette distinction compte.

Le second instrument rejoue des requêtes de production sans vérité terrain. Il compare deux bras sur le même corpus et rapporte ce qui a bougé : il mesure donc le changement et non la justesse. Un top dix identique est inchangé mais non vérifié, et un ensemble de résultats plus grand contient plus de médias accessibles sans prouver qu’ils sont pertinents.

Le panel constitué à la main et le rejeu du journal de requêtes répondent à des questions différentes, et aucun ne remplace l’autre LE PANEL CONSTITUÉ À LA MAIN 63 cas, 15 catégories, vérité terrain dérivée, résolue depuis la base de données Répond : le bon élément est-il retrouvable ? Note recall@10, recall@50, MRR et l’appartenance stricte à un ensemble connu. Ne répond pas : quelqu’un le remarquerait-il ? Ses requêtes sont écrites par nous, pour sonder des mécanismes que nous soupçonnions déjà. LE REJEU DU JOURNAL DE REQUÊTES 55 vraies recherches par mot-clé sur dix semaines du journal d’un tenant, sans vérité terrain Répond : quelque chose s’est-il dégradé ? Compare les dix premiers et le total de chaque requête entre deux bras. Ne répond pas : est-ce correct ? Un top dix identique est inchangé, ce qui n’est pas la même chose que vérifié. CE QUE LE REJEU A MESURÉ, GARANTIE DE TITRE DÉSACTIVÉE ET ACTIVÉE 55 / 55 top dix identiques 5 totaux en hausse 0 totaux en baisse 2 zéro résultat, avant et après
Aucun instrument n’en contient un autre. Le panel connaît la bonne réponse et ignore ce que quelqu’un a demandé ; le rejeu sait ce que tout le monde a demandé et n’a aucune idée de la bonne réponse.

La source contenait 140 recherches couvrant 99 termes distincts sur dix semaines, sur un seul tenant de production. Nous n’avons extrait que le terme, le nombre d’occurrences, le type de recherche et le nombre de résultats ; les identifiants d’utilisateur et de session n’étaient pas inclus, car la comparaison n’en avait pas besoin.

Sur les 99 termes distincts, 55 étaient des recherches par mot-clé et 44 des arbres de filtres avancés sérialisés. Nous avons exclu les arbres : ceux qui n’ont pas de feuille plein texte n’atteignent jamais le chemin hybride et ne pouvaient donc pas changer dans cette comparaison. Les recherches restantes comprenaient des SKU, des noms de fichiers, des expressions de marque, des descriptions, des variantes de casse, une faute d’orthographe, un terme danois et un collage de 79 SKU.

Le top dix est resté identique pour les 55 recherches. Cinq totaux ont augmenté, chacun pour une requête déjà au plafond de 1 000 candidats : le changement n’a donc touché que l’accessibilité au-delà du plafond : greenland est passé de 1 000 à 1 014, iphone à 1 010, et phone et blue products à 1 005. Aucune requête n’a perdu de résultats, et les deux requêtes vides sont restées vides.

C’est une preuve sur la portée du changement, pas sur sa qualité. Le panel constitué à la main se concentre volontairement sur des mécanismes sensibles, tandis que le rejeu reflète les requêtes que les clients ont faites par hasard ; les deux instruments répondent à des questions différentes.

Le rejeu a trouvé une chose que le panel ne pouvait pas trouver. Une fois la bande intégrée, la limite de candidats rapportée est devenue null sur 52 des 55 requêtes alors que l’indicateur de troncature restait activé sur toutes : l’interface passe de « peut être tronqué, limite 1000 » à « peut être tronqué, profondeur non descriptible par un seul nombre ». C’est honnête, et c’est une décision de formulation à prendre avant le déploiement plutôt qu’à découvrir après.

La première mesure de latence suggérait à tort que la bande de titres accélérait la recherche : les bras s’exécutaient l’un après l’autre et le second héritait de caches chauds. En comparant des exécutions à chaud avec des exécutions à chaud, il restait environ cinq millisecondes de différence moyenne, les requêtes individuelles les plus lentes étant moins bonnes de huit à dix millisecondes. Les deux bras ont donc besoin des mêmes conditions de cache.

La limite est l’échantillon. Cinquante-cinq termes distincts d’un seul tenant sur dix semaines, orientés vers les recherches de SKU, forment une petite fenêtre sur exactement la forme de requête la moins susceptible d’être affectée par ce qu’on teste.

Vérifier que les contrôles peuvent échouer

L’instrument lui-même demandait aussi un test de défaillance. Nous avons trouvé quatre contrôles qui apparaissaient dans la sortie des tests ou de la supervision mais produisaient le même signal quand le comportement qu’ils surveillaient était cassé.

Un code de sortie calculé que rien ne consommait. Le runner renvoyait soigneusement 0 pour une réussite, 1 pour des échecs bloquants et 2 pour des erreurs de configuration. Le trait qui l’invoquait jetait l’entier et ne réagissait qu’à une exception levée, et le runner était conçu pour ne jamais lever d’exception : toute invocation réelle sortait donc avec 0, quel que soit le nombre de cas en échec. Les tests propres de la suite passaient, parce que l’environnement de test prend une branche antérieure qui propage bien la valeur de retour. Le seul chemin exercé était le seul chemin correct.

Des métriques qui rapportent mais ne peuvent pas bloquer. Les objectifs souples émettent une note disant que le premier résultat pertinent est arrivé au rang 17 pour un objectif de 10, et les notes ne changent pas un code de sortie. Après le changement du constructeur de documents, cinq cas ont perdu la pertinence dans le top dix, dont deux passés d’un score parfait à zéro, et quatre d’entre eux indiquaient encore pass.

Une supervision qui rapportait le silence pendant que la tâche surveillée mourait, parce qu’elle ne filtrait que les marqueurs de progression et qu’un plantage n’en produit aucun. Un test de départage resté vert après suppression du départage, parce que l’assertion se vérifiait pour des raisons sans rapport avec le comportement qu’elle nommait.

Dans chaque cas, un signal d’échec existait, mais la couche suivante ne le consommait pas selon le même contrat.

Pour chaque barrière, nous demandons maintenant ce qu’elle afficherait si le comportement surveillé cassait, puis nous introduisons une défaillance contrôlée et vérifions que le résultat passe au rouge. Les deux défauts trouvés par cette méthode étaient plus faciles à localiser que les deux découverts par la lecture du code.

Le défaut du code de sortie aurait fait annoncer un succès aux exécutions complètes sur la réplique, quel que soit le nombre d’échecs bloquants. Les objectifs souples restent diagnostiques plutôt que bloquants : le rapport les étiquette donc clairement, sinon une métrique affichée se prend facilement pour une garantie appliquée.

Ce que nous dirions à quiconque construit cela

La version transposable, débarrassée de nos particularités.

  • Partez du principe que vous avez tort sur votre propre recherche, jusqu’à ce qu’un instrument vous contredise. La qualité perçue n’a aucune résolution sous les trois premiers résultats : un système peut donc perdre la plus grande partie d’un ensemble de résultats et paraître correct à tous ceux qui l’utilisent. Quinze échecs sur soixante cas, et pas un seul ticket de support.
  • Exprimez la vérité terrain comme une spécification, pas comme une liste d’identifiants. Un panel qui meurt quand le corpus est remplacé cesse d’être exécuté, et une suite que personne n’exécute est supprimée dans le mois. Les cas constitués à la main sont de futurs événements de maintenance : gardez-les près de zéro.
  • Faites énoncer au rapport la configuration qu’il a mesurée, et imprimez les valeurs stockées plutôt que les valeurs effectives. Un chiffre de rappel sans son plafond de candidats n’est qu’une rumeur, et l’écart entre stocké et effectif dit si quelqu’un a réellement décidé.
  • Dégradez l’assertion sous un plafond, jamais le verdict. Échantillonnez au lieu d’exiger l’appartenance complète, mais gardez une correspondance directe manquante comme échec bloquant, sinon l’instrument devient aveugle à la classe de défauts pour laquelle il a été construit.
  • Classez les échecs par mécanisme avant d’en corriger un seul. Cinq échecs dans une catégorie étaient trois bogues demandant trois correctifs, et un seul concernait les limites de récupération. Corriger une cause déplace plusieurs cas à la fois ; corriger un cas en déplace un et en casse un autre.
  • Écrivez la prédiction avant l’exécution, cas par cas, y compris ce qui ne doit pas bouger, et attendez-vous à payer une facture. Les prédictions négatives révèlent un mauvais diagnostic. Le classement est proche d’un jeu à somme nulle dans un plafond : un changement qui ne montre que des améliorations signifie en général que rien dans votre jeu de cas ne pointe vers ce que vous avez déplacé.
  • Vérifiez que votre instrument peut voir ce que vous lui demandez, et incluez un témoin qui doit réussir avec la fonctionnalité désactivée. Zéro meilleur et trois pires était une mesure vraie et une conclusion fausse, parce qu’aucun cas n’exerçait le contrat testé. Un jeu de cas où tout ne réussit qu’avec la fonctionnalité activée montre que les cas ont été écrits en fonction de la réponse.
  • Utilisez deux instruments aux angles morts différents, et demandez pour chaque barrière ce qu’elle afficherait si ce qu’elle surveille cassait. Un panel constitué à la main connaît la bonne réponse mais pas ce que quelqu’un a demandé ; un journal de requêtes sait ce que tout le monde a demandé mais pas la bonne réponse. Puis cassez vos contrôles exprès et vérifiez qu’ils passent au rouge, car la lecture ne les trouve que si vous étiez déjà méfiant.

Où cela se trouve dans le produit

Rien de tout cela n’est une fonctionnalité. C’est l’instrument qui décide quelles fonctionnalités sont livrées, et sa sortie n’est visible que dans le comportement des recherches ordinaires : la galerie, l’API REST, le SDK TypeScript et tout agent IA connecté via MCP. Les correctifs qu’il a arbitrés sont ceux décrits dans le premier article : les garanties de rappel sous plafond, la sélection des candidats, le rapport d’exhaustivité qui permet à un résultat tronqué de le dire. Le même sujet, vu du produit, est la recherche et la découverte, et le fait que tout cela s’exécute dans une seule instance PostgreSQL est ce qui le rend reproductible sur une réplique, ce qui rend aussi freedam praticable en auto-hébergement.

Les limites méritent d’être énoncées aussi simplement que les résultats, car un instrument qui se survend est exactement la défaillance dont parle cet article. Le panel est exécuté à la main et volontairement hors de l’intégration continue : il demande une réplique d’un vrai corpus client, un sidecar d’embeddings et environ quatre-vingt-dix secondes par exécution, et ses chiffres ne signifient rien sur un autre corpus. Ses requêtes sont uniquement en anglais, parce que le catalogue pour lequel il a été construit est en anglais : il ne mesure donc rien du comportement multilingue décrit dans l’article précédent. Ses cas résolvent leur vérité terrain de nouveau à chaque exécution et survivent à une nouvelle capture, mais un corpus modifié modifie quand même les chiffres : les exécutions 4 à 8 ont mesuré une réplique fraîche de la production, non réindexée depuis le correctif de tokenisation, et ne sont donc pas comparables cas par cas à l’exécution 3, même quand les totaux coïncident.

Prochain article de la série : reconnaissance faciale et recherche de personnes, où les problèmes intéressants cessent de porter sur la qualité de la récupération pour porter sur qui est autorisé à affirmer que deux visages sont une même personne.

Pour les développeurs

À lire aussi