Recherche interne PrestaShop : optimiser suggestions live et tolérance aux fautes

Guide pratique pour transformer la recherche interne PrestaShop en service : suggestions live rapides, typo‑tolérance, indexation asynchrone, cache et métriques.

Écrans d'ordinateur avec code et interface utilisateur dans un bureau sombre.

Table des matières :

  1. Comprendre le pipeline de recherche natif PrestaShop (et ses angles morts)
  2. Suggestions live : contrat de performance (latence < 150 ms p95) et implémentation front
  3. Endpoint AJAX côté PrestaShop 9 (Symfony) : requêtage, ranking et payload minimal
  4. Tolérance aux fautes : du bricolage SQL à la vraie recherche typo-tolérante
  5. Indexation et fraîcheur des données : hooks, files, et tâches CLI
  6. Cache, limites et sécurité : Redis, HTTP, rate limiting
  7. Mesurer, profiler, corriger : KPIs recherche interne et observabilité

La recherche interne PrestaShop est souvent traitée comme un détail “UX”. En pratique, c’est un sous-système critique qui touche à tout : modèle de données multi-boutique, i18n, stock, règles de visibilité, performances SQL, cache applicatif, et surtout latence perçue côté client. Si vous visez des suggestions live (search-as-you-type / autocomplete) et une tolérance aux fautes (typos, accents, variantes), le cœur PrestaShop n’est pas “cassé”, mais il est structurellement limité : il a été conçu pour une requête pleine page, pas pour 5–15 requêtes par seconde pendant une saisie.

Contexte versions pour les exemples ci-dessous : PrestaShop 9.x (Symfony 6.4 côté BO/FO), PHP 8.2/8.3, MySQL 8 (ou MariaDB équivalent). Les principes restent valables en 8.1, mais l’implémentation Symfony est plus propre en 9.

Un point “terrain” à garder en tête : si votre boutique sert majoritairement un public en France/Belgique/Suisse, héberger l’application et le moteur de recherche dans une région proche (même pays ou au minimum même zone UE) aide à tenir un bon p95. À l’inverse, une infra “éclatée” (FO en Europe, moteur aux US) transforme vite un endpoint live en composant instable, même si chaque brique est “rapide” isolément.

Comprendre le pipeline de recherche natif PrestaShop (et ses angles morts)

Le cœur PrestaShop “indexe” historiquement via les tables ps_search_word / ps_search_index et exécute des recherches en pondérant des champs (nom, référence, description, etc.). Sur le papier, c’est acceptable pour un petit catalogue. En charge, c’est fragile : chaque frappe en suggestions live devient une mini recherche qui implique des jointures, des LIKE/comparaisons de mots, et un ranking basique. Résultat typique : p95 > 300–500 ms sur des catalogues moyens si vous n’avez pas verrouillé les index et la stratégie de cache.

Ce que beaucoup découvrent trop tard : la recherche “native” n’est pas seulement une requête SQL, c’est un pipeline avec des compromis implicites :

  • Tokenisation / normalisation : séparation des mots, gestion des accents et apostrophes, caractères spéciaux, longueur minimale des mots indexés. En français, un terme comme l'iPhone peut se fragmenter d’une façon qui surprend si vous ne normalisez pas au même endroit (front, endpoint, index).
  • Multi-boutique / multi-langues : un mot peut exister dans une langue et pas dans l’autre ; un produit peut être visible dans une boutique et pas dans une autre ; un même SKU a des libellés différents.
  • Règles de visibilité : produits actifs, accessibles à un groupe client, catalog mode, “afficher les produits en rupture” ou non, etc. Un “bon” résultat de recherche doit respecter les mêmes règles que les pages catégorie/produit, sinon vous obtenez des suggestions cliquées… qui mènent à une 404/produit inaccessible.

