Как ускорить работу поисковой системы WordPress с помощью статического JSON-индекса

Без комментариев

24.07.2026|

Без комментариев

Tiempo de lectura Lectura: 5 min, 14 s
Número de palabras Palabras: 971
Número de visitas Visitas: 6
Icono de traducción

То, что поисковая система WordPress — полная беда, ни для кого не секрет. С эстетической точки зрения эту проблему относительно легко решить, однако с функциональностью дело обстоит иначе, поскольку она не предлагает никаких настроек, да и производительность у неё просто плачевна.

Когда на сайте WordPress накапливается тысячи записей, встроенная система поиска начинает давать сбои. Она замедляется до такой степени, что становится практически бесполезной. При выполнении поиска WordPress запускает запрос LIKE %слово% в базе данных, который просматривает всё содержимое таблицы wp_posts. Это генерирует SQL-запросы, выполнение которых может занимать от нескольких секунд до бесконечности и вызывать пиковые нагрузки на память, что в итоге приводит к блокировке сервера.

Именно это со мной и происходило. При 6040 опубликованных постах (только на испанском языке) время отклика при поиске с большим количеством результатов было просто кошмаром — оно превышало 10 секунд и более. И настал день, когда нужно было решить эту проблему раз и навсегда.

Долго обдумывая этот вопрос, я пришёл к выводу, что существует как минимум три варианта решения этой проблемы. Первый и самый простой — воспользоваться каким-нибудь платным плагином, например Relevanssi (его бесплатной версии недостаточно), или внешним сервисом, таким как Algolia, но я сразу же отбросил этот вариант, поскольку мне нужно ускорить поиск с помощью встроенных средств.

Из двух оставшихся вариантов я попробовал создать полнотекстовый индекс в 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);

Примечание: Чтобы отключить хук с помощью 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 Мб, что вполне приемлемо, поскольку пока его размер не превысит 15–20 МБ (что соответствует примерно 30.000 или 40 000 обширных записей), беспокоиться не стоит, ведь статические JSON-файлы загружаются сразу целиком в кэш сервера или PHP (file_get_contents), и файл размером в несколько мегабайт считывается менее чем за одну тысячную секунды; однако, если файл чрезмерно разрастётся, то при каждом выполнении поиска PHP будет потреблять дополнительный пиковый объем оперативной памяти, ненужный для сброса гигантского 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. Оперативная память: пиковое потребление памяти останется исключительно низким (около 30 МБ).
  4. Сортировка: записи будут отображаться в строгом убывающем хронологическом порядке (от самых новых к самым старым) с соблюдением встроенной пагинации WordPress.

Я более чем доволен результатом. Я не только избавился от проблемы, которая преследовала меня на протяжении многих лет, но и теперь результат просто потрясающий. Даже при одном из самых сложных запросов, например , с 4887 результатами по запросу «виньетка», поисковик отвечает очень быстро. Что касается запросов с меньшим количеством результатов, то их можно сказать, что они отображаются мгновенно. Вы можете убедиться в этом, выполнив поиск по любому другому запросу.

Как ускорить работу поисковой системы WordPress с помощью статического JSON-индекса 1

Оставьте комментарий

Есть что сказать?

Este blog se aloja en LucusHost

LucusHost, el mejor hosting