Como acelerar o motor de pesquisa do WordPress utilizando um índice JSON estático

No comments

24.07.2026|

No comments

Tiempo de lectura Lectura: 11 min, 21 s
Número de palabras Palabras: 2101
Número de visitas Visitas: 6
Icono de traducción

Que o motor de pesquisa do WordPress é uma porcaria não é segredo para ninguém. No que diz respeito à estética, é relativamente fácil de resolver; no entanto, o seu funcionamento é outra história, uma vez que não oferece qualquer opção de configuração e, além disso, o desempenho é péssimo.

Quando um site em WordPress acumula milhares de entradas, o motor de pesquisa nativo começa a falhar. Fica tão lento que se torna praticamente inútil. Ao efetuar uma pesquisa, o WordPress executa uma consulta LIKE %palavra% na base de dados, que analisa todo o conteúdo da tabela wp_posts. Isto gera consultas SQL que podem demorar desde vários segundos até uma eternidade e provocar picos de memória que acabam por bloquear o servidor.

Era exatamente isso que já me estava a acontecer. Com 6040 publicações (apenas em espanhol), os tempos de resposta das pesquisas com mais resultados eram um verdadeiro inferno, com tempos superiores a 10 segundos ou mais. E chegou o dia de resolver o problema de uma vez por todas.

Depois de pensar muito no assunto, descobri que havia pelo menos três possibilidades para resolver o problema. A primeira e mais simples era recorrer a um plugin pago, como o Relevanssi (a sua versão gratuita é insuficiente), ou a um serviço externo, como o Algolia, o que descartei logo de início, porque o que pretendo é agilizar as pesquisas de forma nativa.

Das duas opções restantes, tentei criar um índice FULLTEXT no MySQL através de consultas MATCH() AGAINST() e falhei redondamente. Apesar de ajustar os parâmetros do motor InnoDB e otimizar os índices, o MySQL continuava a ficar bloqueado ao processar milhares de palavras-chave complexas, apresentando tempos de resposta inaceitáveis e sobrecarregando o servidor. Foi então que descartei completamente a ideia de deixar essa carga pesada a cargo da base de dados e optei pela terceira via: construir um índice estático em JSON.

Neste tutorial, procuramos explicar como resolver este problema, substituindo as consultas pesadas do MySQL por um ficheiro JSON estático que funciona como um índice de pesquisa ultrarrápido.

Passo 1: Detetar e eliminar consultas pesadas ou redundantes

Antes de aplicar o índice JSON, é fundamental preparar o terreno. Muitos plugins (como diretórios, layouters ou plugins de pesquisa secundária) interagem com o processo de pesquisa do WordPress através de hooks (pre_get_posts, posts_where, etc.), desencadeando consultas adicionais e desnecessárias à base de dados.

Como detetar estas consultas com o Query Monitor?

  1. Instale e ative o plugin gratuito Query Monitor.
  2. Faz qualquer pesquisa no teu site.
  3. Abra o menu suspenso do Query Monitor na barra superior e selecione «Consultas à base de dados» (Database Queries).
  4. Filtra por «Consultas lentas» ou analisa a lista à procura das consultas que demoram mais tempo. Já agora, anota também as consultas duplicadas e, se puderes, resolve também esse problema.
  5. Repara na coluna de detalhes das tabelas: aí verás exatamente qual o plugin ou ficheiro que está a executar essa consulta.

No meu caso, tive de resolver algumas consultas duplicadas do plugin Name_Directory e alguns blocos do GenerateBlocks no modelo de publicação, mas qualquer plugin, código personalizado ou elemento adicionado pode estar a gerar consultas que tornam a resposta mais lenta.

Como desativar esses hooks através do código

Depois de identificar o nome da função ou classe que está a causar a consulta lenta, pode desativá-la no seu ficheiro functions.php ou através de um plugin de fragmentos de código (como o Code Snippets, o Code da Perfmatters ou similar):

* Nota. No bloco 1, deve adicionar os seus hooks que geram consultas lentas. Se não for esse o seu caso, pode eliminar esta parte e adicionar apenas o bloco 2 ao snippet.

