La búsqueda de WordPress es lenta: acelérala sin servicios caros

¿Te fue útil?

No hay nada que diga “valoramos tu tiempo” como un cuadro de búsqueda que devuelve resultados cuando tu café ya está frío. En un sitio WordPress con mucho tráfico, la búsqueda por defecto puede torturar a tu base de datos: comodines, tablas gigantes y plugins que tratan wp_postmeta como un cajón de sastre ilimitado.

No necesitas un servicio alojado caro para arreglar esto. Necesitas medir primero y luego aplicar un conjunto reducido de cambios aburridos pero de gran impacto: mejores consultas, los índices correctos, caché sensata y eliminar la complejidad accidental que apareció durante la temporada de “simplemente publícalo”.

Guía rápida de diagnóstico

Si no haces nada más, haz esto en orden. El objetivo es encontrar el cuello de botella en minutos, no pasar un día “optimizando” la capa equivocada.

1) Confirma que realmente es la búsqueda (y no PHP, la red o un plugin haciendo diez cosas más)

  • Revisa el tiempo de la petición en el borde (herramientas de desarrollo del navegador o los logs del CDN).
  • Revisa el log de acceso del servidor para solicitudes ?s= y sus tiempos de respuesta.
  • Si la página es lenta pero el tiempo de BD está bien, tu problema es renderizado en PHP, hooks de plugins o llamadas remotas.

2) Identifica el patrón de consulta y si está usando índices

  • Activa el registro de consultas lentas brevemente (o usa Performance Schema si ya lo tienes).
  • Captura una consulta de búsqueda representativa y lenta.
  • Ejecuta EXPLAIN y busca escaneos completos de tablas, estimaciones enormes de “rows” y “Using temporary; Using filesort”.

3) Decide: arregla consulta/índice primero, luego caché, luego infraestructura

  • Si EXPLAIN muestra escaneo de millones de filas: indexado y modelado de consulta primero.
  • Si la consulta a BD es rápida pero la página es lenta: caché de objetos, profiler PHP, auditoría de plugins.
  • Si la BD está saturada (alto IO wait, misses en el buffer pool): ajusta InnoDB + reduce el bloat; solo entonces considera hardware.

Idea parafraseada (atribuida): Werner Vogels lleva tiempo diciendo que debes “medir, luego optimizar”, porque adivinar es la forma de optimizar la cosa equivocada.

Por qué la búsqueda por defecto de WordPress es lenta (qué hace realmente)

La búsqueda por defecto de WordPress no es “búsqueda”, es una coincidencia de patrón SQL. El núcleo construye una consulta que busca tu término en wp_posts.post_title y wp_posts.post_content, normalmente usando condiciones LIKE. LIKE '%term%' es la trampa clásica de rendimiento: el comodín inicial impide que los índices B-tree normales ayuden. La base de datos a menudo termina escaneando muchas filas.

Eso es antes de que aparezcan los plugins. Muchos “mejoradores de búsqueda” añaden JOINs a wp_postmeta para buscar campos personalizados. wp_postmeta es enorme en muchos sitios y está diseñado para flexibilidad, no velocidad: un almacén clave-valor con valores de texto largos, lleno de opciones autoload y restos de años. Si tu búsqueda toca meta, has convertido un problema de “escanear posts” en un problema de “escanear posts más un almacén completo”.

Y luego está la parte de salida: las páginas de resultados de búsqueda activan plantillas, entradas relacionadas, anuncios, personalización, etiquetas de analíticas y a veces llamadas a APIs remotas. La base de datos puede ser inocente mientras PHP realiza su danza interpretativa.

Regla con opinión: trata la búsqueda lenta como un problema de base de datos hasta que demuestres que no lo es. Luego trátalo como un problema de caché hasta que demuestres que no se puede cachear.

Broma #1: La búsqueda por defecto de WordPress es como hacer grep en un PDF: técnicamente posible, emocionalmente cuestionable.

Datos interesantes y contexto histórico

  1. La búsqueda del core de WordPress es intencionalmente simple. Prioriza la compatibilidad amplia por encima de la calidad de búsqueda, por eso se apoya en LIKE en lugar de algoritmos de ranking.
  2. Los índices FULLTEXT de MySQL no siempre fueron amigables con InnoDB. Versiones antiguas empujaron a muchos sitios hacia MyISAM para FULLTEXT; MySQL/MariaDB modernos soportan FULLTEXT en InnoDB, cambiando los trade-offs.
  3. El “problema wp_postmeta” empeoró con los page builders. Muchos constructores almacenan layouts y bloques en meta. La búsqueda que hace JOIN en meta puede convertirse en un desastre a cámara lenta.
  4. InnoDB se convirtió en el motor por defecto en MySQL 5.5. Ese cambio hizo que el dimensionamiento del buffer pool y los patrones de IO fueran centrales en el trabajo de rendimiento de WordPress.
  5. WordPress introdujo hooks de caché de objetos persistente temprano. La API está ahí; muchos sitios simplemente nunca la conectan a Redis/Memcached.
  6. Las stopwords y la longitud mínima de palabra importan. FULLTEXT ignora palabras muy comunes y (por defecto) tokens cortos, lo que sorprende a equipos que migran desde una búsqueda ingenua con LIKE.
  7. Los conjuntos de caracteres cambiaron las reglas. Pasar a utf8mb4 aumentó los tamaños de índice; algunos índices que “antes cabían” dejaron de caber y el rendimiento cambió.
  8. MariaDB y MySQL se divergen. El comportamiento de FULLTEXT, decisiones del optimizador y los valores por defecto pueden diferir; tus recomendaciones de tuning no siempre son portables.
  9. Algunos proveedores desactivan los registros de consultas lentas en planes compartidos. Por eso las herramientas de logging a nivel de aplicación y la captura de consultas siguen importando para el diagnóstico.

