Hvordan øke hastigheten på WordPress-søkemotoren ved hjelp av et statisk JSON-indeks

No comments

24.07.2026|

No comments

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

At søkefunksjonen i WordPress er elendig, er ingen hemmelighet. Når det gjelder utseendet, er det relativt enkelt å fikse, men funksjonaliteten er en helt annen historie, siden den ikke tilbyr noen innstillingsmuligheter i det hele tatt – og i tillegg er ytelsen elendig.

Når et WordPress-nettsted har tusenvis av innlegg, begynner den innebygde søkefunksjonen å svikte. Den blir tregere og tregere, til den til slutt blir praktisk talt ubrukelig. Når du utfører et søk, kjører WordPress en LIKE %ord%-forespørsel i databasen som skanner hele innholdet i tabellen wp_posts. Dette genererer SQL-forespørsler som kan ta alt fra noen sekunder til en evighet, og forårsake minnebelastninger som til slutt blokkerer serveren.

Dette var akkurat det som allerede skjedde med meg. Med 6040 publiserte innlegg (bare på spansk) var responstiden for søk med mange treff et rent helvete, med ventetider på over 10 sekunder eller mer. Og så kom dagen da jeg endelig kunne løse problemet en gang for alle.

Etter å ha tenkt lenge på saken, fant jeg ut at det var minst tre måter å løse dette på. Den første og enkleste var å bruke et betalt plugin som Relevanssi (gratisversjonen er ikke god nok) eller en ekstern tjeneste som Algolia, noe jeg avviste med en gang, fordi jeg ønsker å effektivisere søkene på en innebygd måte.

Av de to gjenværende alternativene prøvde jeg å opprette en FULLTEXT-indeks i MySQL ved hjelp av MATCH() AGAINST()-spørsmål, men mislyktes fullstendig. Til tross for at jeg justerte parametrene for InnoDB-motoren og optimaliserte indeksene, fortsatte MySQL å henge seg opp ved behandling av tusenvis av komplekse nøkkelord, noe som resulterte i uakseptable responstider og overbelastning av serveren. Det var da jeg helt avskrev tanken om å la databasen ta på seg den tunge belastningen og valgte den tredje løsningen: å bygge en statisk indeks i JSON.

I denne veiledningen skal vi prøve å forklare hvordan man kan løse dette problemet ved å erstatte ressurskrevende MySQL-spørringer med en statisk JSON-fil som fungerer som et lynraskt søkeindeks.

Trinn 1: Oppdage og fjerne tunge eller overflødige spørringer

Før du tar i bruk JSON-indeksen, er det avgjørende å rydde opp. Mange plugins (som kataloger, layout-plugins eller plugins for sekundærsøk) kobler seg til søkeprosessen i WordPress ved hjelp av hooks (pre_get_posts, posts_where osv.), noe som utløser ekstra og unødvendige forespørsler til databasen.

Hvordan kan man oppdage disse spørringene med Query Monitor?

  1. Installer og aktiver det gratis pluginet Query Monitor.
  2. Gjør et hvilket som helst søk på nettstedet ditt.
  3. Åpne rullegardinmenyen for Query Monitor i den øverste linjen, og velg «Databaseforespørsler» (Database Queries).
  4. Filtrer etter «trege spørsmål» eller gå gjennom listen for å finne de spørsmålene som tar lengst tid. Noter forresten også de dupliserte spørsmålene, og hvis du kan, bør du løse dette også.
  5. Se på detaljkolonnen i tabellene: der kan du se nøyaktig hvilket plugin eller hvilken fil som utfører den aktuelle spørringen.

I mitt tilfelle måtte jeg løse et par dupliserte forespørsler fra pluginen Name_Directory og noen GenerateBlocks-blokker i innleggsmalen, men enhver plugin, tilpasset kode eller et element som er lagt til, kan generere forespørsler som forsinker svaret.

Hvordan deaktivere disse hookene via kode

Når du har funnet navnet på funksjonen eller klassen som forårsaker den langsomme forespørselen, kan du deaktivere den i filen functions.php eller ved hjelp av et plugin for kodebiter (som Code Snippets, Code fra Perfmatters eller lignende):