Deuxième limite : la tolérance aux fautes n’est pas native au sens moderne (fuzzy matching, correction orthographique, normalisation agressive). Le moteur interne travaille surtout par mots présents dans l’index. Si l’utilisateur tape iphnoe au lieu de iphone, vous obtenez du “zéro résultat” sauf si vous avez des synonymes, ou si vous implémentez un bricolage (Levenshtein en SQL, trigrams maison…), qui est rarement tenable.

Troisième point, souvent sous-estimé : la recherche PrestaShop n’est pas une API pensée pour le front. Le module de barre de recherche du thème peut déclencher de l’AJAX, mais le format des résultats et la granularité des filtres (par langue, groupe client, stock, catégories) ne sont pas un contrat stable. Si vous partez sur une implémentation sérieuse, traitez la recherche live comme un service : un endpoint dédié, une latence cible, une stratégie de cache, et des garanties de sécurité.

Mini-scenario (typique) : catalogue 25 000 produits, 2 langues (FR/NL) et 2 boutiques. Sans cloisonnement (shop_id, lang_id), vous vous retrouvez avec des suggestions qui “mixent” des libellés, ou pire : des produits de l’autre boutique. Le problème n’est pas l’autocomplete en soi, mais le fait que la recherche n’a pas été pensée comme un service multi-tenant.

Suggestions live : contrat de performance (latence < 150 ms p95) et implémentation front

Une suggestion live correcte impose un contrat brutal : latence réseau + traitement + rendu. Un objectif réaliste en production e-commerce est p95 < 150 ms pour la réponse JSON (idéalement < 100 ms sur la même région), sinon l’utilisateur “sur-tape” et vos résultats clignotent. Ça implique du debounce côté front (pour limiter le QPS), l’annulation des requêtes en vol, et un payload minimal (pas de description, pas de calcul prix complexe si vous n’en avez pas besoin).

Un repère simple : si vous visez 150 ms p95 “end-to-end”, votre budget serveur est rarement > 50–80 ms (le reste part en réseau, TLS, parse JSON, rendu DOM). À ce niveau, le moindre calcul “prix + règles + taxe + devise + arrondi” par item peut vous faire rater la cible, surtout si vous renvoyez 8–10 suggestions.

Côté JavaScript, l’erreur classique consiste à empiler des requêtes, puis à afficher des réponses “en retard”. Utilisez AbortController pour annuler la requête précédente quand l’utilisateur tape. MDN Web Docs — AbortController résume l’intention de l’API sans ambiguïté : « The AbortController interface represents a controller object that allows you to abort one or more Web requests as and when desired. »

Exemple minimal (front) : debounce 120 ms + abort + seuil de 2 caractères. En production, ajoutez aussi un cache mémoire (Map) sur les préfixes les plus fréquents.

let controller = null;
let timer = null;

function liveSuggest(q) {
  if (q.length < 2) return Promise.resolve([]);

  if (controller) controller.abort();
  controller = new AbortController();

  return fetch(`/module/yoursearch/suggest?q=${encodeURIComponent(q)}`, {
    signal: controller.signal,
    headers: { 'Accept': 'application/json' },
  }).then(r => r.ok ? r.json() : []);
}

const input = document.querySelector('#search_widget input[type="search"]');
input.addEventListener('input', (e) => {
  const q = e.target.value.trim();
  clearTimeout(timer);
  timer = setTimeout(async () => {
    const items = await liveSuggest(q);
    renderSuggestions(items);
  }, 120);
});

Dernier point front, rarement traité : l’accessibilité et le SEO interne. Une liste de suggestions doit être un composant ARIA type listbox (navigation clavier, focus). Et si vous exposez des catégories / marques / requêtes populaires, vous créez un graphe interne utile (sans indexer les endpoints AJAX). L’objectif est d’augmenter le taux de clic sur suggestions (CTR) tout en réduisant la part de requêtes “zéro résultat”.