Mide primero: demuestra dónde se va el tiempo

Vas a hacer trabajo real. Eso comienza con datos: tiempos de consulta, filas examinadas y si la base de datos está esperando CPU o disco. A continuación hay tareas prácticas que puedes ejecutar en una caja típica Linux + Nginx/Apache + MySQL/MariaDB. Cada tarea incluye qué estás observando y qué decisión informa.

Tarea 1: Encuentra solicitudes de búsqueda lentas en el log de acceso del servidor web

cr0x@server:~$ sudo awk '$7 ~ /\?s=|&s=/ {print $4,$5,$7,$9,$10}' /var/log/nginx/access.log | tail -n 20
[27/Dec/2025:09:10:12 +0000] GET /?s=backup 200 84123
[27/Dec/2025:09:11:08 +0000] GET /?s=pricing 200 90211

Qué significa la salida: Estás muestreando búsquedas recientes y verificando que existan y no estén siendo redirigidas. La última columna es el tamaño de la respuesta; aún necesitas el tiempo.

Decisión: Si las solicitudes de búsqueda se concentran en términos o endpoints específicos (URL de búsqueda personalizada, rutas de plugins multilenguaje), podrías estar frente a una plantilla o ruta de plugin concreta.

Tarea 2: Añade tiempo de petición a los logs de acceso (Nginx) y localiza búsquedas lentas

Asumiendo que tu formato de log de Nginx incluye $request_time. Si no, añádelo y recarga; luego:

cr0x@server:~$ sudo awk '$7 ~ /\?s=|&s=/ && $NF > 1.5 {print $4,$5,$7,"t="$NF}' /var/log/nginx/access.log | tail -n 20
[27/Dec/2025:09:14:02 +0000] GET /?s=invoice t=2.113
[27/Dec/2025:09:14:55 +0000] GET /?s=api t=1.874

Qué significa la salida: Solicitudes de búsqueda que exceden 1.5s. El umbral es tu decisión; elige algo que duela.

Decisión: Si el tiempo de petición es alto pero luego el tiempo de BD parece bajo, te inclinarás hacia profiling de PHP/plugins.

Tarea 3: Confirma que la base de datos es el punto caliente (chequeo rápido de IO/CPU)

cr0x@server:~$ vmstat 1 5
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
 2  0      0 432112  61264 912340    0    0    60   110  420  700 12  4 79  5  0
 1  1      0 401988  61272 910120    0    0  2800  3400  600  900 18  6 54 22  0

Qué significa la salida: La columna wa es IO wait. Un IO wait sostenido durante búsquedas sugiere trabajo de base de datos limitado por disco.

Decisión: IO wait alto: necesitas índices, menos bloat en tablas y ajuste del buffer pool antes de añadir más CPUs.

Tarea 4: Revisa la carga en MySQL en vivo y la consulta más pesada

cr0x@server:~$ mysql -e "SHOW FULL PROCESSLIST\G" | sed -n '1,120p'
*************************** 1. row ***************************
     Id: 31284
   User: wp
   Host: 127.0.0.1:51614
     db: wordpress
Command: Query
   Time: 3
  State: Sending data
   Info: SELECT SQL_CALC_FOUND_ROWS wp_posts.ID FROM wp_posts WHERE 1=1 AND ((wp_posts.post_title LIKE '%invoice%') OR (wp_posts.post_content LIKE '%invoice%')) AND wp_posts.post_type IN ('post','page') AND (wp_posts.post_status = 'publish') ORDER BY wp_posts.post_date DESC LIMIT 0, 10

Qué significa la salida: Has capturado una consulta real y su estado. “Sending data” a menudo significa que está escaneando/devolviendo muchas filas o haciendo un ordenamiento.

Decisión: Copia la consulta (sanitiza valores) y ejecuta EXPLAIN. Si hace un escaneo completo, vas a cambiar el enfoque de búsqueda o añadir una estrategia de índices.

Tarea 5: Activa el registro de consultas lentas brevemente (MySQL/MariaDB)

cr0x@server:~$ mysql -e "SET GLOBAL slow_query_log = 'ON'; SET GLOBAL long_query_time = 0.5; SET GLOBAL log_queries_not_using_indexes = 'ON';"

Qué significa la salida: Sin salida significa que se ejecutó. La ruta de tu slow log depende de la configuración.

Decisión: Usa esto durante una ventana controlada. Si no puedes hacerlo en producción, hazlo en un clon de staging con datos lo más reales posible.

Tarea 6: Lee el log de consultas lentas buscando patrones de búsqueda

cr0x@server:~$ sudo tail -n 50 /var/log/mysql/mysql-slow.log
# Time: 2025-12-27T09:14:02.123456Z
# User@Host: wp[wp] @ localhost []
# Query_time: 2.107  Lock_time: 0.000  Rows_sent: 10  Rows_examined: 842311
SET timestamp=1766826842;
SELECT SQL_CALC_FOUND_ROWS wp_posts.ID FROM wp_posts WHERE 1=1 AND ((wp_posts.post_title LIKE '%invoice%') OR (wp_posts.post_content LIKE '%invoice%')) AND wp_posts.post_type IN ('post','page') AND (wp_posts.post_status = 'publish') ORDER BY wp_posts.post_date DESC LIMIT 0, 10;

Qué significa la salida: 10 filas devueltas, 842k examinadas. Esa es la firma de un escaneo.

