La búsqueda de texto completo de PostgreSQL puede cubrir muchas aplicaciones. Usa una regla simple, una consulta inicial y una lista de verificación de índices para saber cuándo añadir un motor de búsqueda.

La mayoría de la gente no pide “búsqueda de texto completo”. Pide una caja de búsqueda que se sienta rápida y encuentre lo que quería en la primera página. Si los resultados son lentos, ruidosos o están ordenados de forma extraña, a los usuarios no les importa si usaste PostgreSQL FTS o un motor separado. Simplemente dejan de confiar en la búsqueda.
Esto es una decisión: mantener la búsqueda dentro de Postgres o añadir un motor de búsqueda dedicado. El objetivo no es una relevancia perfecta. Es una línea base sólida que se puede lanzar rápido, es fácil de operar y es lo bastante buena para cómo se usa realmente tu app.
Para muchas aplicaciones, la búsqueda de texto completo de PostgreSQL es suficiente durante mucho tiempo. Si tienes unos pocos campos de texto (título, descripción, notas), un ranking básico y un filtro o dos (estado, categoría, tenant), Postgres puede manejarlo sin infraestructura adicional. Obtienes menos piezas móviles, backups más simples y menos incidentes de “¿por qué la búsqueda está caída si la app está arriba?”.
“Suficiente” suele significar que puedes alcanzar tres objetivos a la vez:
Un ejemplo concreto: un dashboard SaaS donde los usuarios buscan proyectos por nombre y notas. Si una consulta como “onboarding checklist” devuelve el proyecto correcto entre los 5 primeros, en menos de un segundo, y no estás afinando analizadores o reindexando constantemente, eso es “suficiente”. Cuando no puedes cumplir esos objetivos sin añadir complejidad, ahí es cuando “búsqueda integrada vs motor de búsqueda” se convierte en una pregunta real.
Los equipos suelen describir la búsqueda en términos de funcionalidades, no de resultados. La jugada útil es traducir cada característica en lo que cuesta construirla, ajustarla y mantenerla fiable.
Las primeras peticiones suelen sonar así: tolerancia a errores tipográficos, facetas y filtros, resaltado, ranking “inteligente” y autocompletado. Para una primera versión, separa lo imprescindible de lo agradable. Una caja de búsqueda básica normalmente solo necesita encontrar ítems relevantes, manejar formas comunes de palabras (plural, tiempo verbal), respetar filtros simples y mantenerse rápida a medida que crece la tabla. Ahí es exactamente donde la búsqueda de texto completo de PostgreSQL suele encajar.
Postgres brilla cuando tu contenido vive en campos de texto normales y quieres la búsqueda cerca de tus datos: artículos de ayuda, posts del blog, tickets de soporte, docs internas, títulos y descripciones de producto, o notas en registros de clientes. Estos son en su mayoría problemas de “encuéntrame el registro correcto”, no de “construir un producto de búsqueda”.
Los extras son donde se cuela la complejidad. La tolerancia a errores tipográficos y un autocompletado rico suelen empujarte hacia herramientas adicionales. Las facetas son posibles en Postgres, pero si quieres muchas facetas, análisis profundos y conteos instantáneos sobre datasets enormes, un motor dedicado empieza a verse más atractivo.
El coste oculto rara vez es la licencia. Es el segundo sistema. Una vez que añades un motor de búsqueda, también añades sincronización de datos y backfills (y los bugs que crean), monitoreo y upgrades, trabajo de soporte del tipo “¿por qué la búsqueda muestra datos antiguos?” y dos juegos de perillas de relevancia.
Si dudas, empieza con Postgres, lanza algo simple y solo añade otro motor cuando un requisito claro no pueda ser satisfecho.
Usa una regla de tres comprobaciones. Si pasas las tres, quédate con PostgreSQL full-text search. Si fallas una de forma notable, considera un motor de búsqueda dedicado.
Necesidades de relevancia: ¿son aceptables resultados “suficientemente buenos” o necesitas un ranking casi perfecto en muchos casos límite (typos, sinónimos, “la gente también buscó”, resultados personalizados)? Si toleras ordenaciones imperfectas ocasionales, Postgres suele funcionar.
Volumen de consultas y latencia: ¿cuántas búsquedas por segundo esperas en pico y cuál es tu presupuesto real de latencia? Si la búsqueda es una pequeña porción del tráfico y puedes mantener las consultas rápidas con índices apropiados, Postgres está bien. Si la búsqueda se convierte en una de las cargas principales y compite con lecturas/escrituras críticas, eso es una señal de alarma.
Complejidad: ¿buscas en uno o dos campos de texto, o combinas muchas señales (tags, filtros, decaimiento temporal, popularidad, permisos) y múltiples idiomas? Cuanto más complejo sea el lógico, más fricción sentirás dentro de SQL.
Un punto de partida seguro es simple: lanza una base en Postgres, registra consultas lentas y búsquedas sin resultados, y decide después. Muchas apps nunca lo superan y evitas ejecutar y sincronizar un segundo sistema demasiado pronto.
Señales que normalmente apuntan a un motor dedicado:
Señales que indican que puedes quedarte en Postgres:
PostgreSQL full-text search es una forma integrada de convertir texto en algo que la base de datos puede buscar rápidamente, sin escanear cada fila. Funciona mejor cuando tu contenido ya vive en Postgres y quieres búsqueda rápida y decente con operaciones previsibles.
Hay tres piezas que vale la pena conocer:
ts_rank (o ts_rank_cd) para poner las filas más relevantes primero.La configuración de idioma importa porque cambia cómo Postgres trata las palabras. Con la configuración correcta, “running” y “run” pueden coincidir (stemming) y palabras vacías comunes se pueden ignorar (stop words). Con la configuración equivocada, la búsqueda puede parecer rota porque el lenguaje del usuario ya no coincide con lo indexado.
El emparejamiento por prefijo es la función que la gente busca cuando quiere comportamiento tipo “typeahead”, como hacer coincidir “dev” con “developer”. En Postgres FTS eso se hace típicamente con un operador de prefijo (por ejemplo, term:*). Puede mejorar la calidad percibida, pero suele aumentar el trabajo por consulta, así que trátalo como una mejora opcional, no por defecto.
Lo que Postgres no pretende ser: una plataforma de búsqueda completa con todas las funciones. Si necesitas corrección de ortografía difusa, autocompletado avanzado, learning-to-rank, analizadores complejos por campo o indexado distribuido en muchos nodos, estás fuera de la zona de confort integrada. Para muchas apps, sin embargo, PostgreSQL FTS te da la mayor parte de lo que los usuarios esperan con muchas menos piezas móviles.
Aquí tienes una forma pequeña y realista para contenido que quieras buscar:
-- Minimal example table
CREATE TABLE articles (
id bigserial PRIMARY KEY,
title text NOT NULL,
body text NOT NULL,
updated_at timestamptz NOT NULL DEFAULT now()
);
Una buena base para PostgreSQL full-text search es: construir una consulta a partir de lo que escribió el usuario, filtrar filas primero (cuando puedas), y luego rankear las coincidencias restantes.
-- $1 = user search text, $2 = limit, $3 = offset
WITH q AS (
SELECT websearch_to_tsquery('english', $1) AS query
)
SELECT
a.id,
a.title,
a.updated_at,
ts_rank_cd(
setweight(to_tsvector('english', coalesce(a.title, '')), 'A') ||
setweight(to_tsvector('english', coalesce(a.body, '')), 'B'),
q.query
) AS rank
FROM articles a
CROSS JOIN q
WHERE
a.updated_at \u003e= now() - interval '2 years' -- example safe filter
AND (
setweight(to_tsvector('english', coalesce(a.title, '')), 'A') ||
setweight(to_tsvector('english', coalesce(a.body, '')), 'B')
) @@ q.query
ORDER BY rank DESC, a.updated_at DESC, a.id DESC
LIMIT $2 OFFSET $3;
Algunos detalles que ahorran tiempo más adelante:
WHERE antes de rankear (status, tenant_id, rangos de fecha). Rankeas menos filas, así se mantiene rápido.ORDER BY (como updated_at, luego id). Esto mantiene la paginación estable cuando muchos resultados tienen la misma puntuación.websearch_to_tsquery para la entrada del usuario. Maneja comillas y operadores simples de forma que la gente espera.Cuando esta base funcione, mueve la expresión to_tsvector(...) a una columna almacenada. Eso evita recalcularla en cada consulta y facilita el indexado.
La mayoría de las historias de “PostgreSQL FTS es lento” se reducen a una cosa: la base de datos está construyendo el documento de búsqueda en cada consulta. Arregla eso primero almacenando un tsvector preconstruido e indexándolo.
tsvector: columna generada o trigger?Una columna generada es la opción más simple cuando tu documento de búsqueda se construye a partir de columnas en la misma fila. Se mantiene correcta automáticamente y es difícil olvidarla durante las actualizaciones.
Usa un tsvector mantenido por trigger cuando el documento depende de tablas relacionadas (por ejemplo, combinando una fila de producto con el nombre de su categoría), o cuando quieres lógica custom que no es fácil expresar como una sola expresión generada. Los triggers añaden piezas móviles, así que mantenlos pequeños y pruébalos.
Crea un índice GIN en la columna tsvector. Ese es el estándar que hace que PostgreSQL FTS se sienta instantáneo para la búsqueda típica de apps.
Una configuración que funciona para muchas apps:
tsvector en la misma tabla que las filas que buscas con más frecuencia.tsvector.@@ contra el tsvector almacenado, no to_tsvector(...) calculado al vuelo.VACUUM (ANALYZE) después de grandes backfills para que el planner entienda el nuevo índice.Mantener el vector en la misma tabla suele ser más rápido y simple. Una tabla de búsqueda separada puede tener sentido si la tabla base recibe muchas escrituras, o si indexas un documento combinado que abarca muchas tablas y quieres actualizarlo en tu propio horario.
Los índices parciales ayudan cuando solo buscas un subconjunto de filas, como status = 'active', un único tenant en una app multi-tenant o un idioma específico. Reducen el tamaño del índice y pueden acelerar búsquedas, pero solo si tus consultas siempre incluyen el mismo filtro.
Puedes obtener resultados sorprendentemente buenos con PostgreSQL FTS si mantienes las reglas de relevancia simples y previsibles.
La victoria más fácil es ponderar campos: las coincidencias en un título deben contar más que las del cuerpo. Construye un tsvector combinado donde el título tenga mayor peso que la descripción y luego rankea con ts_rank o ts_rank_cd.
Si necesitas que items “frescos” o “populares” suban, hazlo con cuidado. Un pequeño impulso está bien, pero no dejes que anule la relevancia del texto. Un patrón práctico es: rankear por texto primero, luego desempatar con recencia, o añadir un bonus limitado para que un item nuevo irrelevante no venza a una coincidencia perfecta antigua.
Sinónimos y coincidencias por frase son donde las expectativas suelen divergir. Los sinónimos no son automáticos; solo los obtienes si añades un thesaurus o diccionario custom, o expandes los términos de la consulta tú mismo (por ejemplo, tratar “auth” como “authentication”). La coincidencia por frase tampoco es la opción por defecto: las consultas simples coinciden con palabras en cualquier lugar, no con “esta frase exacta”. Si los usuarios escriben frases entrecomilladas o preguntas largas, considera phraseto_tsquery o websearch_to_tsquery para ajustar mejor cómo la gente busca.
Contenido en varios idiomas necesita una decisión. Si conoces el idioma por documento, almacénalo y genera el tsvector con la configuración adecuada (English, Russian, etc.). Si no lo sabes, una alternativa segura es indexar con la configuración simple (sin stemming), o mantener dos vectores: uno específico por idioma cuando se conoce y otro simple para todo.
Para validar la relevancia, mantenlo pequeño y concreto:
Esto suele ser suficiente para búsquedas en cajas de apps como “templates”, “docs” o “projects”.
La mayoría de las historias de “PostgreSQL FTS es lento o irrelevante” vienen de unos pocos errores evitables. Arreglarlos suele ser más simple que añadir un nuevo sistema de búsqueda.
Una trampa común es tratar tsvector como un valor calculado que se mantiene correcto por sí mismo. Si almacenas tsvector en una columna pero no lo actualizas en cada insert y update, los resultados parecerán aleatorios porque el índice ya no coincide con el texto. Si calculas to_tsvector(...) al vuelo dentro de la consulta, los resultados pueden ser correctos pero más lentos, y podrías perder el beneficio de un índice dedicado.
Otra forma fácil de perjudicar el rendimiento es rankear antes de reducir el conjunto candidato. ts_rank es útil, pero normalmente debe ejecutarse después de que Postgres haya usado el índice para encontrar filas coincidentes. Si calculas el ranking para una gran porción de la tabla (o te unes a otras tablas primero), puedes convertir una búsqueda rápida en un escaneo de tabla.
La gente también espera que “contains” funcione como LIKE '%term%'. Los comodines iniciales no encajan bien con FTS porque FTS se basa en palabras (lexemas), no en substrings arbitrarios. Si necesitas búsqueda por substring para códigos de producto o IDs parciales, usa otra herramienta para ese caso (por ejemplo, indexado trigram) en lugar de culpar a FTS.
Los problemas de rendimiento a menudo vienen del manejo de resultados, no de las coincidencias. Dos patrones a vigilar:
OFFSET grande, que hace que Postgres omita más y más filas a medida que paginas.Los problemas operativos también importan. El bloat de índices puede acumularse tras muchas actualizaciones y reindexar puede ser caro si esperas hasta que las cosas ya son dolorosas. Mide tiempos reales de consulta (y revisa EXPLAIN ANALYZE) antes y después de cambios. Sin números, es fácil “arreglar” PostgreSQL FTS empeorándolo de otra manera.
Antes de culpar a PostgreSQL FTS, ejecuta estas comprobaciones. La mayoría de los bugs de “Postgres search es lento o irrelevante” vienen de lo básico faltante, no de la característica en sí.
Construye un tsvector real: almacénalo en una columna generada o mantenida, usa la configuración de idioma correcta (english, simple, etc.) y aplica pesos si mezclas campos (title > subtitle > body).
Normaliza lo que indexas: mantén fuera del tsvector campos ruidosos (IDs, boilerplate, texto de navegación) y recorta blobs enormes si los usuarios nunca los buscan.
Crea el índice correcto: añade un índice GIN en la columna tsvector y confirma que se usa en EXPLAIN. Si solo un subconjunto es searchable (por ejemplo status = 'published'), un índice parcial puede reducir tamaño y acelerar lecturas.
Mantén tablas saludables: las tuplas muertas pueden ralentizar escaneos de índice. El vacuum regular importa, especialmente en contenido que se actualiza con frecuencia.
Ten un plan de reindex: grandes migraciones o índices hinchados a veces necesitan una ventana controlada de reindex.
Una vez que los datos y el índice estén bien, céntrate en la forma de la consulta. PostgreSQL FTS es rápido cuando puede reducir el conjunto candidato temprano.
Filtra primero, luego rankea: aplica filtros estrictos (tenant, idioma, published, categoría) antes de rankear. Rankear miles de filas que luego descartas es trabajo desperdiciado.
Usa orden estable: ordena por rank y luego un desempate como updated_at o id para que los resultados no salten entre actualizaciones.
Evita “la consulta lo hace todo”: si necesitas matching difuso o tolerancia a typos, hazlo intencionalmente (y mide). No fuerces scans secuenciales por accidente.
Prueba consultas reales: recoge las 20 búsquedas principales, verifica relevancia a mano y mantén una pequeña lista de resultados esperados para detectar regresiones.
Vigila caminos lentos: registra consultas lentas, revisa EXPLAIN (ANALYZE, BUFFERS) y monitoriza tamaño de índice y tasa de aciertos en caché para detectar cuando el crecimiento cambia el comportamiento.
Un centro de ayuda SaaS es un buen sitio para empezar porque el objetivo es simple: ayudar a la gente a encontrar el artículo que responde su pregunta. Tienes unos pocos miles de artículos, cada uno con título, resumen corto y cuerpo. La mayoría de visitantes escribe 2 a 5 palabras como “reset password” o “billing invoice”.
Con PostgreSQL FTS, esto puede sentirse resuelto sorprendentemente rápido. Almacenas un tsvector para los campos combinados, añades un índice GIN y rankeas por relevancia. El éxito se ve así: resultados en menos de 100 ms, los 3 primeros resultados suelen ser correctos y no necesitas vigilar el sistema constantemente.
Luego el producto crece. Soporte quiere filtrar por área del producto, plataforma (web, iOS, Android) y plan (free, pro, business). Redactores quieren sinónimos, “quisiste decir” y mejor manejo de typos. Marketing quiere analíticas como “búsquedas principales sin resultados”. El tráfico sube y la búsqueda se convierte en uno de los endpoints más concurridos.
Esas son señales de que un motor dedicado puede valer la pena:
Un camino de migración práctico es mantener Postgres como fuente de la verdad, incluso después de añadir un motor. Empieza registrando consultas y casos sin resultado, luego ejecuta un job async que copie solo los campos buscables al nuevo índice. Ejecuta ambos en paralelo por un tiempo y cambia gradualmente, en lugar de apostar todo el día uno.
Si tu búsqueda es mayormente “encontrar documentos que contengan estas palabras” y tu dataset no es masivo, PostgreSQL FTS suele ser suficiente. Empieza allí, ponlo a funcionar y solo añade un motor dedicado cuando puedas nombrar la característica que falta o el dolor de escala.
Un resumen útil:
tsvector, añadir un índice GIN y tus necesidades de ranking sean básicas.Un siguiente paso práctico: implementa la consulta inicial y el índice de las secciones anteriores, luego registra unas pocas métricas simples durante una semana. Rastrear p95 de tiempo de consulta, consultas lentas y una señal de éxito aproximada como “búsqueda -> click -> sin rebote inmediato” (incluso un contador básico de eventos ayuda). Verás rápido si necesitas mejor ranking o solo mejor UX (filtros, resaltado, snippets mejores).
Si quieres moverte rápido en el lado de la app, Koder.ai (koder.ai) puede ser útil para prototipar la UI y la API de búsqueda vía chat, luego iterar de forma segura usando snapshots y rollback mientras mides lo que realmente hacen los usuarios.
PostgreSQL full-text search es “suficiente” cuando puedes cumplir tres cosas a la vez:
Si puedes lograr esto con un tsvector almacenado + un índice GIN, normalmente estás en una buena situación.
Empieza por defecto con la búsqueda de texto completo de PostgreSQL. Se entrega más rápido, mantiene tus datos y la búsqueda en un solo lugar y evita construir y mantener una canalización de indexado separada.
Pasa a un motor dedicado cuando tengas un requisito claro que Postgres no pueda cubrir bien (tolerancia a errores de escritura de alta calidad, autocompletado avanzado, facetas intensas o una carga de búsqueda que compita con el trabajo principal de la base de datos).
Una regla simple: quédate en Postgres si superas estas tres comprobaciones:
Si fallas una de forma notable (especialmente funciones de relevancia como typos/autocomplete, o tráfico de búsqueda alto), considera un motor dedicado.
Usa Postgres FTS cuando tu búsqueda es sobre todo “encontrar el registro correcto” en unos pocos campos como título/cuerpo/notas, con filtros simples (tenant, estado, categoría).
Es ideal para centros de ayuda, documentación interna, tickets, búsqueda de artículos/blog y dashboards SaaS donde se busca por nombres de proyectos y notas.
Una buena consulta base suele:
websearch_to_tsquery.Almacena un tsvector preconstruido y añade un índice GIN. Así evitas recomputar to_tsvector(...) en cada petición.
Configuración práctica:
Usa una columna generada cuando el documento de búsqueda se construye a partir de columnas de la misma fila (simple y difícil de romper).
Usa una columna mantenida por trigger cuando el texto depende de tablas relacionadas o lógica personalizada.
Elección por defecto: columna generada primero; triggers solo si realmente necesitas composición cross-table.
Comienza con reglas de relevancia previsibles:
Valida con una lista pequeña de consultas reales de usuarios y los resultados esperados en la parte superior.
Postgres FTS trabaja por palabras, no por substrings, por eso no actúa como LIKE '%term%'. Si necesitas búsqueda por substring (IDs, códigos, fragmentos), trátalo por separado (a menudo con índices trigram) en lugar de forzar FTS.
Señales de que superarás Postgres FTS:
Un camino práctico: mantén Postgres como fuente de la verdad y añade indexado asíncrono cuando el requisito esté claro.
@@ contra un tsvector almacenado.ts_rank/ts_rank_cd más un desempate estable como updated_at, id.Esto mantiene los resultados relevantes, rápidos y con paginación estable.
tsvector en la misma tabla que consultas.tsvector_column @@ tsquery.Este es el arreglo más común cuando la búsqueda se siente lenta.