<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Debezium on lo0 — Blog Técnico</title><link>https://blog.lo0.es/tags/debezium/</link><description>Recent content in Debezium on lo0 — Blog Técnico</description><generator>Hugo -- gohugo.io</generator><language>es</language><lastBuildDate>Thu, 21 May 2026 06:50:00 +0200</lastBuildDate><atom:link href="https://blog.lo0.es/tags/debezium/index.xml" rel="self" type="application/rss+xml"/><item><title>PostgreSQL + Qdrant en la etapa de ingestión: patrones de sincronización, microservicios y cómo encaja todo sin romperse</title><link>https://blog.lo0.es/posts/postgresql-qdrant-ingestion-microservicios/</link><pubDate>Thu, 21 May 2026 06:50:00 +0200</pubDate><guid>https://blog.lo0.es/posts/postgresql-qdrant-ingestion-microservicios/</guid><description>&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>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. &lt;strong>Mantener los dos sincronizados es el problema operacional número uno&lt;/strong> que el campo LLMOps ha codificado en 2026 con tres patrones canónicos: &lt;strong>dual-write&lt;/strong> (simple, frágil, válido para prototipos), &lt;strong>transactional outbox + CDC con Debezium&lt;/strong> (la opción &amp;ldquo;correcta&amp;rdquo; para producción seria) y &lt;strong>event-driven directo a Kafka&lt;/strong> (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 —&lt;strong>filtered search 6ms vs 29ms&lt;/strong> en 500K vectores, &lt;strong>65% menos memoria&lt;/strong> con scalar quantization, &lt;strong>HNSW filterable&lt;/strong> 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 &lt;strong>etapa Data&lt;/strong> 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.&lt;/p>
&lt;blockquote>
&lt;p>Este es el &lt;strong>cuarto post de la serie MLOps para LLMs&lt;/strong> y el primero que aplica el patrón &amp;ldquo;estás aquí&amp;rdquo; sobre el mini-mapa que definimos en el &lt;a href="https://blog.lo0.es/posts/pipeline-llmops-seis-etapas/">post anterior sobre el pipeline de seis etapas&lt;/a>. Aquí estamos plenamente en la primera etapa: &lt;strong>Data&lt;/strong>.&lt;/p>
&lt;/blockquote>
&lt;h2 id="estás-aquí-etapa-data-del-pipeline">Estás aquí: etapa Data del pipeline&lt;/h2>
&lt;div class="diagram" style="max-width:780px;margin:1rem auto;">
&lt;svg viewBox="0 0 780 90" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="estás aquí: Data">
&lt;style>.box{stroke:#444;stroke-width:1.4;rx:6}.active{fill:#ff8a4c;stroke-width:3}.idle{fill:#f4f4f4}.lbl{font:600 12px sans-serif;fill:#222}.arr{stroke:#666;stroke-width:1.4;fill:none;marker-end:url(#nv1)}.cyc{stroke:#888;stroke-width:1.2;fill:none;stroke-dasharray:4 2;marker-end:url(#nv1)}&lt;/style>
&lt;defs>&lt;marker id="nv1" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="#666"/>&lt;/marker>&lt;/defs>
&lt;text x="390" y="20" text-anchor="middle" class="lbl">Estás aquí: DATA · PostgreSQL + Qdrant + patrones de sincronización en microservicios&lt;/text>
&lt;rect x="30" y="35" width="110" height="35" class="box active"/>&lt;text x="85" y="58" text-anchor="middle" class="lbl">1 · Data&lt;/text>
&lt;rect x="155" y="35" width="110" height="35" class="box idle"/>&lt;text x="210" y="58" text-anchor="middle" class="lbl">2 · Tune&lt;/text>
&lt;rect x="280" y="35" width="110" height="35" class="box idle"/>&lt;text x="335" y="58" text-anchor="middle" class="lbl">3 · Eval&lt;/text>
&lt;rect x="405" y="35" width="110" height="35" class="box idle"/>&lt;text x="460" y="58" text-anchor="middle" class="lbl">4 · Deploy&lt;/text>
&lt;rect x="530" y="35" width="110" height="35" class="box idle"/>&lt;text x="585" y="58" text-anchor="middle" class="lbl">5 · Observe&lt;/text>
&lt;rect x="655" y="35" width="110" height="35" class="box idle"/>&lt;text x="710" y="58" text-anchor="middle" class="lbl">6 · Retrain&lt;/text>
&lt;path class="arr" d="M140,52 L155,52"/>&lt;path class="arr" d="M265,52 L280,52"/>&lt;path class="arr" d="M390,52 L405,52"/>&lt;path class="arr" d="M515,52 L530,52"/>&lt;path class="arr" d="M640,52 L655,52"/>
&lt;path class="cyc" d="M710,72 L710,82 L85,82 L85,72"/>
&lt;/svg>
&lt;/div>
&lt;h2 id="la-pregunta-que-define-la-arquitectura-una-db-o-dos">La pregunta que define la arquitectura: ¿una DB o dos?&lt;/h2>
&lt;p>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:&lt;/p>
&lt;p>&lt;strong>Opción A — pgvector dentro de Postgres&lt;/strong>: añades la extensión &lt;code>vector&lt;/code>, una columna &lt;code>embedding vector(1536)&lt;/code>, 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. &lt;strong>Una sola fuente de verdad&lt;/strong>.&lt;/p>
&lt;p>&lt;strong>Opción B — Qdrant separado&lt;/strong>: dejas Postgres como está y montas Qdrant como servicio stateful aparte. Tu microservicio escribe a las dos. &lt;strong>Dos fuentes parciales que mantener en sync&lt;/strong>.&lt;/p>
&lt;p>La elección depende de números. Vamos a ellos.&lt;/p>
&lt;h3 id="cuándo-pgvector-basta-y-cuándo-no">Cuándo pgvector basta y cuándo no&lt;/h3>
&lt;p>&lt;a href="https://qdrant.tech/blog/pgvector-tradeoffs/">Los benchmarks 2026&lt;/a> son consistentes. La regla del pulgar:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Hasta ~1M vectores&lt;/strong>: pgvector es excelente. Setup en minutos, cero overhead operacional, queries ACID con JOINs naturales.&lt;/li>
&lt;li>&lt;strong>1-10M vectores&lt;/strong>: pgvector funciona pero ya empiezas a sufrir. Index builds tardan, recall baja bajo carga, memoria sube linealmente.&lt;/li>
&lt;li>&lt;strong>&amp;gt;10M vectores&lt;/strong>: pgvector se hunde a no ser que tunes mucho. Index build pasa de horas; query p95 deriva por encima de 200ms.&lt;/li>
&lt;li>&lt;strong>&amp;gt;50M vectores&lt;/strong>: pgvector deja de ser opción razonable en single-node.&lt;/li>
&lt;/ul>
&lt;p>Qdrant escala a billones con sharding. Numéricamente, en 500K vectores con 3 condiciones de payload:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Qdrant&lt;/strong>: 6 ms p95 (filtered HNSW).&lt;/li>
&lt;li>&lt;strong>pgvector&lt;/strong>: 29 ms p95 (heap scans rompen la localidad del índice).&lt;/li>
&lt;/ul>
&lt;p>Y en memoria: &lt;strong>Qdrant con scalar quantization usa 65% menos RAM&lt;/strong> 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.&lt;/p>
&lt;p>Pero pgvector tiene una ventaja decisiva en proyectos pequeños y medianos: &lt;strong>es gratis, embebido, y lo opera tu DBA&lt;/strong>. 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.&lt;/p>
&lt;h3 id="el-veredicto-operativo-2026">El veredicto operativo 2026&lt;/h3>
&lt;ul>
&lt;li>Empieza con &lt;strong>pgvector&lt;/strong> si tu corpus es &amp;lt;5M vectores y tu equipo es pequeño.&lt;/li>
&lt;li>Migra a &lt;strong>Qdrant&lt;/strong> 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.&lt;/li>
&lt;li>&lt;strong>No migres anticipadamente&lt;/strong>: el coste operacional de Qdrant es real; sufre cuando lo necesitas, no por si acaso.&lt;/li>
&lt;/ul>
&lt;p>Lo importante: &lt;strong>diseña la capa de acceso a embeddings con una abstracción&lt;/strong> (un &lt;code>VectorStore&lt;/code> interface en tu código) para que cambiar de pgvector a Qdrant sea cambiar la implementación, no reescribir la app.&lt;/p>
&lt;h2 id="qdrant-en-detalle-lo-que-ofrece-sobre-pgvector">Qdrant en detalle: lo que ofrece sobre pgvector&lt;/h2>
&lt;p>Si decides que Qdrant es la opción, vale la pena entender qué te da más allá del rendimiento bruto. Cinco features dominantes:&lt;/p>
&lt;h3 id="1-filterable-hnsw">1. Filterable HNSW&lt;/h3>
&lt;p>El &lt;strong>HNSW filterable&lt;/strong> es lo que más se nota en producción. En pgvector, filtrar por metadata (&lt;code>WHERE category = 'tech' AND date &amp;gt; '2026-01-01'&lt;/code>) 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 &lt;strong>podar la búsqueda con filtros dentro del propio recorrido del grafo&lt;/strong>, sin escapar a heap scans externos. Para queries con filtros densos (lo normal en RAG con permisos multi-tenant), la diferencia es brutal.&lt;/p>
&lt;h3 id="2-multivector-y-late-interaction-colbert">2. Multivector y late-interaction (ColBERT)&lt;/h3>
&lt;p>Qdrant permite almacenar &lt;strong>una matriz de vectores por punto&lt;/strong>, no solo un vector. Esto soporta nativamente modelos late-interaction como ColBERT, que codifican un vector por token y comparan con &lt;code>MaxSim&lt;/code>. La calidad de retrieval con ColBERT-style multivectors es típicamente 5-15% mejor que single-vector en cargas semánticas complejas.&lt;/p>
&lt;h3 id="3-sparse--dense-hybrid-search">3. Sparse + dense hybrid search&lt;/h3>
&lt;p>&lt;a href="https://qdrant.tech/articles/sparse-vectors/">Hybrid search&lt;/a> combina un vector denso (semántico, eg embeddings de SentenceTransformers) con un vector disperso (lexical, eg SPLADE, BM25 reproducido como sparse). El denso captura &amp;ldquo;esto es semánticamente similar&amp;rdquo;; el disperso captura &amp;ldquo;esta palabra concreta aparece&amp;rdquo;. 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.&lt;/p>
&lt;h3 id="4-quantization-escalar-y-binaria">4. Quantization escalar y binaria&lt;/h3>
&lt;p>Para cargas grandes, Qdrant ofrece &lt;strong>scalar quantization&lt;/strong> (&lt;code>int8&lt;/code> en lugar de &lt;code>float32&lt;/code>, 4× menos memoria con pérdida marginal de recall) y &lt;strong>binary quantization&lt;/strong> (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 &lt;strong>4-bit quantization&lt;/strong>, que será un punto medio.&lt;/p>
&lt;h3 id="5-named-vectors">5. Named vectors&lt;/h3>
&lt;p>Una colección Qdrant puede tener &lt;strong>múltiples espacios vectoriales por punto&lt;/strong>, llamados named vectors. Caso típico: el mismo documento se indexa con un vector denso (&lt;code>text-embedding-3-small&lt;/code>) y un vector sparse (SPLADE), bajo el mismo &lt;code>point_id&lt;/code>. Las queries pueden buscar en el vector concreto que les interesa.&lt;/p>
&lt;p>A esto se suma el roadmap 2026: &lt;strong>4-bit quantization, read-write segregation, expanded inference capabilities&lt;/strong> (Qdrant puede embeddar texto él mismo, sin un servicio externo).&lt;/p>
&lt;h2 id="la-arquitectura-de-microservicios-dónde-encaja-cada-pieza">La arquitectura de microservicios: dónde encaja cada pieza&lt;/h2>
&lt;p>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 &lt;strong>cinco microservicios&lt;/strong> que tocan estas piezas, cada uno con su responsabilidad clara:&lt;/p>
&lt;div class="diagram" style="max-width:780px;margin:1.5rem auto;">
&lt;svg viewBox="0 0 780 460" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Microservicios PG + Qdrant">
&lt;style>.title{font:700 13px sans-serif;fill:#222}.lbl{font:600 12px sans-serif;fill:#222}.sm{font:11px sans-serif;fill:#555}.tiny{font:10px sans-serif;fill:#666}.svc{stroke:#444;stroke-width:1.5;rx:6}.domain{fill:#ffe9d6}.emb{fill:#d6eaff}.idx{fill:#d9f5d6}.retr{fill:#e9d6f5}.llm{fill:#ffd6d6}.db{stroke:#666;stroke-width:1.5;rx:4}.pg{fill:#fff5b0}.qd{fill:#d6f0ff}.kafka{fill:#f4d6ff}.arr{stroke:#666;stroke-width:1.4;fill:none;marker-end:url(#nv2)}.async{stroke:#aa6;stroke-width:1.4;fill:none;marker-end:url(#nv2);stroke-dasharray:5 3}&lt;/style>
&lt;defs>&lt;marker id="nv2" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="#666"/>&lt;/marker>&lt;/defs>
&lt;text x="390" y="22" text-anchor="middle" class="title">Microservicios típicos en un RAG con PostgreSQL + Qdrant&lt;/text>
&lt;rect x="30" y="50" width="150" height="60" class="svc domain"/>&lt;text x="105" y="74" text-anchor="middle" class="lbl">Domain Service&lt;/text>&lt;text x="105" y="92" text-anchor="middle" class="sm">CRUD de documentos&lt;/text>&lt;text x="105" y="106" text-anchor="middle" class="sm">/users, /products...&lt;/text>
&lt;rect x="30" y="180" width="150" height="80" class="db pg"/>&lt;text x="105" y="204" text-anchor="middle" class="lbl">PostgreSQL&lt;/text>&lt;text x="105" y="222" text-anchor="middle" class="sm">documents (main)&lt;/text>&lt;text x="105" y="238" text-anchor="middle" class="sm">outbox (events)&lt;/text>&lt;text x="105" y="254" text-anchor="middle" class="sm">ACID&lt;/text>
&lt;rect x="220" y="180" width="140" height="80" class="db kafka"/>&lt;text x="290" y="204" text-anchor="middle" class="lbl">Kafka&lt;/text>&lt;text x="290" y="222" text-anchor="middle" class="sm">documents.changes&lt;/text>&lt;text x="290" y="238" text-anchor="middle" class="sm">documents.embedded&lt;/text>&lt;text x="290" y="254" text-anchor="middle" class="sm">retention 30d+&lt;/text>
&lt;rect x="400" y="50" width="160" height="60" class="svc emb"/>&lt;text x="480" y="74" text-anchor="middle" class="lbl">Embedding Service&lt;/text>&lt;text x="480" y="92" text-anchor="middle" class="sm">consume Kafka,&lt;/text>&lt;text x="480" y="106" text-anchor="middle" class="sm">batch embed, escribe&lt;/text>
&lt;rect x="400" y="180" width="160" height="60" class="svc idx"/>&lt;text x="480" y="204" text-anchor="middle" class="lbl">Indexing Worker&lt;/text>&lt;text x="480" y="222" text-anchor="middle" class="sm">consume embedded,&lt;/text>&lt;text x="480" y="238" text-anchor="middle" class="sm">upsert a Qdrant&lt;/text>
&lt;rect x="580" y="180" width="170" height="80" class="db qd"/>&lt;text x="665" y="204" text-anchor="middle" class="lbl">Qdrant&lt;/text>&lt;text x="665" y="222" text-anchor="middle" class="sm">collection: documents&lt;/text>&lt;text x="665" y="238" text-anchor="middle" class="sm">HNSW + payload&lt;/text>&lt;text x="665" y="254" text-anchor="middle" class="sm">3 réplicas&lt;/text>
&lt;rect x="220" y="310" width="160" height="60" class="svc retr"/>&lt;text x="300" y="334" text-anchor="middle" class="lbl">Retrieval Service&lt;/text>&lt;text x="300" y="352" text-anchor="middle" class="sm">query Qdrant + reranker&lt;/text>&lt;text x="300" y="366" text-anchor="middle" class="sm">+ enrich con PG&lt;/text>
&lt;rect x="430" y="310" width="160" height="60" class="svc llm"/>&lt;text x="510" y="334" text-anchor="middle" class="lbl">LLM Service&lt;/text>&lt;text x="510" y="352" text-anchor="middle" class="sm">vLLM / API externa&lt;/text>&lt;text x="510" y="366" text-anchor="middle" class="sm">recibe context + query&lt;/text>
&lt;path class="arr" d="M105,110 L105,180"/>
&lt;text x="115" y="148" class="tiny">tx con outbox&lt;/text>
&lt;path class="async" d="M180,228 L220,228"/>
&lt;text x="200" y="222" class="tiny">CDC&lt;/text>
&lt;text x="200" y="245" class="tiny">Debezium&lt;/text>
&lt;path class="arr" d="M360,228 L360,148 L400,80"/>
&lt;text x="365" y="160" class="tiny">consume&lt;/text>
&lt;text x="365" y="173" class="tiny">events&lt;/text>
&lt;path class="arr" d="M480,110 L480,180"/>
&lt;text x="490" y="148" class="tiny">produce&lt;/text>
&lt;text x="490" y="161" class="tiny">embedded&lt;/text>
&lt;path class="arr" d="M560,225 L580,225"/>
&lt;text x="565" y="217" class="tiny">upsert&lt;/text>
&lt;path class="arr" d="M580,225 C600,250 600,300 510,310"/>
&lt;text x="590" y="285" class="tiny">query&lt;/text>
&lt;path class="arr" d="M180,340 L220,340"/>
&lt;text x="185" y="333" class="tiny">enrich&lt;/text>
&lt;text x="180" y="346" class="tiny">metadata&lt;/text>
&lt;path class="arr" d="M380,340 L430,340"/>
&lt;text x="395" y="333" class="tiny">context&lt;/text>
&lt;text x="395" y="346" class="tiny">+ query&lt;/text>
&lt;/svg>
&lt;/div>
&lt;p>Vamos a cada microservicio.&lt;/p>
&lt;h3 id="1-domain-service">1. Domain Service&lt;/h3>
&lt;p>&lt;strong>Responsabilidad&lt;/strong>: la lógica de negocio. CRUD de documentos, productos, conversaciones. Endpoints REST/gRPC para el front-end o para otros servicios. &lt;strong>Solo conoce PostgreSQL como sistema de persistencia&lt;/strong>; no sabe nada de Qdrant.&lt;/p>
&lt;p>Esto es &lt;strong>importante por diseño&lt;/strong>: 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.&lt;/p>
&lt;h3 id="2-postgresql">2. PostgreSQL&lt;/h3>
&lt;p>&lt;strong>Responsabilidad&lt;/strong>: source of truth transaccional. Schemas relacionales, constraints, triggers, ACID. &lt;strong>Y la outbox table&lt;/strong> que veremos en breve, que es lo que va a permitir la sincronización fiable.&lt;/p>
&lt;p>Patrón típico de despliegue: HA con Patroni + repmgr + PgBouncer para connection pooling, replicas de lectura para offloading.&lt;/p>
&lt;h3 id="3-kafka">3. Kafka&lt;/h3>
&lt;p>&lt;strong>Responsabilidad&lt;/strong>: 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 &lt;a href="https://blog.lo0.es/posts/rag-kafka-datalake-arquitectura/">RAG sobre Kafka&lt;/a>.&lt;/p>
&lt;p>Topics típicos:&lt;/p>
&lt;ul>
&lt;li>&lt;code>documents.changes&lt;/code>: eventos crudos de cambio (insert/update/delete).&lt;/li>
&lt;li>&lt;code>documents.embedded&lt;/code>: eventos con embedding ya calculado.&lt;/li>
&lt;/ul>
&lt;h3 id="4-embedding-service">4. Embedding Service&lt;/h3>
&lt;p>&lt;strong>Responsabilidad&lt;/strong>: consumir eventos de cambio, calcular embeddings, publicar al topic &lt;code>embedded&lt;/code>. Esta es la pieza que más coste consume si usas embeddings vía API (OpenAI, Cohere, Voyage AI).&lt;/p>
&lt;p>Estructura típica:&lt;/p>
&lt;ul>
&lt;li>Consumer Kafka con consumer group propio.&lt;/li>
&lt;li>Batching de eventos para llamadas embedding (mucho más eficiente que uno a uno).&lt;/li>
&lt;li>Llamadas paralelas con concurrency control.&lt;/li>
&lt;li>Retry con exponential backoff ante rate limits.&lt;/li>
&lt;li>Métricas exportadas (latencia, throughput, errores, coste).&lt;/li>
&lt;li>Idempotencia (key del topic = doc_id, mismo doc no se re-embedea sin necesidad).&lt;/li>
&lt;/ul>
&lt;p>Patrón de optimización clave: &lt;strong>deduplicate por hash de contenido&lt;/strong>. 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.&lt;/p>
&lt;h3 id="5-indexing-worker">5. Indexing Worker&lt;/h3>
&lt;p>&lt;strong>Responsabilidad&lt;/strong>: consumir el topic &lt;code>embedded&lt;/code> y hacer &lt;code>upsert&lt;/code> 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 &lt;strong>idempotente&lt;/strong> (el mismo &lt;code>doc_id&lt;/code> puede llegar varias veces si el consumer reinicia) y &lt;strong>resiliente&lt;/strong> (si Qdrant está caído, reintentar sin perder eventos).&lt;/p>
&lt;p>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.&lt;/p>
&lt;h3 id="6-retrieval-service">6. Retrieval Service&lt;/h3>
&lt;p>&lt;strong>Responsabilidad&lt;/strong>: 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.&lt;/p>
&lt;p>Es &lt;strong>el único servicio que consulta Qdrant&lt;/strong>. Esto centraliza la lógica de retrieval: cuando quieras añadir reranking, hybrid search, query rewriting, lo haces aquí sin tocar el resto.&lt;/p>
&lt;h3 id="7-llm-service">7. LLM Service&lt;/h3>
&lt;p>&lt;strong>Responsabilidad&lt;/strong>: 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í.&lt;/p>
&lt;h2 id="el-problema-del-dual-write-y-los-tres-patrones-de-solución">El problema del dual-write y los tres patrones de solución&lt;/h2>
&lt;p>Aquí está la pieza arquitectónica más importante del post. El problema: tu Domain Service necesita escribir a &lt;strong>dos lugares&lt;/strong>: 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 &lt;strong>el problema del dual-write&lt;/strong>:&lt;/p>
&lt;ul>
&lt;li>La escritura a Postgres tiene éxito, pero la publicación del evento a Kafka falla → &lt;strong>el embedding no se calcula, Qdrant nunca se entera&lt;/strong>.&lt;/li>
&lt;li>La publicación a Kafka tiene éxito, pero el commit a Postgres falla → &lt;strong>evento fantasma&lt;/strong>, el embedding se calcula sobre algo que no existe.&lt;/li>
&lt;li>El servicio crashea entre las dos operaciones → &lt;strong>estado parcial&lt;/strong>, no sabes qué pasó.&lt;/li>
&lt;/ul>
&lt;p>Distributed transactions (two-phase commit) son la solución teórica pero &lt;strong>nadie las quiere en producción&lt;/strong>: requieren coordinator XA, latencia alta, locking distribuido. La solución práctica son los patrones modernos. Tres opciones:&lt;/p>
&lt;h3 id="patrón-1--dual-write-naïve-prototipos">Patrón 1 — Dual-write naïve (prototipos)&lt;/h3>
&lt;p>El Domain Service escribe a Postgres, luego publica a Kafka:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="k">async&lt;/span> &lt;span class="k">def&lt;/span> &lt;span class="nf">create_document&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">doc&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">async&lt;/span> &lt;span class="k">with&lt;/span> &lt;span class="n">db&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">transaction&lt;/span>&lt;span class="p">():&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">doc_id&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">await&lt;/span> &lt;span class="n">db&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">execute&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;INSERT INTO documents ...&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">await&lt;/span> &lt;span class="n">kafka&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">publish&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;documents.changes&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="o">...&lt;/span>&lt;span class="p">})&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">doc_id&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>Funciona&lt;/strong> en happy path. &lt;strong>Falla&lt;/strong> cuando algo entre las dos operaciones se rompe. Para prototipos donde la inconsistencia es aceptable, vale; para producción seria, no.&lt;/p>
&lt;h3 id="patrón-2--transactional-outbox--cdc-con-debezium-la-opción-correcta">Patrón 2 — Transactional outbox + CDC con Debezium (la opción correcta)&lt;/h3>
&lt;p>Solución elegante: &lt;strong>el Domain Service escribe a Postgres en una sola transacción que incluye tanto la tabla principal como una &lt;code>outbox&lt;/code> table&lt;/strong>. La outbox no es consumida directamente; &lt;strong>Debezium lee el WAL de Postgres y produce a Kafka los eventos de la outbox&lt;/strong>.&lt;/p>
&lt;p>Schema típico:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="cl">&lt;span class="k">CREATE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">TABLE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">outbox&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">UUID&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">PRIMARY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">KEY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">DEFAULT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">gen_random_uuid&lt;/span>&lt;span class="p">(),&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">aggregate&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">TEXT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NOT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NULL&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">-- &amp;#39;document&amp;#39;, &amp;#39;user&amp;#39;, &amp;#39;product&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">aggregate_id&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">TEXT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NOT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NULL&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">-- el doc_id que cambió
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">event_type&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">TEXT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NOT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NULL&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">-- &amp;#39;created&amp;#39;, &amp;#39;updated&amp;#39;, &amp;#39;deleted&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">payload&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">JSONB&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NOT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NULL&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">created_at&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">TIMESTAMPTZ&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">DEFAULT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">NOW&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Cuando el Domain Service crea un documento:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="k">async&lt;/span> &lt;span class="k">def&lt;/span> &lt;span class="nf">create_document&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">doc&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">async&lt;/span> &lt;span class="k">with&lt;/span> &lt;span class="n">db&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">transaction&lt;/span>&lt;span class="p">():&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">doc_id&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">await&lt;/span> &lt;span class="n">db&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">execute&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;INSERT INTO documents (id, body) VALUES (...)&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="o">...&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">await&lt;/span> &lt;span class="n">db&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">execute&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;INSERT INTO outbox (aggregate, aggregate_id, event_type, payload) VALUES (...)&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;document&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">doc_id&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;created&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">json&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">dumps&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">doc&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># transacción committed; Debezium leerá el WAL y publicará a Kafka&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">doc_id&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>Lo crucial&lt;/strong>: las dos inserciones están en &lt;strong>la misma transacción ACID&lt;/strong> de Postgres. O las dos van, o ninguna va. Garantía absoluta de consistencia local.&lt;/p>
&lt;p>Configuración Debezium para leer la outbox:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;outbox-debezium-connector&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;config&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;connector.class&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;io.debezium.connector.postgresql.PostgresConnector&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;database.hostname&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;postgres&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;database.dbname&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;app&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;table.include.list&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;public.outbox&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;transforms&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;outbox&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;transforms.outbox.type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;io.debezium.transforms.outbox.EventRouter&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;transforms.outbox.route.by.field&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;aggregate&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;transforms.outbox.table.field.event.id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;id&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;transforms.outbox.table.field.event.key&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;aggregate_id&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;transforms.outbox.table.field.event.type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;event_type&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;transforms.outbox.table.field.event.payload&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;payload&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;transforms.outbox.route.topic.replacement&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;${routedByValue}.changes&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;key.converter&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;org.apache.kafka.connect.storage.StringConverter&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;value.converter&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;org.apache.kafka.connect.json.JsonConverter&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>EventRouter&lt;/code> enruta a topics distintos según el valor de &lt;code>aggregate&lt;/code>: eventos de &lt;code>document&lt;/code> van a &lt;code>document.changes&lt;/code>, los de &lt;code>user&lt;/code> a &lt;code>user.changes&lt;/code>, etc.&lt;/p>
&lt;p>&lt;strong>Ventajas&lt;/strong>: garantía &amp;ldquo;exactly-once&amp;rdquo; desde el punto de vista de la aplicación; eventos en orden del commit; sin polling.&lt;/p>
&lt;p>&lt;strong>Coste&lt;/strong>: una tabla extra, una configuración Debezium, ~5-10 ms extra de latencia en la escritura.&lt;/p>
&lt;h3 id="patrón-3--event-driven-directo-event-sourcing-puro">Patrón 3 — Event-driven directo (event sourcing puro)&lt;/h3>
&lt;p>Variante más radical: &lt;strong>el evento es el primer ciudadano&lt;/strong>; 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. &lt;strong>No hay tabla principal, no hay outbox&lt;/strong>; el log Kafka es la fuente de verdad.&lt;/p>
&lt;p>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.&lt;/p>
&lt;h3 id="comparativa">Comparativa&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Patrón&lt;/th>
&lt;th>Setup&lt;/th>
&lt;th>Consistencia&lt;/th>
&lt;th>Cuando&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Dual-write naïve&lt;/td>
&lt;td>Trivial&lt;/td>
&lt;td>Frágil&lt;/td>
&lt;td>Prototipos, PoC&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Outbox + CDC&lt;/td>
&lt;td>Medio&lt;/td>
&lt;td>&lt;strong>Sólido&lt;/strong>&lt;/td>
&lt;td>&lt;strong>Producción seria&lt;/strong> (default)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Event-driven directo&lt;/td>
&lt;td>Alto&lt;/td>
&lt;td>Sólido&lt;/td>
&lt;td>Greenfield con event sourcing&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>El default en 2026 para &lt;strong>producción&lt;/strong> es &lt;strong>outbox + CDC con Debezium&lt;/strong>. Es lo suficientemente simple para mantenerse, lo suficientemente robusto para no preocupar de noche.&lt;/p>
&lt;h2 id="manifest-completo-despliegue-qdrant-en-kubernetes">Manifest completo: despliegue Qdrant en Kubernetes&lt;/h2>
&lt;p>Ya cubrimos cómo se monta el resto del pipeline (Kafka, Debezium, Flink) en el &lt;a href="https://blog.lo0.es/posts/rag-kafka-datalake-arquitectura/">post anterior de Kafka&lt;/a>. La pieza que añadimos aquí es Qdrant. Despliegue típico vía Helm chart oficial:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># values.yaml para qdrant/qdrant chart&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">replicaCount&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">3&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># cluster con 3 réplicas&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">repository&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">qdrant/qdrant&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tag&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;v1.14.0&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">persistence&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">enabled&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">storageClassName&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;fast-ssd&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">size&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">200Gi&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">resources&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">requests&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">cpu&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">2&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">memory&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">8Gi&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">limits&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">cpu&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">8&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">memory&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">16Gi&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c"># clustering: cada réplica conoce a las otras&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">cluster&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">enabled&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">consensus&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tickPeriodMs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">100&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c"># auth via API key&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiKey&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">enabled&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">secretKeyRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">qdrant-auth&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">key&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">api-key&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c"># observability&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metrics&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">enabled&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">serviceMonitor&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">enabled&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># scrapping desde kube-prometheus&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c"># snapshots periódicos&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">snapshots&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">enabled&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">schedule&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;0 3 * * *&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># diario a las 3 AM&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">retention&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">7&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">storage&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;s3&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">s3&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">bucket&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;qdrant-snapshots-prod&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">config&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">storage&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">performance&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">max_search_threads&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">8&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">quantization&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">always_ram&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># quantized vectors en RAM&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">service&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">enable_tls&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Y la creación de la colección con configuración para hybrid search:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">qdrant_client&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">QdrantClient&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">qdrant_client.models&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">VectorParams&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">SparseVectorParams&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">Distance&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">HnswConfigDiff&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">ScalarQuantization&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">ScalarType&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">client&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">QdrantClient&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">url&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;https://qdrant.internal:6333&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">api_key&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">API_KEY&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">client&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">create_collection&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">collection_name&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;documents&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">vectors_config&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;dense&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">VectorParams&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">size&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">1536&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">distance&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">Distance&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">COSINE&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">on_disk&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">False&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1"># en RAM para latencia&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">sparse_vectors_config&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;sparse&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">SparseVectorParams&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="c1"># para BM25-style lexical&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">hnsw_config&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">HnswConfigDiff&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">m&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">16&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">ef_construct&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">128&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">on_disk&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">False&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">quantization_config&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">ScalarQuantization&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">scalar&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">ScalarType&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">INT8&lt;/span> &lt;span class="c1"># 65% menos memoria&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">on_disk_payload&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">True&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1"># payload en disco&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">shard_number&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">6&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1"># particionado para escala&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">replication_factor&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">2&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1"># cada shard replicado&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">write_consistency_factor&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">1&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>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.&lt;/p>
&lt;h2 id="observabilidad-ver-qué-está-pasando">Observabilidad: ver qué está pasando&lt;/h2>
&lt;p>Cuatro métricas que cualquier dashboard mínimo de la etapa Data debería tener:&lt;/p>
&lt;h3 id="1-lag-del-outbox">1. Lag del outbox&lt;/h3>
&lt;p>&lt;code>debezium_lag_seconds&lt;/code>: cuánto tarda Debezium en leer un evento desde que se commitea. &lt;strong>Objetivo: &amp;lt;1 segundo&lt;/strong>. Si sube, indica WAL retention insuficiente o consumer rate menor que producer.&lt;/p>
&lt;h3 id="2-lag-del-embedding-service">2. Lag del embedding service&lt;/h3>
&lt;p>&lt;code>embedding_service_consumer_lag_messages&lt;/code>: cuántos eventos pendientes hay en el topic &lt;code>documents.changes&lt;/code>. &lt;strong>Objetivo: &amp;lt;100 sostenido&lt;/strong>. 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.&lt;/p>
&lt;h3 id="3-tasa-de-upsert-a-qdrant">3. Tasa de upsert a Qdrant&lt;/h3>
&lt;p>&lt;code>qdrant_upsert_rate&lt;/code> y &lt;code>qdrant_upsert_p95_latency&lt;/code>. &lt;strong>Objetivo: latencia &amp;lt;50 ms p95, tasa estable acorde al CDC rate&lt;/strong>. Si la latencia sube, Qdrant está degradado (memory pressure, disk slow, conn pool saturado).&lt;/p>
&lt;h3 id="4-recall-en-producción-offline-check">4. Recall en producción (offline check)&lt;/h3>
&lt;p>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.&lt;/p>
&lt;h2 id="trampas-operativas-comunes">Trampas operativas comunes&lt;/h2>
&lt;h3 id="sin-outbox-el-equipo-aprende-dual-write-a-base-de-incidentes">Sin outbox: el equipo aprende dual-write a base de incidentes&lt;/h3>
&lt;p>Lo más común. La primera versión hace dual-write directo &amp;ldquo;para empezar simple&amp;rdquo;; un día se cae Kafka durante 10 minutos y miles de embeddings quedan sin generar. Migrar a outbox &lt;strong>después de tener tráfico&lt;/strong> es caro porque hay que backfill. &lt;strong>Outbox desde el día 1&lt;/strong>.&lt;/p>
&lt;h3 id="reembedding-ignorante-del-coste">Reembedding ignorante del coste&lt;/h3>
&lt;p>Cambias el modelo de embedding (&lt;code>text-embedding-3-small&lt;/code> → &lt;code>text-embedding-3-large&lt;/code>). Tu pipeline reemboda los 5M documentos. &lt;strong>17 horas y $1500 de coste&lt;/strong> que nadie anticipó. &lt;strong>Calcular reembedding upfront&lt;/strong>: documentos × tokens promedio × coste/1k tokens × throughput limits.&lt;/p>
&lt;h3 id="snapshot-de-qdrant-sin-testear-restore">Snapshot de Qdrant sin testear restore&lt;/h3>
&lt;p>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. &lt;strong>Test trimestral de restore&lt;/strong> en entorno paralelo, obligatorio para producción.&lt;/p>
&lt;h3 id="qdrant-detrás-de-service-clusterip-estándar-sin-grpc-affinity">Qdrant detrás de Service ClusterIP estándar sin gRPC affinity&lt;/h3>
&lt;p>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. &lt;strong>Headless Service + client-side load balancing&lt;/strong> o gRPC-aware service mesh.&lt;/p>
&lt;h3 id="pg-y-qdrant-sin-shared-trace-id">PG y Qdrant sin shared trace id&lt;/h3>
&lt;p>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. &lt;strong>OTel context propagation&lt;/strong> por el topic Kafka (vía headers Kafka), igual que hicimos en el &lt;a href="https://blog.lo0.es/posts/mcp-observability-otel/">post de MCP observability&lt;/a>.&lt;/p>
&lt;h3 id="vector-y-metadata-en-sync-nominal-pero-no-real">Vector y metadata en sync nominal pero no real&lt;/h3>
&lt;p>PG dice &amp;ldquo;documento X tiene categoría tech&amp;rdquo;; Qdrant dice &amp;ldquo;documento X tiene categoría legal&amp;rdquo; (porque el cambio de categoría se actualizó en PG pero el evento de update no llegó a regenerar el payload en Qdrant). Filtras &lt;code>category=tech&lt;/code>, no aparece. &lt;strong>Tests periódicos de consistencia cross-store&lt;/strong> sobre muestreo aleatorio.&lt;/p>
&lt;h3 id="dimensión-del-vector-hardcodeada-en-mil-sitios">Dimensión del vector hardcodeada en mil sitios&lt;/h3>
&lt;p>&lt;code>1536&lt;/code> 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. &lt;strong>Configuración centralizada&lt;/strong> del modelo + dimensión.&lt;/p>
&lt;h3 id="sin-rate-limiting-al-embedding-provider">Sin rate limiting al embedding provider&lt;/h3>
&lt;p>Tu CDC procesa una migración masiva: 1M documentos cambian. El embedding service intenta procesar todo a la vez. &lt;strong>OpenAI te rate-limita&lt;/strong>, el consumer queda atascado, los eventos se acumulan, tu cluster Kafka queda con horas de lag. &lt;strong>Rate limiting en el consumer&lt;/strong>, no en el producer.&lt;/p>
&lt;h2 id="cuándo-no-usar-qdrant-el-contrapunto-honesto">Cuándo NO usar Qdrant: el contrapunto honesto&lt;/h2>
&lt;p>Para no presentar Qdrant como bala de plata:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Tu corpus es &amp;lt;1M vectores&lt;/strong> y no esperas crecer. pgvector basta y te ahorra un servicio.&lt;/li>
&lt;li>&lt;strong>Tu equipo es pequeño y no tiene capacidad de operar un stateful service más&lt;/strong>. Qdrant añade snapshots, gRPC, mTLS, observabilidad propia. Cada uno de esos puntos es un día de trabajo de un SRE.&lt;/li>
&lt;li>&lt;strong>Tu retrieval es batch off-hours&lt;/strong>, no real-time. Si solo haces RAG para reportes nocturnos, la latencia de pgvector no duele.&lt;/li>
&lt;li>&lt;strong>Necesitas JOINs nativos&lt;/strong> entre embeddings y tablas relacionales en queries críticos. pgvector permite hacer &lt;code>JOIN documents d ON d.id = embedding.doc_id WHERE d.tenant_id = X&lt;/code>. Qdrant lo simula con payload pero menos elegante.&lt;/li>
&lt;/ul>
&lt;p>Y al revés, cuando Qdrant &lt;strong>gana claramente&lt;/strong>:&lt;/p>
&lt;ul>
&lt;li>Corpus &amp;gt;10M vectores con queries con filtros densos.&lt;/li>
&lt;li>Necesidad de hybrid search nativo (sparse + dense + multivector).&lt;/li>
&lt;li>Multi-tenant con strict latency requirements por cliente.&lt;/li>
&lt;li>Quantization agresiva para mantener todo en RAM en hardware limitado.&lt;/li>
&lt;li>Cluster mode con sharding horizontal real.&lt;/li>
&lt;/ul>
&lt;h2 id="lo-que-no-hemos-cubierto">Lo que no hemos cubierto&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>Migración pgvector → Qdrant en vivo&lt;/strong>: patrón con dual-read durante la transición.&lt;/li>
&lt;li>&lt;strong>Vector search federation&lt;/strong>: queries que cruzan múltiples Qdrant collections o múltiples vector stores. Tema propio.&lt;/li>
&lt;li>&lt;strong>Multi-tenancy en Qdrant&lt;/strong>: payload filters + namespace isolation + per-tenant rate limiting.&lt;/li>
&lt;li>&lt;strong>Cold storage para vectores antiguos&lt;/strong>: archivo de partitions a object storage con índice secundario.&lt;/li>
&lt;li>&lt;strong>Embedding model self-hosted con vLLM&lt;/strong>: alternativa a OpenAI API que reduce coste y mejora privacidad. Tema cruzado con la serie de inferencia.&lt;/li>
&lt;/ul>
&lt;h2 id="referencias">Referencias&lt;/h2>
&lt;p>PostgreSQL y pgvector:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://github.com/pgvector/pgvector">PostgreSQL pgvector extension (GitHub)&lt;/a> — el de toda la vida.&lt;/li>
&lt;li>&lt;a href="https://www.tigerdata.com/blog/pgvector-vs-qdrant">Pgvector vs Qdrant (Tiger Data)&lt;/a> — comparativa con números.&lt;/li>
&lt;li>&lt;a href="https://qdrant.tech/blog/pgvector-tradeoffs/">Start with pgvector: Why You&amp;rsquo;ll Outgrow It Faster Than You Think (Qdrant blog)&lt;/a> — los tradeoffs honestos desde Qdrant.&lt;/li>
&lt;/ul>
&lt;p>Qdrant:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://qdrant.tech/">Qdrant — sitio oficial&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://qdrant.tech/documentation/private-cloud/changelog/">Qdrant changelog&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://qdrant.tech/articles/sparse-vectors/">Sparse Vectors in Qdrant&lt;/a> — hybrid search nativo.&lt;/li>
&lt;li>&lt;a href="https://qdrant.tech/documentation/tutorials-search-engineering/using-multivector-representations/">Multivectors and Late Interaction&lt;/a> — ColBERT-style.&lt;/li>
&lt;li>&lt;a href="https://qdrant.tech/blog/2025-recap/">Qdrant 2025 Recap: Powering the Agentic Era&lt;/a> — estado del proyecto y roadmap.&lt;/li>
&lt;/ul>
&lt;p>Outbox y CDC:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://debezium.io/blog/2019/02/19/reliable-microservices-data-exchange-with-the-outbox-pattern/">Reliable Microservices Data Exchange With the Outbox Pattern (Debezium blog)&lt;/a> — el post canónico.&lt;/li>
&lt;li>&lt;a href="https://streamkap.com/resources-and-guides/outbox-pattern-explained">The Outbox Pattern Explained (Streamkap)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://thorben-janssen.com/outbox-pattern-with-cdc-and-debezium/">Outbox Pattern with Debezium (Thorben Janssen)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://debezium.io/blog/2020/02/10/event-sourcing-vs-cdc/">Distributed Data for Microservices — Event Sourcing vs CDC (Debezium blog)&lt;/a> — comparativa entre patrones.&lt;/li>
&lt;/ul>
&lt;p>Comparativas 2026:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://www.knowsync.ai/blog/choosing-vector-database-qdrant-pinecone-pgvector-2026">Choosing Your Vector Database: Qdrant vs Pinecone vs pgvector in 2026 (KnowSync)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://open-techstack.com/blog/pgvector-vs-qdrant-2026/">pgvector vs Qdrant: Production Tradeoffs 2026 (Open Techstack)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://markaicode.com/vs/qdrant-vs-pgvector/">qdrant vs pgvector: Which Vector Database Should You Choose in 2026 (Markaicode)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://www.marktechpost.com/2026/05/10/best-vector-databases-in-2026-pricing-scale-limits-and-architecture-tradeoffs-across-nine-leading-systems/">Best Vector Databases in 2026: Pricing, Scale Limits, Architecture Tradeoffs (MarkTechPost)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://callsphere.ai/blog/vector-database-benchmarks-2026-pgvector-qdrant-weaviate-milvus-lancedb">Vector Database Benchmarks 2026: pgvector 0.9, Qdrant, Weaviate, Milvus, LanceDB (CallSphere)&lt;/a>.&lt;/li>
&lt;/ul>
&lt;p>Cross-references:&lt;/p>
&lt;ul>
&lt;li>Post anterior: &lt;a href="https://blog.lo0.es/posts/pipeline-llmops-seis-etapas/">Pipeline LLMOps de 6 etapas&lt;/a> — donde definimos el mini-mapa.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/rag-kafka-datalake-arquitectura/">RAG sobre Kafka — arquitectura técnica&lt;/a> — la pieza que precede a Qdrant en el pipeline.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/mlops-llms-panorama-2026/">Panorama MLOps LLMs 2026&lt;/a> — el marco general.&lt;/li>
&lt;li>Series previas: &lt;a href="https://blog.lo0.es/posts/evals-llm-la-capa-despues-de-tracing/">post-tracing&lt;/a> y &lt;a href="https://blog.lo0.es/posts/ebpf-cilium-tcp-ip-bypass/">eBPF&lt;/a>.&lt;/li>
&lt;/ul></description></item><item><title>RAG sobre Kafka: arquitectura técnica de referencia para datalakes en streaming, con embeddings frescos y vector stores siempre al día</title><link>https://blog.lo0.es/posts/rag-kafka-datalake-arquitectura/</link><pubDate>Thu, 21 May 2026 06:00:00 +0200</pubDate><guid>https://blog.lo0.es/posts/rag-kafka-datalake-arquitectura/</guid><description>&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>La pieza que más bloquea proyectos GenAI empresariales en 2026 no es el modelo, ni siquiera los guardrails: es la &lt;strong>ingestión de datos para RAG&lt;/strong>. Las empresas tienen información valiosa en bases de datos OLTP, en logs operacionales, en sistemas SaaS, y todo eso está silenciosamente cambiando cada segundo. Los RAG batch que se reindexan cada noche llegan tarde —la respuesta del modelo está respaldada en un snapshot de hace 18 horas— y dan paso a alucinaciones operacionales aunque el retriever sea perfecto. La respuesta dominante en producción en 2026 es montar la &lt;strong>pieza RAG sobre Kafka como source-of-truth&lt;/strong>: log inmutable, throughput masivo, schema evolution gestionada, y un ecosistema de stream processing maduro (Flink, Kafka Streams, RisingWave) que permite &lt;strong>transformar y embedder eventos a medida que ocurren&lt;/strong>, llevándolos en milisegundos a vector stores (Milvus, Qdrant, Weaviate, pgvector). El patrón canónico: &lt;strong>origen → CDC con Debezium → topics Kafka → Flink SQL con embedding UDF → sink connector a vector store → serving con vLLM o equivalente&lt;/strong>. Las novedades 2026 que cambian el juego: &lt;strong>Confluent Tableflow&lt;/strong> convierte topics Kafka en tablas Iceberg/Delta automáticamente (lectura desde Snowflake/Databricks/Trino sin ETL, 30-50% menos TCO); &lt;strong>Flink SQL nativo&lt;/strong> trae &lt;code>openai_embedding()&lt;/code> y vector search integrado con Cosmos DB y Amazon S3 Vectors; el &lt;strong>MCP server oficial de Confluent&lt;/strong> permite a agentes IA consultar Kafka/Flink/Tableflow en lenguaje natural. Este post desarrolla la arquitectura end-to-end con manifests, código Flink SQL y números concretos.&lt;/p>
&lt;blockquote>
&lt;p>Este es el &lt;strong>segundo post de la serie MLOps específico para LLMs&lt;/strong>. El primero (&lt;a href="https://blog.lo0.es/posts/mlops-llms-panorama-2026/">Panorama 2026&lt;/a>) estableció el marco. Aquí bajamos a la pieza más operacional del stack: cómo se conecta un sistema empresarial real a un agente LLM &lt;strong>manteniendo el RAG fresco&lt;/strong> sin caer en complejidad explosiva.&lt;/p>
&lt;/blockquote>
&lt;h2 id="la-analogía-kafka-como-el-single-source-of-truth">La analogía: Kafka como el &amp;ldquo;single source of truth&amp;rdquo;&lt;/h2>
&lt;p>Quien lleva tiempo en sistemas distribuidos ha visto el patrón una y otra vez: &lt;strong>un log inmutable, append-only, replicado, ordenado en el tiempo&lt;/strong> se ha vuelto la primitiva canónica para reconstruir sistemas complejos. Los DBAs lo conocen como &lt;strong>write-ahead log&lt;/strong> (PostgreSQL WAL, MySQL binlog). Los desarrolladores de sistemas de eventos lo conocen como &lt;strong>event sourcing&lt;/strong>. Los arquitectos de datos lo conocen como &lt;strong>Kappa architecture&lt;/strong>. Kafka es la implementación masiva, distribuida y madura de esa primitiva: un log que vive en disco, particionado para escalar, replicado para durabilidad, retenido por tiempo o tamaño, &lt;strong>legible desde cualquier punto histórico&lt;/strong>.&lt;/p>
&lt;p>Cuando se piensa en RAG, esto es &lt;strong>exactamente&lt;/strong> lo que se necesita. Un sistema RAG bien diseñado tiene dos preguntas críticas: ¿cómo se mantiene fresco el índice? y ¿cómo se reconstruye el índice cuando algo se rompe? Las dos las contesta Kafka de manera natural: &lt;strong>fresco&lt;/strong> porque cada cambio en el origen se publica como evento al log y el pipeline lo procesa en milisegundos; &lt;strong>reconstruible&lt;/strong> porque el log entero está ahí: borras el vector store, dispones del topic Kafka desde el offset 0 y vuelves a construir el índice tal como estaba.&lt;/p>
&lt;p>Hay además una segunda capa de analogía. Kafka, para una arquitectura GenAI moderna, juega el papel del &lt;strong>WAL del sistema entero&lt;/strong>. Igual que el WAL de Postgres es el evangelio del estado de la base de datos —si pierdes la DB pero conservas el WAL, puedes reconstruirla—, el log de Kafka es el evangelio del estado del &lt;strong>conjunto del negocio&lt;/strong>: pedidos, usuarios, transacciones, documentos. Conectar tu agente IA a Kafka es conectarlo al pulso real del sistema, no a snapshots obsoletos.&lt;/p>
&lt;h2 id="el-problema-del-rag-estático">El problema del RAG estático&lt;/h2>
&lt;p>Antes de presentar la arquitectura, vale la pena fijar &lt;strong>qué problema concreto&lt;/strong> estamos resolviendo. El antipattern que tropieza a la mayoría de proyectos GenAI:&lt;/p>
&lt;ol>
&lt;li>Equipo construye RAG sobre un dataset estático: vuelca documentos de Confluence, PDFs de productos, snapshots de base de datos.&lt;/li>
&lt;li>Lo embedea con un cron nocturno que regenera el índice cada 24 horas.&lt;/li>
&lt;li>Lanza el producto.&lt;/li>
&lt;li>&lt;strong>Día 2&lt;/strong>: usuario pregunta sobre un cambio que ocurrió hace dos horas. El RAG no lo tiene; el modelo responde sobre la versión vieja.&lt;/li>
&lt;li>Equipo añade lógica frágil: &amp;ldquo;si la query menciona una fecha reciente, escalar a un agente humano&amp;rdquo;.&lt;/li>
&lt;li>&lt;strong>Día 30&lt;/strong>: el dataset se ha movido tanto que media RAG está desactualizado. El equipo decide refactor y migrar a streaming.&lt;/li>
&lt;/ol>
&lt;p>Es la historia repetida de tantos proyectos que el ecosistema ha aprendido la lección: &lt;strong>streaming desde el día 1&lt;/strong>, aunque el volumen sea bajo. La complejidad operacional de un pipeline streaming bien diseñado es &lt;strong>constante&lt;/strong>; la complejidad de migrar de batch a streaming en proyecto vivo es &lt;strong>enorme&lt;/strong>.&lt;/p>
&lt;h2 id="del-lambda-al-kappa-al-streaming-rag">Del Lambda al Kappa al Streaming RAG&lt;/h2>
&lt;p>Tres arquitecturas en orden histórico:&lt;/p>
&lt;p>&lt;strong>Lambda (clásica de big data 2014)&lt;/strong>: dos pipelines paralelos, uno batch para precisión y uno streaming para freshness. La consulta combina ambos. Funciona pero exige mantener dos pipelines.&lt;/p>
&lt;p>&lt;strong>Kappa (Jay Kreps 2014, mainstream desde 2020)&lt;/strong>: solo un pipeline streaming. El batch es un caso particular del streaming (reprocesar desde el principio). Simplifica mucho.&lt;/p>
&lt;p>&lt;strong>Streaming RAG (emergente 2025-2026)&lt;/strong>: variante específica de Kappa donde el output del pipeline son &lt;strong>embeddings indexados en un vector store&lt;/strong> que el LLM consulta en runtime. El log Kafka es la &lt;strong>fuente de verdad&lt;/strong>, el vector store es un &lt;strong>proyección consultable&lt;/strong>.&lt;/p>
&lt;p>La conversión mental: piensa en el vector store como la &lt;strong>vista materializada&lt;/strong> del log Kafka. Si la vista se corrompe, la reconstruyes desde el log. Si quieres una vista nueva (otro embedding model, otro chunking strategy), creas otro consumer del log y construyes una segunda vista en paralelo.&lt;/p>
&lt;h2 id="la-arquitectura-de-referencia">La arquitectura de referencia&lt;/h2>
&lt;p>Vamos al diagrama. Voy a presentar la arquitectura canónica que se ha estabilizado en 2026, mostrando dónde encaja cada componente:&lt;/p>
&lt;pre tabindex="0">&lt;code>[OLTP DB (Postgres)] [Otros origenes]
│ │
│ WAL via logical decoding │
▼ ▼
┌──────────────────────────────────────────────────────────┐
│ Debezium / Kafka Connect (Sources) │
└──────────────────────────────────────────────────────────┘
│
▼ produce eventos
┌──────────────────────────────────────────────────────────┐
│ Kafka cluster │
│ ┌───────────────────────────────────────────────────┐ │
│ │ topic: orders.raw (3 particiones, RF=3) │ │
│ │ topic: users.raw (3 particiones, RF=3) │ │
│ │ topic: documents.raw (6 particiones, RF=3) │ │
│ └───────────────────────────────────────────────────┘ │
│ + Schema Registry (Avro/Protobuf) │
└──────────────────────────────────────────────────────────┘
│
▼ consume y transforma
┌──────────────────────────────────────────────────────────┐
│ Flink SQL streaming jobs │
│ - chunking text │
│ - llamadas a embedding model (UDF) │
│ - enriquecimiento con metadata │
│ - sink a topic curado: documents.embedded │
└──────────────────────────────────────────────────────────┘
│
┌───────────┼────────────────────┐
▼ ▼ ▼
[Vector store] [Tableflow] [Iceberg/Delta]
Milvus/Qdrant auto-convert para analytics
/pgvector/ topics →
Weaviate tables
│
▼ consultado en runtime
┌──────────────────────────────────────────────────────────┐
│ LLM serving (vLLM / SGLang) + Retriever │
│ - recibe query del agente │
│ - busca top-K en vector store │
│ - construye prompt + contexto │
│ - genera respuesta con citas │
└──────────────────────────────────────────────────────────┘
&lt;/code>&lt;/pre>&lt;p>Las &lt;strong>cinco capas&lt;/strong> que ves —&lt;strong>fuente, ingestión (CDC), transporte (Kafka), procesamiento (Flink), almacenamiento (vector + tablas)&lt;/strong>— son las que estructuran cualquier RAG sobre datalake serio en 2026. Vamos a cada una.&lt;/p>
&lt;h2 id="capa-1--fuentes-tu-oltp-como-punto-de-partida">Capa 1 — Fuentes: tu OLTP como punto de partida&lt;/h2>
&lt;p>La fuente típica es una &lt;strong>base de datos OLTP&lt;/strong> (Postgres, MySQL, SQL Server). Es donde vive el estado vivo del negocio. La técnica para extraer cambios en tiempo real es &lt;strong>Change Data Capture (CDC)&lt;/strong>: leer el log de transacciones de la base de datos (PostgreSQL WAL, MySQL binlog) y convertir cada commit en un evento Kafka.&lt;/p>
&lt;p>El estándar OSS es &lt;strong>&lt;a href="https://debezium.io/">Debezium&lt;/a>&lt;/strong>. Soporta Postgres, MySQL, SQL Server, MongoDB, Oracle, Cassandra y otros. Despliegue típico como cluster Kafka Connect con conectores Debezium.&lt;/p>
&lt;p>Ejemplo de configuración Debezium para PostgreSQL:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;postgres-orders-connector&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;config&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;connector.class&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;io.debezium.connector.postgresql.PostgresConnector&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;tasks.max&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;1&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;database.hostname&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;postgres.prod.internal&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;database.port&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;5432&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;database.user&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;debezium&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;database.password&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;${secret:postgres-creds}&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;database.dbname&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;ecommerce&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;database.server.name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;ecommerce-prod&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;table.include.list&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;public.orders,public.users,public.products&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;publication.autocreate.mode&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;filtered&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;slot.name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;debezium_slot&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;plugin.name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;pgoutput&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;topic.prefix&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;ecommerce&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;key.converter&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;io.confluent.connect.avro.AvroConverter&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;value.converter&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;io.confluent.connect.avro.AvroConverter&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;key.converter.schema.registry.url&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;http://schema-registry:8081&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;value.converter.schema.registry.url&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;http://schema-registry:8081&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Esto produce, por cada commit en la base de datos, un evento Avro al topic correspondiente (&lt;code>ecommerce.public.orders&lt;/code>, &lt;code>ecommerce.public.users&lt;/code>, etc.) con el cambio: tipo (INSERT/UPDATE/DELETE), valores antes y después, timestamp del commit, posición en el WAL.&lt;/p>
&lt;p>&lt;strong>Alternativa más simple para 2026&lt;/strong>: &lt;a href="https://risingwave.com/">RisingWave&lt;/a> puede leerse el WAL de Postgres &lt;strong>directamente, sin Debezium ni Kafka Connect intermedio&lt;/strong>. Cuando el caso es solo CDC sin más fuentes, es operacionalmente más simple. Para arquitecturas con múltiples fuentes (CDC + APIs + scrapers + logs), Debezium sigue siendo la pieza estándar.&lt;/p>
&lt;h2 id="capa-2--kafka-como-transporte-y-persistencia">Capa 2 — Kafka como transporte y persistencia&lt;/h2>
&lt;p>El cluster Kafka es donde aterrizan todos los eventos. Decisiones operativas clave:&lt;/p>
&lt;h3 id="topics-raw-vs-curated">Topics: raw vs curated&lt;/h3>
&lt;p>Convención que se ha establecido en 2026:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>&lt;code>*.raw&lt;/code>&lt;/strong>: el evento crudo tal como llegó. CDC sin transformar, log de aplicación sin parsear.&lt;/li>
&lt;li>&lt;strong>&lt;code>*.cleaned&lt;/code>&lt;/strong>: tras dedup, validación de schema, normalización de tipos.&lt;/li>
&lt;li>&lt;strong>&lt;code>*.enriched&lt;/code>&lt;/strong>: tras añadir metadatos (geolocalización, identificadores cruzados, etc.).&lt;/li>
&lt;li>&lt;strong>&lt;code>*.embedded&lt;/code>&lt;/strong>: el evento con su vector embedding ya calculado.&lt;/li>
&lt;/ul>
&lt;p>Multi-stage topics permite &lt;strong>debug por capa&lt;/strong> y &lt;strong>reprocesamiento parcial&lt;/strong>: si cambias el embedding model, descartar &lt;code>*.embedded&lt;/code> y reconstruir desde &lt;code>*.enriched&lt;/code> cuesta horas; reconstruir desde &lt;code>*.raw&lt;/code> cuesta días.&lt;/p>
&lt;h3 id="schema-registry">Schema Registry&lt;/h3>
&lt;p>Sin &lt;strong>schema registry&lt;/strong>, los topics se rompen silenciosamente cuando alguien cambia el schema en origen. &lt;a href="https://docs.confluent.io/platform/current/schema-registry/index.html">&lt;strong>Confluent Schema Registry&lt;/strong>&lt;/a> o el OSS &lt;a href="https://www.apicur.io/registry/">Apicurio&lt;/a> son las opciones dominantes.&lt;/p>
&lt;p>Formatos comunes:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Avro&lt;/strong>: schema versionado, evolution rules estrictas. El default histórico.&lt;/li>
&lt;li>&lt;strong>Protobuf&lt;/strong>: compatible con stacks gRPC, buena performance.&lt;/li>
&lt;li>&lt;strong>JSON Schema&lt;/strong>: textual, debuggable a ojo, menos eficiente.&lt;/li>
&lt;/ul>
&lt;p>Para RAG sobre Kafka recomendamos &lt;strong>Avro&lt;/strong> por defecto. Schema evolution es importante porque las tablas origen cambian con el tiempo, y un esquema sin versión rompe consumidores aguas abajo.&lt;/p>
&lt;h3 id="particiones-replicación-y-retención">Particiones, replicación y retención&lt;/h3>
&lt;p>Decisiones operativas para topics de RAG:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Particiones&lt;/strong>: típicamente 3-12. Más particiones = más paralelismo en consumer Flink, pero más overhead. La regla del pulgar: &lt;strong>particiones = pico esperado de eventos/s ÷ 1000&lt;/strong>.&lt;/li>
&lt;li>&lt;strong>Replication factor&lt;/strong>: 3 mínimo en producción. La replicación protege contra fallo de broker; con RAG el coste de perder un topic puede ser semanas de re-embedding.&lt;/li>
&lt;li>&lt;strong>Retención&lt;/strong>: para topics que alimentan RAG, &lt;strong>retención larga&lt;/strong> o &lt;strong>compactada por key&lt;/strong>. Si el documento &lt;code>doc-42&lt;/code> cambia 100 veces, compactación solo guarda el último estado por key, dejando un log más pequeño y reconstruible. Para datos que no se actualizan (logs históricos), retención por tiempo (90 días, 1 año).&lt;/li>
&lt;/ul>
&lt;h3 id="replicación-cross-cluster">Replicación cross-cluster&lt;/h3>
&lt;p>Para deployments multi-región o multi-cloud, &lt;strong>MirrorMaker 2&lt;/strong> o &lt;strong>&lt;a href="https://docs.confluent.io/platform/current/multi-dc-deployments/cluster-linking/index.html">Cluster Linking&lt;/a>&lt;/strong> (Confluent) replican topics entre clusters Kafka. El RAG puede consultar el cluster local sin tener que cruzar región.&lt;/p>
&lt;h2 id="capa-3--flink-como-procesador-streaming">Capa 3 — Flink como procesador streaming&lt;/h2>
&lt;p>&lt;a href="https://flink.apache.org/">Apache Flink&lt;/a> es la pieza dominante de stream processing en 2026. Apache 2.0, distribución mature, ecosistema amplio. La alternativa principal es Kafka Streams (más simple, Java-only); RisingWave es la opción emergente para casos SQL puros.&lt;/p>
&lt;p>Lo que Flink añade a Kafka:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Stateful streaming&lt;/strong>: agregaciones temporales, joins entre streams, sesiones.&lt;/li>
&lt;li>&lt;strong>Exactly-once semantics&lt;/strong>: con checkpoint coordination.&lt;/li>
&lt;li>&lt;strong>Watermarks&lt;/strong>: handling correcto de eventos out-of-order.&lt;/li>
&lt;li>&lt;strong>UDFs en Python/Java&lt;/strong>: incluyendo llamadas a modelos LLM.&lt;/li>
&lt;/ul>
&lt;h3 id="flink-sql-la-pieza-más-operacional">Flink SQL: la pieza más operacional&lt;/h3>
&lt;p>Flink SQL es la pieza más usable de Flink para data engineers que no son streaming experts. Veamos un ejemplo realista de pipeline RAG:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="cl">&lt;span class="c1">-- 1. Definir la fuente: topic Kafka con eventos CDC de documentos
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">CREATE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">TABLE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">documents_raw&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">doc_id&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">STRING&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">title&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">STRING&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">body&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">STRING&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">category&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">STRING&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">updated_at&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">TIMESTAMP_LTZ&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">3&lt;/span>&lt;span class="p">),&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">PRIMARY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">KEY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">doc_id&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NOT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">ENFORCED&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">WITH&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;connector&amp;#39;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;upsert-kafka&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;topic&amp;#39;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;ecommerce.public.documents&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;properties.bootstrap.servers&amp;#39;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;kafka:9092&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;key.format&amp;#39;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;avro-confluent&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;value.format&amp;#39;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;avro-confluent&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;value.fields-include&amp;#39;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;EXCEPT_KEY&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1">-- 2. Definir el sink: vector store via Kafka topic intermedio
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">CREATE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">TABLE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">documents_embedded&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">doc_id&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">STRING&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">chunk_id&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">INT&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">title&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">STRING&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">chunk_text&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">STRING&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">category&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">STRING&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">embedding&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">ARRAY&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="nb">FLOAT&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">embedded_at&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">TIMESTAMP_LTZ&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">3&lt;/span>&lt;span class="p">),&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">PRIMARY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">KEY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">doc_id&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">chunk_id&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NOT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">ENFORCED&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">WITH&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;connector&amp;#39;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;upsert-kafka&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;topic&amp;#39;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;rag.documents.embedded&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;properties.bootstrap.servers&amp;#39;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;kafka:9092&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;key.format&amp;#39;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;json&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;value.format&amp;#39;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;json&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1">-- 3. UDF para chunking (definida en Python o Java)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">-- CREATE TEMPORARY FUNCTION chunk_text AS &amp;#39;com.example.ChunkingUDF&amp;#39;;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1">-- 4. Pipeline: chunkear, embedder, escribir al sink
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">INSERT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">INTO&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">documents_embedded&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">SELECT&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">doc_id&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">chunk_idx&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">AS&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">chunk_id&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">title&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">chunk&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">AS&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">chunk_text&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">category&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">OPENAI_EMBEDDING&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">chunk&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;text-embedding-3-small&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">AS&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">embedding&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">CURRENT_TIMESTAMP&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">AS&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">embedded_at&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">documents_raw&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">CROSS&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">JOIN&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">UNNEST&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">chunk_text&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">body&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">512&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">64&lt;/span>&lt;span class="p">))&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">WITH&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">ORDINALITY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">AS&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">t&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">chunk&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">chunk_idx&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Lo que pasa aquí, línea a línea:&lt;/p>
&lt;ul>
&lt;li>La tabla &lt;code>documents_raw&lt;/code> lee el topic CDC en modo &lt;strong>upsert-kafka&lt;/strong> (cada nuevo evento por la misma key reemplaza el anterior). Esto refleja correctamente la semántica &amp;ldquo;esta es la última versión del doc 42&amp;rdquo;.&lt;/li>
&lt;li>La tabla &lt;code>documents_embedded&lt;/code> será el topic intermedio donde Flink escribe los chunks embedded.&lt;/li>
&lt;li>La UDF &lt;code>chunk_text&lt;/code> (definida en Python o Java) divide cada doc en chunks de 512 tokens con overlap de 64.&lt;/li>
&lt;li>La consulta &lt;code>INSERT INTO&lt;/code> se ejecuta continuamente: cada evento nuevo en &lt;code>documents_raw&lt;/code> se chunkea, cada chunk se embedea con &lt;code>OPENAI_EMBEDDING&lt;/code> (función built-in de Flink SQL en Confluent Cloud 2026), y se escribe al topic embedded.&lt;/li>
&lt;/ul>
&lt;p>&lt;code>OPENAI_EMBEDDING&lt;/code> puede sustituirse por una función custom que llame a un modelo self-hosted (vLLM con un encoder), a SentenceTransformers, o a un servicio managed. La sintaxis es la misma; cambias el provider.&lt;/p>
&lt;h3 id="watermarks-y-late-events">Watermarks y late events&lt;/h3>
&lt;p>Para casos donde un evento puede llegar tarde (eg el WAL de Postgres se retrasa porque hubo un network blip), Flink permite definir &lt;strong>watermarks&lt;/strong>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="cl">&lt;span class="k">CREATE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">TABLE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">documents_raw&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">doc_id&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">STRING&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">title&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">STRING&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">body&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">STRING&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">updated_at&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">TIMESTAMP_LTZ&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">3&lt;/span>&lt;span class="p">),&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">WATERMARK&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">FOR&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">updated_at&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">AS&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">updated_at&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">INTERVAL&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;5&amp;#39;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">MINUTE&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">WITH&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(...)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Esto le dice a Flink &amp;ldquo;asume que ningún evento llega más de 5 minutos tarde respecto al timestamp del evento&amp;rdquo;. Para joins y agregaciones temporales, Flink usa el watermark para decidir cuándo &amp;ldquo;cerrar&amp;rdquo; una ventana.&lt;/p>
&lt;h2 id="capa-4--sinks-a-vector-stores">Capa 4 — Sinks a vector stores&lt;/h2>
&lt;p>El último paso es indexar los embeddings en un vector store. Tres patrones en 2026:&lt;/p>
&lt;h3 id="patrón-a--kafka-connect-sink-directo">Patrón A — Kafka Connect sink directo&lt;/h3>
&lt;p>Cada vector store tiene su connector oficial:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>&lt;a href="https://github.com/zilliztech/kafka-connect-milvus">Milvus&lt;/a>&lt;/strong>: sink connector oficial de Zilliz. Soporta named/unnamed dense/sparse vectors.&lt;/li>
&lt;li>&lt;strong>&lt;a href="https://github.com/qdrant/qdrant-kafka">Qdrant&lt;/a>&lt;/strong>: sink connector oficial. Soporta dense, sparse, multi-vector.&lt;/li>
&lt;li>&lt;strong>pgvector&lt;/strong>: no tiene connector dedicado, pero se usa el &lt;a href="https://www.confluent.io/hub/confluentinc/kafka-connect-jdbc">JDBC Sink Connector&lt;/a> con SQL custom.&lt;/li>
&lt;li>&lt;strong>Weaviate&lt;/strong>: connector community.&lt;/li>
&lt;li>&lt;strong>LanceDB&lt;/strong>: connector community.&lt;/li>
&lt;/ul>
&lt;p>Ejemplo de configuración Milvus sink:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;milvus-rag-embeddings-sink&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;config&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;connector.class&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;com.milvus.io.kafka.MilvusSinkConnector&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;tasks.max&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;3&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;topics&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;rag.documents.embedded&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;milvus.host&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;milvus.prod.internal&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;milvus.port&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;19530&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;milvus.collection.name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;documents&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;milvus.collection.dim&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;1536&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;milvus.collection.partition&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;default&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;key.converter&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;org.apache.kafka.connect.storage.StringConverter&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;value.converter&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;org.apache.kafka.connect.json.JsonConverter&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;value.converter.schemas.enable&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">false&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Tres tasks en paralelo (&lt;code>tasks.max: 3&lt;/code>) consumen el topic embedded y escriben a la colección Milvus. La latencia desde &amp;ldquo;evento en Kafka&amp;rdquo; hasta &amp;ldquo;vector indexable en Milvus&amp;rdquo; es típicamente &lt;strong>&amp;lt;5 segundos&lt;/strong>.&lt;/p>
&lt;h3 id="patrón-b--pgvector-con-cdc-pipe-directo">Patrón B — pgvector con CDC pipe directo&lt;/h3>
&lt;p>Para equipos que ya viven en PostgreSQL, &lt;strong>pgvector&lt;/strong> es la opción de menor fricción. Patrón: el mismo cluster Postgres origen tiene una segunda DB para embeddings con extensión pgvector activada; el pipeline Flink escribe directamente vía JDBC.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="cl">&lt;span class="c1">-- En el cluster Postgres con pgvector activado
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">CREATE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">EXTENSION&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">IF&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NOT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">EXISTS&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">vector&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">CREATE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">TABLE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">document_embeddings&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">doc_id&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">TEXT&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">chunk_id&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">INT&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">chunk_text&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">TEXT&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">category&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">TEXT&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">embedding&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">vector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">1536&lt;/span>&lt;span class="p">),&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">embedded_at&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">TIMESTAMP&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">PRIMARY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">KEY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">doc_id&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">chunk_id&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">CREATE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">INDEX&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">ON&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">document_embeddings&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">USING&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">hnsw&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">embedding&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">vector_cosine_ops&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">WITH&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">m&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">16&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">ef_construction&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">64&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Ventajas: tu mismo DBA opera todo, transactionality cross-tables, joins con metadatos relacionales triviales. Limitación: a &amp;gt;10M vectores, el rendimiento de pgvector empieza a ceder respecto a sistemas dedicados.&lt;/p>
&lt;h3 id="patrón-c--confluent-tableflow--iceberg--vector-search-flink-sql">Patrón C — Confluent Tableflow → Iceberg + vector search Flink SQL&lt;/h3>
&lt;p>Esta es la novedad 2026 que cambia la mecánica. &lt;a href="https://www.confluent.io/product/tableflow/">Confluent Tableflow&lt;/a> materializa &lt;strong>automáticamente&lt;/strong> topics Kafka como tablas &lt;strong>Apache Iceberg&lt;/strong> o &lt;strong>Delta Lake&lt;/strong>. Características:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Sin pipeline ETL&lt;/strong>: no escribes Flink/Spark jobs para mover Kafka a tabla. Lo hace Tableflow.&lt;/li>
&lt;li>&lt;strong>Schema evolution automática&lt;/strong>: cambios en el schema del topic se reflejan en la tabla.&lt;/li>
&lt;li>&lt;strong>Catálogo unificado&lt;/strong>: la tabla aparece en Glue, Unity Catalog, Snowflake, Databricks. Cualquier motor analítico la consulta sin copiar datos.&lt;/li>
&lt;li>&lt;strong>CDC nativo&lt;/strong>: maneja inserts, updates, deletes correctamente.&lt;/li>
&lt;li>&lt;strong>30-50% menos TCO&lt;/strong> según las cifras que Confluent publica vs pipelines tradicionales.&lt;/li>
&lt;/ul>
&lt;p>Y desde 2026, Tableflow + Flink SQL ofrecen &lt;strong>vector search nativo integrado con Cosmos DB y Amazon S3 Vectors&lt;/strong>. La consulta RAG se puede hacer directamente en Flink SQL:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="cl">&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">doc_id&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">chunk_text&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">category&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">documents_embedded&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">WHERE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">VECTOR_SEARCH&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">embedding&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">OPENAI_EMBEDDING&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;query del usuario&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;text-embedding-3-small&amp;#39;&lt;/span>&lt;span class="p">),&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">top_k&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">10&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="mi">7&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">ORDER&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">BY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">VECTOR_SEARCH_SCORE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">DESC&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Esto unifica capas que antes eran separadas (vector store + analytics). Para muchos casos, &lt;strong>elimina&lt;/strong> la necesidad de mantener un vector store dedicado.&lt;/p>
&lt;h2 id="el-mcp-server-oficial-de-confluent">El MCP server oficial de Confluent&lt;/h2>
&lt;p>Una pieza añadida en 2026 que merece mención: Confluent ha publicado &lt;strong>un MCP server oficial&lt;/strong> que expone Kafka, Flink y Tableflow como tools accesibles a agentes IA vía MCP. Cualquier MCP client (Claude Desktop, Cursor, agentes propios) puede:&lt;/p>
&lt;ul>
&lt;li>Listar topics, leer mensajes recientes, publicar a topics.&lt;/li>
&lt;li>Ejecutar queries Flink SQL en lenguaje natural (&amp;ldquo;dame las órdenes de las últimas 24 horas con valor &amp;gt; 1000€&amp;rdquo;).&lt;/li>
&lt;li>Consultar tablas Tableflow Iceberg.&lt;/li>
&lt;li>Gestionar conectores Kafka Connect.&lt;/li>
&lt;/ul>
&lt;p>Esto cierra el círculo: tu agente IA, además de &lt;strong>leer datos&lt;/strong> del datalake vía RAG (con vector search), puede &lt;strong>escribir datos&lt;/strong> al log (vía MCP) y disparar transformaciones (vía Flink SQL en natural language). Es el punto de fusión más profundo entre LLM ops y data ops del año.&lt;/p>
&lt;p>Conexión con la serie anterior: este MCP server emite traces con las OpenTelemetry GenAI MCP semantic conventions que cubrimos en el &lt;a href="https://blog.lo0.es/posts/mcp-observability-otel/">post de MCP observability&lt;/a>. Los spans aparecen en Langfuse, Phoenix o tu OTel backend con la cardinalidad correcta. Cero código de instrumentación.&lt;/p>
&lt;h2 id="vector-stores-comparativa-2026">Vector stores: comparativa 2026&lt;/h2>
&lt;p>Las cinco opciones dominantes:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Vector store&lt;/th>
&lt;th>Licencia&lt;/th>
&lt;th>Operación&lt;/th>
&lt;th>Cuándo encaja&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>pgvector&lt;/strong>&lt;/td>
&lt;td>Postgres ext, OSS&lt;/td>
&lt;td>Tu DBA&lt;/td>
&lt;td>&amp;lt;10M vectores, equipo Postgres-heavy&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Qdrant&lt;/strong>&lt;/td>
&lt;td>Apache 2.0&lt;/td>
&lt;td>Self-host o managed&lt;/td>
&lt;td>Mid-scale, foco performance&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Milvus&lt;/strong>&lt;/td>
&lt;td>Apache 2.0&lt;/td>
&lt;td>Self-host o Zilliz Cloud&lt;/td>
&lt;td>Large-scale, foco escalabilidad&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Weaviate&lt;/strong>&lt;/td>
&lt;td>BSD-3&lt;/td>
&lt;td>Self-host o managed&lt;/td>
&lt;td>Hybrid search nativo, semantic rich&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>LanceDB&lt;/strong>&lt;/td>
&lt;td>Apache 2.0&lt;/td>
&lt;td>Embedded o serverless&lt;/td>
&lt;td>Small-medium, simplicidad&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>La selección depende de:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Escala&lt;/strong>: pgvector se queda corto &amp;gt;10M vectores. Milvus y Qdrant escalan a billones.&lt;/li>
&lt;li>&lt;strong>Hybrid search&lt;/strong>: Weaviate trae lexical + vector nativo. Otros lo soportan pero menos integrado.&lt;/li>
&lt;li>&lt;strong>Operación&lt;/strong>: pgvector si ya tienes Postgres operado. Qdrant si quieres simplicidad. Milvus si necesitas máxima escala.&lt;/li>
&lt;li>&lt;strong>Cloud managed&lt;/strong>: Zilliz Cloud para Milvus, Qdrant Cloud para Qdrant, Pinecone si quieres SaaS puro (sin OSS detrás).&lt;/li>
&lt;/ul>
&lt;h2 id="freshness-vs-accuracy-el-trade-off-operativo">Freshness vs accuracy: el trade-off operativo&lt;/h2>
&lt;p>Una decisión crítica que cualquier sistema RAG sobre Kafka debe responder: &lt;strong>¿cuándo se considera que un nuevo documento está &amp;ldquo;live&amp;rdquo; en el índice?&lt;/strong>&lt;/p>
&lt;p>Tres opciones:&lt;/p>
&lt;p>&lt;strong>Streaming síncrono&lt;/strong>: el evento llega a Kafka, Flink lo embedea, el sink lo escribe al vector store, y solo entonces se considera live. &lt;strong>Latencia típica: 1-5 segundos&lt;/strong>. La mejor freshness. Pero si el embedding model falla o el vector store es lento, los eventos se acumulan en el topic.&lt;/p>
&lt;p>&lt;strong>Streaming asíncrono con baseline&lt;/strong>: el evento se considera live inmediatamente; un proceso de fondo lo embedea cuando puede. Mientras tanto, queries que pidan ese documento no lo encuentran. &lt;strong>Latencia típica: 5-60 segundos&lt;/strong>. Aceptable para la mayoría de aplicaciones.&lt;/p>
&lt;p>&lt;strong>Batch micro&lt;/strong>: se procesa en mini-batches cada 1-5 minutos. Menos eficiente que streaming continuo pero más estable bajo carga variable. &lt;strong>Latencia: 1-5 minutos&lt;/strong>.&lt;/p>
&lt;p>La decisión depende del SLA del producto. Para chatbots de soporte al cliente, 5-60 segundos es aceptable. Para sistemas que reaccionan a eventos críticos (precios financieros, alarmas), streaming síncrono es necesario.&lt;/p>
&lt;h2 id="schema-evolution-y-reembedding">Schema evolution y reembedding&lt;/h2>
&lt;p>Cuando el embedding model cambia (cambias de &lt;code>text-embedding-3-small&lt;/code> a &lt;code>text-embedding-3-large&lt;/code>, o pasas de OpenAI a Cohere), los vectores existentes en el índice son &lt;strong>incompatibles&lt;/strong>: dimensiones distintas, espacios semánticos distintos. La distancia entre un vector viejo y uno nuevo no significa nada.&lt;/p>
&lt;p>Patrón estándar para handle de esto: &lt;strong>dual-index&lt;/strong> durante la migración.&lt;/p>
&lt;ol>
&lt;li>&lt;strong>T0&lt;/strong>: índice activo es V1 (embedding model A).&lt;/li>
&lt;li>&lt;strong>T1&lt;/strong>: empieza pipeline paralelo que escribe a un índice V2 (embedding model B), consumiendo el topic desde offset 0 (reprocesar todo el log).&lt;/li>
&lt;li>&lt;strong>T2&lt;/strong>: V2 ha caught-up al presente.&lt;/li>
&lt;li>&lt;strong>T3&lt;/strong>: cambias el retriever para que use V2.&lt;/li>
&lt;li>&lt;strong>T4&lt;/strong>: una semana después, descartas V1.&lt;/li>
&lt;/ol>
&lt;p>El log de Kafka hace este patrón factible porque es &lt;strong>inmutable y reproducible&lt;/strong>. Sin el log, este patrón se vuelve un proyecto de migración de datos de semanas.&lt;/p>
&lt;h2 id="trampas-operativas">Trampas operativas&lt;/h2>
&lt;h3 id="topics-sin-retención-adecuada">Topics sin retención adecuada&lt;/h3>
&lt;p>Configurar topics con retención de 7 días pensando &amp;ldquo;ya tengo el vector store&amp;rdquo; lleva a perder la capacidad de reconstruir si el vector store falla. &lt;strong>Retención larga (90+ días) o compactada por key&lt;/strong> para topics que alimentan RAG.&lt;/p>
&lt;h3 id="cdc-pesado-en-cargas-pico">CDC pesado en cargas pico&lt;/h3>
&lt;p>Debezium leyendo el WAL en horas pico puede impactar performance de la base de datos origen. &lt;strong>Replica de lectura dedicada&lt;/strong> para Debezium, no la primaria de producción. O usar &lt;strong>logical replication&lt;/strong> específica solo para las tablas necesarias.&lt;/p>
&lt;h3 id="embedding-cost-run-away">Embedding cost run-away&lt;/h3>
&lt;p>&lt;code>OPENAI_EMBEDDING&lt;/code> en cada evento de un topic con millones de mensajes/día son &lt;strong>miles de USD/mes&lt;/strong>. Estrategias: filtrar antes de embedder (solo embedder lo que aporta valor); deduplicar por hash de contenido; usar embedding models open-source self-hosted (BGE, E5, GTE) cuando el coste cloud sea prohibitivo.&lt;/p>
&lt;h3 id="reembedding-lento-por-throughput-limitado">Reembedding lento por throughput limitado&lt;/h3>
&lt;p>Recalcular 10M embeddings con OpenAI API a 3000 req/min tarda &lt;strong>55 horas&lt;/strong>. Si esperas a un incidente para reembeder, son dos días sin servicio. &lt;strong>Embedding throughput es un capacity planning explícito&lt;/strong>; reservar capacity o tener un job offline pre-arrancable.&lt;/p>
&lt;h3 id="schema-breaks-aguas-abajo">Schema breaks aguas abajo&lt;/h3>
&lt;p>Un cambio en el schema del topic raw rompe Flink jobs aguas abajo. &lt;strong>Schema Registry con compatibility BACKWARD obligatoria&lt;/strong>; nunca ALLOW_ALL. Y test schema evolution en CI.&lt;/p>
&lt;h3 id="vector-store-sin-backup">Vector store sin backup&lt;/h3>
&lt;p>Tu vector store tiene 50M vectores. Es la única copia (los topics expiraron). Un fallo lo borra. &lt;strong>Vector stores deben ser backed up&lt;/strong> igual que cualquier persistencia primaria. Para Milvus/Qdrant: snapshots periódicos. Para pgvector: el propio pg_dump.&lt;/p>
&lt;h3 id="multi-region-sin-replicación-cross-cluster">Multi-region sin replicación cross-cluster&lt;/h3>
&lt;p>Tu RAG sirve a usuarios en US y EU. El vector store está en US-east. Latencia desde EU = 100ms+ por query. &lt;strong>MirrorMaker o Cluster Linking&lt;/strong> para replicar topics y vector stores en ambas regiones.&lt;/p>
&lt;h2 id="lo-que-no-hemos-cubierto">Lo que no hemos cubierto&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>Hybrid search en producción&lt;/strong>: combinar BM25/lexical + vector + reranker. Tema de su propio post.&lt;/li>
&lt;li>&lt;strong>Multimodal RAG&lt;/strong>: indexar imágenes, audio, vídeo además de texto. Embeddings multimodales (CLIP, Imagebind), arquitectura específica.&lt;/li>
&lt;li>&lt;strong>GraphRAG&lt;/strong>: usar conocimiento estructurado (knowledge graphs) además de vector retrieval. Microsoft GraphRAG, LlamaIndex KnowledgeGraphQueryEngine.&lt;/li>
&lt;li>&lt;strong>RAG con ACL multi-tenant&lt;/strong>: filtrar por permisos en runtime. Patrón con metadatos en el vector store + filtros server-side.&lt;/li>
&lt;li>&lt;strong>Query rewriting con LLM&lt;/strong>: usar un primer LLM para expandir la query antes del retrieval (HyDE, multi-query, step-back prompting).&lt;/li>
&lt;/ul>
&lt;h2 id="referencias">Referencias&lt;/h2>
&lt;p>Kafka y stream processing:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://kafka.apache.org/">Apache Kafka&lt;/a> y &lt;a href="https://debezium.io/">Debezium&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://docs.confluent.io/platform/current/schema-registry/index.html">Confluent Schema Registry&lt;/a> y &lt;a href="https://www.apicur.io/registry/">Apicurio Registry&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://flink.apache.org/">Apache Flink&lt;/a> y &lt;a href="https://nightlies.apache.org/flink/flink-docs-stable/docs/dev/table/sql/overview/">Flink SQL docs&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://risingwave.com/">RisingWave&lt;/a> — alternativa SQL streaming con embedding built-in.&lt;/li>
&lt;/ul>
&lt;p>Vector store connectors:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://github.com/zilliztech/kafka-connect-milvus">Milvus Sink Connector (Zilliz, GitHub)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://milvus.io/docs/kafka-connect-milvus.md">Connect Apache Kafka with Milvus (docs)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://github.com/qdrant/qdrant-kafka">Qdrant Kafka Sink (GitHub)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://callsphere.ai/blog/vector-database-benchmarks-2026-pgvector-qdrant-weaviate-milvus-lancedb">Vector Database Benchmarks 2026&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://streamkap.com/resources-and-guides/streaming-vector-databases-platform-comparison">Streaming to Vector Databases (Streamkap)&lt;/a>.&lt;/li>
&lt;/ul>
&lt;p>Tableflow y arquitectura 2026:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://www.confluent.io/product/tableflow/">Tableflow — Confluent&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://www.confluent.io/blog/tableflow-ga-kafka-snowflake-iceberg/">Tableflow GA: Real-Time Kafka to Iceberg (Confluent Blog)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://www.confluent.io/blog/tableflow-delta-lake-databricks-unity-catalog-ga/">Tableflow + Databricks Unity Catalog (Confluent Blog)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://www.confluent.io/blog/data-lake-governance-tableflow/">Better-Governed Data Lake Architectures with Tableflow (Confluent Blog)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://www.kai-waehner.de/blog/2025/12/10/top-trends-for-data-streaming-with-apache-kafka-and-flink-in-2026/">Top Trends for Data Streaming with Kafka and Flink in 2026 (Kai Waehner)&lt;/a>.&lt;/li>
&lt;/ul>
&lt;p>RAG streaming:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://risingwave.com/blog/rag-architecture-2026/">RAG Architecture in 2026: How to Keep Retrieval Actually Fresh (RisingWave)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://streamkap.com/resources-and-guides/streaming-to-vector-databases">Streaming CDC Events to Vector Databases (Streamkap)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://www.kai-waehner.de/blog/2023/11/08/apache-kafka-flink-vector-database-llm-real-time-genai/">Apache Kafka + Vector Database + LLM = Real-Time GenAI (Kai Waehner)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://arxiv.org/pdf/2508.05662">From Static to Dynamic: A Streaming RAG Approach (arxiv 2508.05662)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://developer.confluent.io/confluent-tutorials/gen-ai-vector-embedding/flinksql/">How to generate vector embeddings for RAG with Flink SQL (Confluent Developer)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://dasroot.net/posts/2026/03/event-driven-architectures-ai-pipelines-kafka-flink/">Event-Driven Architectures for AI Pipelines (dasroot)&lt;/a>.&lt;/li>
&lt;/ul>
&lt;p>Cross-references:&lt;/p>
&lt;ul>
&lt;li>Post anterior: &lt;a href="https://blog.lo0.es/posts/mlops-llms-panorama-2026/">MLOps específico para LLMs en 2026: el panorama&lt;/a>.&lt;/li>
&lt;li>Serie post-tracing: &lt;a href="https://blog.lo0.es/posts/evals-llm-la-capa-despues-de-tracing/">Evals&lt;/a>, &lt;a href="https://blog.lo0.es/posts/guardrails-safety-llm/">Guardrails&lt;/a>, &lt;a href="https://blog.lo0.es/posts/mcp-observability-otel/">MCP observability&lt;/a>, &lt;a href="https://blog.lo0.es/posts/ebpf-on-device-inference-drift/">eBPF + drift&lt;/a>.&lt;/li>
&lt;li>Serie eBPF: &lt;a href="https://blog.lo0.es/posts/ebpf-cilium-tcp-ip-bypass/">eBPF de cero a Cilium&lt;/a>, &lt;a href="https://blog.lo0.es/posts/tetragon-runtime-security/">Tetragon&lt;/a>, &lt;a href="https://blog.lo0.es/posts/hubble-observabilidad-ebpf/">Hubble&lt;/a>, &lt;a href="https://blog.lo0.es/posts/agentsight-tracing-llm/">AgentSight&lt;/a>.&lt;/li>
&lt;/ul></description></item></channel></rss>