Deux clientes tapent « veste » dans la recherche. Jusqu’à fin septembre, elles voyaient les mêmes produits, dans le même ordre. L’une a mis trois blazers noirs dans sa liste d’envies la veille, l’autre ne regarde que des vestes en cuir : le moteur l’ignorait.

Le 29 septembre 2026 à 14:11 UTC, on a allumé Amazon Personalize sur les neuf storefronts du site. Il réordonne la première page des résultats de recherche et la grille des pages catégorie, à partir de ce que chaque visiteur a regardé, ajouté au panier, mis de côté ou acheté. Pas partout, pas pour tout le monde : dans un bras d’un test A/B, face à notre outil de merchandising actuel.

Ce qui suit n’est pas une démonstration de machine learning. C’est l’histoire d’une intégration où presque chaque décision a consisté à retirer quelque chose : un SDK, des données, des campagnes, un champ. Et d’une règle tenue jusqu’au bout : Personalize peut améliorer l’ordre d’une page, il ne peut jamais l’empêcher de s’afficher.

Pourquoi une recette sur mesure

AWS propose par défaut des « recommandeurs » e-commerce prêts à l’emploi. On les a écartés pour une raison simple : aucun ne sait réordonner une liste qu’on lui fournit. Or nos deux surfaces, la recherche et la grille catégorie, sont exactement ça. Le moteur de recherche ou le catalogue décide quels produits sont pertinents et en stock ; la personnalisation ne doit décider que de l’ordre.

C’est le métier de la recette Personalized-Ranking-v2 : on lui donne jusqu’à cent identifiants de produits et un visiteur, elle les rend triés pour lui. Elle ne propose jamais un produit absent de la liste. Pour un site de mode, c’est une garantie précieuse : la personnalisation ne peut pas faire remonter un article épuisé, ni un produit hors de la catégorie.

Les gestes passent par nous

Le premier choix d’architecture a été de ne mettre aucun SDK AWS dans le navigateur. Les gestes partent vers Helios, notre API catalogue, qui les valide puis les transmet à Personalize. Le navigateur ne détient aucun identifiant AWS, et c’est notre code, pas un script tiers, qui décide de ce qui sort.

Deux chemins alimentent Personalize : les gestes en direct, envoyés par le navigateur à Helios qui les valide puis les transmet, et les exports nocturnes de Magento. Les deux nourrissent le jeu de données, qui entraîne chaque jour la campagne de ranking. AWS Personalize Navigateur si consentement Helios valide, transmet Magento chaque nuit Exports catalogue, clients Jeu de données 7 types de gestes Campagne ranking réentraînée chaque jour PutEvents import
Aucun SDK AWS dans le navigateur : les gestes passent par notre API, qui décide de ce qui part.

Le contrat est volontairement étroit. Sept gestes, pas un de plus : vue de fiche produit, clic dans une liste, ajout au panier, liste d’envies, alerte de retour en stock, essayage virtuel et achat. Le schéma Zod d’Helios est strict, et chaque champ optionnel n’est accepté que sur le geste qui le porte :

const eventSchema = z.object({
  eventType: z.enum(['view', 'select', 'add-to-cart', 'wishlist',
    'purchase', 'try-on', 'stock-alert']),
  itemId: z.string().regex(/^[A-Z0-9_]{4,40}$/),
  visitorId: z.string().min(8).max(64),
  userId: universalId,
  storeCode: z.enum(['fr_fr', ...Object.keys(storeConfig)]),
  eventValue: z.number().nonnegative().max(100_000).optional(),
  orderId: z.string().regex(/^[A-Za-z0-9_-]{1,32}$/).optional(),
}).strict(); // + un superRefine : eventValue et orderId réservés à l'achat

L’achat porte son numéro de commande, qui sert de clé de rejeu : une page de confirmation rechargée ne compte pas deux achats. Sa valeur est calculée par Magento, en euros, jamais par le front.

Le consentement est traité de deux façons, selon ce qui est en jeu. L’envoi des gestes échoue fermé : tant que le visiteur n’a pas accepté la finalité de mesure dans le bandeau de consentement, rien ne part. L’affichage, lui, ne dépend pas du consentement : un visiteur qui refuse voit simplement la page dans l’ordre du moteur, sans personnalisation.

À côté du direct, Magento exporte chaque nuit ce que le navigateur ne peut pas dire : le catalogue de chaque marché, les profils clients (dont les tailles, déduites des commandes) et l’historique des achats. Personalize importe ces fichiers, puis s’entraîne.