Decisión: No vas a “afinar” esto con un servidor más grande. Necesitas una estrategia de búsqueda indexable o reducir drásticamente el conjunto de candidatos.

Tarea 7: EXPLAIN la consulta e interpreta el daño

cr0x@server:~$ mysql wordpress -e "EXPLAIN SELECT wp_posts.ID FROM wp_posts WHERE ((wp_posts.post_title LIKE '%invoice%') OR (wp_posts.post_content LIKE '%invoice%')) AND wp_posts.post_type IN ('post','page') AND (wp_posts.post_status = 'publish') ORDER BY wp_posts.post_date DESC LIMIT 10\G"
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: wp_posts
   partitions: NULL
         type: ALL
possible_keys: type_status_date
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 921344
     filtered: 2.50
        Extra: Using where; Using filesort

Qué significa la salida: type: ALL significa escaneo completo de tabla. Using filesort significa que MySQL está ordenando resultados en lugar de leerlos en orden de índice.

Decisión: O (a) pasa a FULLTEXT, (b) restringe la búsqueda a un subconjunto indexable, o (c) introduce una tabla de índice dedicada que controles.

Tarea 8: Inspecciona el tamaño de las tablas (encuentra a los verdaderos culpables)

cr0x@server:~$ mysql -e "SELECT table_name, engine, table_rows, ROUND((data_length+index_length)/1024/1024,1) AS total_mb FROM information_schema.tables WHERE table_schema='wordpress' ORDER BY (data_length+index_length) DESC LIMIT 12;"
+----------------+--------+------------+----------+
| table_name     | engine | table_rows | total_mb |
+----------------+--------+------------+----------+
| wp_postmeta    | InnoDB |    18322104|  2860.4  |
| wp_posts       | InnoDB |      921344|   640.2  |
| wp_options     | InnoDB |       81234|    94.7  |
+----------------+--------+------------+----------+

Qué significa la salida: Si wp_postmeta eclipsa todo lo demás, cualquier búsqueda que toque meta será cara a menos que la rediseñes.

Decisión: Si meta es enorme: deja de buscarla indiscriminadamente. Lista blanca de claves, reduce la basura almacenada o crea una tabla de índice separada.

Tarea 9: Encuentra opciones autoload muy grandes (el peso de página silencioso)

cr0x@server:~$ mysql wordpress -e "SELECT option_name, LENGTH(option_value) AS bytes FROM wp_options WHERE autoload='yes' ORDER BY bytes DESC LIMIT 10;"
+----------------------------+---------+
| option_name                | bytes   |
+----------------------------+---------+
| some_builder_global_styles | 1948120 |
| plugin_cache_blob          |  822114 |
| theme_mods_mytheme         |  310992 |
+----------------------------+---------+

Qué significa la salida: WordPress carga opciones autoload en cada petición—búsquedas incluidas. Si son megabytes, estás pagando por ello en todos lados.

Decisión: Reduce el tamaño de autoload: arregla plugins/temas que almacenan blobs grandes en autoload, o cambia opciones concretas a autoload='no' (con cuidado y probando).

Tarea 10: Revisa la salud del buffer pool de InnoDB (¿lees desde RAM o disco?)

cr0x@server:~$ mysql -e "SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_read%';"
+---------------------------------------+------------+
| Variable_name                         | Value      |
+---------------------------------------+------------+
| Innodb_buffer_pool_read_requests      | 1928831123 |
| Innodb_buffer_pool_reads              | 18299312   |
+---------------------------------------+------------+

Qué significa la salida: reads son lecturas físicas; read_requests son lógicas. La ratio te dice con qué frecuencia fallas en la caché.

Decisión: Si los misses son frecuentes bajo carga, aumenta innodb_buffer_pool_size (dentro de los límites de RAM) y reduce el tamaño del working set (bloat e índices inútiles).

Tarea 11: Captura el perfil de una búsqueda con EXPLAIN ANALYZE (MySQL 8+)

cr0x@server:~$ mysql wordpress -e "EXPLAIN ANALYZE SELECT ID FROM wp_posts WHERE post_status='publish' AND post_type IN ('post','page') AND (post_title LIKE '%invoice%' OR post_content LIKE '%invoice%') ORDER BY post_date DESC LIMIT 10;"
+----------------------------------------------------------------------------------------------------------------------------------+
| EXPLAIN                                                                                                                          |
+----------------------------------------------------------------------------------------------------------------------------------+
| -> Limit: 10 row(s)  (cost=... rows=10) (actual time=0.105..2107.331 rows=10 loops=1)                                         |
|     -> Sort: wp_posts.post_date DESC, limit input to 10 row(s) per chunk  (actual time=0.104..2107.329 rows=10 loops=1)       |
|         -> Filter: ((wp_posts.post_title like '%invoice%') or (wp_posts.post_content like '%invoice%'))  (rows=...)          |
|             -> Table scan on wp_posts  (actual time=0.050..2001.002 rows=921344 loops=1)                                      |
+----------------------------------------------------------------------------------------------------------------------------------+

Qué significa la salida: El escaneo de tabla domina. Esta es tu pistola humeante con marcas de tiempo.

Decisión: Deja de intentar micro-ajustar. Cambia el mecanismo de búsqueda o estrecha el escaneo.

Tarea 12: Verifica si el caché de objetos está activo (lado WordPress)

cr0x@server:~$ wp --path=/var/www/html cache type
Default

Qué significa la salida: “Default” suele significar que no hay caché de objetos persistente conectada.

Decisión: Instala/configura Redis o Memcached si tienes páginas dinámicas y consultas repetidas. No arregla un escaneo de tablas, pero evita que el resto de la página aumente la presión en la BD.

