Cara mempercepat mesin pencari WordPress dengan menggunakan indeks JSON statis

No comments

24.07.2026|

No comments

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

Bukan rahasia lagi bagi siapa pun bahwa mesin pencari WordPress itu payah. Dari segi tampilan, masalah ini relatif mudah diatasi; namun, kinerjanya adalah cerita lain karena tidak menyediakan opsi konfigurasi apa pun, dan yang lebih parah lagi, kinerjanya sangat buruk.

Ketika sebuah situs WordPress memiliki ribuan postingan, mesin pencari bawaan mulai mengalami gangguan. Kinerjanya melambat hingga menjadi hampir tidak berguna. Saat melakukan pencarian, WordPress menjalankan kueri LIKE %kata% di basis data yang memindai seluruh konten tabel wp_posts. Hal ini menghasilkan kueri SQL yang dapat memakan waktu mulai dari beberapa detik hingga waktu yang sangat lama, serta menyebabkan lonjakan penggunaan memori yang pada akhirnya dapat memblokir server.

Inilah tepatnya yang sedang saya alami. Dengan 6.040 postingan yang telah dipublikasikan (hanya dalam bahasa Spanyol), waktu respons pencarian dengan hasil terbanyak benar-benar menyiksa, dengan waktu yang melebihi 10 detik atau lebih. Dan tibalah saatnya untuk menyelesaikan masalah ini sekali dan untuk selamanya.

Setelah memikirkannya berulang kali, saya menemukan setidaknya ada tiga kemungkinan untuk mengatasinya. Yang pertama dan paling sederhana adalah menggunakan plugin berbayar seperti Relevanssi (versi gratisnya tidak memadai) atau layanan eksternal seperti Algolia, namun saya langsung menyingkirkannya karena yang saya inginkan adalah mempercepat pencarian secara native.

Dari dua opsi yang tersisa, saya mencoba membuat indeks FULLTEXT di MySQL menggunakan kueri MATCH() AGAINST() dan gagal total. Meskipun telah menyesuaikan parameter mesin InnoDB dan mengoptimalkan indeks, MySQL tetap macet saat memproses ribuan kata kunci yang kompleks, menghasilkan waktu respons yang tidak dapat diterima, dan membuat server kewalahan. Baru pada saat itulah saya sepenuhnya mengesampingkan gagasan untuk membebani basis data dengan tugas berat tersebut dan memilih opsi ketiga, yaitu membuat indeks statis dalam format JSON.

Tutorial ini bertujuan untuk menjelaskan cara mengatasi masalah ini dengan mengganti kueri MySQL yang memakan waktu dengan berkas JSON statis yang berfungsi sebagai indeks pencarian yang sangat cepat.

Langkah 1: Mendeteksi dan menghilangkan kueri yang berat atau berlebihan

Sebelum menerapkan indeks JSON, sangat penting untuk membersihkan sistem terlebih dahulu. Banyak plugin (seperti direktori, pembuat tata letak, atau plugin pencarian sekunder) terhubung ke proses pencarian WordPress melalui hook (pre_get_posts, posts_where, dll.), sehingga memicu kueri tambahan yang tidak perlu ke basis data.

Bagaimana cara mendeteksi kueri-kueri ini menggunakan Query Monitor?

  1. Pasang dan aktifkan plugin gratis Query Monitor.
  2. Lakukan pencarian apa pun di situs web Anda.
  3. Buka menu tarik-turun Query Monitor di bilah atas, lalu pilih " Kueri Basis Data " (Database Queries).
  4. Saring berdasarkan kueri yang lambat atau telusuri daftar tersebut untuk mencari kueri yang memakan waktu paling lama. Sekaligus, catat juga kueri yang duplikat dan, jika memungkinkan, selesaikan masalah tersebut juga.
  5. Perhatikan kolom detail pada tabel: di sana Anda akan melihat dengan tepat plugin atau file mana yang sedang menjalankan kueri tersebut.

Dalam kasus saya, saya harus mengatasi beberapa kueri ganda dari plugin Name_Directory, serta beberapa blok GenerateBlocks pada templat posting, tetapi plugin apa pun, kode khusus, atau elemen tambahan apa pun dapat menghasilkan kueri yang memperlambat respons.

Cara menonaktifkan hook-hook tersebut melalui kode

Setelah menemukan nama fungsi atau kelas yang menyebabkan kueri lambat, Anda dapat menonaktifkannya di berkas functions.php Anda atau melalui plugin potongan kode (seperti Code Snippets, Code dari Perfmatters, atau yang serupa):

* Catatan. Di blok 1, Anda harus menambahkan hook yang menyebabkan kueri lambat. Jika hal ini tidak berlaku bagi Anda, Anda dapat menghapus bagian ini dan hanya menambahkan blok 2 ke dalam snippet.

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