Checklist UX (simple, mais qui change tout) :

  • afficher un état “chargement” (discret) si la réponse dépasse ~150–200 ms ;
  • ne pas “sauter” l’UI : conserver la hauteur de la liste (évite le layout shift) ;
  • gérer explicitement “aucun résultat” (et proposer une action : voir tous les résultats, contact, catégories proches) ;
  • journaliser côté front les erreurs réseau (utile quand le problème vient d’un adblock, d’un WAF trop strict, etc.).

Endpoint AJAX côté PrestaShop 9 (Symfony) : requêtage, ranking et payload minimal

En PrestaShop 9, vous avez intérêt à éviter les contrôleurs legacy pour ce cas d’usage. Faites un module avec un contrôleur Symfony FO dédié, une route, et un service “SearchProvider” (injection de dépendances) qui encapsule la logique de recherche (SQL natif, Meilisearch, Elasticsearch…). Ça rend testable, et ça vous évite d’empiler du code dans un FrontController historique.

Pré-requis : connaître la structure d’un module PS9 (services, routes). Si vous n’êtes pas à l’aise avec l’architecture Symfony dans PrestaShop, commencez par l’article sur la structure et les bonnes pratiques : Module PrestaShop 9 : structure, services et bonnes pratiques Symfony.

Exemple de contrôleur (simplifié) : on force un contexte (langue, shop), on normalise la query, on limite la taille du payload, et on coupe net dès que la query est trop courte.

<?php

namespace Vendor\Module\Controller\Front;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use Vendor\Module\Search\SuggestService;

final class SuggestController extends AbstractController
{
    public function __construct(private readonly SuggestService $suggest) {}

    #[Route('/module/yoursearch/suggest', name: 'yoursearch_suggest', methods: ['GET'])]
    public function __invoke(Request $request): JsonResponse
    {
        $q = trim((string) $request->query->get('q', ''));
        if (mb_strlen($q) < 2) {
            return $this->json([]);
        }

        $limit = max(1, min(10, (int) $request->query->get('limit', 8)));
        $items = $this->suggest->suggest($q, $limit);

        return $this->json($items, 200, [
            'Cache-Control' => 'private, max-age=10',
        ]);
    }
}

Le vrai travail est dans SuggestService : décider du ranking (exact match > prefix match > fuzzy), filtrer la visibilité (produits actifs, en stock si nécessaire, disponibilité par groupe client), et surtout éviter les N+1 sur prix/attributs. Pour des suggestions live, renvoyez typiquement : id_product, name, url, image_small, éventuellement price si vous avez déjà la donnée pré-calculée. Si vous calculez le prix à la volée via toutes les règles de prix (specific prices, taxes, devises), vous allez exploser votre latence.

Pour garder un endpoint stable, formalisez votre “contrat” en interne : ce que l’endpoint fait toujours, et ce qu’il ne doit jamais faire.

Décision Recommandation pour suggestions live Risque si ignoré
Taille du payload 5 champs max + image petite surcharge réseau + rendu DOM lent
Calcul prix pré-calcul / cache / optionnel latence variable, p95 instable
Ranking exact > prefix > fuzzy, stable “résultats qui dansent” et CTR en baisse
Filtrage visibilité au niveau query/service produits non accessibles en suggestions
Dépendances contexte shop/lang/group/currency explicites résultats incohérents multi-boutique

Deux détails d’implémentation qui évitent des bugs “fantômes” :

  • Normalisation : mettez en place une normalisation identique entre index et requête (minuscules, accents, espaces multiples). Si vous utilisez un moteur dédié, utilisez ses analyzers/filters (ex. asciifolding) plutôt que de bricoler au cas par cas dans le code.
  • Encodage et sécurité sortie : même si vous renvoyez du JSON, vous renvoyez du texte “catalogue” (noms produits, marques). Assurez-vous que votre rendu front échappe correctement (évite qu’un libellé mal saisi en BO devienne un XSS).