Dix cases, pas une de plus

Personalize limite à dix la somme des types de gestes distincts et des champs optionnels du jeu d’interactions. Détail que la documentation dit à peine : le type de geste et la valeur d’événement comptent eux-mêmes dans ce total. Avec trois champs (type, valeur, pays) et sept gestes, on est pile à dix.

Cette contrainte a tranché plusieurs débats. Un champ « acheté en promotion » était prévu : il dépassait la limite, c’était le seul à imposer la reconstruction complète du jeu de données, et il n’était jamais transmis au moment de la recommandation. Il est parti. Ajouter demain un geste, la recherche par exemple, obligera à en retirer un autre.

Chaque geste a un poids, qui dit au modèle ce qu’il doit chercher à prédire. On les a ordonnés selon la probabilité, mesurée sur nos propres données, qu’un geste soit suivi de l’achat du même produit :

Geste Poids
Achat 1
Ajout au panier 0,6
Alerte de retour en stock 0,5
Essayage virtuel 0,5
Liste d’envies 0,35
Vue de fiche produit 0,1
Clic dans une liste 0,05

La grille précédente plaçait la liste d’envies devant le panier, au motif qu’elle est plus rare. C’était confondre rareté et force du signal : les données disent l’inverse.

Moins de données, meilleur modèle

Le premier amorçage contenait plusieurs dizaines de millions de vues anonymes historiques, issues de notre outil d’analytics. On pensait que plus d’historique ferait un meilleur modèle. On a vérifié avant de recharger, sur un test simple : prédire la prochaine fiche produit vue par un visiteur.

Données utilisées Bon produit dans le top 10
Co-vues des 30 derniers jours 40,6 %
Historique anonyme seul 25,5 %
Les deux mélangés 37,6 %

En mode, la navigation récente vaut plus que le volume. L’historique ancien ne faisait pas qu’alourdir chaque entraînement, puisque Personalize facture l’entraînement au nombre d’interactions : il dégradait la prédiction. On ne l’a pas rechargé.

Les visiteurs anonymes ne sont pas oubliés pour autant. Leurs gestes partent en direct sous un identifiant de visiteur, ce qui adapte l’ordre au cours de la visite. Et le jour où ils se connectent, un même geste porte à la fois l’identifiant de visiteur et l’identifiant client : au prochain entraînement complet, Personalize rattache la navigation passée à la cliente. Côté client, on utilise l’identifiant universel partagé avec notre CRM plutôt que l’identifiant interne de Magento, pour qu’une cliente reste la même personne d’un système à l’autre.

Shadow d’abord, puis une seule campagne

L’allumage s’est fait en deux temps. Le 28 septembre à 14:22 UTC, le mode shadow démarre sur les neuf storefronts : les gestes partent, Personalize apprend, mais rien de ce qu’il calcule n’est montré. Ce jour-là, on vérifie que les envois arrivent sans erreur, que le catalogue est frais, et que le premier modèle tient la route sur ses métriques hors ligne.

Le lendemain, une fois le premier entraînement automatique terminé, on ouvre la recherche et la grille dans le bras Personalize du test. Les interrupteurs vivent dans le back-office Magento, par storefront, et atteignent les serveurs du site sans redéploiement, selon le mécanisme décrit dans notre article sur le feature flipping par marché.

Une seule campagne tourne : celle du ranking. Dans Personalize, une campagne ne sert qu’à répondre en direct ; la collecte, les imports et les entraînements s’en passent. Les modèles de recommandation « produits similaires » et « pour vous » continuent donc de s’entraîner, sans campagne, et donc sans coût de service. Le jour où leur surface s’ouvrira, une campagne se recrée en un quart d’heure.

Le modèle de ranking, lui, se réentraîne chaque jour. Cette recette ne se met pas à jour seule et ne sait pas classer un produit qui n’a encore aucune interaction : l’entraînement quotidien intègre les nouveautés dès leurs premières vues.

Réordonner sans rien casser

À chaque affichage, le site demande à Helios de réordonner la liste que le moteur a produite. C’est là que la règle du début devient du code.