Catatan: Untuk melepaskan hook menggunakan `remove_action` atau `remove_filter`, Anda harus menggunakan nama peristiwa yang persis sama (`pre_get_posts`), nama fungsi yang sama seperti yang Anda temukan di Query Monitor, serta prioritas yang sama seperti saat hook tersebut ditambahkan (secara default 10).

Langkah 2: Membuat dan memperbarui berkas JSON pencarian

Untuk menggantikan MySQL, kita perlu membuat berkas statis dalam format JSON yang berisi data kunci dari setiap postingan (ID, judul, dan teks biasa). Berkas ini akan disimpan di folder wp-content/uploads/ dan akan diperbarui secara otomatis setiap kali Anda membuat, mengedit, atau menghapus sebuah postingan.

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

Pengaturan awal: Saat pertama kali menginstal snippet ini, Anda dapat menjalankan my_site_rebuild_full_json_index(); secara sementara, atau cukup menyimpan/memperbarui entri apa pun yang sudah ada agar berkas search-es.json dibuat untuk pertama kalinya. Setelah itu, kunjungi direktori wp-content/uploads/ untuk memastikan berkas tersebut telah dibuat dan mengetahui ukurannya.

Agar Anda bisa membayangkan perkiraan ukuran file search-es.json ini, untuk 6.040 entri yang saya miliki, dihasilkan sebuah file berukuran 3,41 Mb—ukuran yang cukup mudah dikelola karena selama ukurannya belum melebihi 15 atau 20 MB (setara dengan sekitar 30.000 atau 40.000 entri yang panjang), Anda tidak perlu khawatir, karena berkas JSON statis dimuat langsung ke cache server atau PHP (file_get_contents) secara sekaligus, dan ukuran beberapa megabyte dapat dibaca dalam waktu kurang dari seperseribu detik; namun, jika ukuran berkas membengkak secara berlebihan, setiap kali pencarian dijalankan, PHP akan mengonsumsi puncak penggunaan RAM tambahan yang tidak perlu untuk memuat JSON raksasa tersebut. Pada saat itulah sebaiknya dipertimbangkan untuk mengindeks hanya judul dan cuplikan alih-alih seluruh konten lengkap, atau beralih ke solusi pencarian yang diindeks pada basis data khusus.

Langkah 3: Fungsi tambahan untuk menstandarkan pencarian (tanpa tanda aksen)

Untuk memastikan bahwa pencarian menghasilkan hasil terlepas dari cara pengguna mengetik (dengan atau tanpa tanda diakritik, huruf besar atau kecil), didefinisikan sebuah fungsi pembersihan:

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

Langkah 4: Mencegat kueri utama dengan ` posts_pre_query`

Melalui filter `posts_pre_query`, kueri pencarian WordPress dicegat sebelum mencapai basis data. Data JSON dibaca, ID-ID yang sesuai dengan istilah yang dicari diidentifikasi, dan hanya entri-entri tersebut yang dikembalikan ke WordPress dengan menentukan paginasi yang sesuai.

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

Langkah 5: Verifikasi hasil

Setelah langkah-langkah di atas selesai dilakukan, lakukan uji pencarian dan buka Query Monitor:

  1. Waktu total BD: Anda akan melihat bahwa waktutersebut kini berkurang dari beberapa detik menjadi hanya beberapa milidetik (biasanya antara 0,03 detik dan 0,2 detik).
  2. Kueri SQL: Klausul LIKE %istilah% akan dihapus sepenuhnya.
  3. RAM: Penggunaan memori puncak akan tetap sangat rendah (sekitar 30 MB).
  4. Pengurutan: Entri akan ditampilkan dalam urutan kronologis menurun (dari yang terbaru hingga yang terlama), dengan tetap mengikuti pengelompokan halaman bawaan WordPress.

Saya sangat puas dengan hasilnya. Bukan hanya masalah yang sudah saya alami selama bertahun-tahun akhirnya teratasi, tetapi kini hasilnya pun sungguh luar biasa. Pada salah satu pencarian yang paling menantang, seperti pencarian ini yang menghasilkan 4.887 hasil untuk kata kunci "viñeta", mesin pencari merespons dengan sangat cepat. Untuk pencarian dengan hasil yang lebih sedikit, bisa dibilang hasilnya langsung muncul. Kamu bisa mencobanya sendiri dengan mencari hal lain apa pun.

Cara mempercepat mesin pencari WordPress dengan menggunakan indeks JSON statis 1

Tinggalkan komentar

Ada yang ingin dikatakan?

Este blog se aloja en LucusHost

LucusHost, el mejor hosting