Ako zrýchliť vyhľadávač v WordPresse pomocou statického JSON indexu

No comments

24.07.2026|

No comments

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

Nie je žiadnym tajomstvom, že vyhľadávač vo WordPresse je na nič. Pokiaľ ide o vzhľad, dá sa to pomerne ľahko vyriešiť, avšak jeho fungovanie je už iná kapitola, keďže neponúka žiadne možnosti nastavenia a navyše je jeho výkonnosť žalostná.

Keď sa na stránke v systéme WordPress nahromadia tisíce príspevkov, natívny vyhľadávač začne zlyhávať. Spomaľuje sa, až sa stane prakticky nepoužiteľným. Pri vyhľadávaní WordPress spustí v databáze dotaz typu LIKE %slovo%, ktorý prehľadáva celý obsah tabuľky wp_posts. To generuje SQL dotazy, ktorých spracovanie môže trvať od niekoľkých sekúnd až po nekonečne dlhú dobu a spôsobiť špičky v spotrebe pamäte, ktoré nakoniec zablokujú server.

Presne to sa mi už stalo. Pri 6040 zverejnených príspevkoch (len v španielčine) boli časy odozvy pri vyhľadávaniach s väčším počtom výsledkov skutočné peklo – trvali viac ako 10 sekúnd alebo aj dlhšie. A prišiel deň, keď som to musel raz a navždy vyriešiť.

Po dlhom premýšľaní som zistil, že existujú aspoň tri možnosti, ako to vyriešiť. Prvá a najjednoduchšia bola použiť nejaký platený plugin, ako napríklad Relevanssi (jeho bezplatná verzia nestačí), alebo externú službu, ako je Algolia, čo som však hneď vylúčil, pretože mojim cieľom je zrýchliť vyhľadávanie priamo v systéme.

Zo zostávajúcich dvoch možností som vyskúšal vytvorenie FULLTEXT indexu v MySQL pomocou dotazov MATCH() AGAINST() a zlyhal som na celej čiare. Napriek tomu, že som upravil parametre enginu InnoDB a optimalizoval indexy, MySQL sa naďalej zasekával pri spracúvaní tisícov zložitých kľúčových slov, čo viedlo k neprijateľným časom odozvy a preťaženiu servera. Vtedy som úplne vylúčil myšlienku nechať túto ťažkú záťaž na databáze a rozhodol som sa pre tretiu možnosť – vytvorenie statického indexu v JSON.

V tomto návode sa pokúsime vysvetliť, ako tento problém vyriešiť nahradením náročných dotazov v MySQL statickým súborom JSON, ktorý funguje ako bleskovo rýchly vyhľadávací index.

Krok 1: Zistiť a odstrániť náročné alebo nadbytočné dotazy

Pred implementáciou indexu JSON je nevyhnutné pripraviť pôdu. Mnohé pluginy (napríklad adresáre, šablóny alebo pluginy pre sekundárne vyhľadávanie) sa pripájajú k procesu vyhľadávania vo WordPresse prostredníctvom háčikov (pre_get_posts, posts_where atď.), čím spúšťajú dodatočné a zbytočné dotazy do databázy.

Ako tieto dotazy zistiť pomocou nástroja Query Monitor?

  1. Nainštalujte a aktivujte bezplatný doplnok Query Monitor.
  2. Vykonajte akékoľvek vyhľadávanie na vašej webovej stránke.
  3. Otvorte roletové menu nástroja Query Monitor na hornom paneli a vyberte položku Databázové dotazy (Database Queries).
  4. Filtrujte podľa pomalých dotazov alebo prejdite zoznam a vyhľadajte dotazy, ktorých spracovanie trvá najdlhšie. Pri tej príležitosti si tiež poznačte duplicitné dotazy a, ak je to možné, vyriešte aj tento problém.
  5. Pozri sa na stĺpec s podrobnosťami v tabuľkách: tam uvidíš presne, ktorý plugin alebo súbor túto požiadavku spúšťa.

V mojom prípade som musel vyriešiť niekoľko duplicitných dotazov z pluginu Name_Directory a niekoľko blokov GenerateBlocks v šablóne príspevku, ale akýkoľvek plugin, vlastný kód alebo pridaný prvok môže generovať dotazy, ktoré spomaľujú odpoveď.

Ako tieto háčiky deaktivovať pomocou kódu

Akonáhle zistíte názov funkcie alebo triedy, ktorá spôsobuje pomalé spracovanie dotazu, môžete ju deaktivovať v súbore functions.php alebo pomocou pluginu na správu úryvkov kódu (napríklad Code Snippets, Code od Perfmatters alebo podobného):

