วิธีเพิ่มความเร็วของฟังก์ชันการค้นหาใน WordPress โดยใช้ดัชนี JSON แบบคงที่

No comments

24.07.2026|

No comments

Tiempo de lectura Lectura: 5 min, 19 s
Número de palabras Palabras: 985
Número de visitas Visitas: 6
Icono de traducción

ไม่ใช่ความลับเลยว่าฟังก์ชันการค้นหาของ WordPress นั้นแย่มาก ในด้านความสวยงามการแก้ไขนั้นค่อนข้างง่าย แต่ด้านฟังก์ชันการทำงานนั้นเป็นเรื่องที่ต่างออกไปโดยสิ้นเชิง เพราะมันไม่มีตัวเลือกการตั้งค่าใดๆ เลย – และที่แย่ที่สุดคือประสิทธิภาพของมันนั้นต่ำมาก

เมื่อเว็บไซต์ WordPress มีบทความสะสมถึงหลายพันบทความ ฟังก์ชันการค้นหาที่มาพร้อมกับระบบจะเริ่มทำงานผิดปกติ จนช้าลงจนถึงขั้นที่แทบจะใช้งานไม่ได้เลย เมื่อคุณทำการค้นหา WordPress จะรันคำสั่งLIKE %word%บนฐานข้อมูล ซึ่งจะสแกนเนื้อหาทั้งหมดในตารางwp_posts สิ่งนี้ก่อให้เกิดคำสั่ง SQL ที่อาจใช้เวลาตั้งแต่ไม่กี่วินาทีไปจนถึงนานมาก และก่อให้เกิดการเพิ่มขึ้นของหน่วยความจำอย่างกะทันหัน ซึ่งอาจทำให้เซิร์ฟเวอร์ล่มได้

นี่คือสิ่งที่ผมกำลังประสบอยู่พอดีด้วยจำนวนบทความที่เผยแพร่แล้ว 6,040 บทความ(เป็นภาษาสเปนเท่านั้น) เวลาตอบสนองในการค้นหาที่มีผลลัพธ์จำนวนมากนั้นเป็นฝันร้ายจริงๆ โดยต้องใช้เวลามากกว่า 10 วินาที หรือมากกว่านั้น และในที่สุดก็ถึงวันที่ต้องแก้ไขปัญหานี้ให้จบสิ้นไปเสียที

หลังจากคิดอย่างละเอียดแล้ว ผมก็ตระหนักว่ามีอย่างน้อยสามวิธีที่จะแก้ไขปัญหานี้ วิธีแรกและง่ายที่สุดคือใช้ปลั๊กอินแบบเสียเงิน เช่นRelevanssi(เวอร์ชันฟรีไม่เพียงพอ) หรือบริการภายนอก เช่นAlgolia ซึ่งผมได้ตัดทิ้งทันที เพราะสิ่งที่ผมต้องการคือการเพิ่มความเร็วในการค้นหาแบบในตัวระบบ

จากสองตัวเลือกที่เหลืออยู่ ผมได้ลองสร้างดัชนี FULLTEXT ใน MySQL โดยใช้คำสั่ง MATCH() AGAINST() แต่ล้มเหลวอย่างสิ้นเชิง แม้จะปรับพารามิเตอร์ของเอนจิน InnoDB และปรับแต่งดัชนีแล้ว แต่ MySQL ก็ยังคงทำงานช้าลงเมื่อประมวลผลคำสำคัญที่ซับซ้อนหลายพันคำ ส่งผลให้เวลาตอบสนองที่ยอมรับไม่ได้ และทำให้เซิร์ฟเวอร์ทำงานหนักเกินไป จึงเป็นตอนนั้นเองที่ผมตัดสินใจไม่วางภาระหนักลงบนฐานข้อมูลอีกต่อไป และเลือกใช้วิธีการที่สาม คือการสร้างดัชนีแบบคงที่ (static index) ในรูปแบบ JSON

คู่มือนี้มีวัตถุประสงค์เพื่ออธิบายวิธีแก้ไขปัญหานี้ โดยการแทนที่คำสั่ง MySQL ที่ใช้ทรัพยากรมากด้วยไฟล์ JSON แบบคงที่ ซึ่งทำหน้าที่เป็นดัชนีการค้นหาที่รวดเร็วเป็นพิเศษ

ขั้นตอน 1: ระบุและกำจัดคำสั่งค้นหาที่ทำงานช้าหรือซ้ำซ้อน

ก่อนที่จะนำดัชนี JSON มาใช้ จำเป็นต้องเตรียมระบบให้พร้อมก่อน โดยปลั๊กอินหลายตัว (เช่น ปลั๊กอินไดเรกทอรี ปลั๊กอินเลย์เอาต์ หรือปลั๊กอินค้นหาเสริม) จะเชื่อมต่อกับกระบวนการค้นหาของ WordPressผ่านฮุค(เช่น pre_get_posts, posts_where เป็นต้น) ซึ่งทำให้เกิดการเรียกใช้ฐานข้อมูลเพิ่มเติมและไม่จำเป็น

