Hur man snabbar upp WordPress-sökmotorn med hjälp av ett statiskt JSON-index

Inga kommentarer

24.07.2026|

Inga kommentarer

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

Att WordPress sökfunktion är usel är ingen hemlighet. När det gäller utseendet är det relativt enkelt att åtgärda, men hur den fungerar är en helt annan historia, eftersom den inte erbjuder några inställningsmöjligheter alls – och dessutom är prestandan bedrövlig.

När en WordPress-webbplats har samlat på sig tusentals inlägg börjar den inbyggda sökfunktionen att krångla. Den blir allt långsammare tills den praktiskt taget blir oanvändbar. När man gör en sökning kör WordPress en LIKE %ord%-fråga i databasen som genomsöker hela innehållet i tabellen wp_posts. Detta genererar SQL-frågor som kan ta allt från några sekunder till en evighet och orsaka minnesbelastningar som till slut blockerar servern.

Det var precis det som redan hände mig. Med 6 040 publicerade inlägg (enbart på spanska) var svarstiderna för sökningar med många träffar ett rent helvete, med väntetider på över 10 sekunder eller mer. Och så kom dagen då jag skulle lösa problemet en gång för alla.

Efter att ha funderat länge kom jag fram till att det fanns minst tre möjligheter att lösa problemet. Den första och enklaste var att använda ett betaltillägg som Relevanssi (dess gratisversion räcker inte till) eller en extern tjänst som Algolia, vilket jag avfärdade direkt eftersom jag vill effektivisera sökningarna på ett inbyggt sätt.

Av de två återstående alternativen försökte jag skapa ett FULLTEXT-index i MySQL med hjälp av MATCH() AGAINST()-frågor, men misslyckades kapitalt. Trots att jag justerade parametrarna för InnoDB-motorn och optimerade indexen fortsatte MySQL att hänga sig vid bearbetningen av tusentals komplexa nyckelord, vilket resulterade i oacceptabla svarstider och överbelastade servern. Det var då jag helt avskrev tanken på att låta databasen hantera den tunga belastningen och valde istället den tredje vägen: att bygga ett statiskt index i JSON.

I den här handledningen försöker vi förklara hur man löser detta problem genom att ersätta tunga MySQL-frågor med en statisk JSON-fil som fungerar som ett blixtsnabbt sökindex.

Steg 1: Identifiera och ta bort tunga eller överflödiga frågor

Innan du implementerar JSON-indexet är det viktigt att rensa upp. Många plugins (som kataloger, layoutverktyg eller plugins för sekundära sökningar) kopplar sig till WordPress sökprocess via hooks (pre_get_posts, posts_where, etc.), vilket leder till ytterligare och onödiga databasfrågor.

Hur kan man identifiera dessa sökningar med Query Monitor?

  1. Installera och aktivera det kostnadsfria tillägget Query Monitor.
  2. Gör vilken sökning som helst på din webbplats.
  3. Öppna rullgardinsmenyn för Query Monitor i det övre fältet och välj Databasfrågor (Database Queries).
  4. Filtrera efter långsamma frågor eller gå igenom listan för att hitta de frågor som tar längst tid. Passa samtidigt på att notera även dubbla frågor och, om du kan, åtgärda även dem.
  5. Titta i detaljkolumnen i tabellerna: där kan du se exakt vilket plugin eller vilken fil som kör den frågan.

I mitt fall var jag tvungen att åtgärda ett antal dubbla förfrågningar från pluginet Name_Directory och några GenerateBlocks-block i inläggsmallen, men vilket plugin, vilken anpassad kod eller vilket tillagt element som helst kan generera förfrågningar som saktar ner svarstiden.

Hur man inaktiverar dessa hooks via kod

När du har hittat namnet på den funktion eller klass som orsakar den långsamma sökningen kan du inaktivera den i din functions.php-fil eller med hjälp av ett plugin för kodsnuttar (som Code Snippets, Code från Perfmatters eller liknande):

