Todo pipeline RAG llega tarde o temprano a la misma pregunta: ¿dónde guardo los embeddings? Tu Postgres ya habla vectores gracias a pgvector, y un motor dedicado como Qdrant promete velocidad a escala. Elegir mal te cuesta un segundo sistema innecesario o una migración dolorosa seis meses después.
En este deep-dive te muestro la teoría mínima necesaria, una regla de decisión clara que uso en producción y código verificado para ambos motores, para que puedas correr cualquiera de los dos hoy mismo.
1. El Problema: Los Vectores No Entran en un B-Tree
Los embeddings convierten texto en arreglos largos de floats — cientos o miles de dimensiones. Un índice B-tree clásico no puede responder “dame los 10 vecinos más cercanos” de forma eficiente, por eso los motores vectoriales usan búsqueda aproximada: resignan un poco de recall a cambio de consultas en tiempo casi logarítmico. Si comparás por fuerza bruta más allá de unos miles de documentos, la latencia colapsa.
El Error Más Común
Agregar una base vectorial dedicada el día uno “por las dudas”. Para la mayoría de apps con menos de unos millones de vectores, ese segundo sistema te regala bugs de sincronización, una segunda historia de backups y una segunda factura — por un rendimiento que todavía no necesitás.
El objetivo de esta guía es práctico: entender el único algoritmo que importa, saber exactamente cuándo Postgres con pgvector deja de alcanzar y salir con código ejecutable de Docker, SQL y Python para ambos caminos.
2. Conceptos Mínimos: HNSW, Distancia y Recall
Casi todos los motores vectoriales serios usan el mismo algoritmo, así que lo aprendés una vez y la mayoría de las docs de vendors empiezan a tener sentido. Estas son las únicas tres ideas que necesitás antes de tocar código.
HNSW
índice · búsqueda aproximada
Los grafos Hierarchical Navigable Small World construyen un grafo por capas que se recorre en tiempo casi logarítmico. Es voraz y hambriento de memoria: el grafo quiere vivir en RAM. Tanto pgvector como Qdrant lo usan por defecto.
Métricas de Distancia
coseno · L2 · producto interno
pgvector expone <=> para distancia coseno, <-> para L2 y <#> para producto interno, cada una con su propia clase de operador de índice. Para embeddings de texto, la distancia coseno es el default habitual.
Recall vs Velocidad
tuning · ef_search
La búsqueda aproximada resigna un poco de precisión por mucha velocidad. En consulta lo calibrás — pgvector con SET hnsw.ef_search, Qdrant con sus settings de ef/hnsw — balanceando recall contra latencia.
Búsqueda Filtrada
WHERE · filtros de payload
El RAG en producción casi nunca busca en todo el corpus: primero filtrás por tenant, permisos o fecha. El post-filtrado ingenuo destruye el recall en filtros selectivos, y ahí es donde la elección del motor realmente muerde.
El Párrafo para Recordar
HNSW te da búsqueda aproximada rápida si el grafo entra en RAM. La métrica de distancia debe matchear tu modelo de embeddings — coseno para texto. Y la calidad de búsqueda filtrada, no la velocidad cruda, es lo que separa a los motores en producción.
3. pgvector vs Qdrant: La Regla de Decisión
Ambos son open source y ambos indexan con HNSW. La diferencia es operativa: pgvector agrega vectores al Postgres que ya corrés, Qdrant es un segundo servicio escrito en Rust y construido a propósito para workloads vectoriales a escala.
🟦 Elegí pgvector cuando…
- • Ya corrés Postgres y tenés menos de unos pocos millones de vectores — una migración, cero infraestructura nueva.
- • Los vectores deben ser consistentes con los datos relacionales: misma transacción, mismos backups, joins SQL y filtros WHERE gratis.
- • Tu equipo es chico y cada servicio stateful extra cuesta atención que no tenés.
🟧 Elegí Qdrant cuando…
- • Escalás hacia decenas o cientos de millones de vectores y necesitás sharding más replicación entre nodos.
- • La búsqueda filtrada exigente es el corazón de tu producto — su traversal HNSW consciente de filtros sostiene la latencia donde el post-filtrado se degrada.
- • La memoria es tu restricción: la cuantización escalar, por producto y binaria con rescoring mantiene índices enormes en RAM.
⚖️ Mi Regla Default para 2026
- • ¿Ya estás en Postgres con menos de ~10M de vectores? Usá pgvector en versión reciente — los iterative index scans cerraron gran parte de la vieja brecha de filtrado.
- • ¿Workload puramente vectorial a escala muy grande, o filtros selectivos sobre un corpus enorme? Qdrant se gana su lugar.
- • Migrar después es un ejercicio de carga de datos, no una re-arquitectura — empezá simple y movete cuando las métricas te lo pidan.
Tip: Dejá Abierto el Camino de Migración
Guardá el texto fuente junto a cada vector y mantené portable tu pipeline de embeddings. Así, pasar de pgvector a Qdrant después es una carga masiva más una reescritura de queries, no una reescritura de tu app.
4. Tutorial: Corriendo Ambos en 15 Minutos
Teoría lista — corramos ambos motores. Cada comando está verificado contra las docs oficiales linkeadas en Fuentes, así que copiá con confianza.
Paso 1 — Levantá Qdrant con Docker
Pulleá la imagen oficial y correla. Con la configuración default todos los datos caen en ./qdrant_storage, y el dashboard aparece en localhost:6333/dashboard.
docker pull qdrant/qdrant
docker run -p 6333:6333 -p 6334:6334 \
-v "$(pwd)/qdrant_storage:/qdrant/storage:z" \
qdrant/qdrantPaso 2 — Activá pgvector en Postgres
Una sentencia agrega el tipo vector. Después creá un índice HNSW por cada función de distancia que consultes — coseno abajo — y calibrá el recall en consulta con hnsw.ef_search.
CREATE EXTENSION vector;
CREATE TABLE documents (
id BIGSERIAL PRIMARY KEY,
content TEXT,
embedding vector(1536)
);
CREATE INDEX ON documents
USING hnsw (embedding vector_cosine_ops);
-- más alto = mejor recall, un poco más lento
SET hnsw.ef_search = 100;
SELECT content
FROM documents
ORDER BY embedding <=> $1
LIMIT 5;Paso 3 — Hablale a Qdrant desde Python
Instalá el cliente oficial, apuntalo a tu contenedor local e insertá y consultá documentos. Los helpers add/query embedden el texto por vos vía FastEmbed; la llamada de search muestra una query filtrada por metadata.
pip install 'qdrant-client[fastembed]'
from qdrant_client import QdrantClient
client = QdrantClient(url="http://localhost:6333")
client.add(
collection_name="demo_collection",
documents=["Qdrant has Langchain integrations",
"Qdrant also has Llama Index integrations"],
)
results = client.query(
collection_name="demo_collection",
query_text="vector search integrations",
limit=3,
)
print(results)Ejemplo de Búsqueda Filtrada
Para scoping por permisos o tenant, pasale un Filter con FieldCondition y MatchValue a la llamada unificada query_points (1.10+, verificada en el cliente 1.19.0) — query acepta un vector denso, un vector disperso o una query híbrida con fusión, así un solo método reemplaza al legacy client.search. El filtrado ocurre dentro del traversal del índice en vez de como post-filtro, que es la verdadera ventaja de Qdrant a escala, y with_payload=True devuelve el payload coincidente.
from qdrant_client.http.models import Filter, FieldCondition, MatchValue
results = client.query_points(
collection_name="demo_collection",
query=[0.2, 0.1, 0.9, 0.7],
query_filter=Filter(
must=[FieldCondition(
key="city",
match=MatchValue(value="London")
)]
),
with_payload=True,
limit=5,
).points5. Errores Comunes en Producción
La demo siempre funciona. Estos son los detalles poco glamorosos que muerden cuando llegan el tráfico y los datos reales — presupuéstalos ahora.
RAM Subdimensionada para HNSW
El grafo HNSW quiere vivir en RAM. Subprovisioná memoria y el rendimiento colapsa mucho antes de que aplique cualquier número de benchmark. Dimensioná para el grafo, no solo para las filas.
Dos Sistemas Sin Historia de Sync
Documentos en Postgres más vectores en Qdrant significa que cada escritura toca dos stores. Sin lógica de reintentos o un job de reconciliación terminás con vectores huérfanos o documentos invisibles.
Nunca Tunear ef_search
Los defaults son un punto de partida, no una decisión. Dejar hnsw.ef_search en 40 mientras te quejás del recall es como manejar con el freno de mano puesto y culpar al motor.
Creer Benchmarks de Vendors a Ciegas
Los números publicados discrepan porque miden distintas escalas, recalls y métricas — throughput vs latencia de cola. Reproducí el test con tus propios datos antes de decidir.
Sin Monitoreo de Recall
A medida que agregás, actualizás y borrás vectores, la calidad de retrieval puede degradarse en silencio hasta reindexar. Monitoreá calidad de retrieval, no solo uptime y latencia.
Regla de oro
Empezá con pgvector hasta que duela, y definí “duele” con números: latencia p99 en queries filtradas, headroom de RAM y tiempo de build del índice. Migrá a Qdrant cuando las métricas crucen tu umbral — no cuando lo haga el hype.
Fuentes
- pgvector en GitHub — README: CREATE EXTENSION vector, indexado HNSW
- Docs de Qdrant — quickstart local con Docker y cliente Python
- Cliente python de Qdrant — notebook quickstart (add, query, búsqueda filtrada)
- Release pgvector 0.8.0 — iterative index scans para búsqueda filtrada
- Docs de Supabase — índices HNSW en pgvector
Conclusión
Para la mayoría de apps RAG en 2026, pgvector dentro del Postgres que ya corrés es el default honesto: un solo sistema, transacciones, filtrado SQL y velocidad HNSW que te cubre hasta los millones de vectores. Qdrant es el especialista excelente al que recurrís cuando la escala, la búsqueda filtrada selectiva o la cuantización agresiva exigen de verdad un motor dedicado.
Corré el tutorial de arriba esta semana: levantá ambos, cargá una porción de tu corpus real y medí el p99 filtrado en tus propias queries. Ese solo experimento vale más que diez posts de benchmarks — y si querés ayuda diseñando esa evaluación, escribime.
Tu Plan de Acción Esta Semana
Aprendé
- • HNSW en una sentada
- • Coseno vs L2 vs IP
- • Tradeoff recall-latencia
Construí
- • Qdrant vía Docker
- • Índice HNSW en pgvector
- • add + query en Python
Medí
- • Latencia p99 filtrada
- • Headroom de RAM
- • Recall en tu LIMIT



