
Nav noslēpums, ka WordPress meklētājs ir pilnīgs fiasko. No estētiskā viedokļa to ir salīdzinoši viegli labot, taču tā darbība ir pavisam cita lieta, jo tas nepiedāvā nekādas konfigurācijas iespējas, turklāt tā veiktspēja ir nožēlojama.
Kad WordPress vietnē uzkrājas tūkstošiem ierakstu, iebūvētais meklētājs sāk nedarboties pareizi. Tas palēninās, līdz kļūst praktiski nelietojams. Veicot meklēšanu, WordPress datu bāzē izpilda vaicājumu LIKE %vārds%, kas pārskata visu wp_posts tabulas saturu. Tas ģenerē SQL vaicājumus, kuru izpilde var ilgt no dažām sekundēm līdz pat bezgalībai un izraisīt atmiņas pārslodzi, kas galu galā bloķē serveri.
Tieši tas jau notika ar mani. Ar 6040 publicētiem ierakstiem (tikai spāņu valodā) atbildes laiks meklējumos ar lielāku rezultātu skaitu bija īsta mokas, jo tas pārsniedza 10 sekundes vai pat vairāk. Un beidzot pienāca diena, kad šo problēmu vajadzēja atrisināt reizi par visām reizēm.
Pēc ilgām pārdomām es secināju, ka ir vismaz trīs iespējas, kā to atrisināt. Pirmā un vienkāršākā bija izmantot kādu maksas spraudni, piemēram, „Relevanssi“ (tās bezmaksas versija nav pietiekama), vai ārējo pakalpojumu, piemēram, „Algolia“, taču šo variantu es jau sākumā noraidīju, jo mans mērķis ir paātrināt meklēšanu, izmantojot sistēmas iebūvētos līdzekļus.
No divām atlikušajām iespējām es izmēģināju izveidot FULLTEXT indeksu MySQL, izmantojot MATCH() un AGAINST() vaicājumus, taču cietu briesmīgu neveiksmi. Neskatoties uz to, ka pielāgoju InnoDB dzinēja parametrus un optimizēju indeksus, MySQL joprojām aizķērās, apstrādājot tūkstošiem sarežģītu atslēgvārdu, radot nepieņemamus atbildes laikus un pārslodzot serveri. Tad es pilnībā atteicos no idejas uzlikt šo smago slogu datu bāzei un izvēlējos trešo risinājumu — izveidot statisku indeksu JSON formātā.
Šajā apmācībā tiek mēģināts izskaidrot, kā atrisināt šo problēmu, aizstājot smagos MySQL vaicājumus ar statisku JSON failu, kas darbojas kā ārkārtīgi ātrs meklēšanas indekss.
1. solis: Atklāt un novērst smagus vai liekus vaicājumus
Pirms JSON indeksa piemērošanas ir ļoti svarīgi „sakopt laukumu”. Daudzi spraudņi (piemēram, katalogi, izkārtojuma veidotāji vai papildu meklēšanas spraudņi) pieslēdzas WordPress meklēšanas procesam, izmantojot „hooks” (pre_get_posts, posts_where utt.), tādējādi izraisot papildu un nevajadzīgus datu bāzes vaicājumus.
Kā atklāt šos vaicājumus, izmantojot Query Monitor?
- Instalējiet un aktivizējiet bezmaksas spraudni „Query Monitor”.
- Veic jebkādu meklēšanu savā tīmekļa vietnē.
- Atveriet „Query Monitor“ nolaižamo izvēlni augšējā joslā un izvēlieties „Datubāzes vaicājumi“ (Database Queries).
- Filtrējiet pēc lēnajiem vaicājumiem vai pārskatiet sarakstu, meklējot vaicājumus, kuru apstrāde aizņem visvairāk laika. Vienlaikus atzīmējiet arī dublētos vaicājumus un, ja iespējams, novēršiet arī šo problēmu.
- Pievērs uzmanību tabulu detalizācijas slejai: tur tu redzēsi, kurš tieši spraudnis vai fails izpilda šo vaicājumu.
Manā gadījumā man nācās novērst dažus dublētus Name_Directory spraudņa pieprasījumus un dažus GenerateBlocks blokus raksta veidnē, taču jebkurš spraudnis, pielāgots kods vai pievienots elements var radīt pieprasījumus, kas palēnina atbildes sniegšanu.
Kā atspējot šos „hooks” ar kodu
Kad esi atradis funkcijas vai klases nosaukumu, kas izraisa lēno vaicājumu, vari to atspējot failā functions.php vai izmantojot koda fragmentu spraudni (piemēram, „Code Snippets”, „Code” no „Perfmatters” vai līdzīgu):
* Piezīme. 1. blokā tev jāpievieno savi „hooki”, kas rada lēnus vaicājumus. Ja tas neattiecas uz tevi, vari izdzēst šo daļu un pievienot fragmentam tikai 2. bloku.
/**
* 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);
Piezīme: Lai atceltu hook ar funkciju „remove_action” vai „remove_filter”, tev jāizmanto tieši tas pats notikuma nosaukums (pre_get_posts), tā pati funkcijas nosaukums, ko identificēji programmā „Query Monitor”, un tā pati prioritāte, ar kādu tā tika pievienota (pēc noklusējuma 10).
2. solis: Izveidot un uzturēt atjauninātu meklēšanas JSON failu
Lai aizstātu MySQL, mums ir jāizveido statisks fails JSON formātā, kurā būtu iekļauti mūsu ierakstu galvenie dati (ID, virsraksts un parastais teksts). Šis fails tiks saglabāts mapē wp-content/uploads/ un tiks automātiski atjaunināts katru reizi, kad izveidosi, rediģēsi vai dzēsīsi ierakstu.
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));
}
Sākotnējā izveide: Pirmoreiz instalējot šo koda fragmentu, vari uz laiku izpildīt komandu mi_sitio_reconstruir_indice_json_completo(); vai vienkārši saglabāt/atjaunināt jebkuru esošo ierakstu, lai failu search-es.json izveidotu pirmo reizi. Kad tas ir izdarīts, apmeklējiet ceļu wp-content/uploads/, lai pārliecinātos, ka fails ir izveidots, un uzzinātu tā izmēru.
Lai tu varētu aptuveni iztēloties šī search-es.json faila apjomu, maniem 6040 ierakstiem tika ģenerēts 3,41 Mb liels fails — tas ir diezgan pārvaldāms apjoms, jo, kamēr faila apjoms nepārsniedz 15 vai 20 MB (kas atbilst aptuveni 30.000 vai 40 000 garu ierakstu), nav jāuztraucas, jo statiskie JSON faili tiek ielādēti tieši servera vai PHP (file_get_contents) kešatmiņā vienā reizē, un failu, kura izmērs ir daži megabaiti, var izlasīt mazāk nekā vienā tūkstošdaļā sekundes; tomēr, ja fails kļūst pārmērīgi liels, katru reizi, kad tiek veikta meklēšana, PHP patērēs lieku RAM atmiņas maksimumu, lai ielādētu milzīgu JSON failu, un šādā gadījumā būtu vērts apsvērt iespēju indeksēt tikai virsrakstus un fragmentus, nevis visu pilno saturu, vai pāriet uz meklēšanas risinājumu, kas indeksēts atsevišķā datu bāzē.
3. solis: Palīgfunkcija meklējumu normalizēšanai (bez diakritiskajām zīmēm)
Lai nodrošinātu, ka meklēšanas rezultāti tiek atrasti neatkarīgi no tā, kā lietotājs raksta (ar vai bez diakritiskajām zīmēm, lielajiem vai mazajiem burtiem), tiek definēta attīrīšanas funkcija:
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. solis: Galvenā vaicājuma pārtveršana ar posts_pre_query
Izmantojot filtru „posts_pre_query“, tiek pārtverta WordPress meklēšanas vaicājuma, pirms tā nonāk datu bāzē. Tiek nolasīts JSON, atrasti ID, kas atbilst meklētajiem terminiem, un WordPress tiek atgriezti tikai šie ieraksti, norādot atbilstošo lappušu numerāciju.
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. solis: Rezultātu pārbaude
Pēc iepriekšējo darbību veikšanas veic meklēšanas pārbaudi un atver „Query Monitor“:
- Kopējais BD laiks: Tu redzēsi, ka tagad tas ir samazinājies no vairākām sekundēm līdz tikai dažām milisekundēm (parasti no 0,03 s līdz 0,2 s).
- SQL vaicājumi: Klauzula „LIKE %termins%“ tiks pilnībā likvidēta.
- RAM: Atmiņas izmantošanas maksimums saglabāsies ārkārtīgi zems (apmēram 30 MB).
- Kārtība: Ieraksti tiks parādīti stingrā hronoloģiskā secībā, sākot no jaunākajiem līdz vecākajiem, ievērojot WordPress standarta lappušu numerāciju.
Esmu vairāk nekā apmierināts ar rezultātu. Es ne tikai esmu atbrīvojies no problēmas, kas mani vajāja jau gadiem ilgi, bet tagad rezultāts ir gandrīz vai iespaidīgs. Pat vienā no visprasīgākajām meklēšanām, piemēram , šajā, kurā ir 4887 rezultāti parvārdu „viñeta”, meklētājs reaģē ļoti ātri. Attiecībā uz meklējumiem ar mazāku rezultātu skaitu var teikt, ka tie tiek parādīti uzreiz. To vari pārbaudīt, meklējot jebko citu.








