DanLevy.net

Guía de Búsqueda de Texto en Postgres 2026

Las herramientas de búsqueda que ya están en tu base de datos, y cuándo merece la pena cada una.

La mayoría de los equipos usan una sola herramienta de búsqueda en Postgres. Los que conocen las tres ofrecen mejor búsqueda con menos complejidad — y evitan el costoso desvío hacia un servicio de búsqueda dedicado que aún no necesitaban.

Esta guía cubre todas las opciones nativas de Postgres: qué hace cada una, cuándo es la adecuada y cómo combinarlas.


Las Tres Herramientas

Búsqueda de texto completo (tsvector / índice GIN) es léxica. Tokeniza el texto en lexemas, los reduce a su raíz y compara las consultas contra el índice. “Running” y “runs” colapsan al mismo lexema. Lo mismo ocurre con “dog” y “dogs”. La función de ranking (ts_rank) premia los documentos donde los términos de la consulta aparecen con frecuencia o en posiciones destacadas.

Trigramas (pg_trgm) dividen las cadenas en segmentos superpuestos de 3 caracteres y miden cuántos comparten dos cadenas. “Dan” → " da", "dan", "an ". “Micheal” y “Michael” comparten la mayoría de sus trigramas, por lo que la similitud es alta. Esto hace que pg_trgm sea excelente para coincidencias difusas de nombres, tolerancia a errores tipográficos y autocompletado — el espacio donde FTS funciona mal.

Índices de coincidencia exacta (B-tree, hash) manejan claves primarias, direcciones de correo electrónico, IDs, SKUs y cualquier cosa donde la respuesta sea binaria: coincide o no coincide. Estos no parecen “búsqueda”, pero pertenecen a esta conversación porque el peor patrón es usar búsqueda difusa o semántica para problemas que tienen respuestas correctas.

La elección no se trata de sofisticación. Se trata de adaptar la herramienta a la forma de la consulta.

Mapa de herramientas de búsqueda en PostgresUna comparación de pg_trgm, búsqueda de texto completo, pgvector y búsqueda híbrida según la forma de entrada y la intención de consulta.Elige la primitiva de búsqueda según la forma de entradaLa misma tabla de Postgres puede soportar las cuatro. El truco es adaptar la consulta al texto.Las palabras exactas importanEl significado importaTexto corto / estructuradoProsa larga / fragmentosdifusopg_trgmNombres, direcciones, títulos, errores tipográficos,autocompletado, cadenas parciales.Similitud ortográfica: distancia de escritura.similarpgvectorElementos relacionados, tickets duplicados,recomendaciones a partir de descripciones cortas.Similitud de embeddings: distancia de significado.léxicaBúsqueda de texto completoArtículos, documentación, registros, contenido de soportedonde deben aparecer las palabras de la consulta.Lexemas, stemming, ranking, filtros booleanos.híbridoFTS + pgvectorDocumentación técnica y RAG donde los usuarios preguntancuestiones conceptuales junto con símbolos exactos.Ejecuta ambos, fusiona rangos con RRF.Empieza por la intención de la consulta, luego revisa la forma del texto
Las cuatro primitivas de búsqueda en Postgres mapeadas por intención de consulta (exacta vs. semántica) y forma del texto (estructurado vs. prosa). La misma tabla puede albergar los cuatro índices — la elección es por consulta, no por tabla.

Cuándo Gana la Búsqueda de Texto Completo

Buscar palabras clave en prosa. Artículos de blog, documentación, descripciones de productos, tickets de soporte, documentos legales. FTS fue diseñado para esta forma de contenido: recuperación indexada y clasificada sobre texto en lenguaje natural.

Consultas de usuario basadas en palabras clave. Los usuarios escriben un término de búsqueda, filtran por etiqueta o navegan por palabra clave. FTS maneja esa intención de forma nativa sin necesidad de infraestructura de embeddings.

Resultados clasificados sin dependencias externas. Los índices FTS son rápidos, deterministas y no requieren llamadas a APIs. La señal de relevancia proviene de la frecuencia de términos ponderada por la posición del campo.

Filtrado booleano junto con la búsqueda. FTS se compone naturalmente con tu lógica de consulta existente:

SELECT * FROM posts
WHERE search_vector @@ to_tsquery('english', 'postgres & performance')
AND category = 'tutorial'
AND published_at > NOW() - INTERVAL '6 months';

Configurando FTS