Pour diagnostiquer ce qui coûte cher, activez le profiling SQL et mesurez au lieu de “deviner” : PrestaShop debug profiling : activer et analyser performances SQL et, côté MySQL, Requêtes MySQL lentes PrestaShop : activer slow query log. Un endpoint live qui déclenche des requêtes à 50–200 ms finira en 503 sous trafic, même si le reste du site “va bien”.

Tolérance aux fautes : du bricolage SQL à la vraie recherche typo-tolérante

Soyons clairs : faire de la tolérance aux fautes “correcte” en SQL sur MySQL/MariaDB est un piège. Oui, vous pouvez coder une distance de Levenshtein en SQL, ou générer des trigrammes dans une table annexe. Mais le coût CPU par requête explose, et vous finissez par dimensionner votre base pour une fonctionnalité qui ne devrait pas vivre dans la base transactionnelle. Même avec de bons index, ce genre de logique coûte cher et devient ingérable dès que vous ajoutez multi-langues + attributs + stock.

Dans l’écosystème recherche, la tolérance aux fautes est un problème “résolu” depuis longtemps par les moteurs dédiés (Lucene/Elasticsearch/OpenSearch, Meilisearch, etc.). Elastic le formule explicitement dans sa doc : Elastic Reference — Fuzziness. Autrement dit : fuzzy matching, c’est un algorithme + des structures d’index adaptées, pas une série de LIKE.

Deux approches réalistes sous PrestaShop :

Un bon moyen de décider : listez vos exigences “métier” et regardez lesquelles sont natives :

  • Autocomplete rapide et pertinent (préfixes) ;
  • Tolérance aux fautes (typos) ;
  • Gestion fine du français (accents, apostrophes, pluriels) ;
  • Synonymes (ex. “tel” ↔ “téléphone”, “hdd” ↔ “disque dur”) ;
  • Ranking contrôlable (brand boost, stock boost, marge, nouveautés) ;
  • Multi-index par boutique / langue.

Côté configuration, l’erreur courante est de “tout activer” et de se retrouver avec des suggestions incohérentes. Exemple Meilisearch : vous ajustez la tolérance (taille minimale des mots pour accepter des typos), vous déclarez des synonymes, et vous fixez les attributs recherchables/affichés pour éviter d’exfiltrer des champs inutiles.

{
  "searchableAttributes": ["name", "reference", "brand", "categories"],
  "displayedAttributes": ["id", "name", "url", "image_small", "price"],
  "typoTolerance": {
    "enabled": true,
    "minWordSizeForTypos": {"oneTypo": 4, "twoTypos": 8}
  },
  "synonyms": {
    "ssd": ["solid state drive"],
    "tel": ["telephone", "téléphone"]
  }
}

Sur Elasticsearch, pour un autocomplete propre, vous avez généralement deux patterns : search_as_you_type ou un analyzer edge_ngram. Exemple ultra-simplifié avec search_as_you_type (plus maintenable qu’un edgengram bricolé) et une requête multimatch + fuzziness :

PUT products
{
  "mappings": {
    "properties": {
      "name": {"type": "search_as_you_type"},
      "reference": {"type": "keyword"},
      "active": {"type": "boolean"}
    }
  }
}

GET products/_search
{
  "size": 8,
  "query": {
    "bool": {
      "filter": [{"term": {"active": true}}],
      "must": [{
        "multi_match": {
          "query": "iphnoe",
          "type": "bool_prefix",
          "fields": ["name", "name._2gram", "name._3gram"],
          "fuzziness": "AUTO"
        }
      }]
    }
  }
}

Point d’attention “qualité” : la tolérance aux fautes doit rester contrôlée. Si vous acceptez trop de typos sur des mots courts (2–3 lettres), vous augmentez le bruit (résultats “hors sujet”) et vous dégradez le CTR. D’où l’intérêt de seuils (ex. 1 typo à partir de 4 lettres).

Indexation et fraîcheur des données : hooks, files, et tâches CLI

