Rien ne dit « nous respectons votre temps » comme une boîte de recherche qui renvoie des résultats une fois votre café refroidi. Sur un site WordPress fréquenté, la recherche par défaut peut malmener votre base de données : jokers, tables gigantesques et plugins qui traitent wp_postmeta comme un tiroir à bazar illimité.
Vous n’avez pas besoin d’un service de recherche hébergé coûteux pour corriger cela. Commencez par mesurer, puis appliquez un petit ensemble de changements ennuyeux mais à fort effet : meilleures requêtes, index adaptés, cache raisonnable et suppression de la complexité accidentelle accumulée pendant la phase « on pousse vite ».
Procédure de diagnostic rapide
Si vous ne faites rien d’autre, faites ceci dans l’ordre. L’objectif est de trouver le goulot d’étranglement en quelques minutes, pas de passer une journée à « optimiser » la mauvaise couche.
1) Confirmez que c’est bien la recherche (et non PHP, le réseau ou un plugin qui fait dix autres choses)
- Vérifiez le temps de la requête au bord (outils dev du navigateur ou vos logs CDN).
- Vérifiez le journal d’accès du serveur pour les requêtes
?s=et leurs temps de réponse. - Si la page est lente mais que le temps BD est correct, votre problème vient du rendu PHP, des hooks de plugins ou d’appels distants.
2) Identifiez le schéma de requête et si des index sont utilisés
- Activez brièvement la journalisation des requêtes lentes (ou utilisez Performance Schema si vous l’avez déjà).
- Capturez une requête de recherche lente représentative.
- Exécutez
EXPLAINet cherchez les scans de table complets, des estimations énormes de « rows », et « Using temporary; Using filesort ».
3) Décidez : corriger la requête/index d’abord, puis le cache, puis l’infrastructure
- Si
EXPLAINmontre un scan de millions de lignes : commencez par l’indexation et le façonnage de requête. - Si la requête BD est rapide mais que la page est lente : cache d’objets, profileur PHP, audit des plugins.
- Si la BD est saturée (fort
IO wait, misses du buffer pool) : ajustez InnoDB et réduisez l’encombrement ; ne pensez au matériel que ensuite.
Idée paraphrasée (attribuée) : Werner Vogels recommande depuis longtemps de « mesurer, puis optimiser », car deviner conduit à optimiser la mauvaise chose.
Pourquoi la recherche WordPress est lente (ce qu’elle fait réellement)
La recherche par défaut de WordPress n’est pas une « recherche » sophistiquée, c’est un pattern SQL. Le cœur construit une requête qui cherche votre terme dans wp_posts.post_title et wp_posts.post_content, souvent en utilisant des conditions LIKE. LIKE '%term%' est le piège classique : le wildcard en début empêche les index B-tree d’aider. La base de données finit souvent par balayer beaucoup de lignes.
Et ce, avant l’arrivée des plugins. Beaucoup d’« améliorations de recherche » ajoutent des JOIN vers wp_postmeta pour chercher dans les champs personnalisés. wp_postmeta est énorme sur de nombreux sites, conçu pour la flexibilité et non la vitesse : un store clé-valeur avec de longs textes, rempli d’options autoloadées et d’années de restes. Si votre recherche touche le meta, vous transformez un « scanner les posts » en un « scanner les posts plus un entrepôt ».
Ensuite, il y a la partie rendu : les pages de résultats déclenchent des templates, posts relatifs, publicités, personnalisation, balises analytics et parfois des appels API distants. La base de données peut être innocente pendant que PHP exécute sa danse interprétative.
Règle d’opinion : considérez la recherche lente comme un problème de base de données tant que vous n’avez pas prouvé le contraire. Puis comme un problème de cache jusqu’à preuve du contraire.
Blague #1 : La recherche par défaut WordPress, c’est comme greper un PDF : techniquement possible, émotionnellement discutable.
Faits intéressants et contexte historique
- La recherche de base de WordPress est volontairement simple. Elle privilégie la compatibilité large plutôt que la qualité de recherche, d’où l’usage de
LIKEplutôt que d’algorithmes de classement. - Les index FULLTEXT de MySQL n’ont pas toujours été compatibles InnoDB. Les versions anciennes poussaient vers MyISAM pour FULLTEXT ; MySQL/MariaDB modernes supportent FULLTEXT sur InnoDB, modifiant les compromis.
- Le « problème wp_postmeta » a empiré avec la montée des page builders. Beaucoup de constructeurs stockent mises en page et blocs dans le meta. Une recherche qui joint le meta peut devenir catastrophique.
- InnoDB est devenu le moteur par défaut de MySQL en 5.5. Ce changement a rendu la taille du buffer pool et les schémas d’IO centraux pour la performance WordPress.
- WordPress a introduit tôt les hooks de cache d’objets persistants. L’API existe ; beaucoup de sites ne la connectent jamais à Redis/Memcached.
- Les stopwords et la taille minimale des mots comptent. FULLTEXT ignore les mots très communs et (par défaut) les tokens courts, ce qui surprend les équipes venant d’un LIKE naïf.
- Les jeux de caractères ont changé la donne. Le passage à
utf8mb4a augmenté la taille des index ; certains index qui « tenaient avant » n’ont plus tenu et la performance en a souffert. - MariaDB et MySQL ont divergé. Le comportement de FULLTEXT, les décisions de l’optimiseur et les valeurs par défaut peuvent différer ; vos recommandations de tuning ne sont pas toujours portables.
- Certaines hébergeurs désactivent les slow query logs sur les plans partagés. D’où l’importance des logs applicatifs et des outils de capture de requêtes pour le diagnostic.
Mesurez d’abord : prouvez où se passe le temps
Vous allez faire du vrai travail. Cela commence par des données : temps de requête, lignes examinées et si la base attend le CPU ou le disque. Ci-dessous des tâches pratiques à exécuter sur une machine Linux + Nginx/Apache + MySQL/MariaDB typique. Chaque tâche indique ce que vous regardez et quelle décision en découle.
Task 1: Find slow search requests in the web server access log
cr0x@server:~$ sudo awk '$7 ~ /\?s=|&s=/ {print $4,$5,$7,$9,$10}' /var/log/nginx/access.log | tail -n 20
[27/Dec/2025:09:10:12 +0000] GET /?s=backup 200 84123
[27/Dec/2025:09:11:08 +0000] GET /?s=pricing 200 90211
Ce que signifie la sortie : Vous échantillonnez des recherches récentes et vérifiez qu’elles existent et ne sont pas redirigées. La dernière colonne est la taille de réponse ; il vous faut encore le temps d’exécution.
Décision : Si les requêtes de recherche se concentrent sur des termes ou endpoints spécifiques (URL de recherche personnalisée, routes de plugin multilingue), vous traitez peut‑être un template ou un chemin de plugin particulier.
Task 2: Add request time to access logs (Nginx) and locate slow searches
Assuming your Nginx log format includes $request_time. If not, add it and reload; then:
cr0x@server:~$ sudo awk '$7 ~ /\?s=|&s=/ && $NF > 1.5 {print $4,$5,$7,"t="$NF}' /var/log/nginx/access.log | tail -n 20
[27/Dec/2025:09:14:02 +0000] GET /?s=invoice t=2.113
[27/Dec/2025:09:14:55 +0000] GET /?s=api t=1.874
Ce que signifie la sortie : Requêtes de recherche dépassant 1,5 s. Le seuil est à votre appréciation ; choisissez ce qui vous fait mal.
Décision : Si le temps de requête est élevé mais que le temps BD semble faible, vous vous orienterez vers le profilage PHP/plugins.
Task 3: Confirm the database is the hot spot (quick IO/CPU sanity)
cr0x@server:~$ vmstat 1 5
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
2 0 0 432112 61264 912340 0 0 60 110 420 700 12 4 79 5 0
1 1 0 401988 61272 910120 0 0 2800 3400 600 900 18 6 54 22 0
Ce que signifie la sortie : La colonne wa est le temps d’attente IO. Une attente IO soutenue pendant les recherches suggère un travail BD lié au disque.
Décision : Forte attente IO : vous avez besoin d’index, de moins de bloat de table et d’un ajustement du buffer pool avant d’ajouter du CPU.
Task 4: Check live MySQL load and the top running query
cr0x@server:~$ mysql -e "SHOW FULL PROCESSLIST\G" | sed -n '1,120p'
*************************** 1. row ***************************
Id: 31284
User: wp
Host: 127.0.0.1:51614
db: wordpress
Command: Query
Time: 3
State: Sending data
Info: SELECT SQL_CALC_FOUND_ROWS wp_posts.ID FROM wp_posts WHERE 1=1 AND ((wp_posts.post_title LIKE '%invoice%') OR (wp_posts.post_content LIKE '%invoice%')) AND wp_posts.post_type IN ('post','page') AND (wp_posts.post_status = 'publish') ORDER BY wp_posts.post_date DESC LIMIT 0, 10
Ce que signifie la sortie : Vous avez capturé une requête réelle et son état. « Sending data » signifie souvent qu’elle scanne/retourne beaucoup de lignes ou trie.
Décision : Copiez la requête (sanctuarisez les valeurs) et exécutez EXPLAIN. Si elle effectue un scan complet, changez l’approche de recherche ou ajoutez une stratégie d’indexation.
Task 5: Turn on slow query logging briefly (MySQL/MariaDB)
cr0x@server:~$ mysql -e "SET GLOBAL slow_query_log = 'ON'; SET GLOBAL long_query_time = 0.5; SET GLOBAL log_queries_not_using_indexes = 'ON';"
Ce que signifie la sortie : Pas de sortie signifie que les commandes ont été appliquées. Le chemin du slow log dépend de la configuration.
Décision : Utilisez ceci pendant une fenêtre contrôlée. Si vous ne pouvez pas le faire en prod, faites-le sur un clone de staging avec des données réalistes.
Task 6: Read the slow query log for search patterns
cr0x@server:~$ sudo tail -n 50 /var/log/mysql/mysql-slow.log
# Time: 2025-12-27T09:14:02.123456Z
# User@Host: wp[wp] @ localhost []
# Query_time: 2.107 Lock_time: 0.000 Rows_sent: 10 Rows_examined: 842311
SET timestamp=1766826842;
SELECT SQL_CALC_FOUND_ROWS wp_posts.ID FROM wp_posts WHERE 1=1 AND ((wp_posts.post_title LIKE '%invoice%') OR (wp_posts.post_content LIKE '%invoice%')) AND wp_posts.post_type IN ('post','page') AND (wp_posts.post_status = 'publish') ORDER BY wp_posts.post_date DESC LIMIT 0, 10;
Ce que signifie la sortie : 10 lignes retournées, 842k examinées. C’est la signature d’un scan.
Décision : Vous n’en sortirez pas avec plus de serveur. Il vous faut une stratégie de recherche indexable, ou réduire drastiquement l’ensemble de candidats.
Task 7: EXPLAIN the query and interpret the damage
cr0x@server:~$ mysql wordpress -e "EXPLAIN SELECT wp_posts.ID FROM wp_posts WHERE ((wp_posts.post_title LIKE '%invoice%') OR (wp_posts.post_content LIKE '%invoice%')) AND wp_posts.post_type IN ('post','page') AND (wp_posts.post_status = 'publish') ORDER BY wp_posts.post_date DESC LIMIT 10\G"
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: wp_posts
partitions: NULL
type: ALL
possible_keys: type_status_date
key: NULL
key_len: NULL
ref: NULL
rows: 921344
filtered: 2.50
Extra: Using where; Using filesort
Ce que signifie la sortie : type: ALL signifie scan complet de la table. Using filesort signifie que MySQL trie les résultats au lieu de les lire dans l’ordre d’un index.
Décision : Soit (a) migrer vers FULLTEXT, (b) restreindre la recherche à un sous-ensemble indexable, soit (c) introduire une table d’index de recherche dédiée que vous contrôlez.
Task 8: Inspect table sizes (find your true villains)
cr0x@server:~$ mysql -e "SELECT table_name, engine, table_rows, ROUND((data_length+index_length)/1024/1024,1) AS total_mb FROM information_schema.tables WHERE table_schema='wordpress' ORDER BY (data_length+index_length) DESC LIMIT 12;"
+----------------+--------+------------+----------+
| table_name | engine | table_rows | total_mb |
+----------------+--------+------------+----------+
| wp_postmeta | InnoDB | 18322104| 2860.4 |
| wp_posts | InnoDB | 921344| 640.2 |
| wp_options | InnoDB | 81234| 94.7 |
+----------------+--------+------------+----------+
Ce que signifie la sortie : Si wp_postmeta écrase tout le reste, toute recherche touchant le meta sera coûteuse sauf si vous la repensez.
Décision : Si le meta est énorme : arrêtez de le rechercher aveuglément. Listez les clés utiles, réduisez les déchets stockés ou créez une table d’index séparée.
Task 9: Find bloated autoloaded options (the silent page weight)
cr0x@server:~$ mysql wordpress -e "SELECT option_name, LENGTH(option_value) AS bytes FROM wp_options WHERE autoload='yes' ORDER BY bytes DESC LIMIT 10;"
+----------------------------+---------+
| option_name | bytes |
+----------------------------+---------+
| some_builder_global_styles | 1948120 |
| plugin_cache_blob | 822114 |
| theme_mods_mytheme | 310992 |
+----------------------------+---------+
Ce que signifie la sortie : WordPress charge les options autoloadées à chaque requête—recherche incluse. Si elles pèsent des mégaoctets, vous payez ce coût partout.
Décision : Réduisez la taille des autoload : corrigez les plugins/thèmes qui stockent de gros blobs en autoload, ou basculez certaines options en autoload='no' (avec prudence et tests).
Task 10: Check InnoDB buffer pool health (are you reading from RAM or disk?)
cr0x@server:~$ mysql -e "SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_read%';"
+---------------------------------------+------------+
| Variable_name | Value |
+---------------------------------------+------------+
| Innodb_buffer_pool_read_requests | 1928831123 |
| Innodb_buffer_pool_reads | 18299312 |
+---------------------------------------+------------+
Ce que signifie la sortie : reads sont des lectures physiques ; read_requests sont logiques. Le ratio indique la fréquence des misses du cache.
Décision : Si les misses sont fréquents sous charge, augmentez innodb_buffer_pool_size (en respectant les limites de RAM) et réduisez la taille du working set (bloat et index inutiles).
Task 11: Capture a single search query profile with EXPLAIN ANALYZE (MySQL 8+)
cr0x@server:~$ mysql wordpress -e "EXPLAIN ANALYZE SELECT ID FROM wp_posts WHERE post_status='publish' AND post_type IN ('post','page') AND (post_title LIKE '%invoice%' OR post_content LIKE '%invoice%') ORDER BY post_date DESC LIMIT 10;"
+----------------------------------------------------------------------------------------------------------------------------------+
| EXPLAIN |
+----------------------------------------------------------------------------------------------------------------------------------+
| -> Limit: 10 row(s) (cost=... rows=10) (actual time=0.105..2107.331 rows=10 loops=1) |
| -> Sort: wp_posts.post_date DESC, limit input to 10 row(s) per chunk (actual time=0.104..2107.329 rows=10 loops=1) |
| -> Filter: ((wp_posts.post_title like '%invoice%') or (wp_posts.post_content like '%invoice%')) (rows=...) |
| -> Table scan on wp_posts (actual time=0.050..2001.002 rows=921344 loops=1) |
+----------------------------------------------------------------------------------------------------------------------------------+
Ce que signifie la sortie : Le scan de table domine. C’est l’élément concluant avec des timestamps.
Décision : Cessez d’essayer de micro-tuner. Changez le mécanisme de recherche ou réduisez le scan.
Task 12: Verify whether object caching is active (WordPress-side)
cr0x@server:~$ wp --path=/var/www/html cache type
Default
Ce que signifie la sortie : « Default » signifie généralement qu’aucun cache d’objets persistant n’est branché.
Décision : Installez/configurez Redis ou Memcached si vous avez des pages dynamiques et des requêtes répétées. Cela ne résoudra pas un scan de table, mais évitera les répétitions douloureuses.
Task 13: Confirm Redis is reachable (if you enable it)
cr0x@server:~$ redis-cli ping
PONG
Ce que signifie la sortie : Redis répond.
Décision : Si vous n’obtenez pas de PONG stable, ne basez pas votre plan de cache de recherche dessus.
Task 14: Inspect the heaviest meta keys (targets for cleanup or exclusion)
cr0x@server:~$ mysql wordpress -e "SELECT meta_key, COUNT(*) AS rows, ROUND(SUM(LENGTH(meta_value))/1024/1024,1) AS value_mb FROM wp_postmeta GROUP BY meta_key ORDER BY value_mb DESC LIMIT 10;"
+-----------------------+----------+----------+
| meta_key | rows | value_mb |
+-----------------------+----------+----------+
| _builder_data | 310221 | 940.3 |
| _some_plugin_cache | 901122 | 512.7 |
| _thumbnail_id | 610010 | 12.4 |
+-----------------------+----------+----------+
Ce que signifie la sortie : Quelques clés expliquent souvent la majorité du poids du meta. Ce sont celles qui punissent les JOINs et le churn du cache.
Décision : Excluez ces clés de la recherche ; déplacez‑les hors du meta si un plugin le permet ; purgez les blobs de cache obsolètes.
Task 15: Check for long-running table maintenance or locking (rare, but ugly)
cr0x@server:~$ mysql -e "SHOW ENGINE INNODB STATUS\G" | sed -n '1,120p'
TRANSACTIONS
------------
Trx id counter 123456789
Purge done for trx's n:o < 123456700 undo n:o < 0 state: running
History list length 4123
...
Ce que signifie la sortie : Une grande history list peut indiquer des transactions longues retardant le purge, ce qui bloat l’undo et nuit à la performance.
Décision : Si vous voyez cela lors de ralentissements, chassez les transactions longues (souvent sauvegardes, analytics ou outils d’administration) et corrigez-les.
Corrections côté base de données qui changent la donne
1) Arrêtez de faire semblant que LIKE est une stratégie indexable
Si votre requête actuelle est LIKE '%term%' sur de grands champs texte, la base fait de la force brute. Vous pouvez jeter de la RAM dessus, mais c’est comme acheter un tapis roulant plus rapide pour que votre hamster court plus vite.
Trois options pragmatiques ne nécessitant pas de service externe coûteux :
- Utiliser des index FULLTEXT sur
wp_postspour le titre et le contenu. - Maintenir votre propre table d’index de recherche (une « document » dénormalisé par post) et interroger celle-ci.
- Réduire agressivement l’ensemble de candidats (types de posts, fenêtres de date, filtres de taxonomie, partitionnement langue/site) et accepter que ce soit toujours basé sur LIKE si nécessaire.
2) Ajouter FULLTEXT sur posts (MySQL/MariaDB modernes), puis adapter WordPress
FULLTEXT est la réponse « utilisez la base que vous payez déjà ». Ce n’est pas une recherche parfaite, mais infiniment mieux que scanner tous les posts pour chaque frappe.
À surveiller : stopwords, taille minimale des tokens, absence de stemming natif, comportement de classement et le fait que FULLTEXT n’est pas une recherche par sous‑chaîne. Les utilisateurs cherchant « inv » ne trouveront pas « invoice » sans gestion de préfixe (souvent un fallback LIKE pour très courts termes).
cr0x@server:~$ mysql wordpress -e "ALTER TABLE wp_posts ADD FULLTEXT KEY ft_title_content (post_title, post_content);"
Ce que signifie la sortie : Sur de grandes tables, cela peut prendre du temps et de l’IO. Si cela verrouille trop sur votre version, planifiez une maintenance ou utilisez des capacités DDL en ligne si disponibles.
Décision : Si votre hébergement ne peut pas gérer cela en ligne, faites‑le pendant un faible trafic ou sur une réplica promue durant la coupure de maintenance.
Important : WordPress n’utilisera pas automatiquement MATCH ... AGAINST. Il faut un petit plugin ou mu-plugin qui hooke posts_search / posts_where pour réécrire la condition de recherche en FULLTEXT lorsque la longueur du terme est raisonnable.
3) Construire une table d’index de recherche dédiée (ennuyeux, contrôlable, rapide)
Si vous devez chercher des meta sélectionnés, des extraits ou des noms de taxonomie, je recommande une petite table annexe :
- Une ligne par post (ou par version de langue), contenant un « document » texte préconstruit que vous choisissez.
- Un index FULLTEXT sur ce blob.
- Mise à jour lors de
save_post, reconstruisable en batch, et simple à raisonner.
Cela évite les multi-JOINs au moment de la requête. Vous payez le coût à l’écriture pour gagner en vitesse à la lecture. C’est le bon compromis pour la recherche.
Exemple de schéma (conceptuel ; adaptez-le) :
cr0x@server:~$ mysql wordpress -e "CREATE TABLE wp_search_index (post_id BIGINT UNSIGNED NOT NULL PRIMARY KEY, lang VARCHAR(12) NOT NULL DEFAULT 'en', doc LONGTEXT NOT NULL, updated_at DATETIME NOT NULL, FULLTEXT KEY ft_doc (doc), KEY lang_updated (lang, updated_at)) ENGINE=InnoDB;"
Ce que signifie la sortie : Table créée, prête à être peuplée.
Décision : Si vous ne pouvez pas modifier en toute sécurité la génération de requêtes WordPress, vous pouvez diriger la recherche vers un endpoint personnalisé qui interroge directement cette table (toujours via WordPress), puis affiche les résultats.
4) Si vous devez chercher dans le meta, listez les clés et indexez en conséquence
Rechercher tout le meta est la manière la plus rapide de faire fondre votre base tout en retournant des résultats inutiles (« button_color: #ffffff » n’est pas l’intention utilisateur). Au lieu de cela :
- Choisissez 3–10 clés meta réellement utiles (SKU, code produit, nom de l’auteur, etc.).
- Assurez‑vous que ces clés sont courtes, cohérentes et ne stockent pas de gros blobs.
- Ajoutez un index composite qui aide votre pattern de JOIN.
cr0x@server:~$ mysql wordpress -e "ALTER TABLE wp_postmeta ADD INDEX meta_key_post_id (meta_key(191), post_id);"
Ce que signifie la sortie : Cela aide les requêtes du type « trouver les posts avec une meta_key donnée puis joindre sur posts ». Cela n’accélère pas un meta_value LIKE '%term%', mais réduit la quantité de meta touchée.
Décision : Si votre plugin recherche meta_value avec des wildcards en début, vous avez encore besoin d’une autre stratégie (table d’index dédiée ou FULLTEXT sur un document dérivé du meta).
5) Tuez SQL_CALC_FOUND_ROWS et les patterns de pagination coûteux
Beaucoup de requêtes WordPress utilisent SQL_CALC_FOUND_ROWS pour calculer le total des résultats pour la pagination. C’est pratique et souvent lent, surtout avec des filtres lourds. Vous pouvez fréquemment l’éviter :
- Affichez « Suivant » sans afficher « Page 1 de 500 ».
- Limitez les résultats (« top 100 ») pour la recherche, ce qui est normal côté UX.
- Calculez les totaux de manière asynchrone ou en cache.
Si un thème attend le nombre total, pensez à mettre en cache les totaux par signature de requête avec un TTL court.
6) Réduisez le bloat : vos tables ne sont pas seulement grosses, elles sont désordonnées
La performance de la recherche corrèle fortement avec « combien de déchets la requête doit traverser ». Trois sources courantes :
- Révisions de posts et autosaves.
- Meta orphelin de posts supprimés ou plugins abandonnés.
- Transients et blobs de cache laissés par des plugins.
Nettoyage pratique (prudence ; faites une sauvegarde d’abord) :
cr0x@server:~$ wp --path=/var/www/html post delete $(wp --path=/var/www/html post list --post_type='revision' --format=ids --posts_per_page=2000) --force
Success: Trashed post 1201.
Success: Trashed post 1202.
Ce que signifie la sortie : Révisions supprimées. Selon votre configuration WP CLI, cela peut mettre à la corbeille puis forcer la suppression.
Décision : Si les révisions sont nécessaires pour le business, limitez-les par configuration plutôt que de les supprimer sans cesse.
Vérification des meta orphelins :
cr0x@server:~$ mysql wordpress -e "SELECT COUNT(*) AS orphan_meta FROM wp_postmeta pm LEFT JOIN wp_posts p ON p.ID = pm.post_id WHERE p.ID IS NULL;"
+-------------+
| orphan_meta |
+-------------+
| 421233 |
+-------------+
Ce que signifie la sortie : Lignes meta référant des posts manquants. C’est du gaspillage pur.
Décision : Si ce volume est non négligeable, planifiez un nettoyage (hors pic) puis surveillez la croissance pour identifier le plugin fautif.
7) Tondez InnoDB raisonnablement (ne transformez pas votre BD en foire scientifique)
Pour WordPress, les réglages peu excitants importent :
innodb_buffer_pool_size: suffisamment grand pour contenir les données chaudes et les index.innodb_log_file_size: pas trop petit ; évitez la pression constante de checkpoints.- Stockage rapide et paramètres fs stables (surtout sur volumes cloud).
Mais souvenez‑vous : le tuning aide quand le plan de requête est raisonnable. Il ne sauve pas un scan complet déclenché par des wildcards en début.
Façonnage des requêtes : réduisez le travail, n’« ajustez » pas la douleur
1) Rendre la recherche moins globale
Beaucoup de sites WordPress traitent la recherche comme « tout rechercher ». Cela inclut des pages que personne ne devrait trouver, d’anciens communiqués de presse et des types de posts créés uniquement pour un plugin. Restreindre le champ n’est pas de la triche ; c’est de la clarté produit.
- Excluez les types de posts non destinés aux utilisateurs.
- Excluez les posts attachment sauf si vous proposez une recherche média UX.
- Excluez évidement les drafts/privés.
- Privilégiez la recherche sur title + excerpt ; n’incluez le content que si nécessaire.
2) Ne triez pas par date si ce n’est pas nécessaire
Trier par post_date est courant, mais si vous utilisez FULLTEXT vous voudrez plutôt un classement par pertinence. Trier par date force un travail de tri et peut contrarier l’optimiseur.
Avec FULLTEXT, ordonnez par score de match, puis par date en tie-break. Si vous restez en LIKE, trier par date après filtrage est cher si le filtrage est coûteux—ce qui est le cas.
3) Les requêtes courtes ne doivent pas déclencher une logique coûteuse
Les recherches d’un ou deux caractères sont du bruit. Elles coûtent cher avec LIKE.
- Imposez une longueur minimale (ex. 3 caractères) avant d’exécuter la recherche.
- Pour les termes courts, affichez des suggestions, pages populaires ou un message « tapez plus de caractères ».
4) Évitez que le « search as you type » frappe PHP + MySQL à chaque frappe
L’autocomplétion est acceptable. Celle qui lance une requête WP complète à chaque pression de touche est un déni de service auto-infligé.
- Debounce côté client (300–500 ms).
- Exiger une longueur minimale.
- Mettre en cache agressivement avec un TTL court.
Blague #2 : Le search-as-you-type sans debounce, c’est comme demander à votre base « on arrive quand ? » toutes les 100 millisecondes.
Mettre en cache la recherche sans tromper les utilisateurs
Mettre en cache les résultats de recherche n’est pas seulement autorisé ; c’est normal. L’astuce est de cacher d’une façon qui n’énerve pas l’équipe éditoriale quand de nouveaux posts n’apparaissent pas pendant des heures.
1) Mettre en cache par signature de requête normalisée
Normalisez la chaîne de recherche (trim, lowercase, collapse spaces) et construisez une clé de cache qui inclut :
- le terme de recherche
- la langue/site/blog ID (pour multisite)
- le filtre de type de post
- le rôle utilisateur si les résultats diffèrent par permissions (généralement non)
- le numéro de page (ou mieux : pagination par curseur)
TTL suggérés : 30–300 secondes pour sites à fort churn ; 5–30 minutes pour sites plutôt statiques. Vous pouvez aussi invalider le cache sur save_post pour les types concernés.
2) Mettre en cache la partie coûteuse, pas tout le HTML
Le cache full page fonctionne bien pour le trafic anonyme, mais les termes de recherche explosent l’espace des clés. Au lieu de cela :
- Cachez la liste des IDs de posts correspondants.
- Puis récupérez les posts par ID (peu coûteux, index-friendly), ou laissez WP le faire.
Cela garde les valeurs de cache petites et stables, et évite de mettre en cache le bruit du template.
3) Utilisez le cache d’objets persistant (Redis/Memcached) correctement
Le cache d’objets WordPress aide sur les requêtes répétées et le chargement d’options. Il ne corrige pas automatiquement une requête pathologique, mais évite que le reste de la page ne surcharge la base.
Quand vous activez Redis :
- Surveillez l’usage mémoire de Redis et la politique d’éviction.
- Ne mettez pas des TTL trop longs qui renvoient des résultats obsolètes pour les workflows éditoriaux.
- Ne stockez pas de blobs gigantesques (certains plugins le font ; il faudra les maîtriser).
Réglages d’infrastructure (les parties que l’on exagère)
1) Séparer BD et web seulement si vous savez ce que vous corrigez
Séparer la BD de la machine web peut aider la contention des ressources, mais ajoute de la latence réseau et de la complexité opérationnelle. Si votre recherche fait des scans complets, déplacer la base ne résout pas le problème, ça le répartit.
2) Le stockage importe : la latence bat le débit pour l’OLTP
Les workloads WordPress sont typiquement beaucoup de petites lectures/écritures, pas de gros transferts séquentiels. Pour les bases, un stockage à faible latence gagne. Si vous êtes sur des volumes réseau, comprenez la classe de performance et le modèle « burst vs sustained ».
3) Les réplicas aident le reporting, pas les requêtes de recherche cassées
Les replicas en lecture sont excellentes pour décharger le reporting et les sauvegardes. Mais la recherche fait partie du trafic utilisateur. Vous pouvez diriger les lectures de recherche vers une réplica si vous tolérez le lag de réplication et que vos besoins de cohérence sont faibles. Beaucoup de sites de contenu le peuvent. Les sites e‑commerce souvent non.
4) PHP-FPM n’est pas le méchant, mais il peut amplifier la douleur
Si les appels BD bloquent, les workers PHP s’accumulent, des files se forment et Nginx commence à timeouter. Assurez‑vous d’avoir suffisamment de workers pour l’opération normale, mais ne « corrigez » pas une recherche lente en doublant les workers et en laissant plus de requêtes concurrentes assommer la BD.
Trois mini-histoires du monde de l’entreprise
Incident : une mauvaise hypothèse (« C’est juste du contenu, ça ne peut pas être si gros »)
Chez une entreprise de contenu moyenne, la latence de recherche est passée discrètement de « correcte » à « pénible » sur un an. L’équipe a supposé que c’était la croissance du trafic. Ils ont augmenté les serveurs web, puis encore, parce que les dashboards montraient un CPU PHP élevé lors des pics.
L’hypothèse erronée était subtile : ils croyaient que les posts étaient la table lourde. En réalité, un page builder stockait de gros blobs sérialisés dans wp_postmeta, et un plugin « meilleure recherche » joinait le meta à chaque requête—sans whitelist. En plus, l’UI de recherche faisait de l’AJAX search-as-you-type sans debounce.
Une fois le slow query logging activé, le schéma est devenu évident : les recherches provoquaient des requêtes examinant des millions de lignes meta. Le CPU PHP était élevé parce que les workers attendaient la base, pas parce que le rendu était coûteux. Les moyennes de charge trompaient comme elles savent le faire.
La correction n’était pas glamour : désactiver par défaut la recherche meta, whitelist de deux clés utiles, imposer une longueur minimale, et ajouter un debounce client. Ils ont aussi créé une petite table d’index pour le meta whitelisté et l’ont reconstruite chaque nuit. La recherche est devenue rapide. Le page builder a conservé ses blobs. Tout le monde a arrêté de crier.
Optimisation qui s’est retournée contre eux (cache qui a provoqué une panne auto-infligée)
Un site marketing SaaS voulait des résultats instantanés. Quelqu’un a livré un « cachez tout » : mettre en cache le HTML rendu des pages de recherche par chaîne de requête, avec un TTL long. C’était brillant en démo.
En prod, cela a causé deux problèmes. D’abord, churn du cache : les recherches en longue traîne ont créé un nombre infini d’entrées, expulsant des objets utiles. Ensuite, exactitude du contenu : quand le service juridique a demandé de retirer un document, il continuait d’apparaître dans les pages de recherche en cache jusqu’à l’expiration du TTL.
La réponse fut gênante car ce n’était pas une panne traditionnelle. Le site « fonctionnait », mais servait des résultats qui n’auraient pas dû exister. C’est le type de panne de fiabilité qui vous attire des réunions avec des gens qui portent des chaussures chères.
L’équipe a rollbacké le cache full-page pour la recherche et a caché seulement la liste des IDs de posts pour un TTL court, avec invalidation explicite sur changement de contenu. Le cache a cessé de croître sans contrôle et les demandes « retirer maintenant » sont devenues solvables sans vider tout le cache.
Pratique ennuyeuse mais correcte qui a sauvé la mise (réplicas + slow logs + contrôle de changement)
Une base de connaissances interne tournait sur WordPress (oui vraiment). La recherche est devenue lente après une mise à jour de plugin, juste avant un gros déploiement interne. Le SRE de garde a fait la chose la moins excitante : suivre la procédure de changement.
Ils ont activé le slow query logging 15 minutes, capturé les principaux coupables et comparé au baseline de la semaine précédente. Le delta montrait un nouveau pattern joignant wp_postmeta à chaque recherche, même quand aucun champ meta n’était configuré.
Parce qu’ils avaient une réplica, ils ont testé l’ajout d’un index et une modification de requête là‑bas d’abord. Parce qu’ils avaient le contrôle de changement, ils ont programmé le DDL en prod sur une fenêtre faible et communiqué le risque bref. Parce qu’ils avaient un plan de rollback, ils n’ont pas paniqué quand la première tentative a pris plus de temps que prévu.
Le résultat final était peu spectaculaire : un réglage de plugin, un index ajouté, et un petit mu-plugin pour imposer une longueur minimale. Pas d’héroïsme. Pas de war room. Juste moins de gens qui râlaient « la recherche est cassée » chaque matin.
Erreurs courantes : symptômes → cause racine → correction
1) Symptom: search is slow only for common terms
Cause racine : les termes communs correspondent à beaucoup de posts ; la requête scanne et trie un ensemble énorme ; les stopwords FULLTEXT peuvent aussi altérer le comportement.
Correction : utilisez FULLTEXT avec ordre par pertinence et envisagez d’ajouter des filtres (type de post, taxonomie). Cachez les résultats des requêtes courantes avec un TTL court.
2) Symptom: search latency spikes during editorial activity
Cause racine : options autoloadées ou invalidations du cache d’objets qui thrashent ; écritures fréquentes expulsent les caches ; certains plugins font un travail lourd sur save_post.
Correction : auditez les options autoloadées, activez un cache d’objets persistant et déplacez les rebuilds coûteux en jobs asynchrones/batchs.
3) Symptom: database CPU is high, but adding CPUs barely helps
Cause racine : scans complets + tris sont liés mémoire/IO ; l’optimiseur ne peut pas utiliser les index avec des wildcards en début.
Correction : changez la stratégie de requête (FULLTEXT ou table d’index). Tunez le buffer pool seulement après que le plan soit sain.
4) Symptom: search is fast for admins, slow for anonymous users (or vice versa)
Cause racine : différents plugins/hooks s’exécutent selon le rôle ; ou le cache s’applique seulement au trafic anonyme ; ou logique de personnalisation.
Correction : comparez le SQL généré et les hooks ; standardisez la logique de recherche selon les rôles ; cachez au niveau des données (IDs), pas par HTML par rôle sauf si nécessaire.
5) Symptom: only page 2+ of search results is slow
Cause racine : pagination OFFSET profonde (LIMIT 10000,10) force à scanner/sauter beaucoup de lignes.
Correction : limitez la profondeur de pagination, utilisez une pagination par curseur, ou mettez en cache les résultats et paginez les IDs en mémoire.
6) Symptom: slow after installing “search enhancement” plugin
Cause racine : le plugin ajoute des JOINs meta et LIKE sur meta_value, ou des JOINs de taxonomie sans index.
Correction : configurez-le pour ne chercher que title/content ; whitelist de clés meta ; ou remplacez-le par une approche basée FULLTEXT.
7) Symptom: search causes lock waits
Cause racine : pas typique pour un SELECT, mais possible avec transactions longues, changements de schéma ou pression sur les tables temporaires.
Correction : identifiez les bloqueurs (processlist, InnoDB status). Planifiez le DDL, corrigez les transactions longues et assurez-vous que l’espace temp n’est pas contraint.
8) Symptom: “random” slow searches after migrating hosts
Cause racine : comportement différent de MySQL/MariaDB, changements de valeurs par défaut, latence de stockage plus lente ou buffer pool plus petit.
Correction : comparez versions et configs ; mesurez le taux de hit du buffer pool ; vérifiez la latence du stockage ; re-vérifiez les plans de requête.
Listes de contrôle / plan étape par étape
Phase 0: Ne pas casser la production (30–60 minutes)
- Confirmez que vous avez une sauvegarde récente (et qu’elle peut être restaurée).
- Choisissez une fenêtre de 15 minutes pour activer temporairement le slow query logging si autorisé.
- Notez « ce à quoi ressemble un bon résultat » : cible p95 du temps de réponse de recherche, staleness acceptable pour le cache.
Phase 1: Victoires rapides (même jour)
- Longueur minimale : imposer 3+ caractères avant d’exécuter la recherche.
- Debounce autocomplete : délai client 300–500 ms.
- Réduction de périmètre : restreindre les types de posts ; envisager d’exclure post_content si title/excerpt suffit.
- Désactiver la recherche meta par défaut : whitelist seulement les clés pertinentes.
- Réduire le bloat autoload : identifier les principaux coupables et corriger les réglages plugins/thèmes.
Phase 2: Corrections structurelles (1–3 jours)
- Activer visibilité des requêtes lentes : slow logs ou Performance Schema.
- Ajouter un index FULLTEXT : sur
wp_postsou une table d’index dédiée. - Réécrire les requêtes : utiliser
MATCH ... AGAINSTquand c’est possible ; garder un fallback pour les très courts termes. - Cachez les IDs de recherche : TTL court, normalisation de clé, invalidation explicite à la publication/mise à jour pour les types pertinents.
Phase 3: Durcissement et maintenance (en continu)
- Nettoyage programmé : révisions, orphelins, transients — automatisez.
- Suivre les régressions : conservez un baseline hebdomadaire des requêtes lentes ; alertez sur les changements de forme de requête, pas seulement le CPU.
- Planification de capacité : assurez-vous que le buffer pool tient le working set ; vérifiez la latence du stockage sous charge.
- Contrôle des changements : les index et changements de plugins passent par tests et plans de déploiement.
Garde‑fous opérationnels (ce que j’appliquerais)
- Aucun plugin ne doit JOIN
wp_postmetadans la recherche sans une allowlist écrite des meta keys. - Aucun endpoint de recherche sans longueur minimale et limitation de débit.
- Pas de « cachez tout » sans revue de la cardinalité des clés de cache.
- Toute DDL sur de grandes tables nécessite un plan pour les locks, la durée et le rollback.
FAQ
1) Puis-je rendre la recherche WordPress par défaut rapide sans changer le SQL ?
Vous pouvez la rendre moins mauvaise en réduisant le périmètre (moins de types de posts, exclure le contenu, longueur minimale) et en mettant en cache les résultats. Mais si vous conservez LIKE '%term%' sur de grandes tables, vous payez fondamentalement pour des scans.
2) Ajouter un index sur post_title va-t-il régler la recherche ?
Pas pour les LIKE avec wildcard en début. Un index aide LIKE 'term%' (recherche préfixe), mais pas '%term%'. C’est pourquoi FULLTEXT existe.
3) FULLTEXT est‑il « suffisant » pour les vrais utilisateurs ?
Souvent oui pour les sites de contenu et bases de connaissances. Vous gagnez en rapidité et en pertinence basique. Vous n’aurez pas de stemming avancé, synonymes, tolérance aux fautes ou classement personnalisé sans travail supplémentaire — mais beaucoup de sites n’en ont pas besoin pour cesser d’être pénibles.
4) Et la recherche des PDF, attachments ou champs personnalisés ?
N’ajoutez pas de JOINs aléatoires au meta au moment de la requête et espérez le meilleur. Construisez une table d’index soignée qui stocke le texte extrait ou les champs sélectionnés, puis appliquez FULLTEXT sur cette table.
5) Redis va‑t‑il automatiquement accélérer la recherche ?
Redis accélère les consultations répétées et réduit le bavardage DB. Il ne rend pas une requête pathologique unique rapide. Utilisez‑le pour réduire les dommages collatéraux et pour cacher les IDs de résultats de recherche.
6) Est‑il sûr de nettoyer wp_postmeta et les révisions ?
Cela peut être sûr, et cela peut aussi casser si vous supprimez des données attendues par un plugin. Faites des backups, commencez par les orphelins et les blobs de cache évidents, et testez le comportement des plugins en staging.
7) Pourquoi la page 10 des résultats est‑elle si lente ?
La pagination OFFSET force la base à trouver et jeter beaucoup de lignes. Envisagez de limiter les pages, utiliser « charger plus » avec curseurs, ou mettre en cache la liste d’IDs et la découper.
8) Dois‑je d’abord migrer vers un serveur BD plus puissant ?
Seulement après avoir confirmé que votre plan de requête est raisonnable. Les serveurs plus gros sont excellents pour faire plus de travail. Ils ne font pas disparaître le mauvais travail.
9) Hébergement mutualisé : que puis‑je faire raisonnablement ?
Vous ne contrôlez peut‑être pas la config MySQL ou les logs, mais vous pouvez toujours : réduire le périmètre de recherche, imposer une longueur minimale, désactiver la recherche meta, nettoyer les révisions et ajouter un cache au niveau application. Si FULLTEXT est autorisé, c’est toujours la meilleure amélioration sans service externe.
10) Comment savoir que la correction a fonctionné ?
Mesurez le p95 du temps de réponse des recherches, les lignes examinées pour des requêtes représentatives et le CPU/IO wait pendant une tranche de trafic. Si les lignes examinées ont chuté d’un ordre de grandeur, vous avez réellement corrigé le problème.
Prochaines étapes (pratiques, pas poétiques)
Faites trois choses cette semaine :
- Prouvez le goulot avec les slow query logs et
EXPLAIN. Si vous scannez des centaines de milliers de lignes par recherche, arrêtez de négocier. - Choisissez une vraie stratégie de recherche : FULLTEXT sur
wp_postspour les besoins simples, ou une table d’index dédiée si vous devez inclure meta/taxonomie sélectionnés. - Stabilisez avec des garde‑fous : longueur minimale, autocomplétion débouncée, types de posts limités et cache des IDs de résultats avec TTL court plus une invalidation raisonnable.
Si vous le faites bien, la boîte de recherche cesse d’être un outil de test de charge et redevient une fonctionnalité. Le calme est l’objectif.