Implementaste la búsqueda. Los usuarios la adoraron durante una semana. Luego llegaron los tickets de “la búsqueda va lenta”, seguidos de “los resultados de búsqueda son incorrectos”, y finalmente el clásico ejecutivo: “¿Podemos hacerlo más parecido a Google?”
Es entonces cuando finanzas lanza la pregunta más operacionalmente peligrosa: “¿Es más barato Elasticsearch que hacerlo solo en Postgres?”
La respuesta honesta es que “más barato” depende menos del precio de lista y más de a qué te comprometes a operar en los próximos tres años: movimiento de datos, higiene de índices, dominios de fallo y el costo humano de gestionar un sistema distribuido más.
El marco de decisión: qué estás comprando realmente
La búsqueda de texto completo en PostgreSQL (FTS) y Elasticsearch no son rivales en el sentido de “casillas de características”. Son rivales en el sentido de “¿cuántos avisos a las 3 a. m. quieres por trimestre?”.
Ambos pueden responder “encuentra documentos que coincidan con estos términos, ordenados razonablemente.” Los costos divergen con el tiempo porque los modelos operativos divergen.
Qué mantiene barato en Postgres
- Menos piezas móviles. Mismo sistema de copias de seguridad, misma estrategia de alta disponibilidad, mismo stack de observabilidad, mismo calendario de actualizaciones.
- Consistencia transaccional por defecto. Los resultados de búsqueda reflejan escrituras confirmadas sin una canalización CDC o escrituras dobles.
- Corpus pequeño a mediano con consultas previsibles. Catálogos de producto, tickets, notas, registros CRM, documentación interna.
- Equipos que ya son buenos en Postgres. Reutilizar habilidades no es un beneficio abstracto; es una partida presupuestaria.
Qué mantiene barato en Elasticsearch
- Relevancia compleja y consultas estilo analíticas. Facetas, agregaciones, fuzziness, sinónimos, boosting por campo, “¿querías decir?” y experimentos intensivos de ranking multi-campo.
- Alto fan-out de consultas con ganancias de caché. Si tu tráfico de lectura eclipsa las escrituras, ES puede ser una máquina de consultas rentable—cuando está afinado.
- Corpus de texto grande y búsqueda multi-inquilino cuando aceptas la sobrecarga operativa y dimensionas los shards con sentido.
- Organizaciones que ya ejecutan Elastic bien. Si tienes un equipo de plataforma con playbooks probados, el costo marginal baja.
La pregunta de “qué es barato” a largo plazo son en realidad tres preguntas
- ¿Quieres un solo sistema de registro o dos? Los clústeres de búsqueda rara vez son sistemas de registro, lo que implica pipelines de ingestión, backfills y reconciliación.
- ¿Puedes tolerar consistencia eventual en los resultados de búsqueda? Si no, pagarás en algún lugar—usualmente en complejidad.
- ¿Quién será responsable de la calidad de la relevancia? Si es “nadie”, PostgreSQL FTS suele ganar por ser “suficientemente bueno” y estable.
Idea parafraseada de Werner Vogels (CTO de Amazon): Todo falla, todo el tiempo; diseña sistemas asumiendo fallos.
Eso no es poesía. Es una previsión presupuestaria.
Datos interesantes y breve historia (porque esto no apareció ayer)
- FTS de PostgreSQL no es nuevo. La búsqueda de texto completo principal llegó en PostgreSQL 8.3 (2008), basándose en módulos tsearch anteriores.
- Los índices GIN fueron un cambio radical. El soporte de Generalized Inverted Index (GIN) hizo prácticas las búsquedas token→fila a escala para datos tsvector.
- Elasticsearch cabalgó la ola de Lucene. Lucene precede a Elasticsearch por una década; ES empaquetó Lucene en un servicio distribuido con APIs REST y funciones de clúster.
- “Casi en tiempo real” es literal. Los sistemas basados en Lucene refrescan segmentos periódicamente; los documentos recién indexados son buscables tras el refresh, no instantáneamente al confirmar.
- El ranking de Postgres está enraizado en IR. ts_rank/ts_rank_cd implementan ideas clásicas de recuperación de información; no es magia, pero tampoco coincidencia de subcadenas ingenua.
- El conteo de shards por defecto de Elasticsearch ha quemado a muchos equipos. Los shards no son gratis; demasiados shards aumentan overhead y tiempo de recuperación.
- Vacuum es una palanca de costo. El bloat de Postgres y la churn de índices pueden convertir “búsqueda barata” en “¿por qué nuestro disco está al 90%?”
- Los cambios de mapping pueden ser operativamente caros. En ES, muchos cambios de mapping requieren reindexación; lo pagas en IO y tiempo.
- Los sinónimos son un problema organizacional. Sea Postgres o ES, las listas de sinónimos se vuelven política de producto—alguien debe hacerse cargo de las discusiones.
Broma #1: La relevancia de búsqueda es como el café de oficina—todos tienen opiniones y nadie quiere mantener la máquina.
Modelo de costos a largo plazo: cómputo, almacenamiento, personas y riesgo
Bucket de costo 1: cómputo y memoria
Postgres FTS suele consumir CPU en tiempo de consulta (ranking, filtrado) y durante escrituras (mantenimiento de índices GIN, triggers, columnas generadas). También se beneficia enormemente de la caché: las páginas calientes de índice en memoria importan.
Si ejecutas Postgres con “suficiente RAM justo”, descubrirás que las consultas de texto completo son excelentes para convertir IO aleatorio en un estilo de vida.
Elasticsearch consume memoria para heap (metadatos del clúster, lectores de segmento, cachés) y usa intensamente la caché de páginas del SO para segmentos Lucene. Si infra-provisionas heap obtendrás drama de GC; si sobreasignas heap privas a la caché del SO. De cualquier forma, tendrás una hoja de cálculo.
Bucket de costo 2: amplificación de almacenamiento
El almacenamiento es donde las matemáticas a largo plazo a menudo cambian de signo. Los índices de búsqueda no son compactos. Son redundancia deliberada para acelerar consultas.
- Postgres: una copia de los datos (más WAL), y uno o más índices. FTS añade almacenamiento tsvector y un índice GIN (o GiST). El bloat es el multiplicador silencioso.
- Elasticsearch: una copia del source (a menos que la desactives), estructuras de índice invertido, doc values para agregaciones y típicamente al menos una réplica. Eso es 2× antes de pestañear. Los snapshots añaden otra capa.
Si comparas una instancia única de Postgres con un clúster ES con réplicas, no estás comparando software. Estás comparando tolerancia al riesgo.
Bucket de costo 3: movimiento de datos y backfills
Postgres FTS: los datos ya están ahí. Añades una columna, construyes un índice y listo—hasta que cambies la configuración de tokenización o agregues un nuevo campo y necesites recomputar vectores.
Elasticsearch: debes ingerir. Eso significa CDC (decodificación lógica), patrón outbox, pipeline de streaming o escrituras dobles. También significa backfills. Los backfills ocurren en el peor momento: cuando ya dependes de la búsqueda para ingresos.
Bucket de costo 4: personas y procesos
Elasticsearch en producción no es “instalar y buscar”. Son políticas de ciclo de vida de índices, dimensionado de shards, intervalos de refresh, mappings, analizadores y actualizaciones de clúster que no se pueden tratar como una simple actualización de biblioteca.
Postgres FTS tampoco es gratis, pero pagas un “impuesto por nuevo sistema” más pequeño.
El sistema más barato es el que tu on-call puede explicar bajo presión. Si tu equipo nunca ha hecho un reinicio rolling de un clúster ES mientras los shards se reubican y el tráfico de usuarios se dispara, no has terminado de presupuestar.
Bucket de costo 5: riesgo y radio de explosión
Elasticsearch aísla la carga de búsqueda de tu base de datos primaria. Eso puede reducir riesgo si las consultas de búsqueda son pesadas e impredecibles.
Postgres mantiene todo junto: menos sistemas, pero mayor probabilidad de que una mala consulta de búsqueda se convierta en un incidente de base de datos.
Barato a largo plazo no es “la factura mensual más baja”. Barato a largo plazo es “menos incidentes multi-equipo y fines de semana de reindexación de emergencia”.
Búsqueda de texto completo en PostgreSQL: qué hace bien y qué castiga
Las partes buenas (y por qué siguen siendo buenas)
Postgres FTS destaca cuando la búsqueda es un atributo de tus datos transaccionales, no un producto separado.
Puedes mantener tu modelo de datos simple: una tabla, una columna tsvector, un índice GIN y consultas con to_tsquery o plainto_tsquery.
- Consistencia: Dentro de la misma transacción puedes actualizar contenido y el vector de búsqueda de forma atómica (columnas generadas o triggers).
- Simplicidad operativa: Una copia de seguridad, una restauración. Un lugar para aplicar políticas de seguridad. Un conjunto de controles de acceso.
- Ideal para “búsqueda dentro de una aplicación”: donde la UX es mayormente “escribe palabras, obtén registros”, con necesidades de ranking ligeras.
Los castigos (donde los costos aparecen)
Postgres te hará pagar por tres pecados: filas sobredimensionadas, alta churn de escrituras y patrones de consulta sin límite.
- Amplificación de escrituras: Actualizar un tsvector y un índice GIN puede ser costoso bajo alto volumen de escrituras.
- Bloat: Actualizaciones/eliminaciones frecuentes en columnas indexadas pueden provocar bloat en tablas e índices GIN, aumentando IO y ralentizando VACUUM.
- Techo de relevancia: Puedes aplicar weighting, diccionarios y configuraciones, pero no obtendrás las herramientas de ES para sinónimos, analizadores por campo a gran escala y experimentación de relevancia sin construirlas.
- Peligros multi-inquilino: Si haces “tenant_id AND tsquery” para miles de tenants, puede que necesites índices parciales, particionamiento o ambos.
Cuándo Postgres es la opción más barata a largo plazo
- Tu corpus de búsqueda está por debajo de decenas de millones de filas y puedes mantener los documentos razonablemente pequeños.
- Puedes restringir consultas (sin comodines al inicio, sin consultas “OR que lo abarquen todo” que exploten).
- Necesitas consistencia fuerte y operaciones simples más que relevancia puntera.
- Tu equipo ya está dimensionado para Postgres, no para búsqueda distribuida.
Cuándo Postgres se vuelve la opción costosa
- El tráfico de búsqueda es lo suficientemente grande como para competir con consultas OLTP y no puedes aislarlo con réplicas.
- Necesitas faceteado/aggregaciones intensas sobre muchos campos con baja latencia.
- La afinación de relevancia se vuelve diferenciador de producto y necesitas ciclos de iteración más rápidos que SQL más código personalizado.
Elasticsearch: qué hace bien y qué castiga
Las partes buenas
Elasticsearch está construido para ser buscado. No tiene reparos en precomputar estructuras de índice para mantener la latencia de consulta baja.
También está diseñado para escalar horizontalmente: agrega nodos, rebalancea shards, sigue funcionando. En la práctica, “seguir funcionando” es donde tus runbooks justifican su salario.
- Características de relevancia y UX: analizadores, filtros de tokens, sinónimos, fuzziness, resaltado, boosting por campo, “more like this”.
- Agregaciones: facetas, histogramas, estimaciones de cardinalidad y consultas tipo analítica que Postgres puede hacer pero a menudo con perfiles de costo distintos.
- Aislamiento: Puedes mantener la carga de búsqueda lejos de tu base de datos transaccional.
- Historia de escala horizontal: Con un dimensionado correcto de shards, ES puede escalar lecturas y almacenamiento a través de nodos limpiamente.
Los castigos
Elasticsearch castiga a los equipos que lo tratan como una caja negra y luego se sorprenden cuando se comporta como un sistema distribuido.
Los shards son donde están los problemas.
- Overhead de shards: Demasiados shards desperdician heap, aumentan handles de archivos, ralentizan las actualizaciones del estado del clúster y prolongan la recuperación.
- Impuesto de reindexación: Errores en mappings o cambios de analizador a menudo requieren una reindexación completa. Eso es tiempo, IO y riesgo operativo.
- Consistencia eventual: Debes gestionar el retraso de ingestión, los intervalos de refresh y los tickets de soporte tipo “¿por qué mi escritura no es buscable aún?”.
- Cronograma de upgrades: Los upgrades rolling son factibles, pero versiones, plugins y cambios incompatibles requieren disciplina.
- Acoplamiento oculto: Tu app, pipeline de ingestión, plantillas de índice, ILM y configuración de clúster se convierten en un único gran organismo.
Broma #2: Elasticsearch es fácil hasta que necesitas que sea fiable—entonces se convierte en un curso de sistemas distribuidos al que no te inscribiste.
Cuándo Elasticsearch es la opción más barata a largo plazo
- La búsqueda es una característica principal del producto y necesitas iterar rápidamente sobre la relevancia.
- Tus patrones de consulta incluyen agregaciones/facetas sobre muchos campos con requisitos de baja latencia.
- Tu conjunto de datos es lo suficientemente grande como para que una capa de búsqueda dedicada evite saturar la base OLTP.
- Tienes (o vas a financiar) la madurez operativa: dimensionado, monitorización, ILM, snapshots y una restauración probada.
Cuándo Elasticsearch se vuelve la opción costosa
- No tienes una historia de ingestión limpia y terminas con escrituras dobles inconsistentes.
- Ejecutas demasiados shards “por si acaso” y pagas heap y CPU para siempre.
- Tratas la reindexación como un evento raro y luego la ejecutas en temporada alta.
Patrones arquitectónicos que te mantienen fuera de problemas
Patrón A: “Solo Postgres” con restricciones sensatas
Haz esto si la búsqueda es secundaria. Usa una columna tsvector generada, un índice GIN y acepta que estás construyendo “buenas búsquedas internas”, no una compañía de búsqueda.
Pon guardrails en tu API para que los usuarios no puedan generar consultas patológicas.
- Usa
websearch_to_tsquerypara la entrada de usuarios (mejor UX, menos sorpresas). - Usa pesos y un número pequeño de campos; no metas un blob JSON entero en un vector a menos que lo quieras así.
- Considera réplicas de lectura para aislar la carga de búsqueda.
Patrón B: Postgres como fuente de la verdad + Elasticsearch como proyección
Este es el patrón común “de adultos”: OLTP en Postgres, búsqueda en ES. El costo es la canalización.
Hazlo solo cuando puedas responder con confianza “¿cómo reconstruimos ES desde Postgres?”.
- Usa una tabla outbox y un consumidor para indexar cambios.
- Diseña operaciones de indexado idempotentes.
- Planifica backfills y evolución de esquema (documentos versionados o aliases de índice).
Patrón C: Búsqueda de dos niveles—predeterminado barato, avanzado costoso
Mantén la mayor parte de la búsqueda en Postgres. Enruta solo consultas avanzadas (facetas, coincidencia difusa, ranking pesado) a Elasticsearch.
Esto reduce la carga de ES y mantiene la canalización más pequeña. También crea “dos fuentes de la verdad para el comportamiento de búsqueda”, así que sé deliberado.
Regla difícil: no dejes que la búsqueda se convierta en tu ruta de escritura
Si las escrituras orientadas al usuario dependen de que ES esté sano, has convertido tu clúster de búsqueda en una dependencia transaccional crítica. Así es como “incidente de búsqueda” se convierte en “incidente de ingresos”.
Mantén la ruta de escritura en Postgres; permite que ES tenga cierto retraso en lugar de bloquear.
Tareas prácticas (comandos), qué significa la salida, y la decisión que tomas
Estos son los tipos de comprobaciones que convierten “creo que está lento” en “está lento porque hacemos X, y podemos arreglarlo con Y”.
Los comandos son ejemplos ejecutables. Sustituye nombres de BD y rutas para que coincidan con tu entorno.
Tarea 1: Comprueba tamaños de índices en Postgres (¿la FTS se está comiendo tu disco?)
cr0x@server:~$ psql -d appdb -c "\di+ public.*"
List of relations
Schema | Name | Type | Owner | Table | Persistence | Access method | Size | Description
--------+--------------------+-------+----------+-------------+-------------+---------------+--------+-------------
public | documents_fts_gin | index | app | documents | permanent | gin | 12 GB |
public | documents_pkey | index | app | documents | permanent | btree | 2 GB |
(2 rows)
Qué significa: Tu índice GIN es el grande. Eso es normal—hasta que deja de serlo.
Decisión: Si el índice FTS domina el disco, evalúa si estás indexando demasiados campos, demasiado texto, o sufres bloat por churn. Si el churn es alto, planifica una estrategia VACUUM/REINDEX.
Tarea 2: Inspecciona indicadores de bloat en tabla e índice (aproximación rápida)
cr0x@server:~$ psql -d appdb -c "SELECT relname, n_live_tup, n_dead_tup, last_vacuum, last_autovacuum FROM pg_stat_user_tables ORDER BY n_dead_tup DESC LIMIT 5;"
relname | n_live_tup | n_dead_tup | last_vacuum | last_autovacuum
------------+------------+------------+--------------------+-------------------------
documents | 42000000 | 9800000 | | 2025-12-29 03:12:01+00
events | 180000000 | 1200000 | 2025-12-28 01:02:11| 2025-12-29 02:44:09+00
(2 rows)
Qué significa: Las tuplas muertas son altas. Autovacuum se está ejecutando, pero puede que no esté al día.
Decisión: Ajusta autovacuum para las tablas calientes o reduce el churn de actualizaciones en columnas de texto indexadas. Si las tuplas muertas siguen creciendo, espera caídas de rendimiento y sorpresas de disco.
Tarea 3: Valida que tu consulta FTS use el índice GIN (EXPLAIN ANALYZE)
cr0x@server:~$ psql -d appdb -c "EXPLAIN (ANALYZE, BUFFERS) SELECT id FROM documents WHERE fts @@ websearch_to_tsquery('english','backup policy');"
QUERY PLAN
---------------------------------------------------------------------------------------------------------------
Bitmap Heap Scan on documents (cost=1234.00..56789.00 rows=1200 width=8) (actual time=45.210..62.118 rows=980 loops=1)
Recheck Cond: (fts @@ websearch_to_tsquery('english'::regconfig, 'backup policy'::text))
Heap Blocks: exact=8412
Buffers: shared hit=120 read=8410
-> Bitmap Index Scan on documents_fts_gin (cost=0.00..1233.70 rows=1200 width=0) (actual time=40.901..40.902 rows=980 loops=1)
Index Cond: (fts @@ websearch_to_tsquery('english'::regconfig, 'backup policy'::text))
Buffers: shared hit=10 read=2100
Planning Time: 0.322 ms
Execution Time: 62.543 ms
(10 rows)
Qué significa: Está usando el índice GIN, pero está leyendo muchos bloques heap desde disco.
Decisión: Si las lecturas dominan, añade RAM (caché), reduce el tamaño del conjunto de resultados con filtros o considera una estrategia de cobertura (almacena menos columnas grandes, usa TOAST con prudencia, considera desnormalizar solo para la ruta de búsqueda).
Tarea 4: Comprueba la efectividad de la caché en Postgres (¿estás limitado por IO?)
cr0x@server:~$ psql -d appdb -c "SELECT datname, blks_hit, blks_read, round(100.0*blks_hit/nullif(blks_hit+blks_read,0),2) AS hit_pct FROM pg_stat_database WHERE datname='appdb';"
datname | blks_hit | blks_read | hit_pct
--------+-----------+-----------+---------
appdb | 982341210 | 92341234 | 91.41
(1 row)
Qué significa: 91% de hit ratio es aceptable, no excelente para una BD que busca baja latencia. Para cargas FTS pesadas, usualmente quieres más.
Decisión: Si el hit ratio baja durante pico de búsqueda, estás pagando en latencia por IO en la nube. Considera más RAM, mejor indexado o mover la carga de lectura fuera del primario (réplica o ES).
Tarea 5: Encuentra las consultas FTS más lentas (pg_stat_statements)
cr0x@server:~$ psql -d appdb -c "SELECT mean_exec_time, calls, rows, query FROM pg_stat_statements WHERE query ILIKE '%tsquery%' ORDER BY mean_exec_time DESC LIMIT 3;"
mean_exec_time | calls | rows | query
----------------+-------+------+-----------------------------------------------------------
812.44 | 1200 | 100 | SELECT ... WHERE fts @@ websearch_to_tsquery($1,$2) ORDER BY ...
244.10 | 8400 | 20 | SELECT ... WHERE fts @@ plainto_tsquery($1,$2) AND tenant_id=$3
(2 rows)
Qué significa: Tus búsquedas más lentas son ahora obvias, no míticas.
Decisión: Añade LIMITs, estrecha filtros, ajusta estrategia de ranking/ordenación o precalcula campos de ranking. Si las consultas lentas son “búsqueda global sobre todo”, considera ES.
Tarea 6: Comprueba configuraciones de autovacuum para una tabla caliente (¿estás vacuuming demasiado tarde?)
cr0x@server:~$ psql -d appdb -c "SELECT relname, reloptions FROM pg_class JOIN pg_namespace n ON n.oid=relnamespace WHERE n.nspname='public' AND relname='documents';"
relname | reloptions
-----------+---------------------------------------------
documents | {autovacuum_vacuum_scale_factor=0.02}
(1 row)
Qué significa: Alguien ya bajó el vacuum scale factor (bien).
Decisión: Si el bloat sigue siendo alto, aumenta los workers de autovacuum, ajusta límites de costo o programa REINDEX/pg_repack periódicos (con control de cambios).
Tarea 7: Mide volumen WAL (¿el indexado de búsqueda está inflando costos de escritura?)
cr0x@server:~$ psql -d appdb -c "SELECT pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(),'0/0')) AS wal_since_boot;"
wal_since_boot
----------------
684 GB
(1 row)
Qué significa: El volumen WAL es enorme; puede ser normal para un sistema ocupado, o signo de churn en texto indexado.
Decisión: Si el crecimiento WAL se correlaciona con actualizaciones de texto, reduce la frecuencia de actualizaciones, evita reescribir documentos completos o mueve la proyección de búsqueda a ES.
Tarea 8: Comprueba la salud del clúster Elasticsearch (triage básico)
cr0x@server:~$ curl -s http://localhost:9200/_cluster/health?pretty
{
"cluster_name" : "search-prod",
"status" : "yellow",
"timed_out" : false,
"number_of_nodes" : 6,
"number_of_data_nodes" : 4,
"active_primary_shards" : 128,
"active_shards" : 256,
"unassigned_shards" : 12
}
Qué significa: Amarillo significa que los primarios están asignados pero las réplicas no. Eso reduce redundancia y puede convertirse en problema de rendimiento durante recuperaciones.
Decisión: Si las réplicas no asignadas persisten, arregla problemas de asignación antes de escalar tráfico. No aceptes amarillo como “aceptable” salvo que tengas una renuncia de riesgo explícita.
Tarea 9: Cuenta shards por nodo (¿estás pagando el impuesto de shards?)
cr0x@server:~$ curl -s http://localhost:9200/_cat/shards?v
index shard prirep state docs store ip node
docs-v7 0 p STARTED 912341 18gb 10.0.0.21 data-1
docs-v7 0 r STARTED 912341 18gb 10.0.0.22 data-2
docs-v7 1 p STARTED 901122 17gb 10.0.0.23 data-3
docs-v7 1 r STARTED 901122 17gb 10.0.0.24 data-4
...output truncated...
Qué significa: Puedes ver tamaños por shard y su colocación.
Decisión: Si ves cientos o miles de shards pequeños (sub-GB), consolida (menos shards primarios por índice, políticas de rollover, reindex). Los shards son un overhead fijo; págalo una vez, no para siempre.
Tarea 10: Inspecciona presión del heap JVM (¿GC es tu latencia oculta?)
cr0x@server:~$ curl -s http://localhost:9200/_nodes/stats/jvm?pretty | head -n 25
{
"cluster_name" : "search-prod",
"nodes" : {
"q1w2e3" : {
"name" : "data-1",
"jvm" : {
"mem" : {
"heap_used_in_bytes" : 21474836480,
"heap_max_in_bytes" : 25769803776
},
"gc" : {
"collectors" : {
"young" : { "collection_count" : 124234, "collection_time_in_millis" : 982341 },
"old" : { "collection_count" : 231, "collection_time_in_millis" : 412341 }
}
}
Qué significa: El uso de heap es ~83% del máximo, y los conteos de GC old no son triviales.
Decisión: Si el heap permanece alto con GC old frecuente, reduce el conteo de shards, ajusta cachés o redimensiona el heap (con cuidado). “Agregar heap” no siempre arregla; puede reducir la caché del SO y empeorar.
Tarea 11: Comprueba presión de indexado vía refresh y merges (¿estás limitado por IO por escrituras?)
cr0x@server:~$ curl -s http://localhost:9200/_nodes/stats/indices/refresh,merges?pretty | head -n 40
{
"nodes" : {
"q1w2e3" : {
"indices" : {
"refresh" : {
"total" : 882341,
"total_time_in_millis" : 9123412
},
"merges" : {
"current" : 12,
"current_docs" : 402341,
"total_time_in_millis" : 22123412
}
Qué significa: Alto tiempo de merges y merges actuales sugiere actividad intensa de escrituras/segmentos, a menudo IO-bound.
Decisión: Si el indexado compite con la latencia de búsqueda, ajusta intervalos de refresh, limita ingest, o separa índices hot ingest. No “optimices” deshabilitando refresh a ciegas; solo moverás el dolor.
Tarea 12: Verifica postura de réplicas y snapshots (¿cuál es tu historia de restauración?)
cr0x@server:~$ curl -s http://localhost:9200/_cat/indices?v
health status index uuid pri rep docs.count store.size
yellow open docs-v7 xYz 16 1 18000000 320gb
Qué significa: Amarillo con rep=1 sugiere que hay réplicas pero no están totalmente asignadas (o faltan nodos).
Decisión: Si dependes de réplicas para HA, haz que sea green o reduce la cuenta de réplicas con un tradeoff explícito. Además asegúrate de tener snapshots y haber probado la restauración; las réplicas no son copias de seguridad.
Tarea 13: Mide retraso de ingestión (¿ES está “atrás” respecto a Postgres?)
cr0x@server:~$ psql -d appdb -c "SELECT now() - max(updated_at) AS db_freshness FROM documents;"
db_freshness
--------------
00:00:03.421
(1 row)
cr0x@server:~$ curl -s http://localhost:9200/docs-v7/_search -H 'Content-Type: application/json' -d '{"size":1,"sort":[{"updated_at":"desc"}],"_source":["updated_at"]}' | jq -r '.hits.hits[0]._source.updated_at'
2025-12-30T10:41:12Z
Qué significa: Si max(updated_at) en Postgres es más reciente que la marca temporal más nueva en ES, tu pipeline está atrasado.
Decisión: Decide si la consistencia eventual es aceptable. Si no, arregla el throughput de ingestión, añade backpressure o direcciona consultas críticas de frescura a Postgres.
Tarea 14: Muestreo de latencia Postgres vs ES (deja de adivinar)
cr0x@server:~$ time psql -d appdb -c "SELECT count(*) FROM documents WHERE fts @@ websearch_to_tsquery('english','error budget');"
count
-------
1242
(1 row)
real 0m0.219s
user 0m0.010s
sys 0m0.005s
cr0x@server:~$ time curl -s http://localhost:9200/docs-v7/_search -H 'Content-Type: application/json' -d '{"query":{"match":{"body":"error budget"}},"size":0}' | jq '.took'
37
real 0m0.061s
user 0m0.012s
sys 0m0.004s
Qué significa: “took” de ES es 37ms y end-to-end ~61ms; Postgres es ~219ms aquí.
Decisión: Si ES es consistentemente más rápido y tu pipeline está sano, ES puede ser más barato en términos de experiencia de usuario—aunque cueste más en operaciones. Si Postgres es “suficientemente rápido”, no compres otro clúster para recortar 150ms.
Tarea 15: Comprueba saturación de IO en Linux (el villano universal)
cr0x@server:~$ iostat -xz 1 3
Linux 6.5.0 (db-1) 12/30/2025 _x86_64_ (16 CPU)
avg-cpu: %user %nice %system %iowait %steal %idle
12.1 0.0 4.3 18.7 0.0 64.9
Device r/s w/s rkB/s wkB/s await svctm %util
nvme0n1 820.0 410.0 81200 32100 14.2 0.9 98.7
Qué significa: %util ~99% y await ~14ms: el almacenamiento está saturado.
Decisión: Antes de reescribir consultas, arregla el IO: volúmenes más rápidos, más memoria, menos bloat, mejor caché o mueve la carga de búsqueda. Discos saturados convierten sistemas “bien” en fábricas de pagers.
Playbook de diagnóstico rápido: encuentra el cuello de botella en minutos
Primero: decide si es cómputo, IO o coordinación
- Comprueba la latencia de extremo a extremo. Métricas de la aplicación: p50/p95/p99 para el endpoint de búsqueda. Si p99 está mal, enfócate ahí.
- Comprueba saturación de IO del sistema.
iostat -xzen nodos DB/búsqueda. Alta await y %util es tu señal de “detener todo”. - Comprueba CPU y memoria. Si CPU está al máximo y IO está bien, estás limitado por cómputo (ranking, merges, GC o demasiado parsing de JSON).
Segundo: aísla dónde se gasta el tiempo
- Postgres: usa
EXPLAIN (ANALYZE, BUFFERS). Si los buffers muestran muchas lecturas, es IO/caché. Si es CPU, verás mucho tiempo con principalmente hits. - Elasticsearch: compara
tookvs tiempo del cliente. Si “took” es bajo pero el tiempo cliente alto, tienes problemas de red, balanceador, TLS o colas de threadpool. - Pipeline: comprueba retraso de ingestión. “La búsqueda está mal” suele ser “ES está atrasado”.
Tercero: elige la palanca más pequeña y segura
- Restringe consultas. Añade LIMIT, añade filtros, elimina características de consulta patológicas.
- Arregla bloat/shards. Vacuum/reindex (Postgres) o consolida shards / afina ILM (ES).
- Añade hardware solo después. RAM ayuda a ambos sistemas, pero es la curita más cara cuando el diseño subyacente está roto.
Tres mini-historias corporativas del campo
Incidente causado por una suposición errónea: “La búsqueda es eventual consistente, pero los usuarios no lo notarán”
Una SaaS B2B de tamaño medio añadió Elasticsearch para mejorar la búsqueda en tickets de clientes. Conectaron un stream CDC desde Postgres a un servicio indexador y lo pusieron en producción.
Las métricas tempranas se veían bien: p95 más bajo, equipo de soporte más contento, menos picos en la base de datos.
Entonces llegó la primera semana de auditoría de cumplimiento. El personal de soporte buscaba tickets recién creados y no los veía. Re-crearon tickets, adjuntaron archivos dos veces y escalaron a ingeniería.
El equipo de ingeniería asumió que el “intervalo de refresh” era un ajuste menor. Lo habían aumentado para reducir la sobrecarga de indexado durante una prueba de carga anterior.
Durante la semana de auditoría, el volumen de tickets se disparó. El lag de ingestión aumentó, el indexador se atrasó y el intervalo de refresh ocultó documentos nuevos por más tiempo.
El modo de fallo no fue “Elasticsearch está caído”. Fue peor: el sistema estaba arriba y devolviendo resultados plausibles pero incorrectos.
La solución fue operativa, no filosófica: apretar SLOs alrededor de la frescura, medir el lag explícitamente y enrut ar vistas “recién creadas” a Postgres por una ventana corta.
También añadieron un banner “la búsqueda puede tardar hasta N segundos en reflejar cambios” para flujos internos—UX aburrida y honesta que evitó trabajo duplicado.
Optimización que salió mal: “Reducimos la carga en Postgres indexándolo todo una vez”
Otra compañía apostó por Postgres FTS para una base de conocimiento. Funcionó hasta que el equipo de producto pidió un ranking más rico.
Un ingeniero decidió precomputar un monstruoso tsvector que incluía título, cuerpo, etiquetas, comentarios y texto extraído de PDFs—todo.
La latencia de consulta mejoró al principio porque había menos joins y menos cómputo por petición. El equipo celebró y siguió adelante.
Dos meses después la base de datos empezó a crecer más rápido de lo esperado. Autovacuum quedó atrás. El IO subió. Las réplicas de lectura comenzaron a retrasarse.
La “optimización” aumentó la amplificación de escrituras. Cualquier edición a una parte del documento reescribía una fila más grande y actualizaba un conjunto de entradas GIN mayor.
El sistema no falló de forma estruendosa. Falló lentamente: p95 más alto, timeouts ocasionales y luego un fin de semana de ajustes de vacuum de emergencia y expansión de disco.
Se recuperaron reduciendo lo que se indexaba, separando el texto extraído que rara vez cambiaba en otra tabla y actualizando su vector solo cuando ese texto cambiaba.
La lección: en Postgres puedes comprar velocidad de consulta con costo de escritura. Si no mides ese costo, te llegará la factura después.
Práctica aburrida pero correcta que salvó el día: “Búsqueda reconstruible y restauraciones probadas”
Un fintech ejecutaba tanto Postgres como Elasticsearch. La búsqueda no era sistema de registro, pero era visible para clientes y adyacente a ingresos.
Su equipo de plataforma insistió en un “día de juego” trimestral de reconstrucción de búsqueda: eliminar un índice, reconstruir desde la fuente, medir tiempo y validar conteos y resultados por muestreo.
A nadie le encantaba. No era glamouroso y nunca produjo una diapositiva para la hoja de ruta.
Lo que produjo fue confianza: sabían cuánto tardaba una reindexación, cuánto costaba y dónde estaban los cuellos de botella.
Un día, un error de mapping se coló en producción y causó que ciertas consultas se comportaran de forma extraña. El equipo no entró en pánico.
Avanzaron a una nueva versión de índice detrás de un alias, reindexaron en segundo plano y cambiaron el tráfico cuando la validación pasó.
Los clientes notaron una pequeña fluctuación de relevancia por un corto periodo, no una caída completa. La compañía no necesitó una sala de guerra con doce personas y un comandante de incidentes agotado.
La práctica aburrida salvó el día, que es el mejor elogio en operaciones.
Errores comunes: síntoma → causa raíz → solución
1) “La búsqueda en Postgres se volvió lenta con el tiempo”
Síntoma: La latencia p95 aumenta mes a mes; el uso de disco sube; autovacuum se ejecuta constantemente.
Causa raíz: Bloat en tabla e índice GIN por alto churn de actualizaciones/eliminaciones en campos de texto indexados; autovacuum no ajustado para tablas grandes.
Solución: Reduce scale factors de autovacuum en tablas calientes, incrementa recursos de vacuum, reduce churn de actualizaciones y considera REINDEX/pg_repack periódicos durante ventanas de mantenimiento.
2) “Elasticsearch es rápido en pruebas pero lento en producción”
Síntoma: Los benchmarks de laboratorio son excelentes; la producción tiene picos de latencia y timeouts.
Causa raíz: Demasiados shards, presión de heap y GC, o merges compitiendo con consultas bajo carga real de ingestión.
Solución: Reduce el número de shards, corrige el dimensionado de shards, ajusta intervalos de refresh y tasa de ingestión, verifica balance heap/page cache y monitoriza backlog de merges.
3) “Faltan datos frescos en los resultados de búsqueda”
Síntoma: Los usuarios no encuentran registros recién creados/actualizados, pero el registro existe en Postgres.
Causa raíz: Lag en la canalización de ingestión, intervalo de refresh demasiado alto o eventos de indexado fallidos sin alertas.
Solución: Instrumenta el lag, alerta sobre backlog, añade reintentos idempotentes y proporciona una estrategia de frescura (leer tus escrituras vía Postgres o sesiones “sticky”).
4) “La búsqueda provocó un incidente en la base de datos”
Síntoma: CPU y IO se disparan en el primario; endpoints transaccionales no relacionados se vuelven lentos.
Causa raíz: El endpoint de búsqueda ejecuta consultas costosas en el primario sin LIMITs; no hay aislamiento de lectura; restricciones de consulta débiles.
Solución: Enruta búsqueda a réplicas de lectura, aplica presupuestos de consulta, añade LIMIT, requiere filtros y cachea consultas comunes.
5) “No podemos cambiar el analizador/mapping sin downtime”
Síntoma: Cualquier cambio de relevancia se convierte en un proyecto de reindexación aterrador.
Causa raíz: Falta versionado de índices/aliases; no se ha ensayado el flujo de reindexación.
Solución: Adopta índices versionados con aliases, automatiza pipelines de reindex y practica reconstrucciones regularmente.
6) “Elasticsearch está green pero las consultas aún hacen timeout”
Síntoma: La salud del clúster es green; los usuarios ven timeouts intermitentes.
Causa raíz: Saturación de threadpool, consultas lentas o overhead de coordinación (p. ej., agregaciones pesadas) a pesar de una asignación sana de shards.
Solución: Identifica consultas lentas, añade timeouts/cortacircuitos en la capa de aplicación, reduce cardinalidad de agregaciones y precalcula campos.
7) “El ranking de Postgres FTS se siente ‘mal’”
Síntoma: Los resultados contienen coincidencias pero el orden parece incorrecto para humanos.
Causa raíz: Faltan pesos, configuración/idioma/regconfig equivocado o mezclar campos sin estructura.
Solución: Usa vectores ponderados por campo, elige regconfig correcto, considera consultas por frase y valida con un conjunto de pruebas curado.
Listas de verificación / plan paso a paso
Paso a paso: elegir Postgres FTS (y mantenerlo barato)
- Define restricciones de consulta. Decide qué pueden buscar los usuarios: campos, operadores, longitud máxima, tokens máximos.
- Modela el vector. Mantenlo intencional: título + cuerpo + algunos metadatos, ponderados.
- Indexa con GIN. Construye el índice GIN y valida con EXPLAIN que se usa.
- Presupuesta el churn. Si actualizas documentos frecuentemente, ajusta autovacuum temprano, no después de que aparezca el bloat.
- Protege el primario. Pone la búsqueda en réplicas cuando el tráfico crezca. El incidente de base de datos más barato es el que no tienes.
- Mide. Rastrea latencia de consultas, lecturas de buffer, tuplas muertas y crecimiento de disco mensualmente.
Paso a paso: elegir Elasticsearch (y evitar trampas caras)
- Diseña la canalización de ingestión primero. Outbox/CDC, reintentos, idempotencia, backfill.
- Define versionado de índices. Usa aliases para reindexar sin downtime.
- Dimensiona shards intencionalmente. Elige tamaños de shard que mantengan la recuperación razonable y el overhead bajo; evita shards pequeños.
- Planifica ILM/retención. Hot/warm/cold si es necesario; al menos ten rollover y políticas de borrado.
- Establece SLOs de frescura. Mide lag, no sensaciones.
- Prueba restauraciones. Los ejercicios de snapshot/restore no son opcionales si la búsqueda importa.
- Escribe runbooks. Fallo de nodo, clúster yellow/red, reindex, cambio de mapping, respuesta a presión de heap.
Paso a paso: enfoque híbrido (la mayoría de equipos deberían empezar aquí)
- Empieza con Postgres FTS para flujos centrales y aprende patrones reales de consulta.
- Instrumenta el comportamiento de búsqueda. Registra consultas (de forma segura), mide latencia, captura casos de “sin resultados”.
- Pasa a Elasticsearch solo por características que realmente lo requieran (facetas, fuzzy, iteración intensiva de relevancia).
- Mantén “leer tus propias escrituras” en Postgres para elementos de UI críticos de frescura.
- Haz que ES sea reconstruible. Si no puedes reconstruirlo, no lo posees; él te posee a ti.
Preguntas frecuentes
1) ¿La búsqueda de texto completo en Postgres es “suficiente” para búsqueda orientada al cliente?
A menudo, sí—especialmente para aplicaciones B2B donde los usuarios saben lo que buscan. Si necesitas facetas intensas, coincidencia difusa, sinónimos a escala y iteración de relevancia rápida, ES suele ser una mejor opción.
2) ¿Cuál es el mayor costo oculto a largo plazo de Elasticsearch?
La canalización y la disciplina operativa: versionado de índices, dimensionado de shards, flujos de reindex, y monitorización de frescura. El clúster es la parte fácil; mantenerlo correcto es el costo.
3) ¿Cuál es el mayor costo oculto a largo plazo de Postgres FTS?
La amplificación de escrituras y el bloat. Si indexas campos de texto grandes y mutables y los actualizas con frecuencia, tu índice GIN y la tabla pueden crecer y enlentecerse de maneras que parecen “degradación misteriosa”.
4) ¿Pueden las réplicas de lectura hacer que la búsqueda en Postgres sea “tan buena como” un clúster de búsqueda?
Las réplicas ayudan a aislar la carga, pero no convierten a Postgres en un motor de relevancia. Hacen escalar lo “barato y estable” más lejos. No te dan los analizadores ni la ergonomía de agregaciones al estilo ES.
5) ¿Es seguro hacer dual-write a Postgres y Elasticsearch?
Puede serlo, pero rara vez es la opción más simple y segura. El dual-write introduce problemas de consistencia durante fallos parciales. Prefiere outbox/CDC para que Postgres siga siendo la fuente autorizada y ES una proyección.
6) ¿Cómo decidir solo por tamaño de datos?
El tamaño de datos es un predictor débil sin patrones de consulta y tasa de actualización. Decenas de millones de filas pueden ser manejables en Postgres FTS con buenas restricciones y hardware. Unos pocos millones de documentos pueden ser problemáticos en ES si creas miles de shards pequeños.
7) ¿Las réplicas de Elasticsearch son una copia de seguridad?
No. Las réplicas protegen contra pérdida de nodo, no contra errores de operador, mappings malos, eliminaciones accidentales o corrupción propagada entre copias. Aún necesitas snapshots y restauraciones probadas.
8) ¿Y usar trigramas en Postgres en lugar de texto completo?
Los trigramas son excelentes para coincidencias de subcadenas y coincidencias algo difusas en campos cortos (nombres, identificadores) y pueden complementar FTS. También pueden ser caros si se usan en blobs de texto grandes. Usa el índice correcto para la pregunta.
9) ¿Qué pasa si necesitamos tanto búsqueda como analíticas?
Si llevas agregaciones intensas y dashboards a ES, estás ejecutando efectivamente una carga analítica en un clúster de búsqueda. Eso puede funcionar, pero aumenta la contención de recursos. Separa preocupaciones si empiezan a competir.
10) ¿Cómo mantenemos los costos a largo plazo predecibles?
Mide los impulsores: bloat/WAL en Postgres y conteo de shards/presión de heap/lag de ingest en ES. Los costos se vuelven predecibles cuando las tasas de crecimiento son visibles y ligadas a planes de capacidad, no a incidentes sorpresa.
Próximos pasos prácticos
Si quieres la respuesta más barata a largo plazo para la mayoría de equipos de producto: empieza con Postgres FTS, hazlo con disciplina y añade Elasticsearch solo cuando puedas nombrar la brecha de funcionalidad en una oración.
“Porque todos usan Elasticsearch” no es una brecha; es un movimiento que limita carreras con una factura mensual.
- Inventaría tus requisitos reales. ¿Facetas? ¿Fuzzy? ¿Sinónimos? ¿Multi-idioma? ¿Restricciones de frescura?
- Ejecuta las tareas anteriores en tu sistema actual. Obtén tamaños de índices, señales de bloat, conteo de shards, presión de heap y lag de ingest.
- Decide qué costo estás pagando: contención de base de datos (Postgres) o pipeline + operaciones de clúster (ES).
- Si eliges Postgres: aplica presupuestos de consulta, aísla con réplicas, ajusta autovacuum y mantiene vectores pequeños y significativos.
- Si eliges Elasticsearch: construye una proyección reconstruible con aliases, dimensionado sensato de shards, SLOs de frescura y procedimientos probados de restore/reindex.
La búsqueda barata a largo plazo no es una marca. Es un conjunto de hábitos: restringe consultas, mide los contadores correctos y trata el mantenimiento de índices como la carga de producción que es.