Как да ускорите търсачката на WordPress чрез използване на статичен JSON индекс

No comments

24.07.2026|

No comments

Tiempo de lectura Lectura: 5 min, 18 s
Número de palabras Palabras: 983
Número de visitas Visitas: 58
Icono de traducción

Това, че търсачката на WordPress е пълна катастрофа, не е тайна за никого. Що се отнася до външния вид, проблемът се решава сравнително лесно, но функционирането ѝ е съвсем друга история, тъй като не предлага никакви възможности за настройка, а освен това и производителността ѝ е жалка.

Когато един сайт в WordPress натрупа хиляди публикации, вградената търсачка започва да дава грешки. Тя се забавя до такава степен, че става практически безполезна. При извършване на търсене WordPress изпълнява заявка LIKE %дума% в базата данни, която сканира цялото съдържание на таблицата wp_posts. Това генерира SQL заявки, които могат да отнемат от няколко секунди до цяла вечност и да предизвикат пикове в паметта, които в крайна сметка блокират сървъра.

Точно това ми се случваше. С 6040 публикувани публикации (само на испански) времето за отговор при търсенията с повече резултати беше истински ад – над 10 секунди или дори повече. И дойде денят, в който трябваше да разреша проблема веднъж завинаги.

След като обмислих въпроса дълго, установих, че има поне три възможности да го оправя. Първата и най-простата беше да използвам някой платен плъгин като Relevanssi (безплатната му версия не е достатъчна) или външна услуга като Algolia, но отхвърлих тази възможност още в самото начало, защото целта ми е да ускоря търсенето по вграден начин.

От останалите две възможности опитах да създам FULLTEXT индекс в MySQL чрез заявки MATCH() AGAINST() и се провалих с гръм и трясък. Въпреки че настроих параметрите на InnoDB и оптимизирах индексите, MySQL продължаваше да забива при обработката на хиляди сложни ключови думи, което водеше до неприемливи времена за отговор и претоварване на сървъра. Тогава напълно отхвърлих идеята да оставя тежкия товар на базата данни и избрах третия вариант – да създам статичен индекс в JSON.

В този урок се прави опит да се обясни как да се реши този проблем, като тежките заявки към MySQL се заменят със статичен JSON файл, който действа като свръхбърз индекс за търсене.

Стъпка 1: Откриване и премахване на тежки или излишни заявки

Преди да приложите JSON индекса, е изключително важно да подготвите почвата. Много плъгини (като директории, макетиращи плъгини или плъгини за вторични търсения) се включват в процеса на търсене на WordPress чрез хукове (pre_get_posts, posts_where и др.), което води до допълнителни и ненужни заявки към базата данни.

Как да открием тези заявки с Query Monitor?

  1. Инсталирайте и активирайте безплатния плъгин Query Monitor.
  2. Извършете каквото и да е търсене във вашия уебсайт.
  3. Отвори падащото меню на Query Monitor в горната лента и избери „Заявки към базата данни“ (Database Queries).
  4. Филтрирайте по „Бавни заявки“ или прегледайте списъка, за да откриете заявките, които отнемат най-много време. Между другото, отбележете и дублираните заявки и, ако можете, разрешете и този проблем.
  5. Обърни внимание на колоната с подробности в таблиците: там ще видиш точно кой плъгин или файл изпълнява тази заявка.

В моя случай трябваше да отстраня няколко дублиращи се заявки от плъгина Name_Directory, както и някои блокове от GenerateBlocks в шаблона за публикации, но всеки плъгин, персонализиран код или добавен елемент може да генерира заявки, които забавят отговора.

Как да деактивирате тези хукове чрез код

След като откриете името на функцията или класа, който причинява бавната заявка, можете да я деактивирате във файла functions.php или чрез плъгин за фрагменти от код (като Code Snippets, Code от Perfmatters или подобен):

* Забележка. В блок 1 трябва да добавите вашите хукове, които генерират бавни заявки. Ако това не важи за вас, можете да премахнете тази част и да добавите към фрагмента само блок 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);

Забележка: За да премахнете hook с remove_action или remove_filter, трябва да използвате точно същото име на събитието (pre_get_posts), същото име на функцията, която сте идентифицирали в Query Monitor, и същия приоритет, с който е бил добавен (по подразбиране 10).

Стъпка 2: Създаване и актуализиране на JSON файла за търсене

За да заменим MySQL, трябва да създадем статичен файл във формат JSON, който да съдържа ключовите данни на нашите публикации (ID, заглавие и обикновен текст). Този файл ще се съхранява в папката wp-content/uploads/ и ще се актуализира автоматично всеки път, когато създавате, редактирате или изтривате публикация.

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));
}

Първоначално създаване: Когато инсталирате този фрагмент за първи път, можете временно да изпълните my_site_rebuild_full_json_index(); или просто да запазите/актуализирате някоя съществуваща публикация, за да се създаде файлът search-es.json за първи път. След като направите това, посетете пътя wp-content/uploads/, за да се уверите, че файлът е създаден, и да проверите неговия размер.

За да си представиш приблизителния размер на този search-es.json файл, за моите 6040 записи беше генериран файл с размер 3,41 Mb – доста приемлив размер, тъй като докато не започне да надвишава 15 или 20 MB (което се равнява на около 30.000 или 40 000 обширни записи), няма защо да се притеснявате, защото статичните JSON файлове се зареждат директно в кеш паметта на сървъра или на PHP (file_get_contents) наведнъж, а размер от няколко мегабайта се чете за по-малко от една хилядна от секундата; обаче, ако файлът нарасне прекомерно, при всяко изпълнение на търсене PHP ще консумира ненужен пик на RAM паметта, за да зарежда гигантски JSON файл, като в такъв случай би било целесъобразно да се обмисли индексирането само на заглавията и извлеченията, вместо на цялото съдържание, или да се премине към решение за търсене, индексирано в специална база данни.

Стъпка 3: Спомагателна функция за нормализиране на търсенията (без акценти)

За да се гарантира, че търсенията ще дават резултати, независимо от това как пише потребителят (с или без акценти, с главни или малки букви), се дефинира функция за пречистване:

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: Прехванете основния запит с posts_pre_query

Чрез филтъра posts_pre_query търсенето в WordPress се прехване, преди да достигне до базата данни. JSON файлът се чете, намират се ID-тата, които съвпадат с търсените термини, и на WordPress се връщат само тези записи, като се посочва съответната пагинация.

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: Проверка на резултатите

След като изпълните горните стъпки, направете тест за търсене и отворете Query Monitor:

  1. Общо време за BD: Ще забележиш, че сега то се е съкратило от няколко секунди до едва няколко милисекунди (обикновено между 0,03 с и 0,2 с).
  2. SQL-заявки: Клаузата LIKE %термин% ще бъде напълно премахната.
  3. RAM памет: Максималното потребление на памет ще остане изключително ниско (около 30 MB).
  4. Подреждане: Публикациите ще се показват в строго низходящ хронологичен ред (от най-новата към най-старата), като се запазва оригиналното разделие на страници в WordPress.

Повече от доволен съм от резултата. Не само че се отървах от проблем, който ме тормозеше от години, но и сега резултатът е просто невероятен. При едно от най-взискателните търсения, като това с 4887 резултата за:„винетка, търсачката отговаря много бързо. При търсения с по-малко резултати може да се каже, че те се показват мигновено. Можеш да го провериш, като потърсиш нещо друго.

Как да ускорите търсачката на WordPress чрез използване на статичен JSON индекс 1

Вашият коментар

Имаш ли нещо да кажеш?

Este blog se aloja en LucusHost

LucusHost, el mejor hosting