Hoe je de zoekfunctie van WordPress kunt versnellen met behulp van een statische JSON-index

Geen reacties

24.07.2026|

Geen reacties

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

Dat de zoekfunctie van WordPress waardeloos is, is voor niemand een geheim. Wat het uiterlijk betreft, is dit relatief eenvoudig op te lossen, maar de werking is een heel ander verhaal: er zijn namelijk geen instellingsmogelijkheden en bovendien zijn de prestaties ronduit belabberd.

Wanneer een WordPress-site duizenden berichten bevat, begint de ingebouwde zoekfunctie haperingen te vertonen. Deze wordt steeds trager, totdat hij vrijwel onbruikbaar is. Bij het uitvoeren van een zoekopdracht voert WordPress een LIKE %woord% query uit in de database, die de volledige inhoud van de tabel wp_posts doorzoekt. Dit genereert SQL-query's die enkele seconden tot een eeuwigheid kunnen duren en pieken in het geheugengebruik kunnen veroorzaken, waardoor de server uiteindelijk vastloopt.

Dit is precies wat ik al aan het meemaken was. Met 6040 gepubliceerde berichten (alleen in het Spaans) waren de laadtijden van zoekopdrachten met veel resultaten een ware hel, met wachttijden van meer dan 10 seconden of zelfs langer. En toen brak de dag aan om dit probleem voor eens en voor altijd op te lossen.

Na er lang over nagedacht te hebben, kwam ik tot de conclusie dat er minstens drie mogelijkheden waren om dit op te lossen. De eerste en eenvoudigste was om een betaalde plug-in zoals Relevanssi te gebruiken (de gratis versie volstaat niet) of een externe dienst zoals Algolia, maar dat heb ik meteen afgewezen omdat ik het zoeken op een native manier wil versnellen.

Van de twee overgebleven opties heb ik geprobeerd een FULLTEXT-index in MySQL aan te maken met behulp van MATCH() AGAINST()-query’s, maar dat mislukte jammerlijk. Ondanks het aanpassen van de parameters van de InnoDB-engine en het optimaliseren van de indexen, bleef MySQL vastlopen bij het verwerken van duizenden complexe trefwoorden, wat resulteerde in onaanvaardbare responstijden en een overbelasting van de server. Op dat moment besloot ik definitief om de database niet langer met deze zware taak te belasten en koos ik voor de derde optie: het bouwen van een statische index in JSON.

In deze tutorial wordt uitgelegd hoe je dit probleem kunt oplossen door de zware MySQL-query's te vervangen door een statisch JSON-bestand dat fungeert als een razendsnelle zoekindex.

Stap 1: Zware of overbodige query's opsporen en verwijderen

Voordat je de JSON-index toepast, is het van cruciaal belang om eerst alles op te ruimen. Veel plug-ins (zoals directory’s, lay-outplug-ins of plug-ins voor secundaire zoekopdrachten) koppelen zich via hooks (pre_get_posts, posts_where, enz.) aan het zoekproces van WordPress, waardoor er extra en onnodige query’s naar de database worden gestuurd.

Hoe kun je deze query's met Query Monitor opsporen?

  1. Installeer en activeer de gratis plug-in Query Monitor.
  2. Voer een willekeurige zoekopdracht uit op je website.
  3. Open het vervolgkeuzemenu van Query Monitor in de bovenste balk en selecteer Databasequery's (Database Queries).
  4. Filter op trage query's of doorzoek de lijst op zoek naar de query's die het langst duren. Noteer meteen ook de dubbele query's en los die, indien mogelijk, ook op.
  5. Kijk eens naar de kolom met details in de tabellen: daar kun je precies zien welke plug-in of welk bestand die query uitvoert.

In mijn geval moest ik een aantal dubbele query's van de Name_Directory-plugin en enkele GenerateBlocks-blokken in het bericht-sjabloon oplossen, maar elke plugin, aangepaste code of toegevoegd element kan query's genereren die de responstijd vertragen.

Hoe je die hooks via code kunt uitschakelen

Zodra je de naam van de functie of klasse hebt gevonden die de trage query veroorzaakt, kun je deze uitschakelen in je functions.php -bestand of via een plug-in voor codefragmenten (zoals Code Snippets, Code van Perfmatters of iets dergelijks):

* Opmerking. In blok 1 moet je de hooks toevoegen die trage query's veroorzaken. Als dat bij jou niet het geval is, kun je dit gedeelte weglaten en alleen blok 2 aan het snippet toevoegen.

