
Det er ingen hemmelighed, at WordPress’ søgefunktion er elendig. Hvad det æstetiske angår, er det relativt nemt at løse, men dens funktionalitet er en helt anden historie, da den ikke tilbyder nogen konfigurationsmuligheder overhovedet, og oven i købet er ydeevnen elendig.
Når et WordPress-websted indeholder tusindvis af indlæg, begynder den indbyggede søgefunktion at svigte. Den bliver langsommere og langsommere, indtil den praktisk talt er ubrugelig. Når man foretager en søgning, udfører WordPress en LIKE %ord%-forespørgsel i databasen, der gennemsøger hele indholdet af tabellen wp_posts. Dette genererer SQL-forespørgsler, der kan tage alt fra flere sekunder til en evighed og forårsage spidsbelastninger i hukommelsen, som ender med at blokere serveren.
Det var præcis det, jeg allerede oplevede. Med 6040 offentliggjorte indlæg (kun på spansk) var responstiderne for søgninger med mange resultater et sandt helvede med ventetider på over 10 sekunder eller mere. Og så kom dagen, hvor jeg endelig kunne løse problemet én gang for alle.
Efter at have tænkt længe over det fandt jeg ud af, at der var mindst tre muligheder for at løse problemet. Den første og enkleste var at bruge et betalt plugin som Relevanssi (den gratis version er ikke tilstrækkelig) eller en ekstern tjeneste som Algolia, hvilket jeg dog straks udelukkede, da jeg ønsker at optimere søgningerne direkte i systemet.
Af de to resterende muligheder forsøgte jeg at oprette et FULLTEXT-indeks i MySQL ved hjælp af MATCH() AGAINST()-forespørgsler, men det mislykkedes totalt. Selvom jeg justerede parametrene for InnoDB-motoren og optimerede indekserne, gik MySQL stadig i stå, når den skulle behandle tusindvis af komplekse søgeord, hvilket resulterede i uacceptable responstider og overbelastede serveren. Det var da, jeg helt opgav at lade databasen bære den tunge byrde og valgte den tredje løsning: at oprette et statisk indeks i JSON.
I denne vejledning forsøger vi at forklare, hvordan man løser dette problem ved at erstatte de ressourcekrævende MySQL-forespørgsler med en statisk JSON-fil, der fungerer som et lynhurtigt søgeindeks.
Trin 1: Find og fjern tunge eller overflødige forespørgsler
Inden man implementerer JSON-indekset, er det afgørende at rydde op. Mange plugins (såsom kataloger, layout-plugins eller plugins til sekundære søgninger) kobler sig til WordPress’ søgeproces via hooks (pre_get_posts, posts_where osv.), hvilket udløser yderligere og unødvendige forespørgsler til databasen.
Hvordan kan man spore disse forespørgsler med Query Monitor?
- Installer og aktiver det gratis plugin Query Monitor.
- Foretag en vilkårlig søgning på din hjemmeside.
- Åbn rullemenuen for Query Monitor i den øverste bjælke, og vælg »Databaseforespørgsler« (Database Queries).
- Filtrer efter langsomme forespørgsler, eller gennemgå listen for at finde de forespørgsler, der tager længst tid. Notér i samme omgang også de dublerede forespørgsler, og hvis du kan, så løs også det problem.
- Se på detaljekolonnen i tabellerne: Der kan du se præcis, hvilket plugin eller hvilken fil der udfører den pågældende forespørgsel.
I mit tilfælde måtte jeg løse et par dobbelte forespørgsler fra Name_Directory-pluginet og nogle GenerateBlocks-blokke i indlægsskabelonen, men ethvert plugin, brugerdefineret kode eller tilføjet element kan generere forespørgsler, der forsinker svaret.
Sådan deaktiveres disse hooks via kode
Når du har fundet navnet på den funktion eller klasse, der forårsager den langsomme forespørgsel, kan du deaktivere den i din functions.php-fil eller ved hjælp af et plugin til kodestykker (som f.eks. Code Snippets, Code fra Perfmatters eller lignende):
* Bemærk: I blok 1 skal du tilføje de hooks, der forårsager langsomme forespørgsler. Hvis dette ikke er tilfældet for dig, kan du fjerne denne del og kun tilføje blok 2 til kodestykket.
/**
* 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);
Bemærk: For at fjerne en hook med `remove_action` eller `remove_filter` skal du bruge nøjagtigt det samme begivenhedsnavn (`pre_get_posts`), det samme funktionsnavn, som du fandt i Query Monitor, og den samme prioritet, som den blev tilføjet med (standard 10).
Trin 2: Oprette og holde JSON-søgefilen opdateret
For at erstatte MySQL skal vi oprette en statisk fil i JSON-format, der indeholder nøgledataene for vores indlæg (ID, titel og almindelig tekst). Denne fil gemmes i mappen wp-content/uploads/ og opdateres automatisk, hver gang du opretter, redigerer eller sletter et indlæg.
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));
}
Første oprettelse: Første gang du installerer dette snippet, kan du midlertidigt køre mi_sitio_reconstruir_indice_json_completo(); eller blot gemme/opdatere et hvilket som helst eksisterende indlæg, så filen search-es.json oprettes for første gang. Når dette er gjort, skal du gå til mappen wp-content/uploads/ for at sikre dig, at filen er oprettet, og for at se, hvor stor den er.
For at give dig en idé om den omtrentlige størrelse af denne search-es.json: For mine 6. 040 poster blev der genereret en fil på 3,41 Mb, hvilket er en ganske håndterbar størrelse, da man først skal bekymre sig, når den begynder at veje mere end 15 eller 20 MB (svarende til ca. 30.000 eller 40.000 omfattende poster), er der ingen grund til bekymring, for statiske JSON-filer indlæses direkte i serverens eller PHP’s cache (file_get_contents) på én gang, og en fil på et par megabyte læses på under en tusindedel af et sekund; men hvis filen vokser uforholdsmæssigt meget, vil PHP hver gang, der udføres en søgning, forbruge en unødvendig ekstra spidsbelastning af RAM-hukommelse for at indlæse en gigantisk JSON-fil. I sådanne tilfælde bør man overveje kun at indeksere titler og uddrag i stedet for hele indholdet eller skifte til en dedikeret databaseindekseret søgeløsning.
Trin 3: Hjælpefunktion til standardisering af søgninger (uden accenter)
For at sikre, at søgningerne giver resultater, uanset hvordan brugeren skriver (med eller uden accenter, store eller små bogstaver), defineres der en rensningsfunktion:
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);
}
}
Trin 4: Afbryd hovedforespørgslen med posts_pre_query
Ved hjælp af filteret `posts_pre_query` opfanges WordPress’ søgeforespørgsel, inden den når frem til databasen. JSON-dataene læses, de ID’er, der matcher de søgte termer, findes, og kun disse indlæg returneres til WordPress med angivelse af den relevante paginering.
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);
Trin 5: Kontrol af resultaterne
Når du har gennemført de ovenstående trin, skal du foretage en søgetest og åbne Query Monitor:
- Samlet BD-tid: Du vil se, at den nu er gået fra flere sekunder til blot nogle få millisekunder (normalt mellem 0,03 s og 0,2 s).
- SQL-forespørgsler: LIKE %søgeord%-klausulen vil være helt forsvundet.
- RAM: Det maksimale hukommelsesforbrug vil forblive usædvanligt lavt (omkring 30 MB).
- Sortering: Indlæggene vises i strengt kronologisk rækkefølge i faldende rækkefølge (fra det nyeste til det ældste) i overensstemmelse med WordPress’ oprindelige paginering.
Jeg er mere end tilfreds med resultatet. Ikke alene har jeg løst et problem, der har plaget mig i årevis, men nu er resultatet nærmest spektakulært. Selv ved en af de mest krævende søgninger, som denne med 4887 resultater for »viñeta«, svarer søgemaskinen meget hurtigt. Ved søgninger med færre resultater kan man sige, at de vises med det samme. Du kan selv tjekke det ved at søge på noget andet.







