Cross-selling PrestaShop : automatiser recommandations produits et packs

Automatisez les recommandations et bundles sur PrestaShop sans ralentir votre boutique. Méthodes, architecture batch, cache et conseils pour améliorer le panier moyen.

Interface de vente en ligne affichant un casque audio, des accessoires recommandés et une offre groupée, avec des tableaux de bord et des serveurs en arrière-plan.

Le cross-selling (vente croisée) sur PrestaShop se joue à trois niveaux très différents : accessoires manuels, recommandations du type « les clients ont aussi acheté » et packs (produits de type pack). Les confondre est la meilleure façon de mettre en place une usine à gaz lente… ou de pousser des recommandations pour des produits en rupture de stock.

Pré-requis et contexte techniques utilisés dans cet article : PrestaShop 8.1 à 9.2, avec une version de PHP compatible avec la version exacte de PrestaShop retenue. La compatibilité PHP varie selon les versions de PrestaShop : vérifiez la matrice officielle avant de choisir votre environnement. Tout ce qui suit vise une implémentation sans override, avec des hooks, des commandes Symfony et, éventuellement, Symfony Messenger.

Dans beaucoup de boutiques françaises, l’enjeu est double : augmenter l’AOV (panier moyen) sans dégrader l’UX, et tenir la charge (surtout pendant les pics comme les soldes, le Black Friday ou les opérations « frais de port offerts »). À titre de contexte marché, la FEVAD situe le e-commerce français à 159,9 Md€ en 2023 (Bilan du e-commerce en France, publié en 2024) : dit autrement, l’optimisation par petits gains (recommandations pertinentes et rapides) est loin d’être marginale.

Ce que PrestaShop fait nativement (et ce qu’il ne fait pas) pour la vente croisée

PrestaShop propose nativement une relation d’accessoires entre produits (table ps_accessory). C’est du cross-selling déterministe : vous décidez que le produit A recommande B, point. C’est fiable et totalement contrôlable (règles métier, stock, marge), mais c’est manuel et difficile à gérer sur un catalogue volumineux sans automatisation.

Les recommandations du type « les clients qui ont acheté ce produit ont aussi acheté… » sont généralement fournies par un module, tel que ps_crossselling selon la version et la distribution. Il n’existe pas de table ps_crossselling universelle du cœur de PrestaShop : ces modules peuvent exploiter l’historique des commandes (ps_orders, ps_order_detail) et leur implémentation varie. C’est une forme de filtrage collaboratif : on identifie des co-achats et on affiche les co-occurrences les plus fréquentes. Le problème n’est pas l’idée, mais l’exécution : sur un site à fort volume, recalculer à la volée les produits souvent achetés ensemble peut devenir une source de requêtes lentes et de contention MySQL.

Les packs PrestaShop (produit de type pack, avec une composition enregistrée dans ps_pack) répondent à un autre besoin : vendre un bundle comme un produit unique, avec une composition fixe. Sur le papier, c’est pratique, mais le cœur a des limites : gestion des déclinaisons dans un pack, règles de stock, impact sur la disponibilité, complexité des retours et des avoirs, etc. Autrement dit : recommander un pack et générer automatiquement des packs sont deux sujets différents. Beaucoup de projets échouent en tentant d’automatiser la création de packs là où une simple recommandation de « bundle suggéré » aurait suffi.

Pour clarifier (et éviter de choisir « au feeling »), voici une grille simple :

Mécanisme Où c’est stocké Contrôle métier Charge/perf Cas d’usage typique Pièges fréquents
Accessoires ps_accessory Très fort Faible Compatibilités, B2B, pièces détachées Maintenance manuelle, oubli des variantes ou du stock
« Also bought » Commandes (ps_order_*), selon le module Moyen Potentiellement très élevé si calculé en ligne Compléments réels basés sur les données Recommandations de produits indisponibles, biais saisonnier, requêtes lourdes
Pack (produit pack) ps_product + ps_pack Fort mais lourd Moyen (front), complexe (stock/BO) Bundles stables, offres merchandising Stock du pack vs composants, SAV/avoirs, déclinaisons

Les noms de tables ci-dessus utilisent le préfixe ps_ à titre d’exemple : votre installation peut utiliser un autre préfixe.