คุณสามารถระบุคำค้นหาเหล่านี้ได้อย่างไรโดยใช้ Query Monitor?

  1. ติดตั้งและเปิดใช้งานปลั๊กอินQuery Monitor ที่ใช้ฟรี
  2. ค้นหาได้ทุกที่บนเว็บไซต์ของคุณ
  3. เปิดเมนูแบบเลื่อนลง Query Monitor ในแถบด้านบน แล้วเลือกDatabase Queries’
  4. กรองตาม‘Slow queries’หรือดูผ่านรายชื่อเพื่อระบุคำสั่งที่ใช้เวลานานที่สุดในการทำงาน ขณะที่คุณกำลังทำสิ่งนี้ ให้บันทึกคำสั่งที่ซ้ำกันไว้ และหากทำได้ ก็แก้ไขปัญหาเหล่านั้นด้วย
  5. ลองดูคอลัมน์ ‘Details’ ในตาราง: ที่นั่นคุณจะเห็นได้อย่างชัดเจนว่าปลั๊กอินหรือไฟล์ใดกำลังดำเนินการคำสั่งนั้น

ในกรณีของผม ผมต้องแก้ไขคำถามที่ซ้ำกันไม่กี่ข้อจากปลั๊กอินName_Directory และบล็อกGenerateBlocksไม่กี่บล็อกในเทมเพลตโพสต์ แต่ปลั๊กอิน รหัสที่กำหนดเอง หรือองค์ประกอบที่เพิ่มเข้าไปใด ๆ ก็อาจสร้างคำถามที่ทำให้ความเร็วในการตอบสนองช้าลงได้

วิธีปิดใช้งาน hook เหล่านั้นผ่านโค้ด

เมื่อระบุชื่อฟังก์ชันหรือคลาสที่ก่อให้เกิดการค้นหาข้อมูลช้าแล้ว คุณสามารถปิดใช้งานมันในไฟล์functions.php หรือใช้ปลั๊กอินสำหรับโค้ดสแนปเพต (เช่นCode Snippets,PerfmattersCodeหรือปลั๊กอินที่คล้ายกัน):

* หมายเหตุ: ในบล็อก 1 คุณควรเพิ่ม hook ที่ก่อให้เกิดการค้นหาช้า หากกรณีนี้ไม่เกี่ยวข้องกับคุณ คุณสามารถลบส่วนนี้ออกและเพิ่มเพียงบล็อก 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);

หมายเหตุ:เพื่อยกเลิกการผูกการกระทำด้วย`remove_action` หรือ `remove_filter` คุณต้องใช้ชื่อเหตุการณ์ (`pre_get_posts`) ที่ตรงกันทุกประการ ชื่อฟังก์ชันที่ระบุไว้ใน Query Monitor และระดับความสำคัญ (priority) ที่ใช้เมื่อเพิ่มการกระทำนั้น (ค่าเริ่มต้นคือ 10)

ขั้นตอนที่ 2: สร้างและอัปเดตไฟล์ JSON สำหรับการค้นหาให้ทันสมัยอยู่เสมอ

เพื่อแทนที่ MySQL เราต้องสร้างไฟล์แบบคงที่ในรูปแบบ JSON ที่ประกอบด้วยข้อมูลหลักของบทความ (ID, ชื่อ และข้อความธรรมดา) ไฟล์นี้จะถูกเก็บไว้ในโฟลเดอร์wp-content/uploads/และจะได้รับการอัปเดตอัตโนมัติทุกครั้งที่คุณสร้าง แก้ไข หรือลบบทความ

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

การตั้งค่าเริ่มต้น:เมื่อคุณติดตั้งสแนปเพตนี้ครั้งแรก คุณสามารถเรียกใช้my_site_rebuild_full_json_index();แบบชั่วคราว หรือเพียงบันทึก/อัปเดตโพสต์ที่มีอยู่ใดก็ได้ เพื่อให้ไฟล์ search-es.json ถูกสร้างขึ้นเป็นครั้งแรก เมื่อเสร็จสิ้นแล้ว ให้เข้าสู่ไดเรกทอรีwp-content/uploads/เพื่อตรวจสอบว่าไฟล์ดังกล่าวได้ถูกสร้างขึ้นแล้วหรือไม่ และเพื่อตรวจสอบขนาดของไฟล์