* Merk: I blokk 1 må du legge til de hookene som forårsaker langsomme spørringer. Hvis dette ikke gjelder deg, kan du fjerne denne delen og bare legge til blokk 2 i kodebiten.

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

Merk: For å fjerne en hook med `remove_action` eller `remove_filter`, må du bruke nøyaktig samme hendelsesnavn (`pre_get_posts`), samme funksjonsnavn som du fant i Query Monitor, og samme prioritet som den ble lagt til med (standard 10).

Trinn 2: Opprette og holde søke-JSON-filen oppdatert

For å erstatte MySQL må vi opprette en statisk fil i JSON-format som inneholder nøkkeldataene for innleggene våre (ID, tittel og ren tekst). Denne filen lagres i mappen wp-content/uploads/ og oppdateres automatisk hver gang du oppretter, redigerer eller sletter et innlegg.

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

Første opprettelse: Første gang du installerer dette kodestykket, kan du midlertidig kjøre mi_sitio_reconstruir_indice_json_completo(); eller ganske enkelt lagre/oppdatere et hvilket som helst eksisterende innlegg, slik at filen search-es.json opprettes for første gang. Når dette er gjort, kan du gå til mappen wp-content/uploads/ for å forsikre deg om at filen er opprettet og for å sjekke filstørrelsen.

For å gi deg et inntrykk av den omtrentlige størrelsen på denne search-es.json-filen: for mine 6040 oppføringer ble det generert en fil på 3,41 Mb, noe som er ganske håndterlig, siden man ikke trenger å bekymre seg før filen begynner å veie mer enn 15 eller 20 MB (tilsvarende rundt 30.000 eller 40 000 omfattende oppføringer), trenger du ikke å bekymre deg, fordi statiske JSON-filer lastes direkte inn i serverens eller PHPs cache (file_get_contents) på en gang, og en filstørrelse på noen få megabyte leses inn på under en tusendels sekund; men hvis filen vokser uforholdsmessig mye, vil PHP hver gang et søk utføres forbruke en unødvendig ekstra topp i RAM-minnet for å laste inn en gigantisk JSON-fil. I et slikt tilfelle bør man vurdere å indeksere kun titler og utdrag i stedet for hele innholdet, eller migrere til en dedikert databasebasert søkeløsning.

Trinn 3: Hjelpefunksjon for å normalisere søk (uten aksenter)

For å sikre at søkene gir resultater uavhengig av hvordan brukeren skriver (med eller uten aksenter, store eller små bokstaver), defineres en rensingsfunksjon:

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

Trinn 4: Avskjær hovedforespørselen med ` posts_pre_query`

Ved hjelp av filteret `posts_pre_query` blir søkeforespørselen fra WordPress fanget opp før den når databasen. JSON-dataene leses inn, ID-ene som samsvarer med søkeordene blir funnet, og kun disse oppføringene returneres til WordPress med angivelse av den tilhørende pagineringen.

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

Trinn 5: Kontroll av resultatene

Når du har fullført trinnene ovenfor, kan du utføre en søketest og åpne Query Monitor:

  1. Total tid for BD: Du vil se at tiden nå går fra flere sekunder til bare noen få millisekunder (vanligvis mellom 0,03 s og 0,2 s).
  2. SQL-spørsmål: LIKE %søkeord%-klausulen vil være helt borte.
  3. RAM-minne: Det maksimale minneforbruket vil forbli usedvanlig lavt (rundt 30 MB).
  4. Sortering: Innleggene vises i strengt kronologisk rekkefølge i synkende rekkefølge (fra det nyeste til det eldste), i tråd med WordPress’ innebygde paginering.

Jeg er mer enn fornøyd med resultatet. Ikke bare har jeg løst et problem jeg har slitt med i årevis, men nå er resultatet rett og slett spektakulært. Selv ved en av de mest krevende søkene, som dette med 4887 treff på«viñeta», svarer søkemotoren svært raskt. For søk med færre treff kan man si at resultatene vises umiddelbart. Du kan sjekke det ved å søke på hva som helst annet.

Hvordan øke hastigheten på WordPress-søkemotoren ved hjelp av et statisk JSON-indeks 1
Relaterte artikler

Legg igjen en kommentar

Har du noe å si?

Este blog se aloja en LucusHost

LucusHost, el mejor hosting