-- La columna generada mantiene el índice actualizado automáticamente
ALTER TABLE posts ADD COLUMN search_vector tsvector
GENERATED ALWAYS AS (
setweight(to_tsvector('english', coalesce(title, '')), 'A') ||
setweight(to_tsvector('english', coalesce(body, '')), 'B')
) STORED;
CREATE INDEX posts_search_idx ON posts USING GIN (search_vector);
-- Consulta
SELECT title, ts_rank(search_vector, query) AS rank
FROM posts, to_tsquery('english', 'postgres & performance') query
WHERE search_vector @@ query
ORDER BY rank DESC
LIMIT 10;

setweight asigna importancia: A (título) tiene más peso que B (cuerpo). Ese es todo el modelo de relevancia para la mayoría de los casos de búsqueda de contenido.

Lo Que FTS No Maneja Bien


Cuándo Ganan los Trigramas (pg_trgm)

pg_trgm cubre ese punto intermedio incómodo que FTS siempre falla.

FTS tokeniza el texto en lexemas y los reduce a su raíz. Para prosa esto es correcto. Para nombres e identificadores cortos a menudo no lo es:

pg_trgm también es independiente del idioma, lo cual importa para nombres de diversos orígenes lingüísticos. FTS requiere configuración de diccionario por idioma.

Búsqueda Difusa de Nombres

CREATE EXTENSION IF NOT EXISTS pg_trgm;
CREATE INDEX users_name_trgm_idx ON users USING GIN (name gin_trgm_ops);
-- Encuentra "Micheal Jordan" al buscar "Michael Jordan"
SELECT id, name, similarity(name, $1) AS score
FROM users
WHERE name % $1 -- operador % = umbral de similitud (por defecto 0.3)
ORDER BY score DESC
LIMIT 10;

El operador % usa pg_trgm.similarity_threshold (por defecto 0.3, rango 0–1). Para búsqueda de nombres, 0.3–0.4 captura errores tipográficos manteniendo el ruido bajo.

Autocompletado, Búsqueda por Prefijo y Contención

-- Coincidencia por prefijo para autocompletado. Un índice GIN de trigramas puede ayudar,
-- pero un índice B-tree puede ser mejor para prefijos anclados a la izquierda.
SELECT name FROM users
WHERE name ILIKE $1 || '%'
ORDER BY name
LIMIT 10;
-- word_similarity para coincidencias parciales dentro de cadenas más largas
-- ("Johnson" dentro de "Andrew Johnson III")
SELECT id, name, word_similarity($1, name) AS score
FROM users
WHERE $1 <% name
ORDER BY score DESC
LIMIT 10;

El índice GIN de trigramas es especialmente útil para consultas de contención ILIKE '%pattern%' y coincidencias tolerantes a errores tipográficos — patrones que sin un índice de trigramas suelen ser escaneos completos de tabla.

Cuándo Usar pg_trgm en Lugar de FTS

EscenarioUsar
Búsqueda de nombres personas/empresas con errores tipográficospg_trgm
Autocompletado / búsqueda por prefijopg_trgm (o FTS con consultas de prefijo)
Cadenas cortas, identificadores, códigospg_trgm
Artículos en prosa, documentación, ticketsFTS
Mensajes de registro para palabras claveFTS
Búsqueda de nombres multilingüepg_trgm (independiente del idioma)

Cuándo Gana la Coincidencia Exacta en SQL

Algunos problemas de “búsqueda” no son búsqueda en absoluto.

“Encontrar el usuario con email dan@example.com” es una comprobación de igualdad. “Encontrar el pedido ORD-12345” es una búsqueda por clave primaria. “Listar publicaciones en la categoría tutorial ordenadas por fecha” es una consulta filtrada. Estos pertenecen a índices B-tree o hash.

Usar FTS o trigramas aquí añade complejidad sin mejorar la corrección — y para identificadores exactos, una coincidencia cercana es peor que ninguna coincidencia.

CREATE INDEX users_email_idx ON users (email);
-- Búsqueda exacta: rápida y sin ambigüedad
SELECT id, name FROM users WHERE email = $1;

La lección más amplia: la búsqueda aproximada para problemas con respuestas correctas es un error de categoría. Devuelve algo — que puede estar confiadamente equivocado.


Combinando Estas Herramientas

Estas herramientas se componen limpiamente. No eliges exactamente una.

FTS + pg_trgm para un cuadro de búsqueda que tolera errores tipográficos en palabras clave:

-- La similitud de trigramas en el título captura errores; ts_rank maneja la relevancia del cuerpo
SELECT id, title,
ts_rank(search_vector, to_tsquery('simple', $1)) AS fts_rank,
similarity(title, $1) AS trgm_score
FROM posts
WHERE search_vector @@ to_tsquery('simple', $1)
OR title % $1
ORDER BY (ts_rank(search_vector, to_tsquery('simple', $1)) + similarity(title, $1)) DESC
LIMIT 10;

FTS + unaccent para contenido internacional:

-- Eliminar marcas diacríticas para que "José" coincida con "Jose"
CREATE EXTENSION IF NOT EXISTS unaccent;
CREATE TEXT SEARCH CONFIGURATION public.simple_unaccent (COPY = pg_catalog.simple);
ALTER TEXT SEARCH CONFIGURATION public.simple_unaccent
ALTER MAPPING FOR hword, hword_part, word
WITH unaccent, simple;
ALTER TABLE posts ADD COLUMN search_vector tsvector;
CREATE TRIGGER posts_search_vector_refresh
BEFORE INSERT OR UPDATE OF title, body ON posts
FOR EACH ROW EXECUTE FUNCTION
tsvector_update_trigger(search_vector, 'public.simple_unaccent', title, body);

unaccent + pg_trgm para búsqueda internacional de nombres:

ALTER TABLE users ADD COLUMN name_search text;
CREATE FUNCTION users_name_search_refresh()
RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN
NEW.name_search := unaccent(coalesce(NEW.name, ''));
RETURN NEW;
END;
$$;
CREATE TRIGGER users_name_search_refresh
BEFORE INSERT OR UPDATE OF name ON users
FOR EACH ROW EXECUTE FUNCTION users_name_search_refresh();
CREATE INDEX users_name_search_trgm_idx
ON users USING GIN (name_search gin_trgm_ops);
SELECT id, name
FROM users
WHERE name_search % unaccent($1)
ORDER BY similarity(name_search, unaccent($1)) DESC
LIMIT 10;

Los ejemplos de triggers evitan usar unaccent() dentro de columnas generadas o expresiones de índice, donde las reglas de inmutabilidad de PostgreSQL importan. Si envuelves unaccent() en tu propia función inmutable, documenta que estás aceptando riesgo de actualización/configuración.


Extensiones Destacables

pg_trgm viene incluido en la mayoría de distribuciones de Postgres pero requiere habilitación explícita. La base para la coincidencia difusa de cadenas en Postgres.

unaccent elimina marcas diacríticas antes de indexar y consultar. Funciona bien tanto con pg_trgm como con FTS para contenido en idiomas europeos. Incluido con Postgres.

pg_bigm extiende el enfoque de trigramas a bigramas (segmentos de 2 caracteres), lo que mejora significativamente los resultados para idiomas CJK (chino, japonés, coreano) donde pg_trgm tiene un rendimiento inferior. Debe instalarse por separado; no viene incluido.

pg_search (de ParadeDB) reemplaza la pila estándar GIN / tsvector con un índice BM25 basado en Tantivy. Esto te da puntuación BM25 (a menudo mejor que ts_rank), coincidencia difusa dentro de consultas FTS, búsqueda facetada e indexación dramáticamente más rápida en tablas grandes. Es una ruta de actualización directa cuando FTS estándar empieza a mostrar límites de ranking o rendimiento.

-- pg_search: Búsqueda de texto completo BM25 con coincidencia difusa
CREATE INDEX posts_bm25_idx ON posts
USING bm25 (id, title, body)
WITH (key_field = 'id', text_fields = '{"title": {}, "body": {}}');
-- Consulta con puntuación BM25 + coincidencia difusa (captura "javascipt")
SELECT id, title, paradedb.score(id) AS rank
FROM posts
WHERE posts @@@ paradedb.fuzzy_phrase(field => 'title', value => 'postgres performnce')
ORDER BY rank DESC
LIMIT 10;

pgvector añade almacenamiento de vectores densos y búsqueda por similitud. Es la herramienta adecuada cuando los usuarios describen lo que quieren en lugar de nombrarlo — búsqueda semántica, RAG, recomendaciones de contenido relacionado, consultas multilingües. Cubierto en profundidad en Semantic Vector Search and Hybrid Strategies.


Tabla de Decisiones