/**
 * 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);

Nota: Para desativar um hook com remove_action ou remove_filter, deve utilizar exatamente o mesmo nome do evento (pre_get_posts), o mesmo nome da função que identificou no Query Monitor e a mesma prioridade com que foi adicionado (por predefinição, 10).

Passo 2: Criar e manter atualizado o ficheiro JSON de pesquisa

Para substituir o MySQL, precisamos de criar um ficheiro estático em formato JSON que contenha os dados essenciais das nossas publicações (ID, título e texto simples). Este ficheiro será guardado na pasta wp-content/uploads/ e será atualizado automaticamente sempre que criares, editares ou eliminares uma publicação.

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

Configuração inicial: Na primeira vez que instalar este snippet, pode executar temporariamente a função «my_site_rebuild_full_json_index(); » ou simplesmente guardar/atualizar qualquer entrada existente para que o ficheiro «search-es.json» seja criado pela primeira vez. Depois de fazer isso, aceda à pasta wp-content/uploads/ para se certificar de que o ficheiro foi criado e para verificar o seu tamanho.

Para teres uma ideia do tamanho aproximado deste ficheiro search-es.json, para as minhas 6040 entradas foi gerado um ficheiro de 3,41 Mb, um tamanho bastante razoável, uma vez que, enquanto não ultrapassar os 15 ou 20 MB (o equivalente a cerca de 30.000 ou 40 000 entradas extensas) não há motivo para preocupação, porque os ficheiros JSON estáticos são carregados diretamente na cache do servidor ou do PHP (file_get_contents) de uma só vez, e um ficheiro com alguns megabytes é lido em menos de um milésimo de segundo; no entanto, se o ficheiro crescer desproporcionalmente, sempre que for executada uma pesquisa, o PHP consumirá um pico extra desnecessário de memória RAM para carregar um JSON gigantesco, momento em que seria aconselhável considerar a indexação apenas dos títulos e excertos, em vez de todo o conteúdo completo, ou migrar para uma solução de pesquisa indexada numa base de dados dedicada.

Passo 3: Função auxiliar para normalizar as pesquisas (sem acentos)

Para garantir que as pesquisas encontrem resultados independentemente da forma como o utilizador escrever (com ou sem acentos, em maiúsculas ou minúsculas), define-se uma função de limpeza:

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

Passo 4: Interceptar a consulta principal com posts_pre_query

Através do filtro `posts_pre_query`, a consulta de pesquisa do WordPress é interceptada antes de chegar à base de dados. O JSON é lido, identificam-se os IDs que correspondem aos termos pesquisados e devolvem-se ao WordPress apenas essas entradas, especificando a paginação correspondente.

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

Passo 5: Verificação dos resultados

Depois de concluir os passos anteriores, faça um teste de pesquisa e abra o Query Monitor:

  1. Tempo total do BD: Vais ver que agora passa de vários segundos para apenas algumas milésimas de segundo (normalmente entre 0,03 s e 0,2 s).
  2. Consultas SQL: A cláusula LIKE %termo% terá desaparecido por completo.
  3. Memória RAM: O pico de utilização da memória manter-se-á excepcionalmente baixo (cerca de 30 MB).
  4. Ordenação: As entradas serão apresentadas por ordem cronológica estritamente descendente (da mais recente à mais antiga), respeitando a paginação nativa do WordPress.

Estou mais do que satisfeito com o resultado. Não só resolvi um problema que me acompanhava há anos, como agora o resultado é simplesmente espetacular. Numa das pesquisas mais exigentes, como esta com 4887 resultados para «viñeta», o motor de busca responde muito rapidamente. No caso de pesquisas com menos resultados, pode-se dizer que estes são apresentados instantaneamente. Podes verificar isso pesquisando qualquer outra coisa.

Como acelerar o motor de pesquisa do WordPress utilizando um índice JSON estático 1

Deixe um comentário

Há alguma coisa a dizer?

Este blog se aloja en LucusHost

LucusHost, el mejor hosting