
Те, що пошукова система WordPress — це повна нісенітниця, ні для кого не секрет. Що стосується зовнішнього вигляду, то цю проблему відносно легко вирішити, однак її функціонування — це зовсім інша історія, оскільки вона не пропонує жодних налаштувань, а на додачу до цього її продуктивність просто жахлива.
Коли на сайті WordPress накопичуються тисячі записів, вбудована система пошуку починає давати збої. Вона сповільнюється до такої міри, що стає практично непридатною для використання. Під час пошуку WordPress виконує запит LIKE %слово% у базі даних, який сканує весь вміст таблиці wp_posts. Це генерує SQL-запити, виконання яких може тривати від кількох секунд до нескінченності та спричиняти піки навантаження на пам'ять, що в результаті блокують сервер.
Саме це зі мною й відбувалося. З 6040 опублікованими дописами (лише іспанською мовою) час відгуку при пошуку з великою кількістю результатів був справжнім пеклом — він перевищував 10 секунд і більше. І настав день, коли потрібно було вирішити цю проблему раз і назавжди.
Довго обмірковуючи це питання, я дійшов висновку, що є принаймні три варіанти, як це виправити. Перший і найпростіший — скористатися якимсь платним плагіном, наприклад Relevanssi (його безкоштовна версія недостатня), або зовнішнім сервісом, таким як Algolia, але я відразу відкинув цей варіант, оскільки моєю метою є прискорення пошуку за допомогою вбудованих засобів.
З двох варіантів, що залишилися, я спробував створити FULLTEXT-індекс у MySQL за допомогою запитів MATCH() AGAINST() — і зазнав нищівної поразки. Незважаючи на налаштування параметрів двигуна InnoDB та оптимізацію індексів, MySQL продовжував зависати під час обробки тисяч складних ключових слів, видаючи неприйнятні часи відгуку та перевантажуючи сервер. Саме тоді я остаточно відмовився від ідеї покладати таке велике навантаження на базу даних і обрав третій варіант — створити статичний індекс у форматі JSON.
У цьому посібнику ми спробуємо пояснити, як вирішити цю проблему, замінивши ресурсомісткі запити до MySQL на статичний файл JSON, який виконує роль надшвидкого індексу для пошуку.
Крок 1: Виявлення та усунення важких або надлишкових запитів
Перш ніж застосувати індекс JSON, надзвичайно важливо «підготувати грунт». Багато плагінів (таких як каталоги, макетувальники або плагіни для додаткового пошуку) підключаються до процесу пошуку WordPress за допомогою хуків (pre_get_posts, posts_where тощо), що призводить до надсилання додаткових і непотрібних запитів до бази даних.
Як виявити такі запити за допомогою Query Monitor?
- Встановіть та увімкніть безкоштовний плагін Query Monitor.
- Здійсніть будь-який пошук на вашому веб-сайті.
- Відкрийте випадаюче меню Query Monitor у верхній панелі та виберіть «Запити до бази даних » (Database Queries).
- Відфільтруйте за повільними запитами або перегляньте список, шукаючи запити, які виконуються найдовше. Заодно відзначте також дубльовані запити і, якщо є можливість, усуньте цю проблему.
- Зверніть увагу на стовпець «Деталі» у таблицях: там ви побачите, який саме плагін або файл виконує цей запит.
У моєму випадку мені довелося виправити кілька дубльованих запитів плагіна Name_Directory та деякі блоки GenerateBlocks у шаблоні публікації, але будь-який плагін, власний код або доданий елемент може генерувати запити, що уповільнюють відповідь.
Як вимкнути ці хуки за допомогою коду
Як тільки ви визначите назву функції або класу, що спричиняє повільний запит, ви можете відключити її у файлі functions.php або за допомогою плагіна для фрагментів коду (наприклад, Code Snippets, Code від Perfmatters або подібного):
* Примітка. У блоці 1 слід додати свої хуки, які генерують повільні запити. Якщо це не ваш випадок, ви можете видалити цю частину та додати до фрагмента коду лише блок 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, та той самий пріоритет, з яким він був доданий (за замовчуванням — 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: для моїх 6040 записів було згенеровано файл розміром 3,41 Мб, що є цілком прийнятним розміром, оскільки доки він не перевищить 15–20 МБ (що еквівалентно приблизно 30.000 або 40 000 об’ємних записів), турбуватися не варто, адже статичні файли JSON завантажуються одразу в кеш-пам’ять сервера або PHP (file_get_contents), і файл розміром у кілька мегабайт зчитується менш ніж за одну тисячну частку секунди; проте, якщо файл надмірно збільшиться, то кожного разу під час виконання пошуку PHP споживатиме зайвий піковий обсяг оперативної пам’яті, необхідний для завантаження гігантського 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, знаходяться ідентифікатори, що відповідають шуканим термінам, і 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:
- Загальний час виконання BD: Ви побачите, що тепер він скоротився з декількох секунд до всього лише кількох мілісекунд (зазвичай від 0,03 с до 0,2 с).
- Запити SQL: Клаузула LIKE %термін% повністю зникне.
- Оперативна пам'ять (RAM): Пікове споживання пам'яті залишатиметься надзвичайно низьким (близько 30 МБ).
- Сортування: Записи будуть відображатися у суворому хронологічному порядку, від найновіших до найстаріших, з дотриманням вбудованої системи нумерації сторінок WordPress.
Я більш ніж задоволений результатом. Я не тільки позбувся проблеми, яка тягнулася роками, але й тепер результат є просто вражаючим. Навіть у випадку одного з найскладніших запитів, як-от цей із 4887 результатами за запитом «віньєтка», пошукова система відповідає дуже швидко. Щодо запитів із меншою кількістю результатів, то їх можна сказати, що вони відображаються миттєво. Ви можете перевірити це, здійснивши пошук за будь-яким іншим запитом.