Lo que buscasRecomendado
Artículos en prosa, documentación, ticketsFTS
Nombres de personas/empresas con errores tipográficospg_trgm
Autocompletado, búsqueda por prefijopg_trgm
Códigos cortos, identificadorespg_trgm
Mensajes de registro para palabras claveFTS
Nombres internacionalespg_trgm + unaccent
Contenido grande, mejor rankingpg_search (ParadeDB BM25)
Claves primarias, emails exactos, IDsÍndice B-tree
Fechas, rangos, listas ordenadasÍndice B-tree
Permisos, categorías, filtrosCláusula WHERE normal
Preguntas, paráfrasis, conceptospgvector (ver siguiente artículo)

En caso de duda: cadenas cortas con variación ortográfica → trigramas. Prosa larga para consultas de palabras clave → FTS. Identificadores estructurados → índices normales. Consultas conceptuales o en lenguaje natural → pgvector.


Búsqueda Híbrida: Dos Señales, Un Solo Ranking

Cuando una consulta como "withRetry timeout errors" llega a un cuadro de búsqueda, lleva dos tipos de intención: nombres de símbolos exactos que el usuario conoce (withRetry) y una descripción conceptual (timeout errors). Ninguna primitiva cubre ambas. Ejecutar FTS y búsqueda vectorial en paralelo — y luego fusionar sus listas clasificadas con Reciprocal Rank Fusion — sí lo hace.

RRF puntuación cada resultado como 1 / (60 + rank) en cada lista y suma entre listas. La constante 60 amortigua la ventaja de los primeros puestos, de modo que un resultado que queda segundo en ambas listas puede vencer a uno que gana una lista y falla en la otra. Es crucial que RRF nunca promedie puntuaciones brutas entre métodos — el ranking FTS y la distancia coseno son monedas diferentes y no pueden combinarse aritméticamente.

Búsqueda híbrida con Reciprocal Rank FusionUna consulta se ramifica hacia búsqueda de texto completo y búsqueda vectorial, cada una produce rangos, y Reciprocal Rank Fusion los combina en una única lista de resultados.La búsqueda híbrida son dos señales honestas, luego un ranking fusionadoNo promedies puntuaciones brutas. El ranking FTS y la distancia coseno son monedas diferentes.Consulta del usuario”withRetrytimeout errors”FTS / BM25Símbolos y palabras exactas1. Referencia de API2. Guía de reintentospgvectorVecinos conceptuales1. Fallos de red2. Guía de reintentosFusión RRFDar a cada resultado crédito porsu posición en cada lista.1 / (60 + rank)Resultados finalesEl mejor resultado es donde los términos exactosy el significado semántico coinciden.
Una consulta se ramifica hacia FTS y pgvector en paralelo. Cada una produce su propia lista clasificada. RRF puntúa cada documento por su posición en cada lista y suma las puntuaciones — el resultado hace aflorar los documentos donde ambas señales coinciden.
-- Búsqueda híbrida: FTS + pgvector fusionados con RRF
WITH fts AS (
SELECT id, ts_rank(search_vector, query) AS score,
ROW_NUMBER() OVER (ORDER BY ts_rank(search_vector, query) DESC) AS rank
FROM docs, to_tsquery('english', 'withRetry & timeout') query
WHERE search_vector @@ query
LIMIT 60
),
vec AS (
SELECT id,
ROW_NUMBER() OVER (ORDER BY embedding <=> $embedding) AS rank
FROM docs
ORDER BY embedding <=> $embedding
LIMIT 60
)
SELECT COALESCE(fts.id, vec.id) AS id,
(COALESCE(1.0 / (60 + fts.rank), 0) +
COALESCE(1.0 / (60 + vec.rank), 0)) AS rrf_score
FROM fts FULL JOIN vec ON fts.id = vec.id
ORDER BY rrf_score DESC
LIMIT 10;

El pool de 60 documentos candidatos por rama (LIMIT 60) es un punto de partida común. Amplíalo si la recuperación es baja; redúcelo para velocidad.


Qué Viene Después

La búsqueda de texto en Postgres cubre mucho terreno, pero tiene un techo. Cuando los usuarios describen lo que quieren en lugar de nombrarlo — “algo que me ayude a dormir en un vuelo”, “artículos sobre depuración de confianza como ingeniero nuevo” — tanto la búsqueda léxica como la de trigramas fallan.

Ese es el territorio de los embeddings vectoriales, la búsqueda semántica y las arquitecturas híbridas. Cubierto en Semantic Vector Search and Hybrid Strategies.