PostgreSQL + Qdrant en la etapa de ingestión: patrones de sincronización, microservicios y cómo encaja todo sin romperse
TL;DR
PostgreSQL es la fuente de verdad transaccional de la mayoría de las empresas; Qdrant es el motor de búsqueda vectorial que más equipos eligen cuando pgvector se queda corto. Combinarlos no es trivial: tu modelo de dominio vive en Postgres con ACID, las relaciones, las constraints, los triggers; los embeddings viven en Qdrant con HNSW filterable, quantization escalar, multivectors, sparse-dense hybrid search. Mantener los dos sincronizados es el problema operacional número uno que el campo LLMOps ha codificado en 2026 con tres patrones canónicos: dual-write (simple, frágil, válido para prototipos), transactional outbox + CDC con Debezium (la opción “correcta” para producción seria) y event-driven directo a Kafka (cuando el evento es el ciudadano de primera y la DB es proyección). La elección de Qdrant sobre pgvector se justifica con números concretos —filtered search 6ms vs 29ms en 500K vectores, 65% menos memoria con scalar quantization, HNSW filterable que no se hunde con metadata, escalabilidad horizontal—. El precio es operacional: un servicio stateful adicional que mantener, snapshots que gestionar, gRPC que asegurar. Este post entra en detalle en cómo se sitúa PostgreSQL + Qdrant en la etapa Data del pipeline LLMOps que dibujamos en el post anterior, qué microservicios participan, cómo se sincronizan, cómo se observan y dónde están las trampas que se ven una y otra vez en producción.
Este es el cuarto post de la serie MLOps para LLMs y el primero que aplica el patrón “estás aquí” sobre el mini-mapa que definimos en el post anterior sobre el pipeline de seis etapas. Aquí estamos plenamente en la primera etapa: Data.
Estás aquí: etapa Data del pipeline
La pregunta que define la arquitectura: ¿una DB o dos?
Antes de hablar de patrones, vamos a la decisión que marca el resto del diseño. Tienes datos transaccionales en PostgreSQL —usuarios, productos, documentos, conversaciones— y necesitas búsqueda vectorial sobre ellos para RAG. Dos respuestas razonables:
Opción A — pgvector dentro de Postgres: añades la extensión vector, una columna embedding vector(1536), un índice HNSW. Cero arquitectura nueva, cero servicio nuevo. Tu DBA sigue siendo el DBA. Una sola DB, ACID con tus tablas relacionales, JOINs entre embedding y metadata. Una sola fuente de verdad.
Opción B — Qdrant separado: dejas Postgres como está y montas Qdrant como servicio stateful aparte. Tu microservicio escribe a las dos. Dos fuentes parciales que mantener en sync.
La elección depende de números. Vamos a ellos.
Cuándo pgvector basta y cuándo no
Los benchmarks 2026 son consistentes. La regla del pulgar:
- Hasta ~1M vectores: pgvector es excelente. Setup en minutos, cero overhead operacional, queries ACID con JOINs naturales.
- 1-10M vectores: pgvector funciona pero ya empiezas a sufrir. Index builds tardan, recall baja bajo carga, memoria sube linealmente.
- >10M vectores: pgvector se hunde a no ser que tunes mucho. Index build pasa de horas; query p95 deriva por encima de 200ms.
- >50M vectores: pgvector deja de ser opción razonable en single-node.
Qdrant escala a billones con sharding. Numéricamente, en 500K vectores con 3 condiciones de payload:
- Qdrant: 6 ms p95 (filtered HNSW).
- pgvector: 29 ms p95 (heap scans rompen la localidad del índice).
Y en memoria: Qdrant con scalar quantization usa 65% menos RAM que pgvector con IVFFlat sobre el mismo dataset. Para 50M vectores de 1024 dimensiones eso son decenas de GB de diferencia. Multiplicado por tres réplicas para HA, es un nodo entero menos.
Pero pgvector tiene una ventaja decisiva en proyectos pequeños y medianos: es gratis, embebido, y lo opera tu DBA. La fricción de adoptar Qdrant —un servicio stateful nuevo, gRPC, snapshots, observabilidad propia— solo se justifica cuando el dolor de pgvector es real, no anticipado.
El veredicto operativo 2026
- Empieza con pgvector si tu corpus es <5M vectores y tu equipo es pequeño.
- Migra a Qdrant cuando uno de los tres siguientes signos aparezca: latencia p95 inaceptable, presión de memoria sobre el cluster Postgres principal, necesidad de hybrid search (sparse + dense) avanzada.
- No migres anticipadamente: el coste operacional de Qdrant es real; sufre cuando lo necesitas, no por si acaso.
Lo importante: diseña la capa de acceso a embeddings con una abstracción (un VectorStore interface en tu código) para que cambiar de pgvector a Qdrant sea cambiar la implementación, no reescribir la app.
Qdrant en detalle: lo que ofrece sobre pgvector
Si decides que Qdrant es la opción, vale la pena entender qué te da más allá del rendimiento bruto. Cinco features dominantes:
1. Filterable HNSW
El HNSW filterable es lo que más se nota en producción. En pgvector, filtrar por metadata (WHERE category = 'tech' AND date > '2026-01-01') hace que el índice HNSW pierda eficiencia: la búsqueda tiene que recorrer más nodos para encontrar los que cumplen el filtro. En Qdrant, el HNSW está construido para podar la búsqueda con filtros dentro del propio recorrido del grafo, sin escapar a heap scans externos. Para queries con filtros densos (lo normal en RAG con permisos multi-tenant), la diferencia es brutal.
2. Multivector y late-interaction (ColBERT)
Qdrant permite almacenar una matriz de vectores por punto, no solo un vector. Esto soporta nativamente modelos late-interaction como ColBERT, que codifican un vector por token y comparan con MaxSim. La calidad de retrieval con ColBERT-style multivectors es típicamente 5-15% mejor que single-vector en cargas semánticas complejas.
3. Sparse + dense hybrid search
Hybrid search combina un vector denso (semántico, eg embeddings de SentenceTransformers) con un vector disperso (lexical, eg SPLADE, BM25 reproducido como sparse). El denso captura “esto es semánticamente similar”; el disperso captura “esta palabra concreta aparece”. Combinados —tipicamente con reciprocal rank fusion o weighted combination— recuperan tanto la similitud semántica como los matches exactos de keyword. Es el patrón de retrieval que más calidad da en 2026 y Qdrant lo trae nativo desde la versión 1.10.
4. Quantization escalar y binaria
Para cargas grandes, Qdrant ofrece scalar quantization (int8 en lugar de float32, 4× menos memoria con pérdida marginal de recall) y binary quantization (1 bit por dimensión, 32× menos memoria con pérdida moderada que se recupera con rescoring de los top-K). En el roadmap 2026 está la 4-bit quantization, que será un punto medio.
5. Named vectors
Una colección Qdrant puede tener múltiples espacios vectoriales por punto, llamados named vectors. Caso típico: el mismo documento se indexa con un vector denso (text-embedding-3-small) y un vector sparse (SPLADE), bajo el mismo point_id. Las queries pueden buscar en el vector concreto que les interesa.
A esto se suma el roadmap 2026: 4-bit quantization, read-write segregation, expanded inference capabilities (Qdrant puede embeddar texto él mismo, sin un servicio externo).
La arquitectura de microservicios: dónde encaja cada pieza
Aquí está lo que el usuario que monta esto en producción tiene que diseñar. La arquitectura típica que se ha estabilizado tiene cinco microservicios que tocan estas piezas, cada uno con su responsabilidad clara:
Vamos a cada microservicio.
1. Domain Service
Responsabilidad: la lógica de negocio. CRUD de documentos, productos, conversaciones. Endpoints REST/gRPC para el front-end o para otros servicios. Solo conoce PostgreSQL como sistema de persistencia; no sabe nada de Qdrant.
Esto es importante por diseño: el domain service no debería tener nunca una referencia directa a Qdrant. Si la tiene, ya estás en el antipattern del dual-write. El domain service escribe a Postgres en una transacción ACID; el resto del pipeline se entera vía eventos.
2. PostgreSQL
Responsabilidad: source of truth transaccional. Schemas relacionales, constraints, triggers, ACID. Y la outbox table que veremos en breve, que es lo que va a permitir la sincronización fiable.
Patrón típico de despliegue: HA con Patroni + repmgr + PgBouncer para connection pooling, replicas de lectura para offloading.
3. Kafka
Responsabilidad: el bus de eventos. Recibe los cambios capturados por CDC (Debezium leyendo el WAL de Postgres o leyendo la outbox table) y los pone disponibles para los consumidores. Cubierto en profundidad en RAG sobre Kafka.
Topics típicos:
documents.changes: eventos crudos de cambio (insert/update/delete).documents.embedded: eventos con embedding ya calculado.
4. Embedding Service
Responsabilidad: consumir eventos de cambio, calcular embeddings, publicar al topic embedded. Esta es la pieza que más coste consume si usas embeddings vía API (OpenAI, Cohere, Voyage AI).
Estructura típica:
- Consumer Kafka con consumer group propio.
- Batching de eventos para llamadas embedding (mucho más eficiente que uno a uno).
- Llamadas paralelas con concurrency control.
- Retry con exponential backoff ante rate limits.
- Métricas exportadas (latencia, throughput, errores, coste).
- Idempotencia (key del topic = doc_id, mismo doc no se re-embedea sin necesidad).
Patrón de optimización clave: deduplicate por hash de contenido. Si el documento se actualiza pero el texto no cambió (solo metadata), no merece la pena re-embedear. Hash + cache de embeddings ahorra 30-70% del coste en cargas reales.
5. Indexing Worker
Responsabilidad: consumir el topic embedded y hacer upsert a Qdrant. Es la pieza más simple de toda la arquitectura: lee del topic, escribe al vector store. Pero importante para la fiabilidad: tiene que ser idempotente (el mismo doc_id puede llegar varias veces si el consumer reinicia) y resiliente (si Qdrant está caído, reintentar sin perder eventos).
Estructura: Consumer Kafka con commit manual de offset solo después de confirmación del upsert. Si Qdrant falla, el offset no se commitea y el evento se reprocesa.
6. Retrieval Service
Responsabilidad: la cara que el LLM Service ve. Recibe una query del usuario, hace búsqueda en Qdrant (vector + filtros + reranker), enriquece los resultados con metadata fresca de PostgreSQL si hace falta, y devuelve top-K documentos con su contenido para que el LLM construya su prompt.
Es el único servicio que consulta Qdrant. Esto centraliza la lógica de retrieval: cuando quieras añadir reranking, hybrid search, query rewriting, lo haces aquí sin tocar el resto.
7. LLM Service
Responsabilidad: la generación. Recibe del Retrieval Service el contexto + query, construye el prompt, llama al LLM (self-hosted vLLM o API externa vía LiteLLM), devuelve la respuesta. Lo cubrimos en posts anteriores; no es el foco aquí.
El problema del dual-write y los tres patrones de solución
Aquí está la pieza arquitectónica más importante del post. El problema: tu Domain Service necesita escribir a dos lugares: PostgreSQL (el documento) y, indirectamente vía pipeline, Qdrant (el embedding del documento). Si lo haces ingenuamente —escribir a uno y luego al otro— tienes el problema del dual-write:
- La escritura a Postgres tiene éxito, pero la publicación del evento a Kafka falla → el embedding no se calcula, Qdrant nunca se entera.
- La publicación a Kafka tiene éxito, pero el commit a Postgres falla → evento fantasma, el embedding se calcula sobre algo que no existe.
- El servicio crashea entre las dos operaciones → estado parcial, no sabes qué pasó.
Distributed transactions (two-phase commit) son la solución teórica pero nadie las quiere en producción: requieren coordinator XA, latencia alta, locking distribuido. La solución práctica son los patrones modernos. Tres opciones:
Patrón 1 — Dual-write naïve (prototipos)
El Domain Service escribe a Postgres, luego publica a Kafka:
async def create_document(doc):
async with db.transaction():
doc_id = await db.execute("INSERT INTO documents ...")
await kafka.publish("documents.changes", {...})
return doc_id
Funciona en happy path. Falla cuando algo entre las dos operaciones se rompe. Para prototipos donde la inconsistencia es aceptable, vale; para producción seria, no.
Patrón 2 — Transactional outbox + CDC con Debezium (la opción correcta)
Solución elegante: el Domain Service escribe a Postgres en una sola transacción que incluye tanto la tabla principal como una outbox table. La outbox no es consumida directamente; Debezium lee el WAL de Postgres y produce a Kafka los eventos de la outbox.
Schema típico:
CREATE TABLE outbox (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
aggregate TEXT NOT NULL, -- 'document', 'user', 'product'
aggregate_id TEXT NOT NULL, -- el doc_id que cambió
event_type TEXT NOT NULL, -- 'created', 'updated', 'deleted'
payload JSONB NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW()
);
Cuando el Domain Service crea un documento:
async def create_document(doc):
async with db.transaction():
doc_id = await db.execute("INSERT INTO documents (id, body) VALUES (...)", ...)
await db.execute(
"INSERT INTO outbox (aggregate, aggregate_id, event_type, payload) VALUES (...)",
"document", doc_id, "created", json.dumps(doc)
)
# transacción committed; Debezium leerá el WAL y publicará a Kafka
return doc_id
Lo crucial: las dos inserciones están en la misma transacción ACID de Postgres. O las dos van, o ninguna va. Garantía absoluta de consistencia local.
Configuración Debezium para leer la outbox:
{
"name": "outbox-debezium-connector",
"config": {
"connector.class": "io.debezium.connector.postgresql.PostgresConnector",
"database.hostname": "postgres",
"database.dbname": "app",
"table.include.list": "public.outbox",
"transforms": "outbox",
"transforms.outbox.type": "io.debezium.transforms.outbox.EventRouter",
"transforms.outbox.route.by.field": "aggregate",
"transforms.outbox.table.field.event.id": "id",
"transforms.outbox.table.field.event.key": "aggregate_id",
"transforms.outbox.table.field.event.type": "event_type",
"transforms.outbox.table.field.event.payload": "payload",
"transforms.outbox.route.topic.replacement": "${routedByValue}.changes",
"key.converter": "org.apache.kafka.connect.storage.StringConverter",
"value.converter": "org.apache.kafka.connect.json.JsonConverter"
}
}
EventRouter enruta a topics distintos según el valor de aggregate: eventos de document van a document.changes, los de user a user.changes, etc.
Ventajas: garantía “exactly-once” desde el punto de vista de la aplicación; eventos en orden del commit; sin polling.
Coste: una tabla extra, una configuración Debezium, ~5-10 ms extra de latencia en la escritura.
Patrón 3 — Event-driven directo (event sourcing puro)
Variante más radical: el evento es el primer ciudadano; PostgreSQL es solo una proyección. El Domain Service publica el evento a Kafka, y un consumer lo escribe a Postgres y otro lo procesa para embedding. No hay tabla principal, no hay outbox; el log Kafka es la fuente de verdad.
Más limpio conceptualmente pero requiere repensar el modelo de dominio (eventos como source of truth, queries reconstruidas de la proyección). Más adecuado para greenfield con equipo que entiende event sourcing.
Comparativa
| Patrón | Setup | Consistencia | Cuando |
|---|---|---|---|
| Dual-write naïve | Trivial | Frágil | Prototipos, PoC |
| Outbox + CDC | Medio | Sólido | Producción seria (default) |
| Event-driven directo | Alto | Sólido | Greenfield con event sourcing |
El default en 2026 para producción es outbox + CDC con Debezium. Es lo suficientemente simple para mantenerse, lo suficientemente robusto para no preocupar de noche.
Manifest completo: despliegue Qdrant en Kubernetes
Ya cubrimos cómo se monta el resto del pipeline (Kafka, Debezium, Flink) en el post anterior de Kafka. La pieza que añadimos aquí es Qdrant. Despliegue típico vía Helm chart oficial:
# values.yaml para qdrant/qdrant chart
replicaCount: 3 # cluster con 3 réplicas
image:
repository: qdrant/qdrant
tag: "v1.14.0"
persistence:
enabled: true
storageClassName: "fast-ssd"
size: 200Gi
resources:
requests:
cpu: 2
memory: 8Gi
limits:
cpu: 8
memory: 16Gi
# clustering: cada réplica conoce a las otras
cluster:
enabled: true
consensus:
tickPeriodMs: 100
# auth via API key
apiKey:
enabled: true
secretKeyRef:
name: qdrant-auth
key: api-key
# observability
metrics:
enabled: true
serviceMonitor:
enabled: true # scrapping desde kube-prometheus
# snapshots periódicos
snapshots:
enabled: true
schedule: "0 3 * * *" # diario a las 3 AM
retention: 7
storage: "s3"
s3:
bucket: "qdrant-snapshots-prod"
config:
storage:
performance:
max_search_threads: 8
quantization:
always_ram: true # quantized vectors en RAM
service:
enable_tls: true
Y la creación de la colección con configuración para hybrid search:
from qdrant_client import QdrantClient
from qdrant_client.models import (
VectorParams, SparseVectorParams, Distance,
HnswConfigDiff, ScalarQuantization, ScalarType
)
client = QdrantClient(url="https://qdrant.internal:6333", api_key=API_KEY)
client.create_collection(
collection_name="documents",
vectors_config={
"dense": VectorParams(
size=1536,
distance=Distance.COSINE,
on_disk=False, # en RAM para latencia
)
},
sparse_vectors_config={
"sparse": SparseVectorParams() # para BM25-style lexical
},
hnsw_config=HnswConfigDiff(
m=16,
ef_construct=128,
on_disk=False
),
quantization_config=ScalarQuantization(
scalar=ScalarType.INT8 # 65% menos memoria
),
on_disk_payload=True, # payload en disco
shard_number=6, # particionado para escala
replication_factor=2, # cada shard replicado
write_consistency_factor=1
)
Con esta config, una colección de 50M vectores de 1536 dimensiones ocupa ~150-200 GB en RAM (vs ~600 GB con float32 puro), con queries p95 sub-10ms en cargas típicas.
Observabilidad: ver qué está pasando
Cuatro métricas que cualquier dashboard mínimo de la etapa Data debería tener:
1. Lag del outbox
debezium_lag_seconds: cuánto tarda Debezium en leer un evento desde que se commitea. Objetivo: <1 segundo. Si sube, indica WAL retention insuficiente o consumer rate menor que producer.
2. Lag del embedding service
embedding_service_consumer_lag_messages: cuántos eventos pendientes hay en el topic documents.changes. Objetivo: <100 sostenido. Si crece, indica que el rate de cambios supera la capacidad del embedding service. Soluciones: más consumers (paralelismo), batching más grande, modelo de embedding más rápido.
3. Tasa de upsert a Qdrant
qdrant_upsert_rate y qdrant_upsert_p95_latency. Objetivo: latencia <50 ms p95, tasa estable acorde al CDC rate. Si la latencia sube, Qdrant está degradado (memory pressure, disk slow, conn pool saturado).
4. Recall en producción (offline check)
Una vez al día, ejecutar un job que toma N queries reales, busca en Qdrant, busca en pgvector si lo mantienes en paralelo, compara recall@k. Si Qdrant deja de devolver lo que debería, lo detectas antes de que un usuario se queje.
Trampas operativas comunes
Sin outbox: el equipo aprende dual-write a base de incidentes
Lo más común. La primera versión hace dual-write directo “para empezar simple”; un día se cae Kafka durante 10 minutos y miles de embeddings quedan sin generar. Migrar a outbox después de tener tráfico es caro porque hay que backfill. Outbox desde el día 1.
Reembedding ignorante del coste
Cambias el modelo de embedding (text-embedding-3-small → text-embedding-3-large). Tu pipeline reemboda los 5M documentos. 17 horas y $1500 de coste que nadie anticipó. Calcular reembedding upfront: documentos × tokens promedio × coste/1k tokens × throughput limits.
Snapshot de Qdrant sin testear restore
Sacas snapshots diarios pero nunca pruebas restaurar. Un día Qdrant se corrompe y descubres que el snapshot está incompleto o que tu storage class no permite recuperarlo. Test trimestral de restore en entorno paralelo, obligatorio para producción.
Qdrant detrás de Service ClusterIP estándar sin gRPC affinity
Qdrant habla gRPC. Si el Service hace round-robin connection-level pero el cliente reusa connections, todo el tráfico va a un solo pod. Headless Service + client-side load balancing o gRPC-aware service mesh.
PG y Qdrant sin shared trace id
El Domain Service recibe un request, lo procesa, escribe a PG, dispara evento. Cuando un día algo va mal, no puedes correlar el span del Domain Service con el span del Indexing Worker porque no propagaste trace context. OTel context propagation por el topic Kafka (vía headers Kafka), igual que hicimos en el post de MCP observability.
Vector y metadata en sync nominal pero no real
PG dice “documento X tiene categoría tech”; Qdrant dice “documento X tiene categoría legal” (porque el cambio de categoría se actualizó en PG pero el evento de update no llegó a regenerar el payload en Qdrant). Filtras category=tech, no aparece. Tests periódicos de consistencia cross-store sobre muestreo aleatorio.
Dimensión del vector hardcodeada en mil sitios
1536 aparece en el código del Domain Service, del Embedding Service, del Indexing Worker, del Retrieval Service, en la creación de la colección Qdrant. Cuando cambias modelo (a uno de 768 dimensiones), olvidas uno y todo se rompe. Configuración centralizada del modelo + dimensión.
Sin rate limiting al embedding provider
Tu CDC procesa una migración masiva: 1M documentos cambian. El embedding service intenta procesar todo a la vez. OpenAI te rate-limita, el consumer queda atascado, los eventos se acumulan, tu cluster Kafka queda con horas de lag. Rate limiting en el consumer, no en el producer.
Cuándo NO usar Qdrant: el contrapunto honesto
Para no presentar Qdrant como bala de plata:
- Tu corpus es <1M vectores y no esperas crecer. pgvector basta y te ahorra un servicio.
- Tu equipo es pequeño y no tiene capacidad de operar un stateful service más. Qdrant añade snapshots, gRPC, mTLS, observabilidad propia. Cada uno de esos puntos es un día de trabajo de un SRE.
- Tu retrieval es batch off-hours, no real-time. Si solo haces RAG para reportes nocturnos, la latencia de pgvector no duele.
- Necesitas JOINs nativos entre embeddings y tablas relacionales en queries críticos. pgvector permite hacer
JOIN documents d ON d.id = embedding.doc_id WHERE d.tenant_id = X. Qdrant lo simula con payload pero menos elegante.
Y al revés, cuando Qdrant gana claramente:
- Corpus >10M vectores con queries con filtros densos.
- Necesidad de hybrid search nativo (sparse + dense + multivector).
- Multi-tenant con strict latency requirements por cliente.
- Quantization agresiva para mantener todo en RAM en hardware limitado.
- Cluster mode con sharding horizontal real.
Lo que no hemos cubierto
- Migración pgvector → Qdrant en vivo: patrón con dual-read durante la transición.
- Vector search federation: queries que cruzan múltiples Qdrant collections o múltiples vector stores. Tema propio.
- Multi-tenancy en Qdrant: payload filters + namespace isolation + per-tenant rate limiting.
- Cold storage para vectores antiguos: archivo de partitions a object storage con índice secundario.
- Embedding model self-hosted con vLLM: alternativa a OpenAI API que reduce coste y mejora privacidad. Tema cruzado con la serie de inferencia.
Referencias
PostgreSQL y pgvector:
- PostgreSQL pgvector extension (GitHub) — el de toda la vida.
- Pgvector vs Qdrant (Tiger Data) — comparativa con números.
- Start with pgvector: Why You’ll Outgrow It Faster Than You Think (Qdrant blog) — los tradeoffs honestos desde Qdrant.
Qdrant:
- Qdrant — sitio oficial.
- Qdrant changelog.
- Sparse Vectors in Qdrant — hybrid search nativo.
- Multivectors and Late Interaction — ColBERT-style.
- Qdrant 2025 Recap: Powering the Agentic Era — estado del proyecto y roadmap.
Outbox y CDC:
- Reliable Microservices Data Exchange With the Outbox Pattern (Debezium blog) — el post canónico.
- The Outbox Pattern Explained (Streamkap).
- Outbox Pattern with Debezium (Thorben Janssen).
- Distributed Data for Microservices — Event Sourcing vs CDC (Debezium blog) — comparativa entre patrones.
Comparativas 2026:
- Choosing Your Vector Database: Qdrant vs Pinecone vs pgvector in 2026 (KnowSync).
- pgvector vs Qdrant: Production Tradeoffs 2026 (Open Techstack).
- qdrant vs pgvector: Which Vector Database Should You Choose in 2026 (Markaicode).
- Best Vector Databases in 2026: Pricing, Scale Limits, Architecture Tradeoffs (MarkTechPost).
- Vector Database Benchmarks 2026: pgvector 0.9, Qdrant, Weaviate, Milvus, LanceDB (CallSphere).
Cross-references:
- Post anterior: Pipeline LLMOps de 6 etapas — donde definimos el mini-mapa.
- RAG sobre Kafka — arquitectura técnica — la pieza que precede a Qdrant en el pipeline.
- Panorama MLOps LLMs 2026 — el marco general.
- Series previas: post-tracing y eBPF.