
Že vyhledávač ve WordPressu je naprostá katastrofa, není pro nikoho žádným tajemstvím. Co se týče vzhledu, dá se to relativně snadno vyřešit, jeho fungování je však úplně jiná věc, protože nenabízí žádné možnosti nastavení a navíc je jeho výkon žalostný.
Jakmile web na WordPressu nashromáždí tisíce příspěvků, začne nativní vyhledávač selhávat. Zpomalí se natolik, že se stane prakticky nepoužitelným. Při vyhledávání provede WordPress v databázi dotaz typu LIKE %slovo%, který prohledává celý obsah tabulky wp_posts. To generuje dotazy SQL, které mohou trvat od několika sekund až po věčnost a způsobit výkyvy v paměti, které nakonec zablokují server.
Přesně to se mi už dělo. S 6040 zveřejněnými příspěvky (pouze ve španělštině) byly doby odezvy při vyhledávání s větším počtem výsledků opravdovým peklem – trvalo to přes 10 sekund i déle. A přišel den, kdy jsem to musel vyřešit jednou provždy.
Po důkladném zvážení jsem zjistil, že existují alespoň tři možnosti, jak to vyřešit. První a nejjednodušší byla možnost využít nějaký placený plugin, jako je Relevanssi (jeho bezplatná verze nestačí), nebo externí službu, jako je Algolia, což jsem však hned vyloučil, protože mým cílem je zrychlit vyhledávání přímo v rámci systému.
Ze zbývajících dvou možností jsem vyzkoušel vytvoření FULLTEXT indexu v MySQL pomocí dotazů MATCH() AGAINST() a naprosto jsem selhal. I přes úpravu parametrů enginu InnoDB a optimalizaci indexů se MySQL při zpracování tisíců složitých klíčových slov stále zasekávalo, což vedlo k nepřijatelným dobám odezvy a přetížení serveru. V tu chvíli jsem zcela zavrhl myšlenku nechat tuto náročnou zátěž na databázi a rozhodl jsem se pro třetí možnost – vytvoření statického indexu v JSON.
V tomto návodu se pokusíme vysvětlit, jak tento problém vyřešit nahrazením náročných dotazů do MySQL statickým souborem JSON, který funguje jako bleskurychlý vyhledávací index.
Krok 1: Zjistit a odstranit náročné nebo nadbytečné dotazy
Před použitím indexu JSON je nezbytné připravit půdu. Mnoho pluginů (jako jsou adresáře, layoutery nebo pluginy pro sekundární vyhledávání) se napojuje na vyhledávací proces WordPressu pomocí hooků (pre_get_posts, posts_where atd.), čímž spouští další a zbytečné dotazy do databáze.
Jak tyto dotazy zjistit pomocí nástroje Query Monitor?
- Nainstalujte a aktivujte bezplatný plugin Query Monitor.
- Proveďte libovolné vyhledávání na vašem webu.
- Otevřete rozbalovací nabídku Query Monitor v horní liště a vyberte možnost Dotazy do databáze (Database Queries).
- Filtrujte podle pomalých dotazů nebo prohledejte seznam a najděte dotazy, které trvají déle. Zároveň si poznamenejte i duplicitní dotazy a pokud je to možné, vyřešte i je.
- Podívej se na sloupec s podrobnostmi v tabulkách: tam uvidíš přesně, který plugin nebo soubor daný dotaz provádí.
V mém případě jsem musel vyřešit několik duplicitních dotazů pluginu Name_Directory a některé bloky GenerateBlocks v šabloně příspěvku, ale jakýkoli plugin, vlastní kód nebo přidaný prvek může generovat dotazy, které zpomalují odezvu.
Jak tyto háčky deaktivovat pomocí kódu
Jakmile zjistíte název funkce nebo třídy, která způsobuje pomalé dotazy, můžete ji deaktivovat v souboru functions.php nebo pomocí pluginu pro kódové úryvky (například Code Snippets, Code od Perfmatters nebo podobného):
* Poznámka: Do bloku 1 je třeba přidat háčky, které způsobují pomalé dotazy. Pokud se vás to netýká, můžete tuto část vynechat a do úryvku kódu přidat pouze 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);
Poznámka: Chcete-li odpojit hook pomocí funkce `remove_action` nebo `remove_filter`, musíte použít přesně stejný název události (`pre_get_posts`), stejný název funkce, který jste identifikovali v Query Monitoru, a stejnou prioritu, s jakou byl hook přidán (výchozí hodnota je 10).
Krok 2: Vytvoření a aktualizace souboru JSON pro vyhledávání
Abychom nahradili MySQL, musíme vytvořit statický soubor ve formátu JSON, který bude obsahovat klíčové údaje o našich příspěvcích (ID, název a prostý text). Tento soubor se uloží do složky wp-content/uploads/ a bude se automaticky aktualizovat pokaždé, když vytvoříte, upravíte nebo smažete příspěvek.
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));
}
Počáteční generování: Při první instalaci tohoto kódu můžete dočasně spustit funkci my_site_rebuild_full_json_index(); nebo jednoduše uložit/aktualizovat jakýkoli existující záznam, aby se soubor search-es.json vytvořil poprvé. Poté přejděte do adresáře wp-content/uploads/, abyste se ujistili, že byl soubor vytvořen, a zjistili jeho velikost.
Abys měl představu o přibližné velikosti souboru search-es.json: u mých 6 040 záznamů vznikl soubor o velikosti 3,41 Mb, což je docela přijatelná velikost, protože dokud soubor nepřesáhne 15 nebo 20 MB (což odpovídá asi 30.000 až 40 000 rozsáhlých záznamů), není třeba se obávat, protože statické soubory JSON se načítají najednou přímo do mezipaměti serveru nebo PHP (file_get_contents) a soubor o velikosti několika megabajtů se načte za méně než tisícinu sekundy; pokud však soubor nadměrně naroste, při každém spuštění vyhledávání PHP spotřebuje zbytečný špičkový nárůst paměti RAM na načtení obrovského souboru JSON. V takovém případě by bylo vhodné zvážit indexování pouze nadpisů a úryvků namísto celého obsahu, případně přejít na řešení vyhledávání s indexem v dedikované databázi.
Krok 3: Pomocná funkce pro normalizaci vyhledávání (bez diakritiky)
Aby bylo zajištěno, že vyhledávání vrátí výsledky bez ohledu na to, jak uživatel zadá dotaz (s diakritikou či bez ní, velkými či malými písmeny), definuje se funkce pro očištění:
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: Zachycení hlavního dotazu pomocí funkce posts_pre_query
Pomocí filtru `posts_pre_query` se zachytí vyhledávací dotaz WordPressu ještě předtím, než se dostane do databáze. Přečte se JSON, vyhledají se ID, která odpovídají hledaným výrazům, a WordPressu se vrátí pouze tyto záznamy s uvedením příslušného stránkování.
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: Ověření výsledků
Po provedení výše uvedených kroků proveďte zkušební vyhledávání a otevřete nástroj Query Monitor:
- Celková doba BD: Uvidíš, že se nyní zkrátila z několika sekund na pouhých několik milisekund (obvykle mezi 0,03 s a 0,2 s).
- SQL dotazy: Klauzule LIKE %termín% zcela zmizí.
- RAM: Špičková spotřeba paměti zůstane mimořádně nízká (kolem 30 MB).
- Seřazení: Příspěvky se budou zobrazovat v přísně sestupném chronologickém pořadí (od nejnovějších po nejstarší) s ohledem na nativní stránkování systému WordPress.
S výsledkem jsem více než spokojený. Nejenže jsem se zbavil problému, který mě trápil už roky, ale nyní je výsledek doslova úžasný. I při jednom z nejnáročnějších vyhledávání, jako je tento příklad s 4 887 výsledky pro výraz „viñeta“, vyhledávač reaguje velmi rychle. U vyhledávání s menším počtem výsledků se dá říci, že se zobrazují okamžitě. Můžete si to ověřit vyhledáním čehokoli jiného.