Tarea 13: Confirma que Redis es accesible (si lo activas)

cr0x@server:~$ redis-cli ping
PONG

Qué significa la salida: Redis está vivo.

Decisión: Si no obtienes un PONG estable, no bases tu plan de caché de búsqueda en él.

Tarea 14: Inspecciona las claves meta más pesadas (objetivos para limpieza o exclusión)

cr0x@server:~$ mysql wordpress -e "SELECT meta_key, COUNT(*) AS rows, ROUND(SUM(LENGTH(meta_value))/1024/1024,1) AS value_mb FROM wp_postmeta GROUP BY meta_key ORDER BY value_mb DESC LIMIT 10;"
+-----------------------+----------+----------+
| meta_key              | rows     | value_mb |
+-----------------------+----------+----------+
| _builder_data         |  310221  |  940.3   |
| _some_plugin_cache    |  901122  |  512.7   |
| _thumbnail_id         |  610010  |   12.4   |
+-----------------------+----------+----------+

Qué significa la salida: Unas pocas claves suelen explicar la mayor parte del payload de meta. Esas son las que castigan los JOINs y el churn de caché.

Decisión: Excluye esas claves de la búsqueda; considera moverlas fuera de meta si el plugin lo permite; purga blobs de caché obsoletos.

Tarea 15: Revisa mantenimiento de tablas o bloqueos de larga duración (raro, pero feo)

cr0x@server:~$ mysql -e "SHOW ENGINE INNODB STATUS\G" | sed -n '1,120p'
TRANSACTIONS
------------
Trx id counter 123456789
Purge done for trx's n:o < 123456700 undo n:o < 0 state: running
History list length 4123
...

Qué significa la salida: Una lista de historial enorme puede indicar transacciones largas que retrasan el purge, lo que puede inflar el undo y perjudicar el rendimiento.

Decisión: Si ves esto durante degradaciones, busca transacciones largas (a menudo backups, analíticas o herramientas administrativas) y arréglalas.

Correcciones de base de datos que mueven la aguja

1) Deja de fingir que LIKE es una estrategia indexable

Si tu consulta actual es LIKE '%term%' contra campos de texto grandes, la base de datos está haciendo fuerza bruta. Puedes tirar RAM al problema, pero es como comprar una cinta de correr más rápida para que tu hámster corra más fuerte.

Hay tres opciones pragmáticas que no requieren un servicio externo caro:

  • Usar índices FULLTEXT en wp_posts para título/contenido.
  • Mantener tu propia tabla de índice de búsqueda (una “documento de búsqueda” desnormalizado por post) y consultarla.
  • Restringir agresivamente el conjunto de candidatos (tipos de post, ventanas de fecha, filtros por taxonomía, particionado por idioma/sitio) y aceptar que siga siendo basado en LIKE.

2) Añade FULLTEXT en posts (MySQL/MariaDB modernos) y adapta las consultas de WordPress

FULLTEXT es la respuesta de “usa la base de datos que ya pagas”. No es una búsqueda perfecta, pero es mucho mejor que escanear todos los posts por cada pulsación.

Qué vigilar: stopwords, tamaño mínimo de token, stemming (no nativo), comportamiento de ranking y el hecho de que FULLTEXT no busca subcadenas. Los usuarios buscando “inv” no coincidirán con “invoice” a menos que manejes prefijos (a menudo con un fallback a LIKE para términos muy cortos).

cr0x@server:~$ mysql wordpress -e "ALTER TABLE wp_posts ADD FULLTEXT KEY ft_title_content (post_title, post_content);"

Qué significa la salida: En tablas grandes, esto puede tomar tiempo e IO. Si bloquea demasiado según tu versión, programa mantenimiento o usa capacidades DDL online donde estén disponibles.

Decisión: Si tu hosting no puede manejarlo online, hazlo en ventanas de bajo tráfico o en un réplica promovida durante un corte de mantenimiento.

Ahora la parte importante: WordPress no usará automáticamente MATCH ... AGAINST. Necesitas un plugin pequeño o un mu-plugin que enganche posts_search / posts_where para reescribir la condición de búsqueda a FULLTEXT cuando la longitud del término sea sensata.

3) Construye una tabla de índice de búsqueda dedicada (aburrida, controlable, rápida)

Si necesitas buscar campos meta seleccionados, extractos o nombres de taxonomías, me gusta construir una pequeña tabla lateral:

  • Una fila por post (o por versión de idioma), que contenga un blob de “documento” preconstruido que elijas.
  • Un índice FULLTEXT sobre ese blob.
  • Actualizada en save_post, reconstruible por lotes y fácil de razonar.

Esto evita JOINs múltiples en tiempo de consulta. Pagas coste en escritura para ganar velocidad en lectura. Ese es el trade-off correcto para búsqueda.

Ejemplo de esquema (conceptual; lo adaptarás):

cr0x@server:~$ mysql wordpress -e "CREATE TABLE wp_search_index (post_id BIGINT UNSIGNED NOT NULL PRIMARY KEY, lang VARCHAR(12) NOT NULL DEFAULT 'en', doc LONGTEXT NOT NULL, updated_at DATETIME NOT NULL, FULLTEXT KEY ft_doc (doc), KEY lang_updated (lang, updated_at)) ENGINE=InnoDB;"

Qué significa la salida: Tabla creada, lista para poblar.

Decisión: Si no puedes cambiar la generación de consultas de WordPress con seguridad, puedes enrutar la búsqueda a un endpoint personalizado que consulte esta tabla directamente (aún dentro de WordPress) y luego renderice resultados.

