Ingesta documental end-to-end: del PDF al chunk indexado
Cuarta pieza de una serie operativa sobre exprimir un cluster LLM on-premise genérico de 4×H100 SXM 80 GB. Las hermanas de esta tanda: servir embeddings y rerankers con TEI en producción detalla la pieza de inferencia que esta ingesta alimenta; GitOps del stack de inferencia con Flux versiona y despliega todo este pipeline; y hardening y secretos del stack soberano protege el corpus y las credenciales que la ingesta toca. El consumidor final de lo que aquí construimos —un asistente soberano end-to-end— se monta en una entrega posterior.
TL;DR
Un sistema RAG hereda la calidad de su corpus, y el corpus hereda la calidad de la ingesta que lo fabricó. Es el garbage-in/garbage-out del retrieval: un chunk mal extraído de una tabla, un encabezado de página repetido mil veces, un PDF escaneado del que solo sacaste ruido OCR —todo eso se embebe, se indexa y reaparece como contexto envenenado en la respuesta del modelo. La ingesta documental no es un script de una tarde, es un pipeline de seis etapas con decisiones de ingeniería en cada una: (1) extraer/parsear —aquí se decide casi todo: layout-aware (Docling, que con Granite-Docling-258M preserva tablas, fórmulas y estructura, y según IBM evita el OCR clásico hasta ~30× más rápido) frente a extracción de texto plano (PyMuPDF), con OCR o un VLM para escaneos—; (2) limpiar/normalizar —quitar boilerplate, normalizar Unicode, reconstruir párrafos rotos—; (3) trocear —y aquí 2026 ha movido el consenso: un benchmark de febrero de 2026 puso el recursive a 512 tokens en cabeza (69% de acierto) por delante del chunking semántico (54%), y el late chunking aporta contexto global sin coste extra de almacenamiento—; (4) enriquecer metadatos —fuente, página, sección, timestamp, ACL/tenant: para retrieval filtrado, citación y auditabilidad—; (5) embeber —vía un servidor de embeddings tipo TEI—; (6) indexar —en pgvector o Qdrant, con payload, dense + sparse—. Entre medias, deduplicación exacta (hash) y near-dup (MinHash/LSH o coseno con umbral), porque los near-duplicados degradan el recall y la diversidad del retrieval. La parte numérica importa: un corpus de $N_{docs}\times$ páginas $\times$ tokens/página da los tokens totales y, a un throughput de CPU dado, el tiempo de ingesta; y $N_{chunks}\times d\times\text{bytes}$ da el tamaño del índice, que en int8 cae 4× frente a fp32. Lectura para el 4×H100: la ingesta es trabajo de CPU —enlaza con la pieza de RAG en CPU—; la GPU solo entra si parseas con un VLM (Granite-Docling) o usas un embedder de 7B.
La analogía: la cadena de catalogación de un archivo
Imagina el departamento de catalogación de un gran archivo documental. No es una persona metiendo papeles en cajas; es una cadena de estaciones de trabajo, cada una con un oficio distinto y un criterio de calidad propio. Un documento entra por un extremo en bruto y sale por el otro convertido en una ficha localizable en segundos. Si una estación hace mal su trabajo, las de abajo heredan el error y lo amplifican.
La primera estación es recepción y lectura. Llega una caja heterogénea: informes mecanografiados, fotocopias torcidas, tablas dobladas, microfichas. Un archivero experto lee de verdad cada pieza: distingue el cuerpo del texto de los márgenes, reconstruye una tabla que ocupa dos páginas, transcribe a mano lo que el escáner no leyó. Un archivero novato, en cambio, fotocopia todo en plano y entrega un churro de texto donde las columnas de las tablas se entrelazan sin sentido. Esa es exactamente la diferencia entre parsing layout-aware y extracción de texto plano, y es donde se gana o se pierde casi toda la calidad.
La segunda estación es el expurgo. Antes de archivar, alguien retira los duplicados —tres copias del mismo memo, dos versiones casi idénticas de un informe— y limpia las marcas inútiles: sellos de “COPIA”, pies de página repetidos en cada hoja, manchas de café. Si no expurgas, el archivo se llena de copias que, cuando alguien busca, devuelven el mismo documento seis veces y entierran lo diverso. Esto es la deduplicación y la limpieza.
La tercera estación trocea en fichas. Un libro de 400 páginas no se cataloga como una sola ficha gigante; se descompone en entradas manejables —por capítulo, por sección— de un tamaño que un lector pueda consultar de un vistazo. Demasiado grande y la ficha mezcla temas; demasiado pequeña y pierde el contexto. Esto es el chunking, y el tamaño de la ficha es la decisión que más condiciona el retrieval.
La cuarta estación etiqueta. Cada ficha lleva signatura, fecha, fondo de procedencia, nivel de acceso (¿esto lo puede ver cualquiera o solo el departamento jurídico?). Sin esas etiquetas no puedes filtrar una búsqueda ni decir de qué documento salió una afirmación. Son los metadatos: fuente, página, sección, timestamp, ACL.
La quinta y sexta estaciones colocan la ficha en la estantería indexada: la traducen al lenguaje del catálogo —un vector— y la colocan en el cajón correcto del fichero, de modo que una consulta encuentre las fichas afines sin recorrerlo entero. Es el embedding y la indexación en el vector store.
La moraleja recorre todo el post: el RAG no puede recuperar mejor de lo que la cadena de catalogación archivó. Si la primera estación troceó mal una tabla, ningún reranker la arreglará después. La ingesta es la estación de calidad del sistema entero, y casi toda ella —como el archivo, lleno de gente paciente trabajando sin que nadie les cronometre— es trabajo de fondo que cabe en CPU.
El pipeline de seis etapas
La tentación de tratar la ingesta como “leer el PDF y trocearlo” es la fuente del 80% de los RAG mediocres. Cada etapa tiene un criterio de calidad y un fallo característico, y los fallos se encadenan hacia abajo: si parseas mal, limpiar no recupera lo perdido; si troceas mal, embeber fija el error en el vector; si no etiquetas, no podrás filtrar ni citar. El resto del post recorre las seis estaciones con sus decisiones.
Etapa 1 — Parsear: layout-aware vs texto plano
Aquí se gana o se pierde casi todo. Un PDF no es texto: es un conjunto de instrucciones de dibujo de glifos sobre un lienzo. “Extraer el texto” de un PDF es reconstruir un orden de lectura que el formato no garantiza, y las tablas, las columnas y las figuras lo rompen sistemáticamente.
Hay dos filosofías, y la elección condiciona el resto del pipeline.
Extracción de texto plano. Herramientas como PyMuPDF (rápida, robusta, sin dependencias pesadas) leen el flujo de texto del PDF y lo vuelcan. Para documentos de una sola columna, prosa corrida y sin tablas, es perfecto: rapidísimo, fiel y barato en CPU. Su límite aparece con la estructura: una tabla de dos columnas sale con las celdas entrelazadas, un documento a doble columna mezcla el final de una con el principio de otra, y una factura escaneada no sale en absoluto porque no hay capa de texto. PyMuPDF también sabe segmentar por la tabla de contenidos (TOC) cuando el PDF la trae, lo que ayuda a un troceado por secciones (Omdena, Document Parsing for RAG: A Complete Guide for 2026).
Parsing layout-aware. Herramientas como Docling (proyecto open source impulsado por IBM Research) y unstructured.io primero entienden el layout —identifican títulos, párrafos, tablas, figuras, listas, fórmulas— y solo después extraen el contenido respetando esa estructura. Docling captura la estructura de las tablas (filas, columnas, encabezados multinivel) y, en su evolución de 2026, lo hace con un VLM: Granite-Docling-258M, liberado por IBM en enero de 2026 bajo Apache 2.0, un modelo visión-lenguaje compacto (~258M parámetros, backbone Granite 3 + encoder visual SigLIP2) que convierte páginas —PDF, diapositivas, escaneos— directamente a un formato estructurado llamado DocTags, preservando tablas, código, matemáticas inline y de bloque, y la jerarquía del documento (IBM, Granite-Docling: End-to-end document understanding; model card en Hugging Face; repo Docling). IBM afirma que la vía VLM evita el OCR clásico y que eso “reduce errores y acelera la solución hasta 30×” frente a un pipeline OCR tradicional —cifra de IBM Research, la cito y la trato como orientativa, no como un benchmark independiente reproducido aquí.
unstructured.io ofrece estrategias de partición graduadas según la complejidad del documento: fast (texto plano, rápido), hi_res (identifica el layout, recomendada cuando importa clasificar bien tablas y elementos), VLM y auto, equilibrando velocidad, coste y precisión (Unstructured, PDF Parsing Strategies for RAG). La regla práctica: fast para prosa, hi_res o VLM para documentos con tablas y estructura.
OCR para escaneos
Cuando el documento no tiene capa de texto —un escaneo, una foto, una microficha digitalizada—, hay que reconocer los caracteres. Tres vías:
- OCR clásico (Tesseract, PaddleOCR, EasyOCR —con el que Docling integra cuando se necesita OCR explícito). Maduro, CPU-friendly, bueno con texto limpio; sufre con tablas, manuscritos y layouts complejos.
- VLM end-to-end (Granite-Docling y similares). El modelo “mira” la página y emite estructura directamente, sin la etapa OCR separada. Mejor con layout complejo; aquí sí entra la GPU si el VLM es grande o el volumen alto.
- Híbrido: OCR para la transcripción de caracteres, modelo de layout para la estructura.
El criterio honesto: para un corpus de PDFs nativos con texto, PyMuPDF o unstructured fast resuelven en CPU y barato. Para un corpus con tablas densas, formularios o escaneos, Docling/Granite-Docling layout-aware paga su coste en calidad de chunk —y es la única etapa del pipeline donde la GPU puede justificarse.
| Caso de documento | Herramienta recomendada | Silicio |
|---|---|---|
| PDF nativo, una columna, prosa | PyMuPDF / unstructured fast | CPU |
| PDF con tablas, doble columna, jerarquía | Docling / unstructured hi_res | CPU (modelos de layout) |
| Escaneo, formulario, manuscrito, layout complejo | Granite-Docling (VLM) o OCR+layout | GPU si VLM grande / alto volumen |
| HTML, DOCX, PPTX | Docling (multi-formato) / parsers nativos | CPU |
Etapa 2 — Limpiar, normalizar y deduplicar
El texto recién parseado viene sucio. Limpiar es retirar lo que no aporta y normalizar lo que se representa de mil formas:
- Boilerplate: encabezados y pies de página repetidos en cada hoja, números de página, marcas de agua, menús de navegación en HTML, banners de cookies. Si no los quitas, se embeben mil veces y contaminan tanto el índice como las respuestas.
- Normalización Unicode (NFC/NFKC), espacios y guiones: el mismo carácter representado de varias formas rompe el matching exacto y ensucia los embeddings.
- Reconstrucción de párrafos: deshacer los saltos de línea duros que el PDF metió a mitad de frase, sin fusionar párrafos que sí debían quedar separados.
Deduplicación: por qué importa
El RAG sufre dos males de los duplicados. El exacto —el mismo documento subido tres veces— hincha el índice y hace que una búsqueda devuelva la misma respuesta repetida, desperdiciando los top-k slots de contexto. El near-duplicado —dos versiones casi idénticas de un informe, un documento y su borrador— es peor: parecen distintos al hash pero dicen lo mismo, y degradan el recall y la diversidad del retrieval. No es teoría: en la colección MS MARCO V2 se ha documentado un solapamiento sustancial de near-duplicados que, sin tratar, degrada la precisión de recuperación y reduce la diversidad de documentos recuperados (Ragnarök / TREC RAG 2024).
Dos niveles de dedup, complementarios:
- Exacto (hash). Calcula un
sha256del contenido normalizado de cada documento (o chunk) y descarta los que coinciden. Coste $O(N)$, trivial. Atrapa duplicados byte a byte. - Near-dup (MinHash + LSH, o coseno de embeddings con umbral). Para los que difieren un poco pero significan lo mismo. MinHash comprime cada documento en una firma compacta tal que la probabilidad de que dos firmas coincidan en una posición iguala la similitud de Jaccard de los conjuntos de shingles originales; combinado con Locality-Sensitive Hashing (LSH) encuentra todos los pares near-duplicados sin comparar todos contra todos, reduciendo un problema cuadrático a casi lineal (Brenndoerfer, MinHash, Jaccard, LSH). Es la técnica dominante en la limpieza de corpus de entrenamiento de LLM (C4, RefinedWeb la usan) y aplica igual al corpus de un RAG (Zilliz, Data Deduplication at Trillion Scale). La alternativa —coseno de embeddings con un umbral (p. ej. > 0.97)— atrapa duplicados semánticos que MinHash no ve (parafraseo), pero exige ya tener los embeddings y es más cara.
La regla práctica: hash exacto siempre (es gratis); MinHash/LSH para corpus grandes con versiones; coseno con umbral si el parafraseo es un problema real. Y deduplica antes de embeber: re-embeber un duplicado es gastar cómputo en basura que luego habrá que filtrar.
Etapa 3 — Trocear (chunking)
El chunking es la decisión que más condiciona el retrieval, y la que más mitos arrastra. El trade-off es triple: tamaño de chunk ↔ granularidad de retrieval ↔ coste de contexto.
- Chunks grandes: cada uno contiene más contexto y menos riesgo de cortar una idea por la mitad, pero la búsqueda es menos precisa (un vector representa demasiados temas) y, al recuperar, metes más tokens en el prompt del LLM —más coste y más riesgo de diluir lo relevante.
- Chunks pequeños: retrieval muy granular y preciso, pero cada chunk pierde contexto (un fragmento de 43 tokens puede no significar nada fuera de su sección) y necesitas recuperar más para cubrir una respuesta.
Las estrategias, de menos a más sofisticada:
- Tamaño fijo + overlap. Cortar cada $N$ tokens con un solapamiento de $k$ tokens entre chunks consecutivos para no partir una frase en seco. Simple, predecible, baseline razonable. El overlap es el seguro contra cortar una idea justo en la frontera.
- Recursivo (
RecursiveCharacterTextSplitterde LangChain). Intenta cortar por separadores en orden de prioridad —párrafo, luego frase, luego palabra— para respetar lo más posible la estructura natural antes de caer al corte duro. Es el caballo de batalla. - Semántico. Embebe frases y corta donde la similitud entre frases consecutivas cae por debajo de un umbral, agrupando por coherencia de significado. Suena mejor sobre el papel; en la práctica de 2026 ha decepcionado (ver abajo).
- Structure/layout-aware (por headings). Aprovecha la jerarquía que el parser layout-aware ya extrajo: un chunk por sección o subsección. unstructured ofrece la estrategia
by_title, que abre un chunk nuevo cuando aparece un elemento de tipo título, evitando mezclar texto de secciones distintas (Unstructured docs). Si parseaste con Docling/hi_res, esta estrategia es casi gratis y suele ser la mejor para documentos bien estructurados. - Late chunking. El giro de 2024–2026: en vez de trocear y luego embeber cada chunk por separado, embebe el documento entero primero (con un encoder de contexto largo) y luego aplica las fronteras de chunk haciendo mean-pooling de los embeddings de token dentro de cada span. El resultado: cada chunk conserva el contexto global del documento —un pronombre o una referencia que solo se entiende por el párrafo anterior queda codificada en el vector— y todo sin coste extra de almacenamiento, porque acabas con un vector por chunk igual que siempre (Jina AI, Late Chunking; arXiv:2409.04701).
Qué dicen los benchmarks de 2026 (y por qué el semántico decepciona)
Conviene ser escéptico con la moda. Un benchmark de Vecta de febrero de 2026 sobre 7 estrategias en 50 papers académicos colocó al recursive a 512 tokens en primer lugar con 69% de acierto, mientras que el chunking semántico quedó en 54%, en parte porque producía fragmentos minúsculos —de media 43 tokens— demasiado pequeños para significar algo (Firecrawl, Best Chunking Strategies for RAG in 2026). Un análisis sistemático de enero de 2026 identificó además un “context cliff” en torno a los 2.500 tokens, donde la calidad de respuesta cae al meter contextos demasiado largos —argumento extra contra los chunks gigantes ([ídem]). La lectura honesta: el recursive de tamaño moderado con overlap sigue siendo el baseline difícil de batir; el late chunking es la mejora con mejor relación coste/beneficio cuando el modelo lo soporta; el semántico promete más de lo que entrega.
Ejemplo numérico de chunking
Pongamos un documento técnico de 30 páginas, ~500 tokens de prosa útil por página tras limpiar (las tablas y figuras se trocean aparte). Son $30 \times 500 = 15{.}000$ tokens de texto. Troceamos con recursive a 512 tokens y un 20% de overlap ($0.20 \times 512 \approx 102$ tokens). El “paso” efectivo entre el inicio de un chunk y el siguiente es:
$$\text{paso} = \text{tamaño} - \text{overlap} = 512 - 102 = 410 \text{ tokens}$$
El número de chunks del documento es entonces, aproximadamente:
$$N_{chunks} \approx \left\lceil \frac{T_{doc} - \text{overlap}}{\text{paso}} \right\rceil = \left\lceil \frac{15{.}000 - 102}{410} \right\rceil \approx \lceil 36.3 \rceil = 37 \text{ chunks}$$
Sin overlap habrían sido $\lceil 15{.}000 / 512 \rceil = 30$ chunks; el 20% de overlap nos cuesta 7 chunks extra (~23% más) a cambio de no partir ideas en las fronteras. Ese es el precio concreto del overlap: más vectores que embeber, indexar y almacenar, a cambio de robustez en el retrieval. Para el dimensionado del corpus completo usaremos este factor.
Etapa 4 — Enriquecer con metadatos
Un chunk sin metadatos es una ficha sin signatura: existe pero no sirve. A cada chunk se le adjunta un payload con, al menos:
- Fuente y localización:
document_id, nombre/URI del documento, número de página, sección/heading (que el parser layout-aware ya te dio). Imprescindible para citar: poder decir “esto sale del documento X, página 12, sección 3.2” es lo que separa un RAG auditable de uno que alucina sin trazabilidad. - Timestamp: cuándo se ingestó y la fecha del documento. Permite filtrar por frescura y detectar contenido obsoleto.
- ACL / tenant: quién puede ver este chunk. Es crítico y se aplica como filtro en el retrieval: un usuario del departamento A no debe recuperar chunks marcados solo para el B. Sin esto, el RAG es una fuga de datos esperando a ocurrir.
- Versión del modelo de embedding (
model_version): para saber con qué embedder se generó cada vector y poder migrar sin mezclar espacios incompatibles.
Estos metadatos no son decoración: habilitan retrieval filtrado (buscar solo en lo que el usuario puede ver, o solo en documentos posteriores a una fecha), citación (reconstruir el origen de cada afirmación) y auditabilidad (saber qué se recuperó, de dónde y cuándo). Todo vive en el payload del punto en el vector store.
Etapas 5 y 6 — Embeber e indexar
Las dos últimas estaciones traducen el chunk a un vector y lo colocan en la estantería.
Embeber. Los chunks se envían en batch a un servidor de embeddings —típicamente TEI (Text Embeddings Inference) de Hugging Face, que expone el contrato OpenAI /v1/embeddings y corre en CPU o GPU—. Es trabajo throughput-bound, sin SLA de latencia: el sitio natural es la CPU con un encoder pequeño en int8 (la pieza hermana de esta tanda, servir embeddings y rerankers con TEI, detalla el cómo). Conviene emitir dense + sparse a la vez: el vector denso captura la semántica, el sparse (SPLADE/BM25-like) el solapamiento léxico exacto, y juntos hacen el retrieval híbrido más robusto.
Indexar. Los vectores, con su payload, se hacen upsert en el vector store. Dos opciones de referencia, ambas vigentes en 2026:
- pgvector (extensión de PostgreSQL). Su gran virtud es vivir dentro de Postgres: transacciones ACID, joins con los metadatos relacionales, una sola base de datos que operar. La versión 0.8 añadió
halfvec(vectores en media precisión, 2× menos almacenamiento) y la 0.9 (principios de 2026) sumó soporte de vectores sparse y mejoras de velocidad. Su límite conocido: no trae cuantización int8 nativa, así que los embeddings de alta dimensión consumen RAM de forma lineal (Encore, pgvector vs Qdrant 2026; Katz, Scalar and binary quantization for pgvector). - Qdrant (motor vectorial dedicado). Soporta cuantización escalar int8 (float32 → int8, 4× menos memoria) y product quantization, vectores sparse nativos y fusión RRF para híbrido. Es más eficiente en memoria y en cuantización; el coste es operar un sistema más además de Postgres (Markaicode, pgvector vs Qdrant 2026).
La regla práctica: pgvector si ya tienes Postgres y el corpus cabe en RAM cómodamente (una sola base que operar y respaldar); Qdrant si la eficiencia de memoria y la cuantización int8 son críticas por el tamaño del corpus. La sincronización entre la verdad relacional (Postgres) y el índice (Qdrant) cuando se usan los dos se detalla en PostgreSQL + Qdrant en microservicios.
Etapa transversal — Ingesta incremental e idempotencia
Un corpus vivo cambia: se añaden documentos, se editan, se borran. Re-indexar todo cada noche es caro y provoca ventanas de indisponibilidad. La alternativa es ingesta incremental con dos pilares:
- Idempotencia por doc-id + hash. Cada chunk se identifica de forma determinista (
{doc_id}_{chunk_index}) y cada documento lleva un hash de contenido. Al re-procesar, si el hash no cambió, no se re-embebe: se ahorra el cómputo. Si cambió, se borran los chunks viejos de esedoc_idy se hace upsert de los nuevos. El upsert con id determinista es idempotente: reprocesar un evento dos veces no genera duplicados. - CDC (Change Data Capture). En vez de hacer polling, Debezium lee el WAL de PostgreSQL y propaga altas, ediciones y borrados al índice en tiempo casi real. Un borrado en Postgres dispara el borrado de los chunks de ese documento en el vector store, evitando los “documentos fantasma” que contaminan el retrieval. El deep dive está en PostgreSQL + Qdrant en microservicios y en el de Debezium y CDC.
Las matemáticas: dimensionar el corpus y el índice
Dos cálculos que hay que saber hacer antes de aprovisionar nada.
Dimensionado del corpus y tiempo de ingesta
Supongamos un corpus corporativo de $N_{docs} = 50{.}000$ documentos, de media 20 páginas y 500 tokens útiles por página tras limpiar. Los tokens totales del corpus:
$$T_{corpus} = N_{docs} \times \text{páginas} \times \text{tokens/página} = 50{.}000 \times 20 \times 500 = 5 \times 10^{8} \text{ tokens}$$
Quinientos millones de tokens. Aplicando el factor de overlap del ejemplo de chunking (~1.23×, el 23% extra de chunks por el 20% de overlap) y un tamaño efectivo de 512 tokens/chunk, el número de chunks es:
$$N_{chunks} \approx \frac{T_{corpus}}{\text{paso}} = \frac{5 \times 10^{8}}{410} \approx 1.22 \times 10^{6} \text{ chunks}$$
Es decir, ~1,22 millones de chunks que embeber. A un throughput de embedding CPU conservador de 3.000 tok/s por servidor Xeon en int8 —la misma cifra que usamos en la pieza de RAG en CPU—, el tiempo de ingesta del primer corpus completo en una caja es:
$$t_{ingesta} = \frac{T_{corpus}}{\text{throughput}} = \frac{5 \times 10^{8}}{3{.}000} \approx 1.67 \times 10^{5} \text{ s} \approx 46 \text{ horas}$$
46 horas en una sola caja suena mal, pero la ingesta es vergonzosamente paralela: el corpus se reparte. Con 8 servidores CPU baja a ~6 horas, holgadamente dentro de una ventana de fin de semana para la carga inicial; y las ingestas incrementales posteriores (solo lo que cambió) son minutos. El parsing layout-aware añade su propio coste —Docling con VLM es más lento que PyMuPDF—, pero también es batch y paraleliza igual.
Tamaño del índice vectorial
Cada vector tiene dimensión $d = 1024$ (la de bge-m3). En float32 (4 bytes/dimensión), cada vector ocupa:
$$\text{bytes}_{fp32} = d \times 4 = 1024 \times 4 = 4096 \text{ B} = 4 \text{ KB}$$
Para los 1,22 M de chunks, solo los vectores densos en fp32:
$$\text{tamaño}{fp32} = N{chunks} \times d \times 4 = 1.22 \times 10^{6} \times 4096 \text{ B} \approx 5.0 \text{ GB}$$
En int8 (1 byte/dimensión), cada vector ocupa 1 KB y el total cae 4×:
$$\text{tamaño}{int8} = N{chunks} \times d \times 1 = 1.22 \times 10^{6} \times 1024 \text{ B} \approx 1.25 \text{ GB}$$
A esto hay que sumar el índice HNSW (~1.2× el tamaño de los vectores para $m=16$) y el payload (metadatos + texto del chunk, ~500 B/chunk → ~0,6 GB). En números redondos:
| Configuración | Vectores | HNSW (~1.2×) | Payload | Total |
|---|---|---|---|---|
| fp32 | ~5,0 GB | ~6,0 GB | ~0,6 GB | ~11,6 GB |
| int8 | ~1,25 GB | ~1,5 GB | ~0,6 GB | ~3,4 GB |
La lectura: un corpus de 50.000 documentos cabe en RAM de un solo nodo incluso en fp32, y en int8 (Qdrant) entra con holgura, lo que mantiene la latencia de búsqueda en milisegundos de un dígito. La cuantización int8 es casi siempre el punto de equilibrio —ahorra 4× con una pérdida de recall típicamente por debajo del 1%. (Estos números son de orden de magnitud, con las constantes y supuestos declarados; sirven para dimensionar, no para clavar una factura.)
Aplicado al cluster genérico 4×H100
Bajemos esto al cluster de la serie: 4×H100 SXM 80 GB más una flota CPU genérica (Xeon con AMX, NUCs). El reparto correcto de la ingesta es casi todo CPU, con la GPU como excepción puntual:
- Limpieza, dedup, chunking, metadatos, embedding de ingesta e indexado → flota CPU. Todo esto es trabajo batch, throughput-bound, sin SLA de latencia. Es exactamente el “plano de datos” del que habla la pieza RAG en CPU: ninguna H100 debería gastar un ciclo troceando documentos o construyendo un índice HNSW (que siempre fue CPU por diseño). El re-indexado incremental nocturno de un corpus que cambia a ritmo de horas es el caso de libro de “trabajo de CPU sin prisa”.
- La GPU solo entra en dos puntos. Primero, en el parsing con VLM: si el corpus tiene escaneos, tablas densas o formularios y eliges Granite-Docling-258M u otro modelo visión-lenguaje, ese parsing puede acelerarse en GPU —aunque a 258M parámetros es ligero y, en volúmenes moderados, corre en CPU sin drama. Segundo, en el embedder grande: si la calidad de recuperación exige un embedder de 7B (gte-Qwen2, NV-Embed) en lugar de
bge-m3(568M), ese embedder vuelve a ser un modelo LLM-class y vive donde viven los 7B, en la GPU. - Las 4×H100 se reservan para generar. Como en toda la serie, el silicio caro y escaso se guarda para lo latency-bound —el LLM produciendo la respuesta— y, como mucho, para los picos de parsing VLM o embedding 7B que la CPU no absorba. Para hacerse una idea del techo: un nodo 4×H100 sirviendo
bge-m3vía TEI ronda los ~2.000 chunks/s, frente a los miles de tok/s de un Xeon en int8; pero usar las H100 para la ingesta diaria es gastar el recurso por el que se pelea toda la organización en un trabajo que la flota CPU hace de noche sin que nadie la eche en falta.
La frase que resume el reparto: la ingesta es la cadena de catalogación del archivo, y casi toda se hace con personal paciente y barato (CPU); el redactor estrella (GPU) solo se molesta cuando hay que leer una página que ningún OCR clásico descifra.
Cierre: la calidad se decide arriba
El error recurrente del RAG mediocre no está en el reranker ni en el prompt: está en una ingesta que parseó mal una tabla, no expurgó los duplicados o troceó con un tamaño que destruye el contexto. Garbage-in, garbage-out: ningún componente de abajo arregla lo que la ingesta estropeó arriba. Invertir en la cadena de catalogación —parsing layout-aware donde haga falta, dedup de verdad, chunking medido, metadatos completos— es lo que más mueve la aguja de la calidad del sistema, y casi todo cabe en la flota CPU. La GPU, como el redactor estrella, solo debería tocar el corpus cuando de verdad hace falta su cabeza.
Ver también
Multimodal on-premise: servir un VLM con vLLM (visión + lenguaje) — el VLM como alternativa al OCR clásico para parsear documentos con layout, tablas o manuscritos.
Servir embeddings y rerankers con TEI en producción — la pieza hermana: el servidor de inferencia que esta ingesta alimenta en la etapa de embedding.
Llevar el RAG a la CPU: plano de datos vs plano de generación — por qué toda esta ingesta es trabajo de CPU y no debe tocar la GPU.
PostgreSQL + Qdrant en la ingestión RAG — la sincronización, el upsert idempotente y el CDC de la ingesta incremental, en detalle.
El corpus curado que esta ingesta debe construir — los fundamentos de curación y filtrado que preceden y guían a la ingesta.
Reranking e hybrid retrieval: fundamentos — qué hace el sistema con los chunks dense + sparse que esta ingesta indexó.
Embeddings 2026: dense, sparse y multivector — el embedder que traduce cada chunk a vector y cuándo justifica un modelo de 7B en GPU.
Evaluar el RAG con RAGAS y un golden dataset — cómo medir si la ingesta realmente mejoró el retrieval, en vez de creerlo.
Referencias
- IBM — Granite-Docling: End-to-end document understanding (Granite-Docling-258M, Apache 2.0, DocTags, enero 2026). https://www.ibm.com/new/announcements/granite-docling-end-to-end-document-conversion
- ibm-granite — granite-docling-258M model card (VLM ~258M, Granite 3 + SigLIP2). https://huggingface.co/ibm-granite/granite-docling-258M
- Docling project — Docling: Get your documents ready for gen AI (layout, tablas, multi-formato). https://github.com/docling-project/docling
- Unstructured — PDF Parsing Strategies for RAG (fast / hi_res / VLM / auto; chunking by_title). https://unstructured.io/blog/mastering-pdf-transformation-strategies-with-unstructured-part-2 · https://docs.unstructured.io/
- Omdena — Document Parsing for RAG: A Complete Guide for 2026 (PyMuPDF, TOC, pipeline). https://www.omdena.com/blog/document-parsing-for-rag
- Firecrawl — Best Chunking Strategies for RAG (and LLMs) in 2026 (benchmark Vecta feb-2026: recursive 512 → 69%, semántico 54%; context cliff ~2.500 tok). https://www.firecrawl.dev/blog/best-chunking-strategies-rag
- Günther, M., et al. — Late Chunking: Contextual Chunk Embeddings Using Long-Context Embedding Models. arXiv 2409.04701. https://arxiv.org/abs/2409.04701 · Jina AI: https://jina.ai/news/late-chunking-in-long-context-embedding-models/
- Brenndoerfer, M. — MinHash: Jaccard Similarity, LSH, and Near-Duplicate Detection. https://mbrenndoerfer.com/writing/minhash-algorithm-jaccard-similarity-lsh-deduplication
- Zilliz — Data Deduplication at Trillion Scale (MinHash LSH dominante en limpieza de corpus). https://zilliz.com/blog/data-deduplication-at-trillion-scale-solve-the-biggest-bottleneck-of-llm-training
- Ragnarök / TREC RAG 2024 — near-duplicados en MS MARCO V2 degradan recuperación y diversidad. https://arxiv.org/pdf/2406.16828
- Encore — pgvector vs Qdrant in 2026 (halfvec, sparse, límites de cuantización de pgvector). https://encore.dev/articles/pgvector-vs-qdrant
- Markaicode — pgvector vs Qdrant (2026 Benchmarks) (Qdrant int8 scalar quantization 4×). https://markaicode.com/vs/pgvector-vs-qdrant/
- Katz, J. — Scalar and binary quantization for pgvector. https://jkatz05.com/post/postgres/pgvector-scalar-binary-quantization/
- Hugging Face — Text Embeddings Inference (TEI), backends CPU/GPU, endpoints OpenAI-compatibles. https://github.com/huggingface/text-embeddings-inference