/**
 * 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);

Opmerking: Om een hook te verwijderen met `remove_action` of `remove_filter`, moet je precies dezelfde gebeurtenisnaam (pre_get_posts) gebruiken, dezelfde functienaam die je in Query Monitor hebt gevonden en dezelfde prioriteit waarmee deze is toegevoegd (standaard 10).

Stap 2: Het JSON-zoekbestand aanmaken en up-to-date houden

Om MySQL te vervangen, moeten we een statisch bestand in JSON-formaat aanmaken dat de belangrijkste gegevens van onze berichten bevat (ID, titel en platte tekst). Dit bestand wordt opgeslagen in de map wp-content/uploads/ en wordt automatisch bijgewerkt telkens wanneer je een bericht aanmaakt, bewerkt of verwijdert.

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));
}

Eerste generatie: De eerste keer dat je dit snippet installeert, kun je tijdelijk my_site_rebuild_full_json_index(); uitvoeren of gewoon een bestaand bericht opslaan/bijwerken, zodat het bestand search-es.json voor het eerst wordt aangemaakt. Ga daarna naar de map wp-content/uploads/ om te controleren of het bestand is aangemaakt en om de bestandsgrootte te bekijken.

Om je een idee te geven van de geschatte grootte van dit search-es.json-bestand: voor mijn 6040 records werd een bestand van 3,41 Mb gegenereerd, een vrij overzichtelijke grootte, aangezien je je geen zorgen hoeft te maken zolang het niet meer dan 15 of 20 MB weegt (wat overeenkomt met ongeveer 30.000 of 40.000 uitgebreide vermeldingen) hoef je je geen zorgen te maken, want statische JSON-bestanden worden in één keer rechtstreeks in de cache van de server of van PHP (file_get_contents) geladen, en een bestand van enkele megabytes wordt in minder dan een duizendste van een seconde gelezen; maar als het bestand buitensporig groot wordt, zal PHP bij elke zoekopdracht een onnodige piek in het RAM-geheugen verbruiken om een gigantisch JSON-bestand in het geheugen te laden. Op dat moment is het raadzaam om te overwegen om alleen de titels en samenvattingen te indexeren in plaats van de volledige inhoud, of om over te stappen op een zoekoplossing met een speciale database-index.

Stap 3: Hulpfunctie om zoekopdrachten te standaardiseren (zonder accenten)

Om ervoor te zorgen dat zoekopdrachten resultaten opleveren, ongeacht hoe de gebruiker het woord schrijft (met of zonder accenten, hoofdletters of kleine letters), wordt een opschoningsfunctie gedefinieerd:

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);
    }
}

Stap 4: De hoofdquery onderscheppen met ` posts_pre_query`

Met behulp van het filter `posts_pre_query` wordt de zoekopdracht van WordPress onderschept voordat deze de database bereikt. De JSON wordt gelezen, de ID’s die overeenkomen met de gezochte termen worden gevonden en alleen die berichten worden teruggestuurd naar WordPress, met vermelding van de bijbehorende paginering.

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);

Stap 5: Controle van de resultaten

Nadat je de bovenstaande stappen hebt doorlopen, voer je een zoektest uit en open je Query Monitor:

  1. Totale BD-tijd: Je zult zien dat deze nu is teruggebracht van enkele seconden tot slechts enkele milliseconden (meestal tussen 0,03 s en 0,2 s).
  2. SQL-query's: De clausule LIKE %term% zal volledig verdwijnen.
  3. RAM-geheugen: Het piekgebruik van het geheugen blijft uitzonderlijk laag (ongeveer 30 MB).
  4. Sortering: De berichten worden weergegeven in strikt aflopende chronologische volgorde (van het meest recente naar het oudste), waarbij de standaardpaginering van WordPress wordt aangehouden.

Ik ben meer dan tevreden met het resultaat. Niet alleen heb ik een probleem opgelost waar ik al jaren mee worstelde, maar de resultaten zijn nu ronduit spectaculair. Bij een van de meest veeleisende zoekopdrachten, zoals deze met 4887 resultaten voor: "viñeta", reageert de zoekmachine zeer snel. Bij zoekopdrachten met minder resultaten kun je zeggen dat de resultaten onmiddellijk worden weergegeven. Je kunt dit zelf controleren door iets anders te zoeken.

Hoe je de zoekfunctie van WordPress kunt versnellen met behulp van een statische JSON-index 1

Plaats een reactie

Nog iets te zeggen?

Este blog se aloja en LucusHost

LucusHost, el mejor hosting