* Obs! I block 1 ska du lägga till de hooks som orsakar långsamma sökningar. Om detta inte gäller dig kan du ta bort den delen och endast lägga till block 2 i kodsnuttet.

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

Obs! För att avaktivera en hook med remove_action eller remove_filter måste du använda exakt samma händelsenamn (pre_get_posts), samma funktionsnamn som du identifierade i Query Monitor och samma prioritet som den lades till med (standardvärdet är 10).

Steg 2: Skapa och hålla sök-JSON-filen uppdaterad

För att ersätta MySQL måste vi skapa en statisk fil i JSON-format som innehåller nyckeldata för våra inlägg (ID, rubrik och ren text). Denna fil sparas i mappen wp-content/uploads/ och uppdateras automatiskt varje gång du skapar, redigerar eller tar bort ett inlägg.

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

Initial generering: När du installerar detta kodavsnitt för första gången kan du tillfälligt köra mi_sitio_reconstruir_indice_json_completo(); eller helt enkelt spara/uppdatera ett befintligt inlägg så att filen search-es.json skapas för första gången. När detta är gjort, gå till mappen wp-content/uploads/ för att kontrollera att filen har skapats och se hur stor den är.

För att du ska få en uppfattning om den ungefärliga storleken på denna search-es.json: för mina 6 040 poster genererades en fil på 3,41 Mb, vilket är en ganska hanterbar storlek eftersom man inte behöver oroa sig förrän filen börjar överstiga 15 eller 20 MB (motsvarande cirka 30.000 eller 40 000 omfattande poster) behöver man inte oroa sig, eftersom statiska JSON-filer laddas direkt in i serverns eller PHP:s cache (file_get_contents) i ett svep, och en filstorlek på några få megabyte läses in på mindre än en tusendel av en sekund; men om filen växer orimligt mycket, kommer PHP varje gång en sökning utförs att förbruka en onödig extra topp i RAM-minnet för att ladda en gigantisk JSON-fil. I det läget bör man överväga att endast indexera rubriker och utdrag istället för hela innehållet, eller migrera till en söklösning med indexering i en dedikerad databas.

Steg 3: Hjälpfunktion för att standardisera sökningar (utan accenter)

För att säkerställa att sökningarna ger resultat oavsett hur användaren skriver (med eller utan accenttecken, stora eller små bokstäver) definieras en rensningsfunktion:

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

Steg 4: Avlyssna huvudfrågan med posts_pre_query

Med hjälp av filtret posts_pre_query avlyssnas WordPress sökfråga innan den når databasen. JSON-data läses in, de ID:n som stämmer överens med söktermerna hämtas och endast dessa inlägg skickas tillbaka till WordPress tillsammans med information om sidnumrering.

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

Steg 5: Kontroll av resultaten

När du har genomfört stegen ovan gör du ett söktest och öppnar Query Monitor:

  1. Total databasbehandlingstid: Du kommer att märka att tiden nu går från flera sekunder till bara några millisekunder (vanligtvis mellan 0,03 s och 0,2 s).
  2. SQL-frågor: Klausulen LIKE %term% kommer att försvinna helt.
  3. RAM-minne: Den maximala minnesanvändningen kommer att förbli exceptionellt låg (cirka 30 MB).
  4. Sortering: Inläggen visas i strikt kronologisk ordning i fallande ordning (från det senaste till det äldsta), i enlighet med WordPress inbyggda paginering.

Jag är mer än nöjd med resultatet. Jag har inte bara blivit av med ett problem som jag haft i flera år, utan nu är resultatet rent av spektakulärt. Även vid en av de mest krävande sökningarna, som denna med 4 887 träffar för”viñeta, svarar sökmotorn mycket snabbt. Vid sökningar med färre träffar kan man säga att resultaten visas omedelbart. Du kan själv testa detta genom att söka på vad som helst.

Hur man snabbar upp WordPress-sökmotorn med hjälp av ett statiskt JSON-index 1

Lämna en kommentar

Har du något att säga?

Este blog se aloja en LucusHost

LucusHost, el mejor hosting