Deux précisions opérationnelles souvent sous-estimées :

  • Multiboutique / multi-pays : une recommandation « globale » peut être fausse localement (catalogue différent, rupture sur un entrepôt, contraintes de transport). Si vous vendez en France et en Belgique, vous pouvez vouloir des recommandations par id_shop (voire par zone) pour éviter d’afficher un produit indisponible en Belgique mais disponible en France.
  • Packs et stock : PrestaShop propose différents modes de décrémentation du stock (pack seul, produits du pack, ou les deux selon la configuration). Cela change le comportement en cas de rupture d’un composant : le pack peut rester vendable ou devenir indisponible. C’est une décision métier à documenter, pas un détail technique.

Modéliser des recommandations produits et packs : signaux, contraintes et métriques

Une recommandation exploitable, ce n’est pas « les plus vendus ». C’est une fonction :

  • Entrées : produit consulté ou ajouté au panier, contexte (catégorie, prix, stock, compatibilité), profil (B2B/B2C, langue, devise), historique (si son utilisation est autorisée).
  • Sorties : une liste ordonnée avec un objectif (AOV, conversion, réduction des retours, déstockage).

Sur PrestaShop, vous avez trois familles de signaux faciles à industrialiser :

  1. Co-achat (order-based) : robuste, neutre, pertinent pour trouver de vrais compléments. C’est le modèle « people also bought ». Une référence classique est l’approche item-to-item décrite chez Amazon (Linden, Smith, York, 2003, Amazon.com Recommendations: Item-to-Item Collaborative Filtering, IEEE Internet Computing) : l’idée clé est de calculer des similarités entre produits, plutôt que de construire un modèle centré sur l’utilisateur, afin de rester scalable. (Lien de référence : https://doi.org/10.1109/MIC.2003.1167344)

  2. Similarité de contenu (content-based) : catégories, marques, attributs, caractéristiques, tags, prix. Très utile quand vous avez peu de commandes (nouveaux catalogues) ou beaucoup de nouveautés. Son défaut : si les données du catalogue sont incohérentes, les recommandations risquent de l’être aussi.

  3. Règles métier (rule-based) : « pour un appareil, proposer une batterie, une housse et un câble », « ne pas recommander une pièce incompatible », « bloquer les produits dont la marge est inférieure à X ». C’est un moyen essentiel de maîtriser le cross-selling en B2B ou dans l’industrie.

Avant d’automatiser, fixez des contraintes techniques mesurables :

  • Disponibilité : exclure les produits désactivés et vérifier la disponibilité réelle, y compris celle des déclinaisons, selon la configuration de gestion du stock.
  • Prix/positionnement : éviter de recommander un produit plus cher que le produit principal si l’intention est de proposer un complément, ou au contraire l’assumer pour de l’upsell.
  • Compatibilité : s’appuyer sur des caractéristiques (« modèle », « année », « référence ») ou sur une table métier.

Ajoutez deux contraintes « terrain » qui font souvent la différence en production :

  • Conformité et données : si vous personnalisez les recommandations à partir d’un historique utilisateur, vérifiez votre cadre RGPD/cookies (consentement, finalité, durée). Le recours à des co-achats agrégés, sans profil individuel, réduit les enjeux liés au profilage, sans les supprimer nécessairement.
  • Logistique : en France, la promesse de livraison (domicile ou point relais, délais) influence la conversion. Recommander un produit « complément » indisponible pour le même mode d’expédition, ou avec un délai beaucoup plus long, peut augmenter les abandons de panier. Sans aller jusqu’à un moteur logistique, un simple filtre « même entrepôt / même disponibilité » peut éviter des contre-performances.

Une bonne pratique simple consiste à combiner plusieurs signaux au lieu de choisir un seul algorithme. Exemple de score (sans sur-ingénierie) :

  • score = 0,6 * coachat_normalisé + 0,3 * similarité_contenu + 0,1 * boost_métier
  • puis des pénalités (rupture de stock, faible marge, incompatibilité, produit non livrable dans la zone).

Mini-scénario (typique B2C français) : un client consulte une tondeuse à barbe.

  • Le co-achat fait ressortir « huile d’entretien » et « lames de rechange » (très pertinent).
  • La similarité de contenu fait ressortir une autre tondeuse (moins pertinente pour du cross-sell, plutôt de l’upsell).
  • Une règle métier impose « nettoyant + housse », mais uniquement si le prix est inférieur à 20 % de celui du produit principal, pour éviter l’effet « trop cher, j’abandonne ».

Côté métriques, il faut distinguer qualité algorithmique et impact business. Qualité : CTR du bloc de recommandations, taux d’ajout au panier depuis une recommandation, couverture (part des fiches ayant au moins N recommandations), diversité (éviter de proposer toujours les mêmes 5 SKU). Impact : AOV (panier moyen), revenu par session et surtout effet sur la performance (TTFB, requêtes SQL). Pour instrumenter correctement, reliez cela à votre monitoring applicatif : voir l’approche KPI et prévention 503 dans Monitoring PrestaShop : KPI e-commerce, paiements et prévention des erreurs 503.

Un point « data » trop souvent oublié : la couverture. Une recommandation parfaite sur 10 % du catalogue peut sous-performer une recommandation correcte sur 80 % des pages. D’où l’intérêt d’avoir un fallback :

  • s’il n’y a pas de co-achat : basculer sur le contenu (même catégorie ou marque, avec filtre de prix) ;
  • si le contenu est trop pauvre : basculer sur les accessoires manuels ou des règles génériques.

Pipeline d’automatisation : batch, index MySQL, cache (sinon ça finit en 503)

Le piège classique : exécuter une requête de co-achat sur chaque page produit. Sur un site à fort trafic, c’est une recette directe pour saturer MySQL, surtout si ps_order_detail est volumineuse. Si vous devez retenir une règle : pré-calculer et servir les résultats en lecture rapide. Le cœur de PrestaShop n’est pas conçu pour faire de la recommandation lourde « en ligne » sans cache.

Concrètement, partez sur un pipeline en deux temps :

  1. Batch (cron ou commande Symfony) qui calcule des recommandations pour chaque produit, ou pour les produits actifs du moment.
  2. Stockage matérialisé : table dédiée ps_modulename_reco (recommandations ordonnées), ou réutilisation de ps_accessory si vous voulez injecter les recommandations dans le mécanisme natif des accessoires.

Le batch doit être index-friendly. Avant d’accuser PrestaShop, vérifiez vos index et vos plans d’exécution : Index MySQL : composer, mesurer avec EXPLAIN et optimiser les requêtes. Il n’existe pas d’index universel adapté à toutes les versions et à tous les volumes. Pour la requête ci-dessous, examinez notamment les index de ps_order_detail sur product_id et id_order, ainsi que ceux de ps_orders sur les colonnes utilisées. Vérifiez le plan d’exécution avec EXPLAIN avant d’ajouter ou de modifier des index.

Exemple de requête de co-achat (simplifiée, résultats regroupés au niveau du produit) :

-- Top produits co-achetés avec :productId dans les commandes valides des 180 derniers jours
SELECT od2.product_id, COUNT(DISTINCT od1.id_order) AS cnt
FROM ps_order_detail od1
JOIN ps_order_detail od2
  ON od2.id_order = od1.id_order
JOIN ps_orders o
  ON o.id_order = od1.id_order
WHERE od1.product_id = :productId
  AND od2.product_id != :productId
  AND o.valid = 1
  AND o.date_add >= (NOW() - INTERVAL 180 DAY)
GROUP BY od2.product_id
ORDER BY cnt DESC
LIMIT 20;

Cette requête compte les commandes distinctes, pas les quantités vendues. Si vous devez distinguer les déclinaisons, adaptez également le regroupement en conséquence.

Si vous exécutez cette requête à la volée, vous « payez » la jointure et l’agrégation à chaque hit. La bonne approche : lancer le calcul en batch, limiter la fenêtre de temps et surtout incrémentaliser (par exemple, ne recalculer que les produits vendus depuis le dernier run, ou calculer les paires (A, B) à partir des nouvelles commandes).

Deux variantes adaptées aux catalogues volumineux, sans moteur externe :

  • Recalcul ciblé : vous maintenez une table ps_modulename_dirty_product alimentée par un hook de validation de commande, ou par un job qui lit les dernières commandes ; le batch ne traite que ces produits.
  • Table de co-occurrence matérialisée : au lieu de recalculer à partir de order_detail à chaque fois, vous maintenez une table de couples (A, B) avec un compteur. Exemple de schéma (conceptuel) :
CREATE TABLE ps_modulename_copurchase (
  id_product_a INT NOT NULL,
  id_product_b INT NOT NULL,
  cnt INT NOT NULL DEFAULT 0,
  date_upd DATETIME NOT NULL,
  PRIMARY KEY (id_product_a, id_product_b),
  KEY idx_b (id_product_b),
  KEY idx_date (date_upd)
) ENGINE=InnoDB;

Ensuite, pour un produit A, vous lisez directement ps_modulename_copurchase (table plus petite et indexable), au lieu de parcourir ps_order_detail.

Enfin, servez les recommandations via un cache applicatif ou Redis. PrestaShop a plusieurs couches de cache (cache Symfony, cache PrestaShop, OPcache côté PHP), mais pour du cross-selling, un cache clé/valeur avec TTL est souvent le plus rentable. Pour éviter de bricoler, alignez-vous sur une stratégie claire (Redis, Varnish, CDN) : Cache PrestaShop : stratégie 2026 OPcache, Redis, Varnish et CDN.

Checklist de « garde-fous perf » (simple mais efficace) :

  • Calcul batch exécuté hors pic (souvent entre 2 h et 6 h, heure locale), avec durée maximale et reprise sur erreur.
  • Lecture front : 1 requête (ou 2 avec jointure produit), jamais de calcul de co-achat.
  • Cache TTL + invalidation (quand un produit passe en rupture de stock, est désactivé ou change de prix si vous filtrez selon le prix).
  • Slow query log activé et consulté après la mise en production (c’est là qu’on voit les vraies dérives).

Implémentation propre : module sans override + commande + Messenger

La base : un module qui expose (1) une commande Symfony bin/console pour recalculer, (2) un rendu front via hook (displayFooterProduct, displayProductAdditionalInfo, selon le thème), (3) une table de stockage et (4) une purge ou invalidation du cache.

Si vous partez sur une implémentation propre, gardez en tête les contraintes de livraison : pas d’override, pas de patch du cœur, solution testable en CI. Le cadrage est détaillé dans Module PrestaShop sans override : méthode de cadrage, tests et livraison du code. C’est particulièrement important ici : les overrides sur Product::getAccessoriesLight() ou sur les classes de commande sont une source d’incompatibilités lors des montées de version.

Pour éviter que le recalcul ne bloque le back-office (ou ne rende un cron trop long), utilisez une exécution asynchrone. Dans PrestaShop 8/9, Symfony Messenger peut être une option si votre version, votre configuration et votre infrastructure le permettent (RabbitMQ, Redis, SQS, etc.) : vous découpez un gros calcul en petits messages, avec reprises sur erreur et contrôle de la concurrence. Pour la mise en production, voir Symfony Messenger : config transports RabbitMQ, Redis ou SQS en production.

Schéma concret :

  • Une commande reco:enqueue envoie des messages RecomputeRecommendations(productId) pour une liste de produits.
  • Un handler consomme les messages, calcule les recommandations (SQL optimisée + filtres) et écrit dans ps_modulename_reco.
  • Un hook front lit uniquement ps_modulename_reco (lecture simple et indexée), puis applique un dernier filtre sur le stock et la visibilité.

Pseudo-code de handler (simplifié) :

final class RecomputeRecommendationsHandler
{
    public function __construct(private Db $db) {}

    public function __invoke(RecomputeRecommendations $msg): void
    {
        $productId = (int) $msg->productId;

        $rows = $this->db->executeS('/* SQL co-achat matérialisé */');

        // 1) purge
        $this->db->delete('modulename_reco', 'id_product='.(int)$productId);

        // 2) insertion ordonnée
        $pos = 1;
        foreach ($rows as $r) {
            $this->db->insert('modulename_reco', [
                'id_product' => $productId,
                'id_product_reco' => (int) $r['product_id'],
                'score' => (int) $r['cnt'],
                'position' => $pos++,
                'date_upd' => date('Y-m-d H:i:s'),
            ]);
            if ($pos > 13) break; // limiter
        }
    }
}

Deux détails d’implémentation qui évitent des bugs « invisibles » :

  • Scoper par boutique et langue si nécessaire : si vos prix ou visibilités diffèrent (multiboutique, groupes), stockez au minimum id_shop (et parfois id_currency si vous appliquez des filtres stricts sur les prix). Une recommandation « globale » peut afficher un produit non vendable à un groupe B2B.
  • Dernier filet côté front : même si vous filtrez en batch, refaites un filtre minimal au rendu (produit actif, visibilité dans le catalogue, stock). C’est votre assurance contre les décalages (un produit peut être désactivé entre deux exécutions).

Pour les packs, soyez pragmatiques : générer automatiquement un produit pack (ps_product de type pack + ps_pack) est lourd, a des impacts sur le stock et nécessite une gouvernance (prix du pack, SEO du pack, images, règles de taxe, etc.). Une approche plus stable consiste à :

  • Calculer un « bundle recommandé » (liste de 2 à 4 produits) et l’afficher comme pack virtuel sur la fiche produit.
  • Ajouter un bouton « Tout ajouter au panier » qui ajoute les articles séparément (ce n’est pas un vrai pack PrestaShop).

Cette approche évite une grande partie des problèmes de stock et de maintenance tout en conservant l’effet cross-selling. Si vous tenez à créer des packs réels, faites-le uniquement pour les bundles stables (paires souvent achetées ensemble sur plusieurs mois, avec une faible saisonnalité) et avec un workflow de validation dans le back-office plutôt qu’une création automatique sans contrôle.

Astuce « terrain » : si vous avez un catalogue avec des variantes, commencez par des bundles sur des produits simples (sans déclinaisons) ou sur des déclinaisons « best-seller » choisies explicitement. Automatiser la sélection de n’importe quelle déclinaison dans un pack est l’un des chemins les plus rapides vers des incohérences de stock.

Mesure, SEO et garde-fous : performance, tracking, observabilité

Un bloc de recommandations ajoute des liens internes et peut influer sur le maillage et la découverte des pages. Sur PrestaShop, la tentation est de l’afficher partout, tout le temps. Mauvaise idée si vous n’avez pas de contrôle : vous créez des liens vers des produits hors stock, des variantes non pertinentes et vous diluez la structure catégorie → produit. Traitez les recommandations comme un composant SEO : cohérence, pertinence et performance. Pour le cadrage SEO produit/catégorie (canonical, maillage, etc.), voir Référencement PrestaShop : optimiser fiches produits, catégories, canonical et hreflang.

Quelques règles SEO/UX concrètes (souvent suffisantes) :

  • Limitez à 4–8 recommandations visibles (au-delà, vous risquez d’augmenter le bruit et le coût côté front).
  • Évitez les « boucles » : si A recommande B, il n’est pas forcément utile que B recommande systématiquement A (diversité et maillage plus naturel).
  • Ne faites pas du cross-sell un second « listing catégorie » : privilégiez les compléments et les produits compatibles plutôt que des produits quasi identiques.
  • Si des pages produit sont en rupture longue, réfléchissez à masquer le bloc ou à basculer sur des « alternatives » (ce n’est plus du cross-sell, mais cela évite des liens peu utiles).

Côté performance, n’acceptez pas « ça passe » : vous devez vérifier que la recommandation ne rajoute pas 20 requêtes et 200 ms de TTFB. La checklist technique est la même que pour n’importe quel module front : profiler, compter les hooks exécutés, limiter les accès DB, mettre un cache. Si votre boutique est déjà fragile, commencez par corriger les basiques : Optimisation PrestaShop : réduire les requêtes SQL et les hooks et, si besoin, un audit reproductible : Audit performance PrestaShop : méthode reproductible et preuves terrain.

Pour la mesure produit, instrumentez proprement (GA4 ou autre) : événement view_item_list pour l’affichage du bloc de recommandations, select_item pour le clic sur une recommandation, et add_to_cart avec un paramètre item_list_name = "cross_sell". L’intérêt n’est pas de « faire du tracking », mais d’être capable de trancher : co-achat, règles ou similarité, quelle stratégie fonctionne le mieux sur vos segments ? Si vous exploitez déjà la recherche interne, vous avez une base solide de métriques à rapprocher : Recherche interne e-commerce : métriques GA4 et actions prioritaires.

Pour éviter les conclusions hâtives, structurez vos analyses autour d’un minimum de rigueur :

  • comparez à périmètre égal (mêmes catégories, mêmes périodes) ;
  • gardez un groupe témoin (par exemple, 5 à 10 % du trafic sans bloc ou avec un bloc « neutre ») ;
  • suivez une métrique de contrepartie : une hausse des clics accompagnée d’une baisse du taux de conversion global peut indiquer que le bloc distrait les visiteurs.

Enfin, l’observabilité : une recommandation mal codée peut se traduire par des pages lentes et des erreurs 503 intermittentes. Journalisez le temps de calcul du batch, le nombre de produits recalculés et les erreurs SQL, et exposez des métriques (Prometheus/StatsD/Netdata). Si vous externalisez une partie du système (moteur de recommandation, PIM, ERP), appliquez les mêmes exigences de santé, de délais d’attente, de reprises sur erreur et de traçabilité que pour des webhooks : Intégrations webhooks : observabilité, télémétrie et gestion de santé des connexions.

En bref : automatiser le cross-selling sur PrestaShop est parfaitement faisable, mais seulement si vous acceptez de sortir du « calcul à la volée » et de traiter la recommandation comme un pipeline de données (batch + index + cache + mesures). Le cœur fournit des points d’ancrage (accessoires, packs, hooks), pas une architecture de recommandation industrialisée. Si vous ne comblez pas cet écart proprement, vous obtiendrez soit des recommandations médiocres, soit un MySQL à genoux.


À lire aussi