4) Si debes buscar meta, lista blanca de claves e indexa en consecuencia

Buscar todo el meta es la forma más rápida de derretir tu base de datos y además devolver resultados basura (“button_color: #ffffff” no es intención del usuario). En su lugar:

  • Elige de 3 a 10 claves meta que realmente importen (SKU, código de producto, nombre del autor, etc.).
  • Asegúrate de que esas claves sean cortas, consistentes y no almacenen blobs enormes.
  • Añade un índice compuesto que ayude tu patrón de JOIN.
cr0x@server:~$ mysql wordpress -e "ALTER TABLE wp_postmeta ADD INDEX meta_key_post_id (meta_key(191), post_id);"

Qué significa la salida: Esto ayuda consultas como “encuentra posts con una meta_key dada y luego haz join a posts.” No hace que meta_value LIKE '%term%' sea mágicamente rápido, pero reduce cuánto meta tocas.

Decisión: Si tu plugin busca meta_value con comodines iniciales, todavía necesitas otra estrategia (tabla de índice dedicada o FULLTEXT sobre un documento derivado de meta curada).

5) Mata SQL_CALC_FOUND_ROWS y patrones de paginación caros

Muchas consultas de WordPress usan SQL_CALC_FOUND_ROWS para computar el total de resultados para paginación. Es conveniente y a menudo lento, especialmente con filtros pesados. Puedes evitarlo frecuentemente:

  • Muestra “Siguiente” sin mostrar “Página 1 de 500”.
  • Limita resultados (“mostrar top 100”) para búsqueda, que además es normal en UX.
  • Calcula totales de forma asíncrona o en caché.

Si estás atado a un tema que espera totales, considera cachear conteos por firma de consulta con un TTL corto.

6) Reduce el bloat: tus tablas no solo son grandes, son desordenadas

El rendimiento de búsqueda se correlaciona fuertemente con “¿cuánta basura debe vadear la consulta?”. Tres fuentes comunes de bloat:

  • Revisiones de posts y autosaves.
  • Meta huérfano de posts eliminados o plugins abandonados.
  • Transients y blobs de caché dejados por plugins.

Limpieza práctica (con cuidado; haz backup primero):

cr0x@server:~$ wp --path=/var/www/html post delete $(wp --path=/var/www/html post list --post_type='revision' --format=ids --posts_per_page=2000) --force
Success: Trashed post 1201.
Success: Trashed post 1202.

Qué significa la salida: Revisions eliminadas. Dependiendo de tu configuración de WP CLI, puede “mover a la papelera” y luego forzar eliminación.

Decisión: Si las revisiones son requeridas por políticas, limita su número en la configuración en lugar de borrarlas repetidamente.

Chequeo de meta huérfano:

cr0x@server:~$ mysql wordpress -e "SELECT COUNT(*) AS orphan_meta FROM wp_postmeta pm LEFT JOIN wp_posts p ON p.ID = pm.post_id WHERE p.ID IS NULL;"
+-------------+
| orphan_meta |
+-------------+
| 421233      |
+-------------+

Qué significa la salida: Filas meta que referencian posts inexistentes. Eso es puro desperdicio.

Decisión: Si esto es considerable, planifica un trabajo de limpieza (en horas de baja) y luego monitoriza el crecimiento para detectar el plugin que lo causa.

7) Ajusta InnoDB con sensatez (no conviertas tu base de datos en una feria de ciencias)

Para WordPress, los parámetros aburridos suelen importar:

  • innodb_buffer_pool_size: lo bastante grande para contener datos calientes e índices.
  • innodb_log_file_size: no pequeño; evita presión constante de checkpoints.
  • Almacenamiento rápido y ajustes de fs estables (especialmente en volúmenes cloud).

Pero recuerda: el tuning ayuda cuando tu plan de consulta es razonable. No redime un escaneo completo provocado por comodines iniciales.

Modelado de consultas: reduce el trabajo, no “ajustes” dolorosos

1) Haz la búsqueda menos global

Muchos sitios WordPress tratan la búsqueda como “buscar en todo”. Eso incluye páginas que nadie debería encontrar, comunicados de prensa antiguos y tipos de post personalizados que existen solo para satisfacer un plugin. Reducir el alcance no es hacer trampa; es claridad de producto.

  • Excluye tipos de post que no son visibles para usuarios.
  • Excluye posts de attachments a menos que tengas una UX de búsqueda de medios.
  • Excluye borradores/privados, obviamente.
  • Prefiere buscar en título + extracto; solo incluye contenido si es imprescindible.

2) No ordenes por fecha si no tienes que hacerlo

Ordenar por post_date es común, pero si usas FULLTEXT probablemente querrás ranking por relevancia. Ordenar por fecha obliga a trabajo de ordenamiento y puede luchar contra el optimizador.

Con FULLTEXT puedes ordenar por score de match y usar la fecha como desempate. Si te quedas con LIKE, ordenar por fecha tras filtrar es caro si el filtrado es caro—que lo es.

3) Las consultas cortas no deberían disparar lógica de búsqueda cara

Búsquedas de uno o dos caracteres son ruido. Además, son costosas cuando se implementan con LIKE.

  • Exige una longitud mínima de consulta (p. ej., 3 caracteres) antes de ejecutar la búsqueda.
  • Para términos cortos, muestra sugerencias, páginas populares o un mensaje “introduce más caracteres”.

4) Evita que “buscar mientras escribes” golpee PHP + MySQL por cada pulsación

El autocompletado está bien. El autocompletado que ejecuta una consulta WP completa en cada pulsación es un DoS que te autocreaste.

  • Debounce en cliente (300–500ms).
  • Requerir longitud mínima.
  • Cachear resultados agresivamente con TTL corto.

