
Senki számára sem titok, hogy a WordPress keresője egy katasztrófa. A megjelenését illetően viszonylag könnyű megoldani a problémát, a működése azonban már más történet, mivel egyáltalán nem kínál beállítási lehetőségeket, ráadásul a teljesítménye is siralmas.
Amikor egy WordPress-oldalon több ezer bejegyzés halmozódik fel, a beépített kereső meghibásodni kezd. Annyira lelassul, hogy gyakorlatilag használhatatlanná válik. Kereséskor a WordPress egy LIKE %szó% lekérdezést futtat az adatbázisban, amely átvizsgálja a wp_posts táblázat teljes tartalmát. Ez olyan SQL-lekérdezéseket generál, amelyek végrehajtása néhány másodperctől akár egy örökkévalóságig is eltarthat, és olyan memóriaigény-csúcsokat okozhat, amelyek végül a szerver leállásához vezetnek.
Pontosan ez történt velem is. 6040 közzétett bejegyzéssel (csak spanyol nyelven) a legtöbb találatot hozó keresések válaszideje igazi pokol volt: 10 másodpercnél is hosszabb ideig tartott. És eljött a nap, amikor egyszer és mindenkorra megoldottam a problémát.
Miután alaposan átgondoltam a dolgot, rájöttem, hogy legalább három lehetőség kínálkozik a probléma megoldására. Az első és legegyszerűbb az volt, hogy valamilyen fizetős bővítményt használjak, például a Relevanssi-t (amelynek ingyenes verziója nem elégséges), vagy egy külső szolgáltatást, mint például az Algolia-t, de ezt már az elején kizártam, mert az a célom, hogy natív módon gyorsítsam fel a keresést.
A két fennmaradó lehetőség közül megpróbáltam létrehozni egy FULLTEXT indexet a MySQL-ben MATCH() AGAINST() lekérdezések segítségével, de ez szerencsétlenül végződött. Annak ellenére, hogy beállítottam az InnoDB motor paramétereit és optimalizáltam az indexeket, a MySQL továbbra is lefagyott, amikor több ezer összetett kulcsszót kellett feldolgoznia, elfogadhatatlanul hosszú válaszidőket eredményezve és túlterhelve a szervert. Ekkor döntöttem úgy, hogy teljesen elvetem azt az ötletet, hogy a nehéz terhet az adatbázisra hárítsam, és a harmadik megoldás mellett döntöttem: statikus indexet hoztam létre JSON-ban.
Ebben az oktatóanyagban megpróbáljuk elmagyarázni, hogyan lehet megoldani ezt a problémát úgy, hogy a MySQL-ben végzett erőforrásigényes lekérdezéseket egy statikus JSON-fájllal helyettesítjük, amely rendkívül gyors keresési indexként működik.
1. lépés: A terhelő vagy felesleges lekérdezések felismerése és eltávolítása
A JSON-index alkalmazása előtt elengedhetetlenül fontos, hogy „megtisztítsuk a terepet”. Számos bővítmény (például könyvtárak, elrendezés-szerkesztők vagy kiegészítő keresőbővítmények) hookok (pre_get_posts, posts_where stb.) segítségével kapcsolódik be a WordPress keresési folyamatába, ami további, felesleges lekérdezéseket indít el az adatbázisban.
Hogyan lehet ezeket a lekérdezéseket a Query Monitor segítségével azonosítani?
- Telepítse és aktiválja a Query Monitor nevű ingyenes bővítményt.
- Végezz el bármilyen keresést a weboldaladon.
- Nyisd meg a Query Monitor legördülő menüjét a felső sávon, és válaszd az „Adatbázis-lekérdezések” (Database Queries) lehetőséget.
- Szűrj a „Lassú lekérdezések” szerint, vagy nézd át a listát, és keressd meg azokat a lekérdezéseket, amelyek több időt vesznek igénybe. Ha már itt tartasz, jegyezd fel az ismétlődő lekérdezéseket is, és ha lehet, azokat is oldd meg.
- Nézd meg a táblázatok részletes oszlopát: ott pontosan láthatod, hogy melyik bővítmény vagy fájl futtatja az adott lekérdezést.
Az én esetemben néhány, a Name_Directory bővítményből származó duplikált lekérdezést kellett megoldanom, valamint néhány GenerateBlocks- blokkot a bejegyzés-sablonban, de bármely bővítmény, egyéni kód vagy hozzáadott elem generálhat olyan lekérdezéseket, amelyek lassítják a válaszadást.
Hogyan lehet kóddal letiltani ezeket a hookokat?
Miután azonosítottad a lassú lekérdezést kiváltó függvény vagy osztály nevét, azt kikapcsolhatod a functions.php fájlban , vagy egy kódrészlet-bővítmény (például a Code Snippets, a PerfmattersCode vagy hasonló) segítségével:
* Megjegyzés: Az 1. blokkba be kell illesztened azokat a hookokat, amelyek lassú lekérdezéseket generálnak. Ha ez nem vonatkozik rád, akkor ezt a részt kihagyhatod, és csak a 2. blokkot illesztheted be a kódrészletbe.
/**
* 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);
Megjegyzés: Ha egy hookot a `remove_action` vagy a `remove_filter` paranccsal szeretnél eltávolítani, pontosan ugyanazt az eseménynevet (pre_get_posts), ugyanazt a függvénynevet, amelyet a Query Monitorban azonosítottál, és ugyanazt a prioritást (alapértelmezés szerint 10) kell használnod, amellyel a hookot hozzáadtad.
2. lépés: A keresési JSON-fájl létrehozása és naprakészen tartása
A MySQL helyettesítéséhez létre kell hoznunk egy JSON formátumú statikus fájlt, amely tartalmazza a bejegyzéseink legfontosabb adatait (azonosító, cím és egyszerű szöveg). Ezt a fájlt a wp-content/uploads/ mappában kell elmenteni, és automatikusan frissülni fog minden alkalommal, amikor bejegyzést hozol létre, szerkesztesz vagy törölsz.
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));
}
Kezdeti létrehozás: Amikor először telepíted ezt a kódrészletet, ideiglenesen futtathatod a mi_sitio_reconstruir_indice_json_completo(); parancsot, vagy egyszerűen menthetsz/frissíthetsz bármely meglévő bejegyzést, hogy a search-es.json fájl először létrejöjjön. Miután ezt elvégezte, látogasson el a wp-content/uploads/ könyvtárba, hogy megbizonyosodjon a fájl létrehozásáról, és megismerje annak méretét.
Hogy képet kapj a search-es.json fájl hozzávetőleges méretéről: az én 6040 bejegyzésem esetén egy 3,41 Mb-os fájl jött létre, ami elég kezelhető méret, mivel addig nem kell aggódni, amíg a mérete nem haladja meg a 15–20 MB-ot (ami körülbelül 30.000 vagy 40 000 terjedelmes bejegyzésnek felel meg), nem kell aggódni, mert a statikus JSON-fájlokat egy csapásra közvetlenül a szerver vagy a PHP (file_get_contents) gyorsítótárába töltik be, és néhány megabájt méretű fájlt kevesebb, mint egy ezredmásodperc alatt beolvasnak; azonban, ha a fájl túlzottan megnő, akkor minden keresés végrehajtásakor a PHP feleslegesen fogyaszt majd extra RAM-memóriát egy hatalmas JSON-fájl betöltéséhez; ilyenkor érdemes megfontolni, hogy a teljes tartalom helyett csak a címeket és kivonatokat indexeljük, vagy áttérjünk egy dedikált adatbázis-alapú keresési megoldásra.
3. lépés: Segédfüggvény a keresések normalizálásához (ékezet nélkül)
Annak biztosítására, hogy a keresések eredményeket találjanak függetlenül attól, hogy a felhasználó hogyan írja be a keresett kifejezést (akár ékezetekkel, akár anélkül, nagy- vagy kisbetűkkel), definiálunk egy tisztító függvényt:
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. lépés: A fő lekérdezés elfogása a `posts_pre_query` segítségével
A posts_pre_query szűrő segítségével a WordPress keresési lekérdezése még az adatbázis elérését megelőzően le van fogva. A rendszer beolvassa a JSON-t, megkeresi a keresett kifejezéseknek megfelelő azonosítókat, és kizárólag ezeket a bejegyzéseket adja vissza a WordPressnek, megadva a megfelelő oldalszámozást.
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. lépés: Az eredmények ellenőrzése
Miután elvégezte a fenti lépéseket, hajtson végre egy keresési tesztet, majd nyissa meg a Query Monitor alkalmazást:
- A BD teljes futási ideje: Látni fogod, hogy ez most már nem több másodperc, hanem csupán néhány milliszekundum (általában 0,03 és 0,2 másodperc között).
- SQL-lekérdezések: A LIKE %kifejezés% záradék teljesen eltűnik.
- RAM-memória: A memóriahasználat csúcsértéke rendkívül alacsony szinten marad (körülbelül 30 MB).
- Rendezés: A bejegyzések szigorúan csökkenő időrendi sorrendben (a legújabbtól a legrégebbiig) jelennek meg, a WordPress natív oldalszámozását figyelembe véve.
Több mint elégedett vagyok az eredménnyel. Nemcsak egy évek óta fennálló problémát oldottam meg, hanem a keresési eredmények is egyszerűen látványosak. Az egyik legigényesebb keresési lekérdezésnél is – mint például ez a 4887 találatot adó„viñeta” keresés – a kereső nagyon gyorsan válaszol. A kevesebb találatot adó keresések esetében szinte azonnal megjelennek az eredmények. Bármilyen más kifejezés re keresve ezt magad is ellenőrizheted.