La qualité des suggestions live dépend autant de l’index que du moteur. Un index “stale” (prix, stock, activations) génère des suggestions inutiles et des clics morts. Dans PrestaShop, vous devez traiter l’indexation comme un pipeline d’événements : création/édition produit, mise à jour stock, changement de prix, traduction, changement de catégorie, etc. L’erreur est de recalculer l’index “en bloc” à chaque modification : vous allez dégrader le BO et vous perdrez en fraîcheur.

Concrètement, captez les événements via hooks, poussez une tâche dans une file (Redis Streams, RabbitMQ, ou à défaut table SQL de jobs), et indexez en asynchrone. Pour identifier précisément quels hooks sont déclenchés et lesquels sont dynamiques selon le thème/modules, appuyez-vous sur : Hooks PrestaShop : rechercher et identifier les hooks dynamiques. L’objectif : indexation incrémentale, idempotente, et retryable.

Exemples de hooks fréquemment utilisés (selon versions/modules) pour déclencher une indexation incrémentale :

  • actionObjectProductAddAfter, actionObjectProductUpdateAfter, actionObjectProductDeleteAfter (cycle de vie produit),
  • actionUpdateQuantity (stock),
  • actionObjectCategoryUpdateAfter (catégories),
  • actionObjectSpecificPriceUpdateAfter (promos / prix spécifiques) si votre index inclut des signaux de prix.

Le principe : vous ne cherchez pas l’exhaustivité “dans le code”, vous cherchez la couverture des événements qui changent les résultats (visibilité, texte indexé, signaux de ranking, disponibilité).

Pour l’exécution, privilégiez des commandes CLI (cron) plutôt que des appels HTTP “secrets” : c’est plus observable et plus contrôlable. Si vous avez besoin d’un rappel sur l’écosystème CLI, voir : Commandes CLI PrestaShop : liste, catégories et options d’aide. En production, le pattern viable est :

  • hook → enfile product_id + shop_id + lang_id + type d’événement,
  • worker CLI (toutes les 30 s / 1 min) → batch 100–500 docs, push vers moteur,
  • métriques : backlog, temps moyen d’indexation, taux d’échec.

Astuce pragmatique : conservez un “mode dégradé” si l’index n’est pas à jour (ex. fallback sur recherche simple) et un garde-fou côté front (ex. masquer l’autocomplete si le endpoint est en erreur). Une recherche live qui renvoie 500/timeout en boucle est pire que pas de recherche live du tout.

Cache, limites et sécurité : Redis, HTTP, rate limiting

Les suggestions live génèrent un trafic “bruité” (mêmes préfixes, mêmes produits). Le cache est votre premier levier, mais pas n’importe comment. Niveau 1 : cache applicatif (en mémoire par worker PHP, si vous avez un runtime long type Swoole/RoadRunner, sinon limité). Niveau 2 : cache partagé Redis sur les réponses par préfixe, scindées par (shop, lang, currency, group, prefix) avec un TTL court (10–60 s). Niveau 3 : cache HTTP privé côté navigateur si votre payload ne dépend pas d’éléments sensibles.

Pour éviter les pièges, explicitez votre clé de cache. Exemple de clé robuste (conceptuellement) :

  • suggest:{shopId}:{langId}:{groupId}:{currencyId}:{normalizedPrefix}

Si vous oubliez groupId ou currencyId, vous risquez de servir des prix ou des produits visibles/invisibles selon le segment client. Si vos suggestions n’affichent pas les prix et ne dépendent pas du groupe, vous pouvez simplifier (et gagner beaucoup en hit rate).

Pour Redis, ne l’installez pas “à l’arrache” : durcissement réseau, auth, persistance, monitoring mémoire. Références internes utiles : Redis sur Linux : installation, configuration et sécurisation production et côté PrestaShop : Redis PrestaShop : configurer le cache sur VPS ou serveur dédié. Si vous ne maîtrisez pas l’impact global du cache sur la boutique, relisez aussi : Cache PrestaShop : Varnish, Redis, Memcached et OPcache côté serveur.

