Comment accélérer le moteur de recherche de WordPress à l'aide d'un index JSON statique

Aucun commentaire

24.07.2026|

Aucun commentaire

Tiempo de lectura Lectura: 12 min, 21 s
Número de palabras Palabras: 2287
Número de visitas Visitas: 6
Icono de traducción

Ce n'est un secret pour personne que le moteur de recherche de WordPress est nul. Sur le plan esthétique, le problème est relativement facile à résoudre; en revanche, son fonctionnement est une autre histoire, car il n'offre aucune option de configuration, sans compter que ses performances sont lamentables.

Lorsqu'un site WordPress accumule des milliers d'articles, le moteur de recherche natif commence à présenter des défaillances. Il ralentit jusqu'à devenir pratiquement inutilisable. Lors d’une recherche, WordPress exécute une requête LIKE %mot% dans la base de données, qui parcourt l’intégralité du contenu de la table wp_posts. Cela génère des requêtes SQL pouvant prendre de quelques secondes à une éternité et provoquer des pics de mémoire qui finissent par bloquer le serveur.

C'est exactement ce qui m'arrivait déjà. Avec 6 040 articles publiés (uniquement en espagnol), les temps de réponse des recherches renvoyant le plus grand nombre de résultats étaient un véritable cauchemar, avec des délais supérieurs à 10 secondes, voire plus. Et le moment était venu de régler ce problème une bonne fois pour toutes.

