如何使用静态 JSON 索引加速 WordPress 搜索功能

No comments

24.07.2026|

No comments

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

WordPress的搜索功能一团糟,这早已不是什么秘密。就界面美观而言,这个问题相对容易解决;然而,其功能表现却另当别论——不仅没有任何配置选项,而且性能更是糟糕透顶。

当一个WordPress网站积累了数千篇帖子时,其原生搜索功能就会开始失灵。搜索速度会变得极其缓慢,几乎无法使用。 进行搜索时,WordPress 会在数据库中执行LIKE %关键词%查询,扫描wp_posts 表中的所有内容。这会生成 SQL 查询,处理时间可能从几秒钟到漫长得像一辈子那么久不等,并导致内存使用量激增,最终导致服务器崩溃。

这正是我之前遇到的情况。由于已发布6040篇帖子(仅限西班牙语),当搜索结果较多时,响应时间简直像地狱一样,往往超过10秒甚至更久。终于到了要一劳永逸地解决这个问题的时候了。

经过反复斟酌,我发现至少有三种解决办法。第一种也是最简单的方法,是使用像Relevanssi这样的付费插件(其免费版功能不足),或者像Algolia这样的外部服务,但我一开始就排除了这种方案,因为我想要的是通过原生方式加快搜索速度。

在剩下的两个选项中,我尝试了通过MATCH() AGAINST()查询在MySQL中创建FULLTEXT索引,结果却彻底失败了。 尽管调整了InnoDB引擎的参数并优化了索引,但MySQL在处理数千个复杂关键词时仍然会卡住,导致响应时间无法接受,并使服务器陷入瘫痪。 于是,我彻底放弃了将这一重担交给数据库的做法,转而选择了第三种方案:在JSON中构建一个静态索引

本教程旨在说明如何通过将耗时的MySQL查询替换为一个静态JSON文件(该文件充当超快速搜索索引)来解决这一问题。

步骤 1:检测并消除耗时或冗余的查询

在应用 JSON 索引之前,必须先清理环境。许多插件(如目录插件、布局插件或辅助搜索插件)会通过钩子(如 pre_get_posts、posts_where 等)挂接到 WordPress 的搜索流程中,从而触发额外且不必要的数据库查询。

如何使用查询监视器来检测这些查询?

  1. 安装并启用免费插件Query Monitor
  2. 在您的网站上进行任意搜索。
  3. 在顶部工具栏中打开“查询监视器”的下拉菜单,然后选择“数据库查询”(Database Queries)。
  4. “慢查询”进行筛选,或浏览列表查找耗时较长的查询。顺便记录下重复的查询,如果可以的话,也一并解决。
  5. 请查看表中的详细信息列:在那里你可以准确看到是哪个插件或文件正在执行该查询。

就我而言,我不得不处理Name_Directory插件中的一小部分重复查询,以及文章模板中GenerateBlocks的某些区块,但任何插件、自定义代码或添加的元素都可能产生拖慢响应速度的查询。

如何通过代码禁用这些钩子

一旦确定了导致查询变慢的函数或类名称,你就可以在functions.php文件中将其注释掉或者通过代码片段插件(如Code SnippetsPerfmattersCode或类似插件)进行处理:

* 注意:在第1个代码块中,你应添加那些会引发慢查询的hooks。如果你的情况并非如此,可以删除这一部分,仅将第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移除一个钩子,必须使用与你在 Query Monitor 中识别到的完全相同的事件名称(pre_get_posts)、函数名称以及添加时设定的优先级(默认值为 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都会因加载巨型JSON文件而消耗额外的峰值RAM内存,此时建议考虑仅对标题和摘要进行索引,而非完整内容,或者迁移到基于专用数据库的索引搜索解决方案。

步骤 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步:结果验证

完成上述步骤后,进行一次搜索测试并打开“查询监视器”:

  1. 数据库总耗时:你会发现,现在耗时从几秒缩短到了仅几毫秒(通常在0.03秒到0.2秒之间)。
  2. SQL 查询: LIKE %术语%子句将彻底消失。
  3. RAM:内存使用峰值将保持在极低的水平(约30 MB)。
  4. 排序:文章将严格按照时间倒序(从最新到最旧)显示,并保留WordPress的原生分页设置。

我对结果非常满意。这不仅解决了困扰我多年的问题,而且现在的搜索结果简直令人惊叹。 在“viñeta”这样结果多达4887条的严苛搜索场景中,搜索引擎的响应速度依然非常快。对于结果较少的搜索,可以说结果几乎是瞬间显示的。你可以尝试搜索其他内容来验证这一点。

如何使用静态 JSON 索引加速 WordPress 搜索功能 1

发表评论

有什么要说的吗?

Este blog se aloja en LucusHost

LucusHost, el mejor hosting