Enfin, protégez l’endpoint. Un champ de recherche live est une surface d’attaque évidente : spam de requêtes, tentatives d’énumération, charge CPU sur le moteur de recherche. Mettez un rate limiting (token bucket) au niveau reverse proxy (HAProxy / Nginx) ou WAF, et loggez les abus. Si votre stack inclut HAProxy, l’article suivant vous donne des patterns concrets (TLS, ACL, rate limiting, métriques Prometheus) : HAProxy reverse proxy : terminaison TLS, rate limiting et supervision Prometheus. Et si vous avez déjà vu des 503 en charge, gardez un runbook : Erreur HTTP 503 : diagnostic serveur, logs et ressources.

Exemple Nginx (simple) : limiter l’autocomplete à un niveau raisonnable par IP, tout en laissant respirer le site. À ajuster selon votre audience (mobile vs desktop) et vos pics.

limit_req_zone $binary_remote_addr zone=suggest_zone:10m rate=10r/s;

location = /module/yoursearch/suggest {
  limit_req zone=suggest_zone burst=20 nodelay;
  proxy_pass http://php_upstream;
}

Côté conformité (important en UE) : si vous loggez les requêtes de recherche (utile pour améliorer synonymes et “zero-results”), traitez-les comme de la donnée potentiellement sensible (requêtes pouvant contenir noms, emails tapés par erreur, etc.). Minimisez, tronquez, fixez une rétention, et documentez la finalité.

Mesurer, profiler, corriger : KPIs recherche interne et observabilité

Sans métriques, vous ne saurez pas si vos suggestions live “aident” ou juste si elles consomment des ressources. KPIs techniques : latence p50/p95/p99 de l’endpoint suggest, taux de cache hit (Redis), débit (req/s), taille des réponses, et coût par requête (CPU/IO côté moteur). KPIs produit : taux de clic sur suggestions, taux de conversion des sessions ayant utilisé la recherche, et surtout zero-results rate (part de requêtes sans résultat). Un objectif pragmatique est de descendre sous 5–8% de zero-results sur un catalogue correctement indexé, sinon votre tolérance aux fautes/synonymes est insuffisante.

Pour rendre ces KPIs actionnables, segmentez-les :

  • par langue (les problèmes de stemming/synonymes ne sont pas les mêmes en FR et en EN),
  • par device (mobile plus sensible à la latence et au rendu),
  • par période (pics type soldes / campagnes),
  • par origine trafic (SEO, SEA, email : les intentions de recherche diffèrent).

Pour l’observabilité, ne vous contentez pas des logs applicatifs. Instrumentez :

Dernier levier : le diagnostic “qualité” des résultats. Faites un corpus de requêtes réelles (top 1000), rejouez-le à chaque release (tests non-régression), et mesurez les deltas sur : précision top-3, rappel, et stabilité du ranking. Si vous changez les règles (synonymes, fuzziness), faites-le sous feature flag et observez. Sur un moteur type Meilisearch/Elasticsearch, ce n’est pas compliqué : c’est une question de pipeline CI/CD et de configuration versionnée.

Un format de tableau de bord simple (et très utile) :

  • Top requêtes + CTR suggestions + conversion,
  • Top “zéro résultat” (à traiter en priorité : synonymes, fautes fréquentes, produits manquants),
  • Latence p95 + hit rate cache (corrélez : quand le cache chute, la latence monte),
  • Erreurs endpoint (429 rate limit, 5xx, timeouts).

Sur PrestaShop, industrialisez aussi la qualité du code et la sécurité des changements (staging, rollback) — les bonnes pratiques du site sur la migration et la validation s’appliquent également à un composant “recherche”, parce que c’est exactement le genre de brique qui casse en prod si on la modifie sans garde-fous : Migration PrestaShop 9 : sécurité, tests et plan de rollback.


À lire aussi