Table des matières :
- Pourquoi la recherche MySQL de PrestaShop plafonne sur les gros catalogues
- Ce qu’apporte Elasticsearch (et ce que PrestaShop ne fera pas “magiquement”)
- Concevoir un index produit robuste : mapping, analyzers, multilingue, facettes
- Synchronisation catalogue → index : hooks, incrémental, et reindex sans downtime
- Construire des requêtes pertinentes : tolérance aux fautes, boosting, facettes et “zéro résultat”
- Exploitation en production : latence, cache, monitoring, sécurité et plan de secours
Pourquoi la recherche MySQL de PrestaShop plafonne sur les gros catalogues
Sur PrestaShop, la “recherche” en front (et une bonne partie du filtrage) finit très vite par retomber sur des patterns SQL coûteux : LIKE '%term%', jointures sur des tables de mots (ps_search_word, ps_search_index), ou du FULLTEXT MySQL qui reste limité dès qu’on combine multilingue, attributs, et scoring fin. Sur un catalogue à 200k+ produits avec déclinaisons, le nombre de tokens à maintenir explose, et la latence devient surtout… variable (pire cas) : requête rapide à chaud, puis 800 ms–2 s quand le buffer pool n’est plus “aligné”.
Le point dur, c’est le leading wildcard (%term%) : même avec de bons index, MySQL ne peut pas utiliser efficacement un index B-Tree pour retrouver des occurrences au milieu d’un champ. Ajoutez à ça les besoins e-commerce “réels” (filtres par marques, catégories, attributs, stock, fourchettes de prix, tri) et vous obtenez des requêtes où :
- le moteur scanne trop de lignes (ou trop de tokens),
- les jointures gonflent (surtout multiboutique + multilingue),
- le tri (
ORDER BY) arrive trop tôt sur un set de résultats encore énorme, - la charge est imprévisible (p95/p99 qui “décollent” au moindre cache froid).
Ce n’est pas “juste” une question d’index SQL. Vous pouvez améliorer des requêtes et vérifier les plans avec EXPLAIN, mais vous ne changerez pas le fait que MySQL n’est pas un moteur de recherche spécialisé (analyse linguistique, tolérance aux fautes, ranking, facettes à coût maîtrisé). Si vous êtes en train de bricoler des index composites pour sauver un ORDER BY sur un set de résultats déjà filtré, relisez les fondamentaux côté base : Index MySQL : optimiser WHERE, JOIN et ORDER BY avec EXPLAIN. Ça aide, mais ça ne remplace pas un index inversé.
Pour clarifier ce qui “coince” le plus souvent sur des catalogues volumineux, voici un aide-mémoire utile (sans tomber dans la théorie) :
| Symptôme observé | Cause fréquente côté MySQL/PrestaShop | Impact utilisateur |
|---|---|---|
| Autocomplete irrégulier | requêtes répétées + LIKE + absence de cache |
suggestions “en retard”, saisie saccadée |
| Résultats incohérents selon la langue | stopwords/stemming non adaptés, champs multilingues mal gérés | “je ne trouve pas X” alors que le produit existe |
| Facettes lentes | agrégations “simulées” via SQL/jointures, cardinalité élevée | filtres qui mettent 1–2 s à se mettre à jour |
| Tri “pertinence/vente/stock” coûteux | scoring business calculé après filtrage | pages de résultats instables, timeouts |
L’impact n’est pas seulement “serveur”. Une recherche lente se traduit en timeouts AJAX, en saisie auto-complétée saccadée, et en INP dégradé si vous faites du rendu côté client avec un back qui répond au hasard. Sur des boutiques “grands catalogues”, la recherche devient une brique UX structurante au même niveau que le cache.
Pour rester concret : dans beaucoup de parcours, la recherche est un multiplicateur de charge (beaucoup de petites requêtes) plutôt qu’une seule requête lourde. Par exemple, un utilisateur tape “chaussure trail” : vous déclenchez 6–12 appels (à chaque caractère ou presque) si vous ne debouncez pas proprement, et chaque appel fait travailler votre base sur un pattern défavorable. Si vous pilotez vos perfs avec des métriques web, reliez la recherche à vos objectifs Core Web Vitals (INP, LCP) : Core Web Vitals : actions concrètes pour améliorer LCP, INP et CLS.
Ce qu’apporte Elasticsearch (et ce que PrestaShop ne fera pas “magiquement”)
Elasticsearch est un moteur de recherche basé sur Lucene, conçu autour d’un index inversé (la structure qui mappe termes → documents). La page officielle d’Elastic décrit Elasticsearch comme un moteur distribué, exposé en REST, orienté recherche et analytics : https://www.elastic.co/elasticsearch/. Concrètement, vous échangez des requêtes SQL difficiles à optimiser contre des requêtes de recherche (JSON DSL) pensées pour le texte, la pertinence, et les agrégations.
Les bénéfices “e-commerce” les plus tangibles (quand c’est bien modélisé) :
- Tolérance aux fautes et variantes : accents (“café/cafe”), pluriels, petites fautes, ordre des mots.
- Pertinence ajustable : vous boostez le nom, la référence, puis des signaux business (stock > 0, marge, popularité).
- Facettes rapides : agrégations natives sur champs
keyword(marque, catégories, attributs), sans requêtes SQL explosives. - Latence plus stable : l’index est fait pour répondre vite, même quand le dataset grandit (à condition de dimensionner et configurer correctement).
Ce que ça change pour PrestaShop : vous ne “branchez” pas Elasticsearch comme on remplacerait MySQL. Vous construisez un pipeline d’indexation (export produit → documents JSON), vous définissez un mapping (types de champs, analyzers), puis vous servez la recherche (autocomplete, résultats, facettes) en interrogeant Elasticsearch. Le cœur de PrestaShop n’a pas de couche d’abstraction native pour ça : il faut un module (custom ou existant), des hooks, et de la discipline sur la synchro.
Deux points que PrestaShop ne fera pas “magiquement” à votre place (et qu’il vaut mieux annoncer dès le cadrage projet) :
- La qualité des données : si vos attributs sont hétérogènes (“Bleu”, “bleu”, “BLEU”), vous aurez des facettes polluées. Elasticsearch ne “nettoie” pas : il indexe ce qu’on lui donne.
- La pertinence métier : un bon ranking demande des itérations (A/B, analyse des requêtes sans résultat, ajustements de boost). Sans boucle de mesure, vous risquez juste de produire un moteur rapide… mais frustrant.
Contexte versions (à adapter à votre stack) : PrestaShop 8.1/9.1 (Symfony 6.4 côté back-office) et PHP 8.2–8.3 sont un couple courant en 2026 ; côté Elasticsearch, visez 8.x (JDK 17, sécurité activée par défaut). Pour le contexte PrestaShop 9, gardez en tête les changements de socle et de performances : PrestaShop 9 : nouveautés techniques Symfony 6.4, API et performances.
Enfin, un mot “terrain” (notamment en France/UE) : la recherche produit est souvent couplée à des logs de requêtes (pour analyser la pertinence). Dès que vous stockez des requêtes associées à un identifiant (cookie, compte, IP), vous touchez à des données personnelles potentielles : il faut cadrer rétention, accès, et minimisation. Ce n’est pas spécifique à Elasticsearch, mais l’ajout d’un composant et de dashboards fait souvent “déborder” l’observabilité.
Risques à annoncer clairement : l’indexation est CPU/RAM intensive, un “full reindex” peut saturer I/O, et une mauvaise config (shards/refresh) peut tuer la latence.
Concevoir un index produit robuste : mapping, analyzers, multilingue, facettes
Le mapping Elasticsearch est l’endroit où 80% des perfs et de la pertinence se gagnent (ou se perdent). Un champ “nom produit” n’est pas un TEXT générique : vous voulez généralement un multi-field text (analysé) + keyword (filtre/tri exact), des types numériques corrects pour les prix, et des champs structurés pour les facettes (marque, catégories, attributs).
Pour une boutique française, deux détails linguistiques reviennent tout le temps :
- Accents et apostrophes (“l’été”, “d’entretien”) : un combo
lowercase+asciifoldingévite beaucoup de “zéro résultat” inutiles. - Unités, tailles, références (“42”, “42.5”, “2L”, “500ml”, “PS-1234”) : il faut souvent indexer un champ “référence” en
keyword(exact) et prévoir une stratégie de recherche qui ne casse pas les tokens de manière absurde (sinon “PS-1234” devient introuvable).
C’est aussi là que vous décidez si vous gérez les variantes (déclinaisons) en documents séparés, en nested, ou en “flattened” — chaque choix a des implications sur les agrégations et les performances.
Voici une synthèse rapide des trois modèles les plus courants pour des catalogues PrestaShop (produit + déclinaisons) :
| Modèle | Avantage | Inconvénient | Quand l’utiliser |
|---|---|---|---|
| 1 doc = 1 produit (avec attributs “aplatis”) | simple, rapide, index compact | facettes “par déclinaison” difficiles (stock/couleur/taille) | si les déclinaisons influencent peu la recherche |
| 1 doc = 1 déclinaison | stock/prix au niveau déclinaison, facettes précises | index plus gros, déduplication du contenu | mode, pièces détachées, catalogues où la déclinaison est le vrai SKU |
nested (déclinaisons dans le doc produit) |
cohérence produit + requêtes fines | requêtes/aggs plus complexes, coût CPU | si vous devez filtrer sur plusieurs champs de variante de façon stricte |
Sur le multilingue, évitez les demi-mesures. Deux stratégies dominent : (1) un index par langue (et souvent par boutique / groupe), ou (2) un index unique avec champs suffixés (name_fr, name_en, …). L’index par langue simplifie les analyzers (stemming, stopwords, elision) et la pertinence ; l’index unique simplifie l’infra mais complexifie requêtes et scoring. Pour un gros catalogue, l’index par langue est souvent plus prévisible en latence parce que vous réduisez le nombre de champs évalués.
Exemple minimal de mapping orienté e-commerce (facettes + recherche + tri), à enrichir selon vos données (Elasticsearch 8.x) :
PUT ps_products_fr_v1
{
"settings": {
"index": {
"number_of_shards": 3,
"number_of_replicas": 1,
"refresh_interval": "5s"
},
"analysis": {
"filter": {
"autocomplete_filter": {
"type": "edge_ngram",
"min_gram": 2,
"max_gram": 15
}
},
"analyzer": {
"fr_autocomplete": {
"type": "custom",
"tokenizer": "standard",
"filter": ["lowercase", "asciifolding", "autocomplete_filter"]
},
"fr_search": {
"type": "custom",
"tokenizer": "standard",
"filter": ["lowercase", "asciifolding"]
}
}
}
},
"mappings": {
"properties": {
"id_product": {"type": "integer"},
"active": {"type": "boolean"},
"name": {
"type": "text",
"analyzer": "fr_search",
"fields": {
"keyword": {"type": "keyword", "ignore_above": 256},
"autocomplete": {"type": "text", "analyzer": "fr_autocomplete"}
}
},
"reference": {"type": "keyword"},
"price_tax_incl": {"type": "scaled_float", "scaling_factor": 100},
"manufacturer": {"type": "keyword"},
"categories": {"type": "keyword"},
"attributes": {"type": "keyword"},
"stock_qty": {"type": "integer"},
"updated_at": {"type": "date"}
}
}
}
Notez le scaled_float pour le prix (évite les surprises de flottants), les keyword pour agrégations/facettes, et un champ autocomplete dédié (edge n-gram). Sur des catalogues multi-marques, l’usage de synonymes (ex. “tv”/“télévision”) peut être utile, mais il faut le versionner et le tester : un mauvais fichier de synonymes dégrade la précision et augmente la recall inutile.
Un point souvent sous-estimé : edge n-gram augmente la taille de l’index (parfois fortement) parce qu’il génère beaucoup de tokens. Deux garde-fous simples évitent des surprises :
- mettez l’autocomplete sur un champ dédié (comme ci-dessus), pas sur tous les champs,
- limitez l’autocomplete à des champs courts (nom, marque) et renvoyez des suggestions avec un
sizefaible.
Enfin, pensez “facettes propres” : si vos catégories sont hiérarchiques, vous pouvez stocker à la fois l’identifiant technique (stable) et un libellé (affichage). En France, c’est particulièrement utile dès que vous avez des libellés marketing qui changent (“Promos été”, “Bonnes affaires”) alors que l’ID reste stable pour l’index et les URLs.
Synchronisation catalogue → index : hooks, incrémental, et reindex sans downtime
Indexer “une fois” ne suffit pas : PrestaShop est un système vivant (stocks, prix spécifiques, règles panier, désactivations, réassort). La question n’est pas “comment indexer”, c’est “comment ne pas diverger”. Le pattern robuste en production consiste à (a) capter les événements via hooks, (b) écrire un outbox transactionnel en base (table de jobs d’indexation), puis (c) traiter en asynchrone avec retry et backoff. Sans outbox, vous perdez des mises à jour dès que le worker tombe ou qu’une requête Elasticsearch timeoute.
Un schéma minimal d’outbox (simple mais efficace) ressemble à ça :
entity_type(product,combination,stock,price, …)entity_id(id produit / id déclinaison)action(upsert,delete)payload_hash(évite de retraiter le même contenu)attempts,next_run_at,last_errorcreated_at,updated_at
Côté worker, une règle vous sauve souvent : idempotence. Autrement dit, si le même job est traité deux fois (réseau instable, retry), le résultat doit être le même. Pour ça, vous pouvez vous appuyer sur updated_at et/ou une version applicative dans le document, et ignorer les mises à jour “plus anciennes”.
Côté hooks, prévoyez au minimum : création/modif produit, modif de déclinaison, modif de prix, modif d’activation, modif de catégorie, et surtout modif de stock (souvent déclenchée par ERP/WMS). Sur PrestaShop 9+, écrire un module “propre” implique d’exposer des services Symfony et d’éviter les appels directs dispersés.
Très concret : si votre stock est mis à jour par lots (ERP) toutes les 5 minutes, vous évitez de lancer 10 000 requêtes “une par SKU”. Vous batchiez (par exemple 200–1 000 documents) et vous utilisez du bulk côté Elasticsearch (même si vous gardez la logique applicative dans PrestaShop). Le gain n’est pas “un micro-optimisation” : c’est souvent la différence entre une synchro qui tient en continu et une synchro qui s’effondre au premier pic.
Pour des patterns d’intégration plus larges (ERP qui pousse des stocks en quasi temps réel), vous pouvez vous inspirer d’une approche bi-directionnelle et idempotente : Intégration e-commerce Odoo : architecture bi-directionnelle et synchronisation stocks.
Pour le reindex complet (migration mapping, ajout de champs, changement d’analyzer), la seule approche acceptable sur une boutique qui vend est l’alias swap : vous indexez dans ps_products_fr_v2, vous validez, puis vous basculez l’alias ps_products_fr vers v2 en une opération atomique. Ça évite le downtime et vous permet un rollback.
Checklist “alias swap” (simple, mais à faire proprement) :
- indexer v2 en parallèle (sans toucher au front),
- comparer volumes (nombre de docs, produits actifs, etc.),
- exécuter un jeu de requêtes de test (top 50 recherches),
- basculer l’alias en une opération,
- garder v1 quelques jours (rollback rapide), puis supprimer.
Attention : un full reindex met votre MySQL sous pression (lecture produits, jointures, images, attributs). Avant de lancer ce genre d’opération, vos routines de maintenance DB doivent être solides : Base de données PrestaShop : routine de maintenance et nettoyage automatisé.
Construire des requêtes pertinentes : tolérance aux fautes, boosting, facettes et “zéro résultat”
Pour éviter le “catalogue introuvable” à la moindre faute de frappe, vous combinez analyzers (normalisation), requêtes multi_match et éventuellement fuzziness. Une remarque de base, souvent oubliée : si votre mapping est mauvais (mauvais tokenizer, champs keyword interrogés comme du texte), aucune magie côté query DSL ne sauvera la pertinence. C’est un principe classique en recherche d’information : l’index inversé et la qualité d’analyse (tokenisation/normalisation) conditionnent le rappel et la précision (cf. Manning, Raghavan, Schütze, Introduction to Information Retrieval, 2008).
Exemple de requête “recherche + filtres + scoring business” (à adapter) :
POST ps_products_fr/_search
{
"size": 24,
"query": {
"function_score": {
"query": {
"bool": {
"must": [
{
"multi_match": {
"query": "air max 90",
"fields": ["name^4", "reference^3", "categories^1", "manufacturer^1"],
"type": "best_fields",
"fuzziness": "AUTO",
"operator": "and"
}
}
],
"filter": [
{"term": {"active": true}},
{"range": {"price_tax_incl": {"gte": 5000, "lte": 25000}}},
{"terms": {"categories": ["chaussures", "running"]}}
]
}
},
"functions": [
{"filter": {"range": {"stock_qty": {"gt": 0}}}, "weight": 1.3}
],
"boost_mode": "multiply"
}
},
"aggs": {
"by_manufacturer": {"terms": {"field": "manufacturer", "size": 20}},
"by_category": {"terms": {"field": "categories", "size": 20}}
}
}
Deux points pratiques : (1) faites des facettes via agrégations sur des champs keyword, sinon vous payez cher et vous obtenez des résultats incohérents ; (2) traitez explicitement le cas “zéro résultat” (fallback sur un match plus permissif, suggestions, ou affichage de catégories).
Un “zéro résultat” n’est pas juste un échec technique : en e-commerce, c’est souvent un signal produit/merchandising (“vous n’avez pas ce que les gens cherchent” ou ils ne savent pas comment vous l’appelez). Trois réponses utiles, sans surcomplexifier :
- Assouplir la requête (retirer
operator: and, baisser les boosts stricts) uniquement en fallback. - Proposer des alternatives via catégories/brands dominantes dans la requête (si vous les extrayez).
- Tracer la requête et le contexte (langue, boutique, filtres) pour améliorer synonymes, orthographes, ou données.
Mesurez : taux de recherches sans résultat, CTR sur résultats, temps médian p95 des endpoints /search et /suggest. Sans instrumentation, vous “optimisez” à l’aveugle. Un mini-tableau de KPI (facile à suivre mensuellement) :
| KPI | Pourquoi c’est utile | Seuil d’alerte (indicatif) |
|---|---|---|
% zéro résultat |
qualité catalogue + pertinence | hausse soudaine après déploiement |
p95 latence /search |
UX réelle sous charge | p95 qui double sans hausse de trafic |
| CTR sur résultats | pertinence + présentation | baisse sur les top requêtes |
| Temps de reindex | capacité infra + pipeline | dérive après ajout de champs/analyzers |
Exploitation en production : latence, cache, monitoring, sécurité et plan de secours
Elasticsearch accélère la recherche, mais n’abolit pas les lois de la physique : vous devez contrôler la latence côté API, et éviter de solliciter le cluster inutilement. Pour l’autocomplete, mettez un endpoint dédié, très court (size faible, champs limités, track_total_hits=false). Pour les pages de résultats, cachez ce qui peut l’être : résultats de facettes populaires, pages sans personnalisation, etc.
Deux optimisations “peu sexy” mais très rentables côté front :
- Debounce côté JS (ex. 150–250 ms) pour éviter de déclencher une requête par frappe.
- Annulation des requêtes en vol (si l’utilisateur continue à taper) pour éviter de saturer inutilement l’API.
Si vous avez déjà une stratégie de cache HTTP avancée, vous pouvez fragmenter proprement certaines zones (ex. bloc de facettes) : Varnish Cache : configuration ESI pour optimiser le cache par fragments.
Sur le monitoring, ne vous contentez pas de “ça répond”. Activez et consultez : slow logs, stats de GC/heap, saturation CPU, temps de refresh, taux de cache hit (query cache / request cache), et surtout la distribution de latence (p50/p95/p99) par type de requête. Kibana aide, mais un export vers votre stack (Prometheus/Grafana, ELK, Datadog…) est souvent plus utile. Et côté PrestaShop/Symfony, surveillez aussi les timeouts HTTP et la consommation mémoire des workers d’indexation.
Un point de dimensionnement souvent oublié : la stabilité. Pour une boutique France/Europe, ce qui compte n’est pas seulement la latence “moyenne”, mais le comportement au pic (soldes, opérations emailing). D’où l’intérêt de suivre :
- saturation CPU (sur nœuds data et nœuds ingest si vous en avez),
- pression mémoire (heap) et fréquence des GC,
- files d’attente côté workers (outbox qui grossit = divergence qui arrive),
- erreurs de timeout côté PHP (sinon vous verrez juste des 500 côté front).
Enfin, sécurité et résilience. Elasticsearch exposé sur Internet sans TLS + auth, c’est non (et ça arrive encore). Activez TLS, utilisez des API keys avec privilèges minimaux, et isolez réseau (VPC, firewall). Côté PrestaShop, la surface d’attaque augmente dès que vous ajoutez des daemons, des cron, des credentials : relisez vos exigences 2026 : Sécurité PrestaShop 2026 : risques majeurs et protections professionnelles.
Et prévoyez un mode dégradé : si Elasticsearch est down, mieux vaut basculer sur une recherche MySQL simplifiée (ou afficher une page d’erreur explicite) que de laisser le front partir en timeout. Deux approches pragmatiques :
- Fail-open contrôlé : si ES ne répond pas en X ms, bascule vers une recherche basique (sur nom/référence) et désactive temporairement certaines facettes.
- Fail-closed UX : afficher une page “recherche momentanément indisponible” + liens catégories top (évite les timeouts et l’INP catastrophique).
Pour un gros catalogue, “PrestaShop + ElasticSearch” n’est pas un gadget : c’est un chantier d’architecture (modèle de données, pipeline d’indexation, requêtes, observabilité). Bien fait, vous gagnez une latence stable, des facettes propres, et un levier direct sur la pertinence. Mal fait, vous ajoutez un système distribué de plus… qui diverge et tombe au pire moment. À vous de choisir lequel des deux vous livrez en prod.