Après y avoir longuement réfléchi, j'ai constaté qu'il existait au moins trois possibilités pour résoudre ce problème. La première, et la plus simple, consistait à utiliser un plugin payant comme Relevanssi (sa version gratuite n'est pas suffisante) ou un service externe comme Algolia, ce que j'ai écarté d'emblée car mon objectif est d'accélérer les recherches de manière native.

Parmi les deux options restantes, j'ai essayé de créer un index FULLTEXT dans MySQL à l'aide de requêtes MATCH() AGAINST(), mais j'ai échoué lamentablement. Malgré l'ajustement des paramètres du moteur InnoDB et l'optimisation des index, MySQL continuait à se bloquer lors du traitement de milliers de mots-clés complexes, générant des temps de réponse inacceptables et saturant le serveur. C’est alors que j’ai complètement écarté l’idée de faire porter cette charge lourde à la base de données et que j’ai opté pour la troisième solution : créer un index statique en JSON.

Ce tutoriel vise à expliquer comment résoudre ce problème en remplaçant les requêtes MySQL lourdes par un fichier JSON statique qui fait office d'index de recherche ultra-rapide.

Étape 1 : Identifier et supprimer les requêtes lourdes ou redondantes

Avant de mettre en place l'index JSON, il est essentiel de faire le ménage. De nombreux plugins (tels que les répertoires, les générateurs de mise en page ou les plugins de recherche secondaire) s'intègrent au processus de recherche de WordPress via des hooks (pre_get_posts, posts_where, etc.), ce qui déclenche des requêtes supplémentaires et inutiles vers la base de données.

Comment détecter ces requêtes à l'aide de Query Monitor ?

  1. Installez et activez le plugin gratuit Query Monitor.
  2. Effectuez n'importe quelle recherche sur votre site web.
  3. Ouvrez le menu déroulant du Query Monitor dans la barre supérieure et sélectionnez « Requêtes de base de données » (Database Queries).
  4. Filtrez les requêtes lentes ou parcourez la liste pour repérer celles qui prennent le plus de temps. Profitez-en pour noter également les requêtes en double et, si possible, corrigez-les aussi.
  5. Regarde la colonne « Détails » des tables : tu y verras exactement quel plugin ou fichier exécute cette requête.

Dans mon cas, j'ai dû résoudre quelques requêtes en double générées par le plugin Name_Directory, ainsi que certains blocs GenerateBlocks du modèle d'article, mais n'importe quel plugin, code personnalisé ou élément ajouté peut générer des requêtes qui ralentissent la réponse.

Comment désactiver ces hooks par code

Une fois que vous avez identifié le nom de la fonction ou de la classe à l'origine de la requête lente, vous pouvez la désactiver dans votre fichier functions.php ou à l'aide d'un plugin de snippets (tel que Code Snippets, Code de Perfmatters ou similaire) :

* Remarque : dans le bloc 1, vous devez ajouter vos hooks qui génèrent des requêtes lentes. Si ce n'est pas votre cas, vous pouvez supprimer cette partie et n'ajouter que le bloc 2 à l'extrait de code.

/**
 * 1. Desactivar funciones invasivas o redundantes en las búsquedas globales de WP
 */
add_action('init', function() {
    // Ejemplo: Si Query Monitor muestra que un plugin de directorio se ejecuta en pre_get_posts:
    // remove_action('hook_de_wordpress', 'nombre_de_la_funcion_del_plugin', prioridad);
    remove_action('pre_get_posts', 'name_directory_search', 10);
    remove_filter('posts_where', 'name_directory_insert_sitewide_search_results', 10);
}, 20);

/**
 * 2. Bloquear la búsqueda SQL nativa tipo LIKE %palabra% en MySQL
 */
add_filter('posts_search', function($search, $wp_query) {
    if (!is_admin() && is_search()) {
        return ''; // Neutraliza el LIKE %palabra% en MySQL globalmente en páginas de búsqueda
    }
    return $search;
}, 10, 2);

Remarque : pour désactiver un hook à l'aide de `remove_action` ou `remove_filter`, vous devez utiliser exactement le même nom d'événement (`pre_get_posts`), le même nom de fonction que celui que vous avez identifié dans Query Monitor et la même priorité que celle avec laquelle il a été ajouté (10 par défaut).

Étape 2 : Créer et mettre à jour le fichier JSON de recherche

Pour remplacer MySQL, nous devons créer un fichier statique au format JSON contenant les données clés de nos articles (ID, titre et texte brut). Ce fichier sera enregistré dans le dossier wp-content/uploads/ et sera mis à jour automatiquement chaque fois que vous créerez, modifierez ou supprimerez un article.

PHP

/**
 * Actualizar el archivo search-es.json automáticamente al guardar o borrar un post
 */
add_action('save_post', 'mi_sitio_actualizar_indice_busqueda_json', 10, 3);
add_action('deleted_post', 'mi_sitio_actualizar_indice_busqueda_json', 10, 1);

function mi_sitio_actualizar_indice_busqueda_json($post_id) {
    if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) return;
    if (wp_is_post_revision($post_id)) return;
    
    $post = get_post($post_id);
    if (!$post || $post->post_type !== 'post') return;

    // Opcional: Filtrar por idioma si usas un plugin multilingüe como Polylang
    if (function_exists('pll_get_post_language') && pll_get_post_language($post_id) !== 'es') {
        return;
    }

    mi_sitio_reconstruir_indice_json_completo();
}

/**
 * Función que recorre todas las entradas publicadas y crea el JSON
 */
function mi_sitio_reconstruir_indice_json_completo() {
    $args = array(
        'post_type'      => 'post',
        'post_status'    => 'publish',
        'posts_per_page' => -1,
        'orderby'        => 'date',
        'order'          => 'ASC',
        'fields'         => 'ids', // Solo IDs para optimizar memoria
    );

    if (function_exists('pll_current_language')) {
        $args['lang'] = 'es';
    }

    $all_post_ids = get_posts($args);
    $indice = array();

    foreach ($all_post_ids as $id) {
        $titulo = get_the_title($id);
        $contenido = get_post_field('post_content', $id);

        $indice[] = array(
            'i' => (int)$id,
            't' => (string)$titulo,
            'k' => (string)wp_strip_all_tags($contenido),
        );
    }

    $upload_dir = wp_upload_dir();
    $json_path  = $upload_dir['basedir'] . '/search-es.json';

    file_put_contents($json_path, json_encode($indice, JSON_UNESCAPED_UNICODE));
}