* Poznámka: Do bloku 1 je potrebné pridať vaše háčiky, ktoré spôsobujú pomalé dotazy. Ak to nie je váš prípad, môžete túto časť vynechať a do úryvku pridať len blok 2.

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

Poznámka: Ak chceš odpojiť hook pomocou remove_action alebo remove_filter, musíš použiť presne ten istý názov udalosti (pre_get_posts), ten istý názov funkcie, ktorý si identifikoval v Query Monitore, a tú istú prioritu, s akou bol pridaný (predvolene 10).

Krok 2: Vytvorenie a aktualizácia vyhľadávacieho súboru JSON

Aby sme nahradili MySQL, musíme vytvoriť statický súbor vo formáte JSON, ktorý bude obsahovať kľúčové údaje našich príspevkov (ID, názov a čistý text). Tento súbor sa uloží do priečinka wp-content/uploads/ a bude sa automaticky aktualizovať zakaždým, keď vytvoríte, upravíte alebo vymažete príspevok.

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

Počiatočné vytvorenie: Pri prvej inštalácii tohto kódu môžete dočasne spustiť funkciu my_site_rebuild_full_json_index(); alebo jednoducho uložiť/aktualizovať akýkoľvek existujúci príspevok, aby sa súbor search-es.json vytvoril po prvýkrát. Potom prejdite do adresára wp-content/uploads/, aby ste sa uistili, že bol súbor vytvorený, a zistili jeho veľkosť.

Aby si si vedel predstaviť približnú veľkosť tohto súboru search-es.json: pre mojich 6040 záznamov sa vygeneroval súbor s veľkosťou 3,41 Mb, čo je celkom zvládnuteľná veľkosť, pretože pokiaľ neprekročí 15 alebo 20 MB (čo zodpovedá približne 30.000 až 40 000 rozsiahlych záznamov), nie je sa čoho obávať, pretože statické súbory JSON sa načítajú naraz priamo do vyrovnávacej pamäte servera alebo PHP (file_get_contents) a súbor s veľkosťou niekoľkých megabajtov sa prečíta za menej ako tisícinu sekundy; ak však súbor nadmerne narastie, pri každom spustení vyhľadávania bude PHP spotrebovávať zbytočný špičkový príkon pamäte RAM na načítanie obrovského súboru JSON, v takom prípade by bolo vhodné zvážiť indexovanie len názvov a úryvkov namiesto celého obsahu, alebo prejsť na riešenie vyhľadávania indexovaného v špecializovanej databáze.

Krok 3: Pomocná funkcia na štandardizáciu vyhľadávania (bez diakritických znamienok)

Aby sa zabezpečilo, že vyhľadávanie nájde výsledky bez ohľadu na to, ako používateľ píše (s diakritikou alebo bez nej, veľkými alebo malými písmenami), definuje sa funkcia na čistenie:

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

Krok 4: Zachytenie hlavného dotazu pomocou posts_pre_query

Prostredníctvom filtra posts_pre_query sa zachytí vyhľadávací dotaz systému WordPress ešte predtým, ako sa dostane do databázy. Prečíta sa JSON, vyhľadajú sa ID, ktoré zodpovedajú hľadaným výrazom, a systému WordPress sa vrátia iba tieto záznamy s uvedením príslušného stránkovania.

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

Krok 5: Overenie výsledkov

Po vykonaní predchádzajúcich krokov vykonajte test vyhľadávania a otvorte nástroj Query Monitor:

  1. Celkový čas BD: Zistíte, že sa teraz skrátil z niekoľkých sekúnd na iba niekoľko milisekúnd (zvyčajne medzi 0,03 s a 0,2 s).
  2. SQL-dotazy: Klauzula LIKE %termín% úplne zanikne.
  3. RAM: Špičková spotreba pamäte zostane mimoriadne nízka (okolo 30 MB).
  4. Usporiadanie: Príspevky sa budú zobrazovať v prísnom chronologickom poradí od najnovších po najstaršie, pričom sa zachová pôvodné stránkovanie systému WordPress.

S výsledkom som viac než spokojný. Nielenže som sa zbavil problému, ktorý ma trápil už roky, ale teraz je výsledok priam úžasný. Aj pri jednom z najnáročnejších vyhľadávaní, ako je toto s 4 887 výsledkami pre výraz„viñeta, vyhľadávač reaguje veľmi rýchlo. Pri vyhľadávaniach s menším počtom výsledkov sa dá povedať, že sa zobrazujú okamžite. Môžete si to overiť vyhľadávaním čohokoľvek iného.

Ako zrýchliť vyhľadávač v WordPresse pomocou statického JSON indexu 1

Vložiť komentár

Chcete niečo povedať?

Este blog se aloja en LucusHost

LucusHost, el mejor hosting