
Não é segredo para ninguém que o mecanismo de busca do WordPress é uma porcaria. No que diz respeito à aparência, é relativamente fácil de resolver; no entanto, seu funcionamento é outra história, já que não oferece nenhuma opção de configuração e, além disso, o desempenho é péssimo.
Quando um site no WordPress acumula milhares de postagens, o mecanismo de busca nativo começa a apresentar falhas. Ele fica tão lento que se torna praticamente inútil. Ao realizar uma busca, o WordPress executa uma consulta LIKE %palavra% no banco de dados, que varre todo o conteúdo da tabela wp_posts. Isso gera consultas SQL que podem levar de alguns segundos a uma eternidade e causar picos de uso de memória que acabam travando o servidor.
Era exatamente isso que já estava acontecendo comigo. Com 6.040 posts publicados (apenas em espanhol), os tempos de resposta das buscas com mais resultados eram um verdadeiro inferno, com tempos superiores a 10 segundos ou mais. E chegou o dia de resolver isso de uma vez por todas.
Depois de pensar bastante no assunto, descobri que havia pelo menos três possibilidades para resolver isso. A primeira e mais simples era recorrer a algum plugin pago, como o Relevanssi (sua versão gratuita é insuficiente), ou a um serviço externo, como o Algolia, o que descartei logo de cara, pois o que procuro é agilizar as buscas de forma nativa.
Das duas opções restantes, tentei criar um índice FULLTEXT no MySQL usando consultas MATCH() AGAINST() e fracassei miseravelmente. Apesar de ajustar os parâmetros do mecanismo InnoDB e otimizar os índices, o MySQL continuava travando 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 toda a carga pesada para o banco de dados e optei pela terceira alternativa: construir um índice estático em JSON.
Neste tutorial, tentamos explicar como resolver esse problema substituindo as consultas pesadas do MySQL por um arquivo JSON estático que funciona como um índice de pesquisa ultrarrápido.
Passo 1: Identificar 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) se conectam ao processo de pesquisa do WordPress por meio de hooks (pre_get_posts, posts_where, etc.), gerando consultas adicionais e desnecessárias ao banco de dados.
Como identificar essas consultas com o Query Monitor?
- Instale e ative o plugin gratuito Query Monitor.
- Faça qualquer pesquisa no seu site.
- Abra o menu suspenso do Query Monitor na barra superior e selecione Consultas ao banco de dados (Database Queries).
- Filtre por “Consultas lentas” ou examine a lista em busca das consultas que demoram mais tempo. Aproveite para anotar também as consultas duplicadas e, se possível, resolva esse problema também.
- Observe a coluna de detalhes das tabelas: lá você verá exatamente qual plugin ou arquivo está executando essa consulta.
No meu caso, tive que resolver algumas consultas duplicadas do plugin Name_Directory e alguns blocos do GenerateBlocks no modelo de postagem, mas qualquer plugin, código personalizado ou elemento adicionado pode estar gerando consultas que tornam a resposta mais lenta.
Como desativar esses hooks por meio de código
Depois de identificar o nome da função ou classe que está causando a consulta lenta, você pode desativá-la no seu arquivo functions.php ou por meio de um plugin de trechos de código (como o Code Snippets, o Code da Perfmatters ou similar):
* Observação. No bloco 1, você deve adicionar os seus hooks que geram consultas lentas. Se esse não for o seu caso, você pode excluir essa 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);
Observação: Para desativar um hook com remove_action ou remove_filter, você deve usar exatamente o mesmo nome do evento (pre_get_posts), o mesmo nome da função que você identificou no Query Monitor e a mesma prioridade com que ele foi adicionado (por padrão, 10).
Passo 2: Gerar e manter atualizado o arquivo JSON de pesquisa
Para substituir o MySQL, precisamos criar um arquivo estático no formato JSON que contenha os dados principais de nossas postagens (ID, título e texto simples). Esse arquivo será salvo na pasta wp-content/uploads/ e será atualizado automaticamente sempre que você criar, editar ou excluir uma postagem.
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));
}
Geração inicial: Na primeira vez que você instalar este snippet, você pode executar temporariamente my_site_rebuild_full_json_index(); ou simplesmente salvar/atualizar qualquer postagem existente para que o arquivo search-es.json seja criado pela primeira vez. Depois disso, acesse a pasta wp-content/uploads/ para verificar se o arquivo foi criado e saber seu tamanho.
Para você ter uma ideia do tamanho aproximado desse arquivo search-es.json: para as minhas 6. 040 entradas, foi gerado um arquivo de 3,41 Mb, um tamanho bastante razoável, já que, a menos que ele comece a ultrapassar 15 ou 20 MB (o equivalente a cerca de 30.000 ou 40.000 entradas extensas), não há motivo para preocupação, pois os arquivos JSON estáticos são carregados diretamente no cache do servidor ou do PHP (file_get_contents) de uma só vez, e um tamanho de alguns megabytes é lido em menos de um milésimo de segundo; no entanto, se o arquivo crescer desproporcionalmente, cada vez que uma busca for executada, o PHP consumirá um pico extra desnecessário de memória RAM para carregar um JSON gigantesco; nesse momento, seria aconselhável considerar indexar apenas os títulos e trechos, em vez de todo o conteúdo completo, ou migrar para uma solução de busca indexada em um banco de dados dedicado.
Passo 3: Função auxiliar para normalizar as buscas (sem acentos)
Para garantir que as buscas encontrem resultados independentemente de como o usuário digitar (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
Por meio do filtro `posts_pre_query`, a consulta de busca do WordPress é interceptada antes de chegar ao banco de dados. O JSON é lido, os IDs que correspondem aos termos pesquisados são identificados e apenas essas entradas são retornadas ao WordPress, 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);
Etapa 5: Verificação dos resultados
Depois de concluir as etapas anteriores, faça um teste de pesquisa e abra o Query Monitor:
- Tempo total do BD: Você verá que agora ele passa de vários segundos para apenas alguns milissegundos (normalmente entre 0,03 s e 0,2 s).
- Consultas SQL: A cláusula LIKE %termo% terá desaparecido completamente.
- Memória RAM: O pico de uso da memória permanecerá excepcionalmente baixo (cerca de 30 MB).
- Ordenação: As postagens serão exibidas em ordem cronológica estritamente decrescente (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. Em uma das consultas mais exigentes, como esta com 4. 887 resultados para “viñeta”, o mecanismo de busca responde muito rapidamente. Para buscas com menos resultados, pode-se dizer que eles são exibidos instantaneamente. Você pode conferir isso pesquisando qualquer outra coisa.