เพื่อให้ท่านเข้าใจขนาดประมาณของไฟล์ search-es.json นี้ สำหรับข้อมูล 6,040 รายการของผมไฟล์ที่สร้างขึ้นมีขนาด 3.41 Mb – ซึ่งเป็นขนาดที่จัดการได้ค่อนข้างดี เพราะยังไม่ต้องกังวลจนกว่าขนาดไฟล์จะเริ่มเกิน 15 หรือ 20 MB (เทียบเท่ากับประมาณ 30,000 หรือ 40,000 รายการที่ยาวมาก) จึงไม่จำเป็นต้องกังวล เพราะไฟล์ JSON แบบคงที่จะถูกโหลดเข้าสู่แคชของเซิร์ฟเวอร์หรือแคชของ PHP (file_get_contents) ทั้งหมดในครั้งเดียว และไฟล์ที่มีขนาดไม่กี่เมกะไบต์จะถูกอ่านในเวลาน้อยกว่าหนึ่งในพันวินาที; อย่างไรก็ตาม หากไฟล์มีขนาดเพิ่มขึ้นอย่างไม่เหมาะสม, ทุกครั้งที่มีการค้นหา PHP จะใช้ RAM เพิ่มขึ้นอย่างไม่จำเป็นเพื่อโหลดไฟล์ JSON ขนาดใหญ่ ในจุดนั้น ควรพิจารณาการสร้างดัชนีเฉพาะสำหรับหัวข้อและส่วนสรุปแทนที่จะใช้เนื้อหาทั้งหมด หรือย้ายไปใช้โซลูชันการค้นหาที่มีดัชนีในฐานข้อมูลเฉพาะ

ขั้นตอนที่ 3: ฟังก์ชันเสริมสำหรับการปรับมาตรฐานการค้นหา (โดยไม่ใช้เครื่องหมายเน้นเสียง)

เพื่อให้แน่ใจว่าการค้นหาจะให้ผลลัพธ์ไม่ว่าผู้ใช้จะพิมพ์อย่างไร (มีหรือไม่มีเครื่องหมายเน้นเสียง ใช้ตัวอักษรใหญ่หรือเล็ก) จึงได้กำหนดฟังก์ชันการทำความสะอาดไว้:

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: สกัดการค้นหาหลักโดยใช้ `posts_pre_query`

โดยใช้ตัวกรอง`posts_pre_query`คำค้นหาของ WordPress จะถูกดักจับก่อนที่จะส่งไปยังฐานข้อมูล JSON จะถูกวิเคราะห์เพื่อระบุ ID ที่ตรงกับคำค้นหา และจะส่งเฉพาะโพสต์เหล่านั้นกลับไปยัง WordPress พร้อมกับการกำหนดการแบ่งหน้าให้เหมาะสม

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: การตรวจสอบผลลัพธ์

เมื่อเสร็จสิ้นขั้นตอนข้างต้นแล้ว ให้ทำการค้นหาทดสอบและเปิดQuery Monitor:

  1. เวลาทำงานของฐานข้อมูลรวม:คุณจะสังเกตเห็นว่าเวลานี้ได้ลดลงจากหลายวินาทีเหลือเพียงไม่กี่มิลลิวินาที (โดยทั่วไปอยู่ระหว่าง 0.03 วินาที ถึง 0.2 วินาที)
  2. คำสั่ง SQL:ข้อกำหนดLIKE %term%จะถูกลบออกอย่างสมบูรณ์
  3. RAM:การใช้หน่วยความจำสูงสุดจะยังคงอยู่ในระดับที่ต่ำมาก (ประมาณ 30 MB)
  4. การจัดเรียง:บทความจะแสดงตามลำดับเวลาที่ลดลงอย่างเคร่งครัด (จากบทความล่าสุดไปยังบทความเก่าที่สุด) โดยยังคงรักษาระบบการแบ่งหน้าตามค่าเริ่มต้นของ WordPress

ผมรู้สึกพอใจกับผลลัพธ์นี้มากกว่าที่คาดไว้ ไม่เพียงแต่ผมได้แก้ไขปัญหาที่ค้างคามานานหลายปีแล้ว แต่ตอนนี้ผลลัพธ์ที่ได้ก็น่าทึ่งอย่างยิ่ง แม้สำหรับการค้นหาที่ท้าทายที่สุด เช่นการค้นหานี้ที่มีผลลัพธ์ 4,887 รายการสำหรับคำว่า‘viñeta’ เครื่องมือค้นหาก็ตอบสนองได้อย่างรวดเร็วมาก สำหรับการค้นหาที่มีผลลัพธ์น้อยกว่า สามารถกล่าวได้ว่าผลลัพธ์ปรากฏขึ้นทันทีคุณสามารถตรวจสอบด้วยตัวเองได้โดยการค้นหาคำอื่น ๆ

วิธีเพิ่มความเร็วของฟังก์ชันการค้นหาใน WordPress โดยใช้ดัชนี JSON แบบคงที่ 1

Leave a Comment

ฝากความเห็นไว้ที่นี่

Este blog se aloja en LucusHost

LucusHost, el mejor hosting