Kuidas kiirendada WordPressi otsingut staatilise JSON-indeksi abil

No comments

24.07.2026|

No comments

Tiempo de lectura Lectura: 10 min, 12 s
Número de palabras Palabras: 1889
Número de visitas Visitas: 65
Icono de traducción

See, et WordPressi otsingumootor on täielik jama, pole kellelegi saladus. Välimuse osas on seda suhteliselt lihtne parandada, kuid selle toimimine on hoopis teine lugu, kuna see ei paku mingeid seadistusvõimalusi ja pealegi on selle jõudlus kahetsusväärne.

Kui WordPressi veebisaidile koguneb tuhandeid postitusi, hakkab sisseehitatud otsingumootor tõrkuma. See aeglustub niivõrd, et muutub praktiliselt kasutuks. Otsingut tehes käivitab WordPress andmebaasis päringu LIKE %sõna%, mis skaneerib kogu tabeli wp_posts sisu. See tekitab SQL-päringuid, mis võivad kesta mõnest sekundist kuni igavikuni ning põhjustada mälukasutuse tippkoormusi, mis lõpuks blokeerivad serveri.

Just see oli mul juba juhtumas. 6040 avaldatud postitusega (ainult hispaania keeles) olid tulemuste arvult kõige mahukamate otsingute vastusajad tõeline põrgu – need ületasid 10 sekundit või isegi rohkem. Ja siis saabus päev, mil otsustasin selle probleemi ükskord ja lõplikult lahendada.

Pärast pikka mõtlemist leidsin, et selle probleemi lahendamiseks on vähemalt kolm võimalust. Esimene ja lihtsaim oli kasutada mõnda tasulist pistikprogrammi, nagu Relevanssi (selle tasuta versioonist ei piisa), või välist teenust, nagu Algolia, kuid selle võimaluse jätsin kohe kõrvale, sest minu eesmärk on otsingut kiirendada süsteemi enda vahenditega.

Kahe järelejäänud võimaluse hulgast proovisin luua MySQLis FULLTEXT-indeksit MATCH() AGAINST()-päringute abil, kuid ebaõnnestusin täielikult. Hoolimata InnoDB-mootori parameetrite kohandamisest ja indeksite optimeerimisest, jäi MySQL ikkagi kinni tuhandete keeruliste märksõnade töötlemisel, andes vastuvõetamatuid vastusaegu ja ülekoormates serverit. Siis otsustasin täielikult loobuda andmebaasile raske koormuse panemisest ja valisin kolmanda võimaluse – luua staatiline indeks JSON-is.

Käesolevas juhendis püütakse selgitada, kuidas seda probleemi lahendada, asendades MySQLi ressursimahukad päringud staatilise JSON-failiga, mis toimib ülikiire otsingutabelina.

1. samm: Aeglaste või dubleerivate päringute tuvastamine ja eemaldamine

Enne JSON-indeksi rakendamist on äärmiselt oluline teha ettevalmistustööd. Paljud pistikprogrammid (nagu kataloogid, kujundusvahendid või täiendava otsingu pistikprogrammid) haakuvad WordPressi otsinguprotsessiga konksude (hooks) kaudu (pre_get_posts, posts_where jne), käivitades andmebaasi suunas täiendavaid ja tarbetuid päringuid.

Kuidas neid päringuid Query Monitoriga tuvastada?

  1. Paigalda ja aktiveeri tasuta pistikprogramm Query Monitor.
  2. Tee oma veebisaidil mis tahes otsing.
  3. Ava ülemisel ribal asuv Query Monitori rippmenüü ja vali „Andmebaasi päringud“ (Database Queries).
  4. Filtreeri tulemusi „Aeglased päringud” järgi või vaata nimekirja läbi, et leida päringuid, mis võtavad kõige rohkem aega. Samal ajal märgi üles ka dubleeritud päringud ja, kui võimalik, lahenda ka need.
  5. Vaata tabelite üksikasjade veergu: seal näed täpselt, milline plugin või fail seda päringut täidab.

Minu puhul tuli mul lahendada mõned Name_Directory-pistikprogrammi dubleeritud päringud ja mõned GenerateBlocksi plokid postituse mallis, kuid iga pistikprogramm, kohandatud kood või lisatud element võib tekitada päringuid, mis aeglustavad vastust.

Kuidas neid konksusid koodi abil välja lülitada

Kui oled leidnud aeglase päringu põhjustava funktsiooni või klassi nime, saad selle oma functions.php -failis või koodilõikude pistikprogrammi (nt Code Snippets, PerfmattersCode või muu sarnane) abil välja lülitada:

* Märkus. 1. plokki tuleb lisada need hookid, mis põhjustavad aeglasi päringuid. Kui see sinu puhul nii ei ole, võid selle osa välja jätta ja lisada koodijupile ainult 2. ploki.

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

Märkus: Et eemaldada hook funktsiooniga `remove_action` või `remove_filter`, pead kasutama täpselt sama sündmuse nime (`pre_get_posts`), sama funktsiooni nime, mille tuvastasid Query Monitoris, ning sama prioriteeti, millega see lisati (vaikimisi 10).

2. samm: Otsingu JSON-faili loomine ja ajakohastamine

MySQLi asendamiseks peame looma JSON-vormingus staatilise faili, mis sisaldab meie postituste põhiandmeid (ID, pealkiri ja lihttekst). See fail salvestatakse kausta wp-content/uploads/ ning seda uuendatakse automaatselt iga kord, kui lood, muudad või kustutad postitust.

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

Esmane loomine: Kui paigaldad selle koodijupi esimest korda, võid ajutiselt käivitada funktsiooni mi_sitio_reconstruir_indice_json_completo(); või lihtsalt salvestada/uuendada mõnda olemasolevat sissekannet, et fail search-es.json loodaks esmakordselt. Kui see on tehtud, külasta kataloogi wp-content/uploads/, et veenduda, et fail on loodud, ja teada saada selle mahtu.

Et saaksid aimu selle search-es.json-faili ligikaudsest suurusest: minu 6040 kirje puhul tekkis 3,41 Mb suurune fail, mis on üsna käepärane suurus, sest kuni faili suurus ei ületa 15–20 MB (mis vastab umbes 30.000 või 40 000 mahuka kirje) ei ole põhjust muretseda, sest staatilised JSON-failid laaditakse korraga otse serveri või PHP (file_get_contents) vahemällu ning mõne megabaidi suurune fail loetakse alla ühe tuhandiku sekundi; kuid kui fail kasvab liiga suureks, tarbib PHP iga otsingu käivitamisel tarbetut RAM-mälu tippkoormust hiiglasliku JSON-faili laadimiseks, mistõttu tasuks kaaluda ainult pealkirjade ja väljavõtete indekseerimist kogu sisu asemel või üleminekut spetsiaalsele andmebaasipõhisele otsingulahendusele.

3. samm: Abifunktsioon otsingute normaliseerimiseks (ilma aktsentideta)

Selleks et tagada, et otsingud annaksid tulemusi sõltumata sellest, kuidas kasutaja sõna kirjutab (tildidega või ilma, suurtähtedega või väiketähtedega), määratletakse puhastamisfunktsioon:

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

4. samm: Peamise päringu peatamine funktsiooniga posts_pre_query

Filtri „posts_pre_query“ abil peatatakse WordPressi otsingupäring enne, kui see jõuab andmebaasi. JSON-fail loetakse läbi, leitakse otsinguterminitega kokku langevad ID-d ning WordPressile tagastatakse ainult need kirjed, täpsustades vastava lehekülgede jaotuse.

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

5. samm: Tulemuste kontrollimine

Kui eelmised sammud on tehtud, tee otsingukatsetus ja ava Query Monitor:

  1. Andmebaasi koguaeg: Näed, et see on nüüd lühenenud mitmest sekundist vaid mõne millisekundini (tavaliselt vahemikus 0,03 s kuni 0,2 s).
  2. SQL-päringud: Klausel LIKE %termin% kaob täielikult.
  3. RAM-mälu: Mälu kasutamise tippväärtus jääb erakordselt madalaks (umbes 30 MB).
  4. Järjestus: Postitused kuvatakse rangelt kronoloogilises kahanevas järjekorras (uusimast vanimani), järgides WordPressi algset lehekülgede jaotust.

Olen tulemusega rohkem kui rahul. Ma pole mitte ainult lahendanud probleemi, mis mind juba aastaid vaevas, vaid nüüd on tulemus lausa suurepärane. Ühes kõige nõudlikumates otsingutes, nagu see , kus on 4887 tulemust sõnale„viñeta”, reageerib otsingumootor väga kiiresti. Vähem tulemusi andvate otsingute puhul võib öelda, et need kuvatakse silmapilkselt. Saad seda ise kontrollida, otsides midagi muud.

Kuidas kiirendada WordPressi otsingut staatilise JSON-indeksi abil 1
Seotud artiklid

Leave a Comment

Kas on midagi öelda?

Este blog se aloja en LucusHost

LucusHost, el mejor hosting