
To, że wyszukiwarka WordPressa jest bezużyteczna, nie jest dla nikogo tajemnicą. Jeśli chodzi o wygląd, problem ten można stosunkowo łatwo rozwiązać, jednak jej działanie to już zupełnie inna historia, ponieważ nie oferuje ona żadnych opcji konfiguracji, a do tego jej wydajność jest żałosna.
Gdy witryna oparta na WordPressie gromadzi tysiące wpisów, wbudowana wyszukiwarka zaczyna zawodzić. Działa coraz wolniej, aż staje się praktycznie bezużyteczna. Podczas wyszukiwania WordPress wykonuje w bazie danych zapytanie typu LIKE %słowo%, które przeszukuje całą zawartość tabeli wp_posts. Generuje to zapytania SQL, których wykonanie może trwać od kilku sekund aż po wieczność i powodować skoki zużycia pamięci, które ostatecznie blokują serwer.
Dokładnie to samo działo się już u mnie. Przy 6040 opublikowanych wpisach (tylko w języku hiszpańskim) czas odpowiedzi na wyszukiwania z większą liczbą wyników był prawdziwą udręką – wynosił ponad 10 sekund, a czasem nawet więcej. Nadszedł więc dzień, by rozwiązać ten problem raz na zawsze.
Po długich rozważaniach doszedłem do wniosku, że istnieją co najmniej trzy możliwości rozwiązania tego problemu. Pierwsza i najprostsza polegała na skorzystaniu z płatnej wtyczki, takiej jak Relevanssi (jej bezpłatna wersja jest niewystarczająca), lub zewnętrznej usługi, takiej jak Algolia, co jednak od razu odrzuciłem, ponieważ moim celem jest usprawnienie wyszukiwania w sposób natywny.
Z dwóch pozostałych opcji spróbowałem utworzyć indeks FULLTEXT w MySQL za pomocą zapytań MATCH() AGAINST(), ale poniosłem sromotną porażkę. Pomimo dostosowania parametrów silnika InnoDB i optymalizacji indeksów MySQL nadal się zawieszał podczas przetwarzania tysięcy złożonych słów kluczowych, generując niedopuszczalne czasy odpowiedzi i przeciążając serwer. Wtedy całkowicie zrezygnowałem z obciążania bazy danych i zdecydowałem się na trzecie rozwiązanie – utworzenie statycznego indeksu w formacie JSON.
W niniejszym samouczku postaramy się wyjaśnić, jak rozwiązać ten problem, zastępując obciążające zapytania do bazy danych MySQL statycznym plikiem JSON, który pełni rolę ultraszybkiego indeksu wyszukiwania.
Krok 1: Wykrywanie i eliminowanie obciążających lub zbędnych zapytań
Przed wdrożeniem indeksu JSON konieczne jest odpowiednie przygotowanie środowiska. Wiele wtyczek (takich jak katalogi, szablony czy wtyczki do wyszukiwania dodatkowego) podłącza się do procesu wyszukiwania w WordPressie za pomocą hooków (pre_get_posts, posts_where itp.), co powoduje wysyłanie dodatkowych i niepotrzebnych zapytań do bazy danych.
Jak wykryć te zapytania za pomocą narzędzia Query Monitor?
- Zainstaluj i włącz bezpłatną wtyczkę Query Monitor.
- Wprowadź dowolne zapytanie w swojej witrynie.
- Otwórz menu rozwijane Query Monitor na górnym pasku i wybierz opcję „Zapytania do bazy danych ” (Database Queries).
- Filtruj wyniki według „powolnych zapytań” lub przejrzyj listę w poszukiwaniu zapytań, których wykonanie zajmuje najwięcej czasu. Przy okazji zanotuj również zduplikowane zapytania i, jeśli to możliwe, rozwiąż ten problem.
- Zwróć uwagę na kolumnę „Szczegóły” w tabelach: tam zobaczysz dokładnie, która wtyczka lub plik wykonuje to zapytanie.
W moim przypadku musiałem rozwiązać problem kilku zduplikowanych zapytań generowanych przez wtyczkę Name_Directory oraz kilka bloków GenerateBlocks w szablonie wpisu, ale każda wtyczka, niestandardowy kod lub dodany element może generować zapytania, które spowalniają czas odpowiedzi.
Jak wyłączyć te haki za pomocą kodu
Po zlokalizowaniu nazwy funkcji lub klasy, która powoduje spowolnienie zapytania, możesz ją wyłączyć w pliku functions.php lub za pomocą wtyczki do fragmentów kodu (takiej jak Code Snippets, Code od Perfmatters lub podobnej):
* Uwaga. W bloku 1 należy dodać własne hooki, które generują powolne zapytania. Jeśli nie dotyczy to Twojego przypadku, możesz pominąć tę część i dodać do fragmentu kodu tylko blok 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);
Uwaga: Aby odłączyć hook za pomocą funkcji `remove_action` lub `remove_filter`, należy użyć dokładnie tej samej nazwy zdarzenia (`pre_get_posts`), tej samej nazwy funkcji, którą zidentyfikowano w Query Monitor, oraz tego samego priorytetu, z jakim został on dodany (domyślnie 10).
Krok 2: Utworzenie i aktualizowanie pliku JSON służącego do wyszukiwania
Aby zastąpić MySQL, musimy utworzyć statyczny plik w formacie JSON zawierający kluczowe dane naszych wpisów (ID, tytuł i tekst zwykły). Plik ten zostanie zapisany w folderze wp-content/uploads/ i będzie automatycznie aktualizowany za każdym razem, gdy utworzysz, edytujesz lub usuniesz wpis.
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));
}
Wstępne wygenerowanie: Przy pierwszej instalacji tego fragmentu kodu możesz tymczasowo uruchomić funkcję mi_sitio_reconstruir_indice_json_completo(); lub po prostu zapisać/zaktualizować dowolny istniejący wpis, aby plik search-es.json został utworzony po raz pierwszy. Następnie przejdź do katalogu wp-content/uploads/, aby upewnić się, że plik został utworzony, i sprawdzić jego rozmiar.
Aby dać ci wyobrażenie o przybliżonym rozmiarze pliku search-es.json: dla moich 6040 wpisów wygenerowano plik o rozmiarze 3,41 Mb, co jest rozmiarem dość łatwym do opanowania, ponieważ dopóki nie zacznie on przekraczać 15 lub 20 MB (co odpowiada około 30.000 lub 40 000 obszernych wpisów) nie ma powodu do obaw, ponieważ statyczne pliki JSON są ładowane bezpośrednio do pamięci podręcznej serwera lub PHP (file_get_contents) za jednym razem, a plik o rozmiarze kilku megabajtów odczytywany jest w mniej niż jedną tysięczną sekundy; jednakże, jeśli plik nadmiernie się powiększy, za każdym razem, gdy zostanie uruchomione wyszukiwanie, PHP zużyje dodatkowy, niepotrzebny szczytowy pobór pamięci RAM w celu wczytania gigantycznego pliku JSON. W takiej sytuacji warto rozważyć indeksowanie wyłącznie tytułów i fragmentów zamiast całej treści lub przejście na rozwiązanie oparte na wyszukiwaniu indeksowanym w dedykowanej bazie danych.
Krok 3: Funkcja pomocnicza służąca do standaryzacji wyszukiwań (bez akcentów)
Aby zapewnić, że wyszukiwanie zwraca wyniki niezależnie od tego, jak użytkownik wpisuje hasło (z akcentami lub bez, wielkimi lub małymi literami), definiuje się funkcję oczyszczania:
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);
}
}
Krok 4: Przechwycenie głównego zapytania za pomocą funkcji ` posts_pre_query`
Za pomocą filtra `posts_pre_query` przechwytuje się zapytanie wyszukiwania WordPressa, zanim dotrze ono do bazy danych. Odczytuje się dane JSON, wyszukuje identyfikatory pasujące do wyszukiwanych terminów i zwraca się do WordPressa wyłącznie te wpisy, określając odpowiednią paginację.
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);
Krok 5: Sprawdzenie wyników
Po wykonaniu powyższych czynności przeprowadź test wyszukiwania i otwórz narzędzie Query Monitor:
- Całkowity czas BD: Zauważysz, że teraz skrócił się z kilku sekund do zaledwie kilku milisekund (zwykle od 0,03 s do 0,2 s).
- Zapytania SQL: Klauzula LIKE %termin% zostanie całkowicie usunięta.
- Pamięć RAM: Szczytowe zużycie pamięci pozostanie wyjątkowo niskie (około 30 MB).
- Kolejność: Wpisy będą wyświetlane w ścisłej kolejności chronologicznej malejącej (od najnowszych do najstarszych), z zachowaniem natywnej paginacji WordPressa.
Jestem więcej niż zadowolony z wyniku. Nie tylko pozbyłem się problemu, który ciągnął się od lat, ale teraz wynik jest po prostu spektakularny. W przypadku jednego z najbardziej wymagających zapytań, takiego jak to z 4887 wynikami dlahasła „viñeta”, wyszukiwarka reaguje bardzo szybko. W przypadku zapytań z mniejszą liczbą wyników można powiedzieć, że są one wyświetlane natychmiast. Możesz to sprawdzić, wpisując dowolne inne hasło.