Broma #2: “Buscar mientras escribes” sin debounce es como preguntarle a tu base de datos “¿ya llegamos?” cada 100 milisegundos.

Caché para búsqueda sin engañar a los usuarios

Cachear resultados de búsqueda no solo está permitido; es normal. El truco es cachear de forma que el equipo de contenido no se vuelva loco cuando las entradas nuevas no aparezcan durante horas.

1) Cachea por firma de consulta normalizada

Normaliza la cadena de búsqueda (trim, minúsculas, colapsar espacios) y crea una clave de caché que incluya:

  • término de búsqueda
  • idioma/sitio/blog ID (para multisite)
  • filtro de tipo de post
  • rol de usuario si los resultados difieren por permisos (normalmente no deberían)
  • número de página (o mejor: usa paginación por cursor)

TTL sugeridos: 30–300 segundos para sitios de alta rotación; 5–30 minutos para sitios mayormente estáticos. También puedes usar invalidación en save_post para tipos relevantes.

2) Cachea la parte cara, no todo el HTML

El cacheo de página completa funciona genial para tráfico anónimo, pero los términos de búsqueda explotan el espacio de claves de caché. En su lugar:

  • Cachea la lista de IDs de posts coincidentes.
  • Luego recupera posts por ID (barato, apto para índices), o deja que WP lo haga.

Esto mantiene los valores de caché pequeños y estables, y evitas cachéar ruido de plantillas.

3) Usa caché de objetos persistente (Redis/Memcached) correctamente

El object cache de WordPress ayuda con consultas repetidas y cargas de opciones. No arregla automáticamente una consulta patológica, pero evita que el resto de la página provoque estampidas sobre la base de datos.

Cuando habilites Redis:

  • Monitorea uso de memoria de Redis y la política de evicción.
  • No pongas TTL tan largos que devuelvas resultados de búsqueda obsoletos para flujos editoriales.
  • No almacenes blobs gigantes (algunos plugins lo hacen; tendrás que controlarlos).

Ajustes de infraestructura (las partes que la gente sobreutiliza)

1) Separa BD y web solo si sabes qué estás arreglando

Separar la BD del servidor web puede ayudar con la contención de recursos, pero también añade latencia de red y complejidad operativa. Si tu búsqueda está haciendo escaneos completos, mover la base de datos solo reparte el dolor en más sitios.

2) El almacenamiento importa: la latencia vence al throughput en OLTP

Las cargas de WordPress suelen ser muchas lecturas/escrituras pequeñas, no grandes transferencias secuenciales. Para bases de datos, el almacenamiento de baja latencia gana. Si estás en volúmenes adjuntos a red, entiende la clase de rendimiento y el modelo “burst vs sustained”.

3) Las réplicas ayudan en reporting, no en consultas de búsqueda rotas

Las réplicas de lectura son geniales para descargar reporting y backups. Pero la búsqueda forma parte del tráfico de usuario. Puedes enrutar lecturas de búsqueda a una réplica si tu configuración tolera lag de replicación y tus necesidades de consistencia son bajas. Muchos sitios de contenido pueden. Sitios de e‑commerce usualmente no.

4) PHP-FPM no es el villano, pero puede amplificar el dolor

Si las llamadas a BD bloquean, los workers PHP se amontonan, se forman colas y Nginx empieza a hacer timeouts. Asegúrate de tener suficientes workers para la operación normal, pero no “arregles” la búsqueda lenta duplicando workers y permitiendo que más consultas concurrentes estampen la base de datos.

Tres mini-historias del mundo corporativo

Incidente: una suposición errónea (“Es solo contenido, no puede ser tan grande”)

En una compañía de tamaño medio, la latencia de búsqueda se volvió de “aceptable” a “dolorosa” durante un año. El equipo supuso que era crecimiento de tráfico. Escalaron servidores web, y luego los escalaron otra vez, porque eso es lo que los dashboards sugerían: CPU en PHP alta durante picos de búsqueda.

La suposición equivocada era sutil: creían que posts eran la tabla pesada. En realidad, un plugin de page builder estaba almacenando grandes blobs serializados de layout en wp_postmeta, y un plugin de “mejor búsqueda” hacía JOIN en meta en cada consulta—sin lista blanca de claves. Además, su UI de búsqueda hacía AJAX por cada pulsación sin debounce.

Cuando activaron slow query logging, el patrón fue obvio: las solicitudes de búsqueda causaban consultas que examinaban millones de filas de meta. La CPU de PHP estaba alta porque los workers esperaban a la base de datos, no porque el renderizado fuera caro. Los promedios de carga mintieron como suelen hacerlo.

La solución no fue glamorosa: desactivar búsqueda en meta por defecto, listar en blanco dos claves que realmente importaban, añadir longitud mínima de consulta y debounce en cliente. También crearon una pequeña tabla de índice para las claves en la lista blanca y la reconstruyeron cada noche. La búsqueda se volvió rápida. El page builder mantuvo sus blobs. Todos dejaron de gritar.

Optimización que salió mal (caché que se convirtió en un outage auto-infligido)

Un sitio SaaS de marketing quería resultados instantáneos de búsqueda. Alguien lanzó un “cachea todo”: cachear el HTML renderizado de páginas de búsqueda indexado por la query string, con TTL largo. En demo parecía brillante.

En producción causó dos problemas. Primero, churn de caché: consultas de cola larga crearon un número ilimitado de entradas de caché, expulsando objetos realmente útiles. Segundo, corrección de contenido: cuando legal pidió retirar un documento, siguió apareciendo en páginas de búsqueda cacheadas hasta que expiró el TTL.