Le moteur fournit les produits candidats dans son ordre ; Helios demande à Personalize de les réordonner en 800 millisecondes au plus. Au moindre écart (pas d'identité, budget atteint, délai dépassé, erreur), la page garde l'ordre du moteur. Requête recherche, grille Moteur candidats Helios cache, budget Personalize 800 ms, 1 essai Liste réordonnée nouveauté sans score : garde sa place grille : déjà acheté en fin de liste Ordre du moteur, inchangé pas d'identité, budget, délai, erreur
Personalize est un bonus sur l'ordre, jamais une condition pour afficher la page.

Côté recherche, le rendu attend Helios au plus 250 millisecondes ; Helios accorde lui-même 800 millisecondes et un seul essai à Personalize. Pas d’identité, budget du mois atteint, délai dépassé, erreur : la page garde l’ordre du moteur. Chaque abstention est journalisée avec sa raison, parce qu’un bras de test dégradé en silence ressemble à s’y méprendre à un modèle qui sous-performe.

Trois détails ont demandé plus de réflexion que le reste.

Les nouveautés gardent leur place. Personalized-Ranking-v2 renvoie les produits qu’il ne connaît pas encore, sans score, après tous les autres. Appliqué tel quel, cela enverrait chaque nouveauté en bas de page le jour de son arrivée, exactement l’inverse de ce que veut une marque de mode. Helios remet donc chaque produit non noté à la place que le moteur lui avait donnée, et ne permute que les produits notés entre eux :

export const keepUnscoredInPlace = (
  ranking: { itemId?: string; score?: number }[] | undefined,
  candidates: string[],
): string[] => {
  const returned = new Map((ranking ?? []).filter((i) => i.itemId)
    .map((i) => [i.itemId as string, i.score]));
  const scored = [...returned].filter(([, score]) => score !== undefined)
    .map(([sku]) => sku);
  return candidates
    .filter((sku) => returned.has(sku))
    .map((sku) => (returned.get(sku) === undefined ? sku : scored.shift() as string));
};

Une liste ne bouge pas sous les doigts. Le classement est mis en cache par visiteur. Pour la recherche, la clé change à chaque nouveau geste, donc l’ordre suit l’intention. Pour une grille catégorie, la clé est la visite de la liste : les gestes faits en la parcourant ne la réordonnent pas pendant qu’on fait défiler.

Les produits déjà achetés passent en fin de grille, sans disparaître. On passe par un filtre dédié aux seuls achats. Le filtre générique des recommandations aurait aussi écarté les produits que l’export du catalogue n’a pas encore vus, autrement dit les nouveautés. Avec le filtre dédié, un produit absent de la réponse ne peut être qu’un produit acheté : Helios le replace en fin de liste, la page reste complète. Et si le filtre échoue, le classement est servi sans relégation plutôt que pas du tout.

Ce qu’on mesure

Le test compare deux couples : le moteur de recherche et l’outil de merchandising actuels, face à Meilisearch avec Personalize. Mesurer le couple est voulu : c’est la configuration qu’on mettrait en production, et le bras Personalize réordonne justement les résultats de Meilisearch.

Chaque clic dans les résultats dit si l’ordre affiché venait de Personalize ou du moteur, ce qui donne la couverture réelle du re-tri, et pas seulement son intention. Le test tourne hors soldes, et il faut environ un mois de trafic pour détecter une hausse de 10 % de la conversion des visiteurs qui cherchent. Les résultats ne sont pas encore là : on en reparlera quand ils diront quelque chose.

Les blocs de recommandation, eux, restent sur notre outil actuel. Ils s’ouvriront un par un, chacun avec son propre test.

Ce qu’on a appris

  • Retirer est le geste le plus rentable. Pas de SDK dans le navigateur, pas d’historique ancien, pas de campagnes inutiles, pas de champ superflu. Chaque retrait a simplifié le système et en a amélioré la qualité ou le coût.
  • Tester ses intuitions sur ses données. « Plus d’historique, meilleur modèle » paraissait évident. Un test hors ligne d’une journée a montré le contraire.
  • La personnalisation est un bonus, jamais une dépendance. Délai court, un seul essai, abstention silencieuse mais journalisée : la page s’affiche toujours, avec ou sans modèle.
  • Le diable est dans les cas limites métier. Les nouveautés reléguées ou les produits qui bougent pendant le défilement auraient ruiné l’expérience sans jamais apparaître dans une métrique d’entraînement.
  • L’ombre avant le plein jour. Une journée de collecte sans effet visible a permis de valider les flux, le catalogue et le premier modèle avant d’exposer une seule cliente.