
Việc công cụ tìm kiếm của WordPress hoạt động kém hiệu quả không phải là điều bí mật với ai cả. Về mặt thẩm mỹ, vấn đề này tương đối dễ khắc phục; tuy nhiên, về mặt chức năng thì lại là một câu chuyện khác, bởi nó không cung cấp bất kỳ tùy chọn cấu hình nào, và hơn nữa, hiệu suất hoạt động của nó còn rất tệ.
Khi một trang web WordPress tích lũy hàng nghìn bài viết, công cụ tìm kiếm mặc định bắt đầu hoạt động không hiệu quả. Nó trở nên chậm chạp đến mức gần như vô dụng. Khi thực hiện tìm kiếm, WordPress sẽ chạy một truy vấn LIKE %từ_khóa% trên cơ sở dữ liệu để quét toàn bộ nội dung trong bảng wp_posts. Điều này tạo ra các truy vấn SQL có thể mất từ vài giây đến rất lâu và gây ra các đợt tăng đột biến về bộ nhớ, dẫn đến việc máy chủ bị treo.
Đây chính xác là điều mà tôi đang gặp phải. Với 6.040 bài đăng đã được xuất bản (chỉ bằng tiếng Tây Ban Nha), thời gian phản hồi của các tìm kiếm có nhiều kết quả nhất thực sự là một cơn ác mộng, với thời gian chờ lên đến hơn 10 giây hoặc thậm chí lâu hơn. Và cuối cùng thì ngày giải quyết vấn đề này một lần và mãi mãi cũng đã đến.
Sau khi suy nghĩ kỹ lưỡng, tôi nhận thấy có ít nhất ba phương án để khắc phục vấn đề này. Phương án đầu tiên và đơn giản nhất là sử dụng một plugin trả phí như Relevanssi (phiên bản miễn phí của nó không đủ) hoặc một dịch vụ bên ngoài như Algolia, nhưng tôi đã loại trừ ngay từ đầu vì mục tiêu của tôi là tối ưu hóa quá trình tìm kiếm ngay trên nền tảng gốc.
Trong hai phương án còn lại, tôi đã thử tạo chỉ mục FULLTEXT trong MySQL bằng các truy vấn MATCH() AGAINST() nhưng đã thất bại thảm hại. Mặc dù đã điều chỉnh các tham số của bộ máy InnoDB và tối ưu hóa các chỉ mục, MySQL vẫn bị treo khi xử lý hàng nghìn từ khóa phức tạp, dẫn đến thời gian phản hồi không thể chấp nhận được và làm quá tải máy chủ. Đó chính là lúc tôi hoàn toàn loại bỏ ý định để cơ sở dữ liệu gánh vác khối lượng công việc nặng nề này và quyết định chọn phương án thứ ba: xây dựng một chỉ mục tĩnh trong JSON.
Trong hướng dẫn này, chúng tôi sẽ giải thích cách giải quyết vấn đề này bằng cách thay thế các truy vấn MySQL tốn nhiều tài nguyên bằng một tệp JSON tĩnh, hoạt động như một chỉ mục tìm kiếm siêu nhanh.
Bước 1: Phát hiện và loại bỏ các truy vấn nặng hoặc dư thừa
Trước khi triển khai chỉ mục JSON, việc dọn dẹp hệ thống là vô cùng quan trọng. Nhiều plugin (như plugin danh mục, plugin trình tạo bố cục hoặc plugin tìm kiếm phụ) can thiệp vào quy trình tìm kiếm của WordPress thông qua các hook (pre_get_posts, posts_where, v.v.), từ đó gây ra các truy vấn bổ sung và không cần thiết đến cơ sở dữ liệu.
Làm thế nào để phát hiện các truy vấn này bằng Query Monitor?
- Hãy cài đặt và kích hoạt plugin miễn phí Query Monitor.
- Hãy thực hiện bất kỳ tìm kiếm nào trên trang web của bạn.
- Mở menu thả xuống của Query Monitor trên thanh công cụ phía trên và chọn “Các truy vấn cơ sở dữ liệu ” (Database Queries).
- Lọc theo các truy vấn chậm hoặc xem qua danh sách để tìm các truy vấn mất nhiều thời gian nhất. Nhân tiện, hãy ghi lại cả các truy vấn trùng lặp và, nếu có thể, hãy khắc phục chúng luôn.
- Hãy chú ý đến cột chi tiết trong các bảng: ở đó, bạn sẽ thấy chính xác plugin hoặc tệp nào đang thực thi truy vấn đó.
Trong trường hợp của tôi, tôi đã phải khắc phục một số truy vấn trùng lặp từ plugin Name_Directory và một số khối GenerateBlocks trong mẫu bài viết, nhưng bất kỳ plugin, mã tùy chỉnh hay thành phần bổ sung nào cũng có thể đang tạo ra các truy vấn làm chậm thời gian phản hồi.
Cách vô hiệu hóa các hook đó bằng mã nguồn
Sau khi xác định được tên hàm hoặc lớp gây ra truy vấn chậm, bạn có thể vô hiệu hóa nó trong tệp functions.php hoặc thông qua một plugin mã mẫu (như Code Snippets, Code của Perfmatters hoặc các plugin tương tự):
* Lưu ý. Trong khối 1, bạn cần thêm các hook gây ra các truy vấn chậm. Nếu trường hợp của bạn không như vậy, bạn có thể loại bỏ phần này và chỉ thêm khối 2 vào đoạn mã.
/**
* 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);
Lưu ý: Để gỡ bỏ một hook bằng remove_action hoặc remove_filter, bạn phải sử dụng chính xác cùng tên sự kiện (pre_get_posts), cùng tên hàm mà bạn đã xác định trong Query Monitor và cùng mức ưu tiên như khi nó được thêm vào (mặc định là 10).
Bước 2: Tạo và cập nhật tệp JSON tìm kiếm
Để thay thế MySQL, chúng ta cần tạo một tệp tĩnh ở định dạng JSON chứa các thông tin chính của các bài viết (ID, tiêu đề và nội dung văn bản thuần túy). Tệp này sẽ được lưu trong thư mục wp-content/uploads/ và sẽ được cập nhật tự động mỗi khi bạn tạo, chỉnh sửa hoặc xóa một bài viết.
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));
}
Khởi tạo ban đầu: Lần đầu tiên cài đặt đoạn mã này, bạn có thể tạm thời chạy hàm my_site_rebuild_full_json_index(); hoặc chỉ cần lưu/cập nhật bất kỳ bài viết hiện có nào để tệp search-es.json được tạo lần đầu tiên. Sau khi hoàn tất, hãy truy cập thư mục wp-content/uploads/ để đảm bảo tệp đã được tạo và kiểm tra dung lượng của nó.
Để bạn có thể hình dung về dung lượng xấp xỉ của tệp search-es.json này, với 6.040 mục của tôi, tệp được tạo ra có dung lượng 3,41 Mb – một mức dung lượng khá dễ quản lý, bởi vì chỉ khi dung lượng vượt quá 15 hoặc 20 MB (tương đương khoảng 30.000 hoặc 40.000 mục dài), bạn không cần lo lắng, bởi vì các tệp JSON tĩnh được tải trực tiếp vào bộ nhớ cache của máy chủ hoặc PHP (file_get_contents) một lần, và một tệp có kích thước vài megabyte sẽ được đọc trong vòng chưa đầy một phần nghìn giây; tuy nhiên, nếu tệp tăng kích thước quá mức, mỗi lần thực hiện tìm kiếm, PHP sẽ tiêu tốn một lượng RAM đỉnh điểm không cần thiết để tải một tệp JSON khổng lồ; lúc này, nên xem xét chỉ lập chỉ mục các tiêu đề và trích dẫn thay vì toàn bộ nội dung, hoặc chuyển sang giải pháp tìm kiếm được lập chỉ mục trên cơ sở dữ liệu chuyên dụng.
Bước 3: Hàm phụ trợ để chuẩn hóa các truy vấn tìm kiếm (không có dấu)
Để đảm bảo các tìm kiếm luôn trả về kết quả bất kể người dùng nhập như thế nào (có hoặc không có dấu, chữ hoa hay chữ thường), ta định nghĩa một hàm làm sạch:
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);
}
}
Bước 4: Chặn truy vấn chính bằng hàm posts_pre_query
Thông qua bộ lọc `posts_pre_query`, truy vấn tìm kiếm của WordPress sẽ được chặn lại trước khi nó truy cập vào cơ sở dữ liệu. Hệ thống sẽ đọc dữ liệu JSON, tìm ra các ID trùng khớp với các từ khóa được tìm kiếm và chỉ trả về cho WordPress những bài viết đó kèm theo thông tin phân trang tương ứng.
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);
Bước 5: Kiểm tra kết quả
Sau khi hoàn tất các bước trên, hãy thực hiện một thử nghiệm tìm kiếm và mở Query Monitor:
- Thời gian thực hiện BD tổng cộng: Bạn sẽ thấy rằng thời gian này giờ đây đã giảm từ vài giây xuống chỉ còn vài mili giây (thường dao động từ 0,03 giây đến 0,2 giây).
- Các truy vấn SQL: Mệnh đề LIKE %từ khóa% sẽ biến mất hoàn toàn.
- Bộ nhớ RAM: Mức sử dụng bộ nhớ cao nhất sẽ duy trì ở mức cực kỳ thấp (khoảng 30 MB).
- Sắp xếp: Các bài đăng sẽ được hiển thị theo thứ tự thời gian giảm dần (từ mới nhất đến cũ nhất), tuân thủ cách phân trang mặc định của WordPress.
Tôi vô cùng hài lòng với kết quả này. Không chỉ giải quyết được một vấn đề đã đeo bám tôi suốt nhiều năm qua, mà giờ đây, kết quả thu được thực sự rất ấn tượng. Trong một trong những truy vấn khó nhất, như trường hợp này với 4.887 kết quả chotừ khóa “viñeta”, công cụ tìm kiếm phản hồi rất nhanh. Đối với các truy vấn có ít kết quả hơn, có thể nói rằng kết quả được hiển thị ngay lập tức. Bạn có thể kiểm chứng điều này bằng cách tìm kiếm bất kỳ từ khóa nào khác.