Configuration initiale : la première fois que vous installez cet extrait de code, vous pouvez exécuter temporairement la fonction « my_site_rebuild_full_json_index() » ou simplement enregistrer/mettre à jour n'importe quelle entrée existante afin que le fichier « search-es.json » soit créé pour la première fois. Une fois cela fait, rendez-vous dans le répertoire wp-content/uploads/ pour vérifier que le fichier a bien été créé et connaître sa taille.

Pour te donner une idée de la taille approximative de ce fichier search-es.json: pour mes 6 040 entrées, un fichier de 3,41 Mo a été généré, une taille tout à fait raisonnable puisqu’il n’y a pas lieu de s’inquiéter tant qu’il ne dépasse pas 15 ou 20 Mo (soit l’équivalent d’environ 30000 à 40 000 entrées volumineuses), il n’y a pas lieu de s’inquiéter, car les fichiers JSON statiques sont chargés d’un seul coup directement dans le cache du serveur ou de PHP (file_get_contents), et un fichier de quelques mégaoctets se lit en moins d’un millième de seconde ; cependant, si le fichier grossit de manière démesurée, à chaque fois qu’une recherche est lancée, PHP consommera un pic supplémentaire et inutile de mémoire RAM pour charger un fichier JSON gigantesque. Dans ce cas, il serait judicieux d’envisager d’indexer uniquement les titres et les extraits plutôt que l’intégralité du contenu, ou de migrer vers une solution de recherche indexée sur une base de données dédiée.

Étape 3 : Fonction auxiliaire permettant de normaliser les recherches (sans accents)

Pour garantir que les recherches aboutissent à des résultats, quelle que soit la façon dont l'utilisateur saisit son requête (avec ou sans accents, en majuscules ou en minuscules), une fonction de nettoyage est définie :

PHP

/**
 * Función auxiliar para eliminar acentos y pasar a minúsculas
 */
if (!function_exists('mi_sitio_limpiar_acentos')) {
    function mi_sitio_limpiar_acentos($cadena) {
        $cadena = mb_strtolower($cadena, 'UTF-8');
        $string = array(
            'á'=>'a', 'à'=>'a', 'ä'=>'a', 'â'=>'a', 'ª'=>'a', 'Á'=>'a', 'À'=>'a', 'Ä'=>'a', 'Â'=>'a',
            'é'=>'e', 'è'=>'e', 'ë'=>'e', 'ê'=>'e', 'É'=>'e', 'È'=>'e', 'Ë'=>'e', 'Ê'=>'e',
            'í'=>'i', 'ì'=>'i', 'ï'=>'i', 'î'=>'i', 'Í'=>'i', 'Ì'=>'i', 'Ï'=>'i', 'Î'=>'i',
            'ó'=>'o', 'ò'=>'o', 'ö'=>'o', 'ô'=>'o', 'Ó'=>'o', 'Ò'=>'o', 'Ö'=>'o', 'Ô'=>'o',
            'ú'=>'u', 'ù'=>'u', 'ü'=>'u', 'û'=>'u', 'Ú'=>'u', 'Ù'=>'u', 'Ü'=>'u', 'Û'=>'u',
            'ñ'=>'n', 'Ñ'=>'n'
        );
        return strtr($cadena, $string);
    }
}

Étape 4 : Intercepter la requête principale avec ` posts_pre_query`

Grâce au filtre `posts_pre_query`, la requête de recherche de WordPress est interceptée avant qu'elle n'atteigne la base de données. Le JSON est lu, les ID correspondant aux termes recherchés sont identifiés, et seuls ces éléments sont renvoyés à WordPress, en précisant la pagination correspondante.

PHP

/**
 * Interceptación de la búsqueda nativa leyendo el archivo JSON
 */