La respuesta al incidente fue incómoda porque no fue un outage tradicional. El sitio “estaba arriba”, pero servía resultados que no deberían existir. Ese tipo de fallo de fiabilidad te gana reuniones con gente que usa zapatos caros.

El equipo revertió el cacheo de página completa para búsquedas y en su lugar cacheó solo la lista de IDs de posts coincidentes por un TTL corto, con invalidación explícita en cambios de contenido. El caché dejó de crecer sin control y las peticiones “esto debe desaparecer ya” se resolvieron sin limpiar todo.

Práctica aburrida pero correcta que salvó el día (réplicas + slow logs + control de cambios)

Una base de conocimiento interna corría en WordPress (sí, de verdad). La búsqueda se volvió lenta tras una actualización de plugin, justo antes de un gran despliegue interno. El SRE de guardia hizo lo menos emocionante posible: siguió su playbook de cambios.

Activaron slow query logging por 15 minutos, capturaron a los principales ofensores y los compararon con la línea base de la semana pasada. El delta mostró un nuevo patrón de consultas haciendo JOIN en wp_postmeta en cada búsqueda, incluso cuando no había campos meta configurados.

Como tenían réplica, probaron una adición de índice y una modificación de consulta ahí primero. Como tenían control de cambios, programaron el DDL en producción en una ventana de baja actividad y comunicaron el riesgo breve. Como tenían un plan de rollback, no entraron en pánico cuando el primer intento tardó más de lo esperado.

El resultado fue aburrido: una opción de plugin cambiada, un índice añadido y un pequeño mu-plugin para forzar longitud mínima de consulta. Sin heroísmos. Sin sala de crisis. Solo menos gente enviando “la búsqueda está rota” cada mañana.

Errores comunes: síntoma → causa raíz → solución

1) Síntoma: la búsqueda es lenta solo para términos comunes

Causa raíz: los términos comunes coinciden con muchos posts; la consulta escanea y ordena un conjunto enorme; las stopwords de FULLTEXT también pueden cambiar el comportamiento.

Solución: usa FULLTEXT con orden por relevancia y considera añadir filtros (tipo de post, taxonomía). Cachea resultados para consultas comunes con TTL corto.

2) Síntoma: la latencia de búsqueda sube durante actividad editorial

Causa raíz: opciones autoload o invalidaciones de caché de objetos se disparan; escrituras frecuentes expulsan caches; algunos plugins hacen trabajo pesado en save_post.

Solución: audita opciones autoload, habilita caché de objetos persistente y mueve reconstrucciones de índice costosas a trabajos asíncronos/por lotes.

3) Síntoma: la CPU de la BD está alta, pero añadir CPUs casi no ayuda

Causa raíz: escaneos completos + ordenamientos son bound a memoria/IO; el optimizador no puede usar índices con comodines iniciales.

Solución: cambia la estrategia de consulta (FULLTEXT o tabla de índice). Ajusta el buffer pool solo después de tener un plan de consulta sensato.

4) Síntoma: la búsqueda es rápida para administradores, lenta para usuarios anónimos (o viceversa)

Causa raíz: hooks/plugins diferentes se ejecutan según rol; o el caché aplica solo a tráfico anónimo; o tienes lógica de personalización.

Solución: compara SQL generado y hooks; estandariza la lógica de búsqueda entre roles; cachea resultados a nivel de datos (IDs), no HTML por rol salvo que sea necesario.

5) Síntoma: solo la página 2+ de resultados es lenta

Causa raíz: paginación con OFFSET profundo (LIMIT 10000,10) obliga a la base de datos a encontrar y descartar muchas filas.

Solución: limita la profundidad de la paginación, usa paginación por cursor o cachea resultados y pagina IDs en memoria.

6) Síntoma: lento tras instalar un “plugin de mejora de búsqueda”

Causa raíz: el plugin añade JOINs en meta y LIKE en meta_value, o añade joins de taxonomía sin índices.

Solución: configúralo para que solo busque título/contenido; lista blanca de claves meta; o sustitúyelo por un enfoque basado en FULLTEXT.

7) Síntoma: la búsqueda causa esperas por bloqueos

Causa raíz: no típico en SELECT, pero puede ocurrir con transacciones largas, cambios de esquema o presión de tablas temporales.

Solución: identifica bloqueadores (processlist, InnoDB status). Programa DDL, arregla transacciones largas y asegúrate de que el espacio para temporales no esté limitado.

8) Síntoma: “búsquedas aleatorias” lentas tras migrar de host

Causa raíz: comportamiento distinto por versión de MySQL/MariaDB, valores por defecto cambiados, latencia de almacenamiento superior o buffer pool más pequeño.

Solución: compara versiones y configuraciones; mide la tasa de aciertos del buffer pool; verifica la latencia de almacenamiento; reexamina planes de consulta.

Listas de verificación / plan paso a paso

Fase 0: No rompas producción (30–60 minutos)

  • Confirma que tienes un backup reciente (y que se puede restaurar).
  • Elige una ventana de 15 minutos para logging de consultas lentas si está permitido.
  • Anota “qué es el objetivo”: objetivo de p95 para tiempo de respuesta de búsqueda, tolerancia de obsolescencia para caché.

Fase 1: Victorias rápidas (mismo día)

  1. Longitud mínima de consulta: exigir 3+ caracteres antes de ejecutar búsqueda.
  2. Debounce del autocompletado: 300–500ms de retardo en cliente.
  3. Reducción de alcance: restringir tipos de post; considerar excluir post_content si título/excerpt es suficiente.
  4. Desactivar búsqueda en meta por defecto: listar en blanco solo claves significativas.
  5. Reducir bloat autoload: identifica los principales ofensores y corrige configuración de plugins/tema.

