Vous tapez un nom de produit dans la barre de recherche, zéro réponse, et pourtant la référence dort bien au catalogue. Sur PrestaShop, peu de pannes déroutent autant, parce qu’aucun signal ne vient prévenir : la fiche s’ouvre normalement, la catégorie l’affiche, et la recherche fait comme si elle n’existait pas.
Peu de monde connaît le point qui explique tout : votre catalogue, le moteur ne le consulte jamais directement. Ce sont deux tables d’index qu’il va trouver, et c’est à vous de les nourrir, puis cinq conditions d’éligibilité filtrent les candidats avant qu’un seul résultat ne sorte. Le code des branches 1.7.8, 8.x et 9.x, je l’ai épluché pour vérifier tout ça (les cinq conditions n’y changent nulle part).

Comment PrestaShop cherche réellement
Deux temps rythment l’affaire. Indexer, d’abord : le texte de chaque fiche est taillé en morceaux, les accents et la ponctuation disparaissent, et ce qui reste va se ranger dans une table de mots où la langue et la boutique sont inscrites. Une seconde table prend le relais, en nouant mot et produit, poids à l’appui, poids que détermine le champ de provenance. Le module de filtres, de son côté, pioche en direct dans vos fiches et ne regarde jamais cet index : les lenteurs du filtre à facettes s’expliquent donc tout autrement.
Deuxième phase, la requête du visiteur passe au même décrassage, et chaque mot va ensuite frapper à la table des mots, laquelle rend les produits accrochés. D’où deux suites très concrètes. Absente de l’index, une fiche reste invisible, quoi qu’on tente. Et toucher aux poids ne donnera rien avant une reconstruction totale (le poids se fige à l’écriture, pas après). Passez les requêtes qui suivent : vous aurez l’état de votre index, sa ventilation par langue et par boutique, ainsi que le compteur que le back-office montre dans les réglages de recherche.
-- Prefixe ps_ suppose : adaptez-le a votre installation.
-- 1. Volumetrie de l'index. ps_search_word contient les mots, avec leur
-- langue et leur boutique ; ps_search_index relie ces mots aux produits
-- avec un poids. La langue n'est portee que par ps_search_word.
SELECT (SELECT COUNT(*) FROM ps_search_word) AS mots_indexes,
(SELECT COUNT(*) FROM ps_search_index) AS liaisons,
(SELECT COUNT(DISTINCT id_product) FROM ps_search_index) AS produits_lies;
-- 2. Repartition par langue et par boutique : un index vide pour une
-- langue explique a lui seul une recherche muette sur cette version du site.
SELECT id_lang, id_shop, COUNT(*) AS mots
FROM ps_search_word
GROUP BY id_lang, id_shop
ORDER BY id_lang, id_shop;
-- 3. Le compteur que le back-office affiche dans Parametres de la boutique
-- puis Recherche : produits indexes sur produits eligibles.
SELECT COUNT(*) AS eligibles,
SUM(ps.indexed) AS indexes
FROM ps_product p
INNER JOIN ps_product_shop ps ON ps.id_product = p.id_product
WHERE ps.visibility IN ('both', 'search')
AND ps.active = 1;
Les cinq conditions pour qu’un produit apparaisse
L’index a rendu ce qu’il avait, puis vient une deuxième requête, celle de l’éligibilité, que le cœur exécute avant d’écrire la moindre ligne à l’écran. Cinq exigences tombent d’un bloc : état actif sur la boutique, visibilité couvrant soit la boutique entière soit la seule recherche, marqueur d’indexation à 1, rattachement à une catégorie active au minimum, et ouverture de cette catégorie au groupe dont dépend le visiteur.
Celles qu’on laisse filer le plus souvent, je les connais : la quatrième et la cinquième. Accrochez une fiche à une catégorie unique, désactivée, et la voilà hors recherche alors que son adresse directe répond toujours. Pareil quand le groupe Visiteur a été décoché sur une catégorie : plus rien ne sort pour vos clients non connectés, tandis que les mêmes articles reviennent dès l’ouverture d’un compte. Le stock, lui, n’apparaît à aucun endroit, je suis allé regarder (ni dans cette requête, ni dans celle qui sort les résultats). Chacune de ces conditions se teste à part, avec les quatre requêtes ci-dessous.
-- Les cinq conditions que le coeur applique avant de retenir un produit
-- dans les resultats. Une seule qui manque suffit a le faire disparaitre.
-- a) Produits actifs et cherchables mais pas encore indexes.
SELECT p.id_product, pl.name
FROM ps_product p
INNER JOIN ps_product_shop ps
ON ps.id_product = p.id_product AND ps.id_shop = 1
INNER JOIN ps_product_lang pl
ON pl.id_product = p.id_product AND pl.id_shop = 1 AND pl.id_lang = 1
WHERE ps.active = 1
AND ps.visibility IN ('both', 'search')
AND ps.indexed = 0
ORDER BY p.id_product;
-- b) Produits indexes mais dont la visibilite les exclut de la recherche.
SELECT p.id_product, ps.visibility
FROM ps_product p
INNER JOIN ps_product_shop ps
ON ps.id_product = p.id_product AND ps.id_shop = 1
WHERE ps.active = 1
AND ps.visibility NOT IN ('both', 'search');
-- c) Produits sans aucune categorie active : le coeur joint
-- ps_category_product et ps_category avec c.active = 1.
SELECT p.id_product, pl.name
FROM ps_product p
INNER JOIN ps_product_shop ps
ON ps.id_product = p.id_product AND ps.id_shop = 1
INNER JOIN ps_product_lang pl
ON pl.id_product = p.id_product AND pl.id_shop = 1 AND pl.id_lang = 1
LEFT JOIN ps_category_product cp ON cp.id_product = p.id_product
LEFT JOIN ps_category c ON c.id_category = cp.id_category AND c.active = 1
WHERE ps.active = 1
AND ps.visibility IN ('both', 'search')
GROUP BY p.id_product, pl.name
HAVING COUNT(c.id_category) = 0;
-- d) Produits dont aucune categorie n'est ouverte au groupe Visiteur.
-- Remplacez 1 par l'identifiant du groupe a controler.
SELECT p.id_product
FROM ps_product p
INNER JOIN ps_product_shop ps
ON ps.id_product = p.id_product AND ps.id_shop = 1
LEFT JOIN ps_category_product cp ON cp.id_product = p.id_product
LEFT JOIN ps_category_group cg
ON cg.id_category = cp.id_category AND cg.id_group = 1
WHERE ps.active = 1
AND ps.visibility IN ('both', 'search')
GROUP BY p.id_product
HAVING COUNT(cg.id_group) = 0;
Reconstruire l’index
Paramètres de la boutique puis Recherche, et vous voilà devant deux boutons. L’un verse à l’index les fiches dont le marqueur n’a jamais quitté zéro, ça va très vite, et derrière un import je m’en contente sans arrière-pensée (deux minutes pour quelques milliers de références). L’autre repart de rien du tout, donc vidage des deux tables et retour de chaque marqueur à zéro avant que ça reprenne.
Catalogue volumineux, reconstruction totale lancée dans le navigateur : le serveur web tranche avant le terme bien plus souvent qu’on ne l’imagine, et ce scénario-là, je le vois revenir tout le temps. Le traitement avance cinquante fiches à la fois, et rien de ce qui est déjà passé ne se perd. Redemandez l’ajout des manquants : la suite repart exactement d’où ça s’était bloqué. Quand l’index part franchement de travers, je le vide plutôt en SQL, puis je relance ailleurs que dans le navigateur, avec la tâche planifiée détaillée plus bas. Le bloc suivant reproduit le geste du cœur juste avant son redémarrage, rien de plus, rien de moins. Aucune reconstruction là-dedans (il ramène les compteurs à zéro, un point c’est tout).
-- Reconstruction complete de l'index, equivalent SQL de ce que fait
-- Search::indexation($full = true) quand le contexte porte sur
-- l'ensemble des boutiques. A executer sur une base sauvegardee.
-- 1. On vide les deux tables de recherche.
TRUNCATE TABLE ps_search_index;
TRUNCATE TABLE ps_search_word;
-- 2. On remet a zero le marqueur d'indexation, dans les deux tables
-- produit : le coeur met les deux a jour ensemble.
UPDATE ps_product SET indexed = 0;
UPDATE ps_product_shop SET indexed = 0;
-- 3. Controle : tout doit etre a zero avant de relancer l'indexation.
SELECT (SELECT COUNT(*) FROM ps_search_index) AS liaisons,
(SELECT COUNT(*) FROM ps_search_word) AS mots,
(SELECT COUNT(*) FROM ps_product_shop WHERE indexed = 1) AS marques;
-- 4. L'indexation elle-meme n'est PAS faite en SQL : elle nettoie les
-- mots, applique les poids et ecrit les liaisons par lots de 50 produits.
-- Elle se declenche depuis le back-office ou par l'URL de tache planifiee
-- donnee plus bas. Le SQL ci-dessus ne fait que remettre le compteur a zero.
Automatiser par tâche planifiée
Un contrôleur d’administration porte l’indexation chez PrestaShop, et on y accède hors session, à condition de présenter le jeton prévu pour ça. Sa valeur découle d’une clé interne au site. La section d’indexation de votre back-office l’affiche, URL complète à l’appui, et impossible de le recalculer depuis dehors. Copiez-le à l’identique.
Deux voies ensuite. Le plus direct consiste à frapper en HTTP avec un client en ligne de commande, mais le délai d’exécution du serveur web s’applique là comme partout ailleurs. Passer carrément en ligne de commande vous sort de cette limite : le fichier de configuration retourne les arguments reçus par le script en paramètres de requête, à la stricte condition de tout empiler dans une seule chaîne. D’ailleurs, deux heures de durée maximale d’exécution, c’est le contrôleur lui-même qui se les accorde, et ça renseigne assez bien sur le temps que prend l’opération. En général je cale ça ainsi : reconstruction totale hebdomadaire, rattrapage des manquants toutes les autres nuits, et les imports quotidiens passent sans que la base reste bloquée.
#!/bin/bash
# L'URL exacte, jeton compris, est affichee dans
# Parametres de la boutique > Recherche > Indexation.
# Le jeton derive de _COOKIE_KEY_ : recopiez-le, ne le recalculez pas.
BO="https://www.ma-boutique.fr/admin1234/index.php"
TOKEN="xxxxxxxx"
# full=1 : reconstruction complete. Sans full, seuls les produits
# marques indexed = 0 sont traites, ce qui est beaucoup plus leger.
curl -sS --max-time 7200 \
"${BO}?controller=AdminSearch&action=searchCron&ajax=1&full=1&token=${TOKEN}" \
-o /dev/null
# Variante en ligne de commande, sans passer par le serveur web ni
# subir son timeout. config.inc.php appelle Tools::argvToGET() en CLI :
# toute la chaine de parametres tient dans un seul argument.
php admin1234/index.php "controller=AdminSearch&action=searchCron&ajax=1&full=1&token=${TOKEN}"
# Crontab : reconstruction complete le dimanche, rattrapage des
# manquants les autres nuits.
# 15 4 * * 0 /usr/local/bin/reindex-recherche.sh full
# 15 4 * * 1-6 /usr/local/bin/reindex-recherche.sh
Longueur minimale des mots et poids des champs
Ce qui entre dans l’index dépend de trois réglages. Sorti de l’installation, un mot doit peser trois caractères au minimum : en dessous, il saute, aussi bien pendant l’indexation qu’au moment où le visiteur le tape. Le plafond est posé à quinze, et la colonne qui garde les mots encaisse trente caractères (toute ambition plus haute s’arrête là).
Côté poids par champ, c’est bâti pareil : le nom du produit vaut six, la référence monte à dix, description et description courte restent à un, catégorie et marque valent trois, les étiquettes quatre, déclinaisons et caractéristiques deux. Posez zéro quelque part et ce champ sort de l’indexation, sans discussion (sur un catalogue très bavard, je ne m’en prive pas). L’écriture fige ces valeurs, si bien que la moindre retouche réclame de votre part une reconstruction totale avant de produire quoi que ce soit. La liste noire, elle, emprunte un autre chemin : traduite, logée dans la table de configuration par langue, d’où le fait qu’une exclusion écrite pour le français laisse vos autres versions du site parfaitement intactes.
-- Reglages qui decident de ce qui entre dans l'index et de ce qui en sort.
SELECT c.name, c.id_shop, c.value
FROM ps_configuration c
WHERE c.name IN (
'PS_SEARCH_INDEXATION',
'PS_SEARCH_MINWORDLEN',
'PS_SEARCH_MAX_WORD_LENGTH',
'PS_SEARCH_FUZZY',
'PS_SEARCH_FUZZY_MAX_LOOP',
'PS_SEARCH_WEIGHT_PNAME',
'PS_SEARCH_WEIGHT_REF',
'PS_SEARCH_WEIGHT_SHORTDESC',
'PS_SEARCH_WEIGHT_DESC',
'PS_SEARCH_WEIGHT_CNAME',
'PS_SEARCH_WEIGHT_MNAME',
'PS_SEARCH_WEIGHT_TAG',
'PS_SEARCH_WEIGHT_ATTRIBUTE',
'PS_SEARCH_WEIGHT_FEATURE'
)
ORDER BY c.name;
-- Valeurs d'installation : MINWORDLEN = 3, MAX_WORD_LENGTH = 15,
-- FUZZY = 1, poids du nom = 6, poids de la reference = 10,
-- description et description courte = 1, categorie et marque = 3,
-- tags = 4, declinaisons et caracteristiques = 2.
-- Un poids a 0 exclut completement le champ de l'indexation.
-- La liste noire est traduite : elle vit dans ps_configuration_lang.
SELECT c.name, cl.id_lang, cl.value
FROM ps_configuration c
INNER JOIN ps_configuration_lang cl ON cl.id_configuration = c.id_configuration
WHERE c.name = 'PS_SEARCH_BLACKLIST';
-- La colonne ps_search_word.word est un VARCHAR(30) : inutile de pousser
-- PS_SEARCH_MAX_WORD_LENGTH au-dela.
-- Toute modification de ces valeurs impose une reconstruction complete :
-- elles sont appliquees au moment d'indexer, pas au moment de chercher.
Surcharges, modules et thème
Index propre, fiches éligibles, et il ne demeure que l’affichage : côté front, le contrôleur de recherche habite un sous-dossier réservé aux pages de listes, tandis qu’une éventuelle surcharge prend place exactement en face, sous le dossier des surcharges, l’arborescence du cœur se trouvant recopiée telle quelle. Une surcharge héritée d’une version plus ancienne, qui a tenu bon pendant une mise à jour, j’en déniche souvent.
Trois gestes, et c’est plié, je ne connais pas plus rapide. Allez aux réglages de performance, basculez d’un coup toutes les surcharges sur off, nettoyez le cache, puis cherchez à nouveau. Ça remonte ? Le dossier des surcharges tient votre coupable. Regardez du côté du thème également, le temps de remettre celui d’origine sur votre boutique (cinq minutes, aucun danger), et du module de barre de recherche, qui embarque parfois son propre gabarit. Un gabarit qu’on retouche justement pour placer la barre de recherche dans le menu.
#!/bin/bash # Le dossier override/ reprend l'arborescence du core. Le controleur # de recherche du front se trouve dans controllers/front/listing/, # sa surcharge se place donc au meme endroit sous override/. echo "### Surcharges presentes" find override -name '*.php' ! -name 'index.php' -print 2>/dev/null echo echo "### Surcharges qui touchent la recherche" grep -rl --include='*.php' -E 'class (SearchController|Search|Product)\b' override 2>/dev/null echo echo "### Surcharges installees par les modules" grep -rl --include='*.php' 'module: ' override 2>/dev/null echo echo "### Index de classes a regenerer apres toute modification" ls -l var/cache/prod/class_index.php 2>/dev/null # Pour desactiver temporairement toutes les surcharges et savoir si # l'une d'elles est en cause, passez PS_DISABLE_OVERRIDES a 1 dans # Parametres avances > Performances, puis videz le cache.
Ce que je vérifie ensuite
Pour ma part, je reprends le compte des fiches indexées, je le mets en face du nombre d’éligibles, et ça langue par langue, boutique par boutique. Si l’écart résiste à une reconstruction complète, le coupable est presque toujours une indexation coupée en route, pas un réglage mal posé. Remettez l’ajout des manquants jusqu’à voir le compteur se figer.
Rejouez derrière ça les termes qui bloquaient, variantes courtes et références comprises, et faites-le en fenêtre privée, histoire d’écarter les caches de session. Trop peu d’articles derrière un terme donné ? La liste est parfois bornée par l’affichage, et augmenter le nombre de résultats du bloc clôt la question. Tenez surtout, sur quelques semaines, le relevé des recherches restées sans réponse : voilà ce qui désigne les alias à créer, et de loin mieux qu’une liste dressée d’avance (sans rien deviner). Elles font aussi office de signal sur votre offre, puisqu’elles disent ce que vos clients viennent chercher sans repartir avec. Et quand rien ne cède, envoyez-moi vos éléments par le formulaire de contact : un développement sur mesure redevient jouable là où le moteur natif s’arrête, pendant que le blog rassemble d’autres analyses techniques.
Questions fréquentes
Pourquoi mes nouveaux produits n’apparaissent-ils pas dans les résultats ?
Parce que la source du moteur, ce sont deux tables d’index, et qu’elles ne se remplissent pas toutes seules. Pour rappel, votre catalogue n’est jamais consulté en direct. Laissez le marqueur d’indexation d’une fiche à zéro et elle ne figurera nulle part dans les résultats, aussi active et complète soit-elle. Dans Paramètres de la boutique puis Recherche, l’action qui verse les manquants à l’index vient à bout du cas courant en quelques secondes.
Quand l’indexation automatique à l’enregistrement a été coupée dans ces mêmes réglages, l’écart grandit à chaque nouvelle fiche. Boutique qui importe souvent : gardez l’option active, et ajoutez par-dessus un passage de nuit. L’oubli guette toujours une vérification faite à la main.
Un produit en rupture de stock est-il exclu de la recherche ?
Non. Sur les trois branches, j’ai relu cette requête d’éligibilité ligne à ligne : état actif de la fiche, visibilité, marqueur d’indexation, présence d’au moins une catégorie active, puis ouverture de cette catégorie au groupe du visiteur. Nulle trace du stock, et du côté de la requête d’affichage, aucun filtre sur les quantités non plus.
Votre produit épuisé a disparu des résultats ? Regardez ailleurs : ce que je trouve la plupart du temps, c’est une catégorie éteinte ou verrouillée pour le groupe Visiteur, et le symptôme obtenu est strictement identique, la fiche restant joignable par son adresse directe.
Faut-il baisser la longueur minimale des mots ?
Seulement dans le cas où des mots d’une ou deux lettres, présents dans vos références ou dans vos noms de produits, comptent vraiment pour vos clients. L’installation pose trois caractères, et ce seuil joue deux fois, à l’écriture du mot dans l’index puis à la saisie du visiteur.
Cette baisse se paie. Chaque mot court en plus fabrique des lignes dans les deux tables, et sur un catalogue descriptif le volume monte à toute vitesse. Elle ne produit surtout aucun effet avant une reconstruction entière de l’index, puisque le filtre intervient à l’écriture. Toucher au réglage sans réindexer laisse croire que rien ne marche.
Peut-on lancer l’indexation en ligne de commande ?
Aucune commande dédiée dans ce que PrestaShop livre d’origine (le dossier des commandes, je l’ai ouvert sur les trois branches) : pas une seule ne s’occupe de l’index de recherche. Le contrôleur d’administration qui mène l’indexation, en revanche, s’attrape très bien en ligne de commande, puisque les arguments donnés au script deviennent des paramètres de requête par le fichier de configuration.
Une réserve tout de même : l’intégralité de vos paramètres doit loger dans un argument unique. Par ce chemin, plus de délai d’exécution côté serveur web, et sur un catalogue volumineux je le trouve nettement plus sûr que l’appel HTTP. Dans les deux cas le jeton reste exigé, et sa copie vient de votre back-office.
Faut-il un index par langue et par boutique ?
Oui, et voilà typiquement l’erreur que je croise en multiboutique. Ce que porte la table des mots, c’est la langue et la boutique, pas la table de liaison : remplissez correctement l’index en français, laissez-le vide en allemand, et seule la version allemande du site renverra une recherche muette, sans qu’aucun message d’erreur ne vous prévienne.
Reconstruisez depuis un contexte réduit à une boutique, et seuls les produits de celle-ci seront purgés par le cœur. Mieux vaut donc vous placer sur toutes les boutiques avant de relancer, puis aller vérifier la ventilation des mots, langue par langue et boutique par boutique, avant de conclure quoi que ce soit.