add_filter('posts_pre_query', function($posts, $wp_query) {
    if (!is_admin() && $wp_query->is_search() && $wp_query->is_main_query()) {
        
        if (function_exists('pll_current_language')) {
            if (pll_current_language() !== 'es') return $posts;
        }

        $s = $wp_query->get('s');
        if (empty($s)) return array();

        $upload_dir = wp_upload_dir();
        $json_path  = $upload_dir['basedir'] . '/search-es.json';

        if (!file_exists($json_path)) return $posts;

        $indice = json_decode(file_get_contents($json_path), true);
        if (!is_array($indice)) return $posts;

        $s_clean = mi_sitio_limpiar_acentos(wp_strip_all_tags($s));
        $palabras_buscadas = array_filter(explode(' ', $s_clean));

        if (empty($palabras_buscadas)) return array();

        $matched_ids = array();

        // Recorrer el índice acumulando coincidencias
        foreach ($indice as $item) {
            $titulo_clean = mi_sitio_limpiar_acentos($item['t']);
            $texto_clean  = mi_sitio_limpiar_acentos($item['k']);

            $coinciden = true;
            foreach ($palabras_buscadas as $palabra) {
                if (mb_strlen($palabra, 'UTF-8') < 2) continue; // Ignorar palabras de 1 letra

                if (mb_strpos($titulo_clean, $palabra) === false && mb_strpos($texto_clean, $palabra) === false) {
                    $coinciden = false;
                    break; 
                }
            }

            if ($coinciden) {
                $matched_ids[] = (int)$item['i'];
            }
        }

        // Si no hay resultados, devolver array vacío
        if (empty($matched_ids)) {
            $wp_query->found_posts = 0;
            $wp_query->max_num_pages = 0;
            return array();
        }

        // Paginación
        $posts_per_page = (int)get_option('posts_per_page', 10);
        $paged = max(1, (int)$wp_query->get('paged'));

        $total_encontrados = count($matched_ids);
        
        $wp_query->found_posts = $total_encontrados;
        $wp_query->max_num_pages = ceil($total_encontrados / $posts_per_page);

        // Recuperar solo los objetos de los posts encontrados, ordenados por fecha
        return get_posts(array(
            'post__in'               => $matched_ids,
            'orderby'                => 'date',
            'order'                  => 'DESC',
            'posts_per_page'         => $posts_per_page,
            'paged'                  => $paged,
            'post_type'              => 'post',
            'no_found_rows'          => true,
            'cache_results'          => true,
            'update_post_meta_cache' => false,
            'update_post_term_cache' => false,
            'suppress_filters'       => true,
        ));
    }

    return $posts;
}, 10, 2);

Étape 5 : Vérification des résultats

Une fois les étapes précédentes effectuées, lancez un test de recherche et ouvrez Query Monitor:

  1. Durée totale de la requête à la base de données : vous constaterez qu'elle passe désormais de plusieurs secondes à à peine quelques millisecondes (généralement entre 0,03 s et 0,2 s).
  2. Requêtes SQL : la clause LIKE %terme% aura complètement disparu.
  3. Mémoire RAM : la consommation maximale de mémoire restera exceptionnellement faible (environ 30 Mo).
  4. Classement : Les articles s'afficheront dans un ordre chronologique strictement décroissant (du plus récent au plus ancien), en respectant la pagination native de WordPress.

Je suis plus que satisfait du résultat. Non seulement j’ai résolu un problème qui me taraudait depuis des années, mais en plus, la réponse est tout simplement spectaculaire. Même pour l'une des requêtes les plus exigeantes, comme celle-ci qui donne 4 887 résultats pour «vignette », le moteur de recherche répond très rapidement. Pour les recherches donnant moins de résultats, on peut dire qu'ils s'affichent instantanément. Vous pouvez le vérifier en effectuant n'importe quelle autre recherche.

Comment accélérer le moteur de recherche de WordPress à l'aide d'un index JSON statique 1

Laisser un commentaire

Vous avez quelque chose à dire ?

Este blog se aloja en LucusHost

LucusHost, el mejor hosting