Fase 2: Correcciones estructurales (1–3 días)

  1. Habilitar visibilidad de consultas lentas: slow logs o Performance Schema.
  2. Añadir índice FULLTEXT: en wp_posts o en una tabla de índice dedicada.
  3. Reescribir consultas: usar MATCH ... AGAINST cuando sea posible; mantener fallback para términos cortos.
  4. Cachear IDs de búsqueda: TTL corto, normalización de claves, invalidación explícita en publish/update para tipos relevantes.

Fase 3: Endurecimiento y mantenimiento (continuo)

  1. Limpieza programada del bloat: revisiones, huérfanos, transients—automatízalo.
  2. Rastrear regresiones: mantener una baseline semanal de consultas lentas; alertar por cambios en la forma de consultas, no solo por CPU.
  3. Planificación de capacidad: asegúrate de que el buffer pool cubre el working set; verifica latencia de almacenamiento bajo carga.
  4. Control de cambios: índices y cambios de plugins con pruebas en staging y planes de despliegue.

Guardarraíles operativos (lo que yo impondría)

  • Ningún plugin puede hacer JOIN a wp_postmeta en búsqueda sin una lista blanca de claves documentada.
  • Ningún endpoint de búsqueda sin longitud mínima de consulta y limitación de tasa.
  • No “cachear todo” sin revisar la cardinalidad de claves de caché.
  • Cualquier DDL en tablas grandes necesita plan para locks, tiempo de ejecución y rollback.

Preguntas frecuentes

1) ¿Puedo hacer rápida la búsqueda por defecto de WordPress sin cambiar SQL?

Puedes hacerla menos mala reduciendo alcance (menos tipos de post, excluir contenido, longitud mínima) y cacheando resultados. Pero si mantienes LIKE '%term%' en tablas grandes, estás pagando por escaneos.

2) ¿Añadir un índice en post_title arreglará la búsqueda?

No para LIKE con comodín al inicio. Un índice puede ayudar LIKE 'term%' (búsqueda por prefijo), pero no '%term%'. Por eso existe FULLTEXT.

3) ¿FULLTEXT es “suficiente” para usuarios reales?

A menudo sí para sitios de contenido y bases de conocimiento. Obtienes velocidad y relevancia básica. No tendrás stemming avanzado, sinónimos, tolerancia a errores tipográficos o ranking personalizado sin trabajo adicional—pero muchos sitios no lo necesitan para dejar de sufrir.

4) ¿Y buscar PDFs, attachments o campos personalizados?

No hagas JOINs en meta al azar en tiempo de consulta y esperes que funcione. Crea una tabla de índice curada que almacene texto extraído o campos seleccionados, y aplica FULLTEXT a esa tabla.

5) ¿Redis acelerará automáticamente la búsqueda?

Redis acelera búsquedas repetidas y reduce el ruido en la base de datos. No hace que una consulta patológica individual sea rápida. Úsalo para reducir daño colateral y para cachear IDs de resultados de búsqueda.

6) ¿Es seguro limpiar wp_postmeta y revisiones?

Puedes hacerlo, y también puedes romper cosas si borras datos que un plugin espera. Haz backups, empieza por filas huérfanas y blobs obvios de caché, y prueba el comportamiento de plugins en staging.

7) ¿Por qué la página 10 de resultados es tan lenta?

La paginación con OFFSET hace que la BD encuentre y descarte muchas filas. Considera limitar páginas, usar “Cargar más” con cursores o cachear la lista de IDs y hacer slices en memoria.

8) ¿Debería moverme a un servidor de BD más grande primero?

Solo después de confirmar que tu plan de consulta es razonable. Servidores más grandes son buenos para hacer más trabajo. Son malos haciendo desaparecer trabajo malo.

9) Alojamiento compartido: ¿qué puedo hacer realmente?

Puede que no controles configuración de MySQL o logs, pero aún puedes: reducir alcance de búsqueda, exigir longitud mínima, desactivar búsqueda en meta, limpiar revisiones y añadir caché a nivel de aplicación. Si FULLTEXT está permitido, sigue siendo la mejor mejora sin servicio externo.

10) ¿Cómo sé que la solución funcionó?

Mide p95 del tiempo de respuesta de búsqueda, filas examinadas por consultas representativas y CPU/IO wait durante una porción de tráfico. Si las filas examinadas bajaron órdenes de magnitud, lo solucionaste de verdad.

Próximos pasos (prácticos, no poéticos)

Haz tres cosas esta semana:

  1. Demuestra el cuello de botella con slow query logs y EXPLAIN. Si estás escaneando cientos de miles de filas por búsqueda, deja de negociar con eso.
  2. Elige una estrategia de búsqueda real: FULLTEXT en wp_posts para necesidades directas, o una tabla de índice dedicada si debes incluir meta/taxonomías seleccionadas.
  3. Estabiliza con guardarraíles: longitud mínima de consulta, autocompletado con debounce, tipos de post limitados y caché de IDs de resultados con TTL corto y invalidación sensata.

Si lo haces bien, el cuadro de búsqueda deja de ser una herramienta de pruebas de carga y vuelve a ser una característica. La tranquilidad es la meta.

← Anterior
Debian 13: SSH tarda en iniciar sesión — DNS y GSSAPI que lo aceleran al instante (caso #5)
Siguiente →
Errores SMTP 4xx temporales: causas principales y soluciones que realmente funcionan

Deja un comentario