Diseñando RAG para 100 millones de documentos: sharding, búsqueda híbrida, costos
Qué cambia cuando un sistema RAG crece hasta alcanzar miles de millones de fragmentos: ingestión asíncrona, enrutamiento, particionamiento, recuperación híbrida, reclasificación, autorización, caché y control de costos.
Lograr que funcione una demostración de RAG lleva toda una tarde. Se divide un PDF en fragmentos, se incrustan estos fragmentos, los vectores se almacenan en una base de datos, se recuperan los cinco vecinos más cercanos, se pegan en un prompt y se llama a un modelo.
Ese procedimiento funciona perfectamente para unos pocos miles de documentos. Pero una vez que el corpus crece hasta 100 millones de documentos, deja de ser viable y la pregunta que se plantea cambia. Ya no es “¿qué base de datos vectorial deberíamos elegir?”, sino: ¿cómo se pueden reducir miles de millones de posibles evidencias a las 10 o 20 partes relevantes, sin afectar la latencia, la calidad de la recuperación, la disponibilidad ni el presupuesto?
Eso es un problema de ingeniería completamente distinto. A esta escala, RAG se convierte en un sistema distribuido de datos e investigación al que se le añade un modelo de lenguaje en el extremo final.
Qué debe hacer el sistema
Para mantener la discusión concreta, supongamos estos requisitos:
- Más de 100 millones de documentos, en formatos como PDF, HTML y DOCX
- Millones de usuarios que hacen preguntas en lenguaje natural
- Citas adjuntas a cada respuesta
- Documentos que se actualizan y eliminan con el tiempo
- Recuperación rápida, aproximadamente de 1 a 3 segundos antes de que comience la generación
- Alta disponibilidad
- Procesamiento asincrónico de documentos
Todo se reduce a un requisito fundamental: encontrar las 10 a 50 piezas de información adecuadas entre potencialmente miles de millones de fragmentos.
Comencemos con la aritmética
El primer error en los diseños grandes de RAG es tratar un documento y un vector como si fueran lo mismo. Supongamos:
Documents = 100,000,000
Average chunks/document = 30
Embedding dimensions = 768
Storage/dimension = 4 bytes (float32)
El recuento de vectores es entonces:
100,000,000 × 30
= 3,000,000,000 vectors
Tres mil millones de vectores. El almacenamiento en formato float bruto para ellos asciende a aproximadamente:
3B × 768 × 4 bytes
≈ 9.2 TB
Y esos son solo los números en sí. Una implementación real también incluye:
ANN index structures
chunk text
document metadata
ACL metadata
IDs
lexical indexes
replicas
snapshots
WALs
temporary indexes
operational headroom
Con dos réplicas del índice vectorial ya se superan los 18 TB de vectores en bruto, sin contar aún las estructuras de búsqueda. Si los documentos tienen en promedio 50 fragmentos y no 30:
100M documents × 50 chunks
= 5 billion vectors
Por eso la frase “pondremos todos los fragmentos en una base de datos vectorial y ejecutaremos búsquedas de similitud” no describe una arquitectura a gran escala. Solo indica por dónde se comienza.
Dos planos, separados
La forma más clara de organizar el sistema es en dos planos independientes: uno para la ingesta y otro para la recuperación de datos. Esta separación es importante. Un cliente que suba 20 millones de documentos nuevos no debería hacer que la latencia de las consultas pase de 300 ms a 8 segundos. La ingesta se ajusta para maximizar el rendimiento, mientras que la recuperación lo hace para reducir la latencia; forzar ambas tareas a través del mismo proceso es una forma segura de generar problemas operativos más adelante.
La base de datos vectorial no es tu fuente de verdad
Guarde los documentos originales en almacenamiento de objetos económico como S3, GCS o Azure Blob. La base de datos vectorial es un índice, y los índices se pueden reconstruir. Si en seis meses pasa del modelo de incrustación E1 a E2, querrá regenerar los vectores a partir de los originales almacenados, en lugar de intentar reconstruir el contenido directamente desde la base de datos vectorial.
La versionación de objetos también es útil aquí, ya que una actualización puede registrarse como una nueva versión en lugar de destruir la anterior. Por ejemplo, Amazon S3 puede conservar varias versiones de un objeto, y las políticas de ciclo de vida pueden mover las versiones más antiguas a niveles más económicos o hacer que caduquen.
La ingesta debe ser asíncrona
Imaginemos que un cliente sube 500,000 PDFs. No se desea una solicitud POST /documents que analice, ejecute OCR, limpiee, divida en partes, incorpore y indexe los archivos para luego devolver un 200. Esa llamada podría tardar horas en completarse. En su lugar, el endpoint debería hacer muy poco:
POST /documents
│
▼
Store document
│
▼
Create ingestion event
│
▼
Return 202 Accepted
Todo lo demás ocurre de forma secundaria, impulsado por los eventos. Kafka es una opción viable como estructura fundamental, ya que los temas se dividen en particiones a través de las cuales los consumidores pueden trabajar en paralelo, y el orden se mantiene dentro de cada partición, lo cual es útil cuando los eventos que comparten una clave de documento o de tenant necesitan una secuencia predecible.
La herramienta específica importa menos que la funcionalidad que ofrece: la ingesta requiere un búfer. Si su servicio de incrustación maneja 30,000 fragmentos por segundo y de repente recibe una carga de 100,000 fragmentos por segundo, el sistema debe crear un acumulo de tareas pendientes y no colapsar.
Hacer que cada paso de ingesta sea idempotente
A esta escala, las tareas se ejecutan dos veces. Los mensajes se vuelven a entregar. Los trabajadores mueren después de escribir los datos pero antes de confirmar que la tarea se completó. Las solicitudes se agotan el tiempo establecido aunque el lado remoto haya terminado el trabajo. Planifique todo esto desde el principio al derivar identificadores deterministas a partir de entradas estables:
document_id
document_version
chunk_index
embedding_model_version
Un ID de bloque puede ser entonces un hash de las partes que lo definen:
hash(
tenant_id +
document_id +
document_version +
chunk_index
)
Recibir el mismo evento de indexación dos veces ya no genera contenido duplicado, porque
upsert(chunk_id, ...)
Es seguro intentarlo de nuevo. Es difícil lograr una ejecución exactamente una vez. Por lo general, el objetivo más realista es la ejecución idempotente.
El particionamiento es una decisión de infraestructura
En un corpus pequeño, cambiar el tamaño del bloque parece inofensivo. Pero con miles de millones de vectores, eso cuesta dinero real. Tomemos 100 millones de documentos: una estrategia que genere 20 bloques por documento produce 2 mil millones de vectores, mientras que una que genere 60 produce 6 mil millones. Un cambio que parece inocente acaba de triplicar todos estos valores:
embedding compute
vector storage
indexing work
retrieval fan-out
reindexing time
replication traffic
Por lo tanto, el fragmentado no es simplemente un ajuste de NLP; es un parámetro de planificación de capacidad.
Cada fragmento también debe contener suficientes metadatos para poder rastrearlo hasta su origen:
{
"chunk_id": "c_982...",
"document_id": "doc_51...",
"document_version": 8,
"tenant_id": "tenant_17",
"page": 43,
"section": "Risk Factors",
"language": "en",
"created_at": "...",
"acl_groups": ["finance", "executives"],
"embedding_version": "embed_v4"
}
El texto en sí puede almacenarse en otro lugar, dependiendo de su estrategia de almacenamiento, pero la información relacionada debe conservarse al recuperarlo. De lo contrario, las citaciones se volverán problemáticas tarde o temprano.
No busque en todo el corpus a menos que sea necesario
Esta podría ser la idea más importante de todo el diseño. Tome la pregunta “¿Qué cambió en nuestra política de reembolsos europea este trimestre?”. Una configuración ingenua simplemente buscaría en los 3,000,000,000 vectores. Pero la pregunta ya nos dice mucho:
organization = current tenant
region = Europe
topic = refund policy
time = current quarter
permissions = documents user may access
Una arquitectura de recuperación de sonidos utiliza estas restricciones de manera intensiva, reduciendo miles de millones de fragmentos a una pequeña porción antes de que comience cualquier búsqueda de similitud. Las cifras exactas dependen de la carga de trabajo, por lo que deben considerarse como ejemplos ilustrativos. El principio sigue siendo el mismo: enrutar primero, recuperar después.
El sharding hace que la capa de vectores sea distribuida
En algún momento un nodo no puede albergar todo el corpus buscable, y el índice debe dividirse en partes. Los motores de vectores distribuidos como Qdrant escalan horizontalmente al distribuir los puntos entre los shards, y la replicación coloca copias de esos shards en varios nodos para garantizar disponibilidad y mayor capacidad de lectura.
Pero hay un problema. Si cada consulta debe distribuirse a cada shard, añadir más shards acaba generando más trabajo de coordinación en lugar de reducirlo. Un diseño mejor permite que la mayor cantidad posible de consultas se enruten. Atributos como estos pueden indicarte qué shards realmente necesitan participar:
tenant_id
region
document namespace
language
time partition
Esto resulta más beneficioso en los sistemas multiinquilinos. En lugar de
Query
↓
128 shards
Una consulta podría ser procesada
tenant_id = 429
↓
Shard group 18
↓
4 shards
El diseño distribuido actual de Qdrant, por ejemplo, permite el sharding definido por el usuario, de modo que las operaciones pueden dirigirse a claves de shard específicas y evitar afectar a toda la colección. Esta lección es aplicable independientemente del proveedor: una buena particionación reduce la dispersión de las consultas.
La replicación responde a una pregunta diferente
La gente confunde el sharding con la replicación, pero ambos resuelven problemas distintos. El sharding aborda cómo dividir un conjunto de datos demasiado grande para una sola máquina. La replicación se ocupa de lo que sucede cuando una de esas máquinas deja de funcionar. Imagine este esquema:
Shard A → Node 1
Shard B → Node 2
Shard C → Node 3
Si el nodo 2 falla, una parte del corpus desaparece junto con él. Con replicación:
Shard A → Node 1 + Node 4
Shard B → Node 2 + Node 5
Shard C → Node 3 + Node 6
El sistema sigue respondiendo a las consultas incluso cuando algunos nodos fallan. La redundancia implica costos en almacenamiento, ancho de banda para escritura y esfuerzo operativo, pero un clúster de búsqueda en producción sin ella presenta modos de fallo problemáticos.
La búsqueda vectorial por sí sola es insuficiente
Imagínese que alguien pregunta: “¿Qué dice la política HR-48291?”. La búsqueda semántica podría devolver material sobre políticas de recursos humanos en general, mientras que la recuperación léxica es excelente para encontrar exactamente el término HR-48291. Ahora imagine la pregunta: “¿Pueden los empleados tomar más días libres después de adoptar un hijo?”. En este caso, la búsqueda semántica resulta útil. Los dos enfoques se superponen pero abordan áreas diferentes, por lo que una etapa de recuperación híbrida que utilice ambos es la opción habitual.
La guía actual de búsqueda híbrida de Elastic sigue el mismo enfoque, combinando la recuperación léxica y vectorial y proponiendo la Fusión de Rangos Recíprocos como una forma de unir las clasificaciones. Una ventaja de la RRF es que no es necesario fingir que una puntuación BM25 y una similitud coseno son valores comparables; en su lugar, se combinan las posiciones de ranking. En forma simplificada:
RRF(d) = Σ 1 / (k + rank_i(d))
donde rank_i(d) es la posición del documento d en el sistema de recuperación i.
Tenga cuidado con el prefiltrado secuencial
Existe un atajo tentador:
3B documents
↓
BM25 returns 10,000
↓
vector search only those
Puede funcionar para algunas cargas de trabajo, pero también puede destruir la recuperación semántica. Imagine a un usuario que escribe “¿Qué reglas se aplican a los empleados que adoptan?” y un documento que dice: “Los empleados que acogen a un niño mediante colocación no biológica tienen derecho a dieciséis semanas de licencia familiar”. La superposición de palabras clave con la formulación del usuario es escasa. Si BM25 descarta el documento antes de que la recuperación semántica pueda procesarlo, ningún modelo de embedding podrá salvarlo.
Por lo tanto, tras las restricciones de enrutamiento estricto, es mejor generar candidatos de forma paralela desde ambos sistemas de recuperación y luego reordenar el conjunto combinado. Las restricciones estrictas en sí forman una categoría separada:
tenant
permissions
document status
Normalmente, estas medidas deben aplicarse antes de que comience la recuperación.
La autorización precede a que el LLM vea cualquier cosa
Imagínese un corpus corporativo que contiene:
Engineering documents
HR documents
Board documents
Legal documents
Executive compensation
Customer contracts
La relevancia semántica no equivale a permiso. El documento de un director financiero podría ser la opción matemáticamente más cercana a la pregunta de un empleado, pero eso aún no le da al empleado el derecho de leerlo. Un diseño incorrecto se ve así:
Vector Search
↓
Retrieve confidential chunk
↓
Send chunk to LLM
↓
Check permission
Para cuando se ejecuta la verificación de permisos, el texto confidencial ya se ha transferido a un componente posterior. El flujo correcto determina primero quién es el usuario:
User
↓
Identity
↓
Groups / roles / ACL
↓
Authorized search space
↓
Retrieval
Eso implica que los metadatos de control de acceso deben participar de manera eficiente en la recuperación de datos. A escala muy grande, un esquema de ACL que necesita escanear millones de registros después de la recuperación se convierte tanto en un problema de seguridad como en uno de rendimiento.
La recuperación genera candidatos, no contexto
Supongamos que la recuperación híbrida produce 400 candidatos. No incluya esos 400 fragmentos en la solicitud. Primero, reordénelos según su relevancia:
Vector candidates: 200
Lexical candidates: 200
│
▼
Fusion
│
▼
~250 unique chunks
│
▼
Reranker
│
▼
top 20–40
La búsqueda en la primera etapa está diseñada para encontrar rápidamente elementos plausiblemente relevantes. El reordenador está creado para una tarea diferente: dado un conjunto mucho más pequeño, estimar la relevancia con mayor cuidado. Este patrón de dos etapas permite que un modelo de relevancia costoso funcione con cientos de elementos en lugar de miles de millones.
Luego, eliminar la redundancia
Incluso un buen reordenador puede ofrecer algo como esto:
Chunk 1 → page 14
Chunk 2 → page 14
Chunk 3 → page 15
Chunk 4 → page 14
Chunk 5 → page 15
Todos los cinco podrían transmitir esencialmente el mismo mensaje, y enviarlos todos desperdicia tokens de contexto. Antes de crear la prompt final, aplique una combinación de estos pasos:
deduplication
document diversity
section diversity
near-duplicate detection
adjacent-chunk merging
token budgeting
A veces, una respuesta también necesita sus contextos adyacentes. Si el bloque 47 contiene la respuesta pero comienza a mitad de una explicación, el generador de contexto puede obtenerla
chunk 46
chunk 47
chunk 48
en lugar de esperar que la búsqueda vectorial obtenga de forma independiente cada fragmento contiguo.
La construcción del contexto merece su propia capa
Una implementación sorprendentemente común trata el contexto como nada más que esto:
context = "\n\n".join(retrieved_chunks)
Los sistemas de producción funcionan mejor con un constructor de contexto dedicado. Su tarea es tomar pruebas clasificadas y generar una carga útil de instrucciones eficiente sin perder la información de citación. Conceptualmente:
Retrieved Evidence
↓
Deduplicate
↓
Merge related chunks
↓
Enforce token budget
↓
Preserve source IDs
↓
Order evidence
↓
Generate context
El modelo podría recibir entonces algo estructuralmente similar a esto:
SOURCE S1
Document: Employee Handbook
Page: 42
Version: 18
Text: ...
SOURCE S2
Document: European Leave Addendum
Page: 7
Version: 4
Text: ...
QUESTION
...
y se le pedirá que cite S1, S2 y así sucesivamente. Eso es mucho más seguro que pedirle a un modelo que recree URLs o identificadores de documentos de memoria.
La comprensión de las consultas debería ser sencilla
Algunas consultas se benefician de la normalización antes de su recuperación. Un usuario escribe “¿Cuánto creció la facturación el año pasado en Europa?”. Teniendo en cuenta la fecha actual y la terminología propia de la empresa, la recuperación podría ser más eficaz si se utilizan campos estructurados extraídos de esa oración:
metric = revenue
region = Europe
year = 2025
intent = financial comparison
En una conversación, una pregunta como “¿Y qué hay de Asia?” necesita ser reescrita teniendo en cuenta el contexto anterior:
How much did revenue grow in Asia in 2025?
El punto clave es que estos pasos rara vez requieren su modelo de razonamiento más costoso. Utilice el componente más económico que cumpla la tarea de manera fiable. Una pila de IA madura suele verse así:
small model
↓
classification
small model
↓
query rewriting
embedding model
↓
retrieval
reranker
↓
relevance
large model
↓
final reasoning
y no así:
largest model everywhere
Descomposición para preguntas de múltiples pasos
Algunas preguntas no pueden responderse de manera fiable a partir de un único texto. Una pregunta como “¿qué producto europeo experimentó la mayor caída en ingresos, y cómo lo explicó la dirección?” se divide en dos búsquedas:
Question 1:
Which European product had the largest decline?
Question 2:
What explanation did management provide for that product?
Luego, la recuperación puede recopilar pruebas para cada subpregunta por separado, y los resultados se unen antes de la generación.
Complex Query
│
▼
Query Decomposer
/ \
/ \
▼ ▼
Subquery A Subquery B
│ │
▼ ▼
Retrieval Retrieval
\ /
\ /
▼ ▼
Evidence Join
│
▼
LLM
En este punto, el RAG a gran escala comienza a parecerse más a un sistema de planificación de consultas que a una simple búsqueda del vecino más cercano.
La actualidad afecta la forma en que indexas
Supongamos que el corpus contiene 100 millones de documentos y 2 millones de ellos cambian cada día. Reconstruirlo todas las noches probablemente no sea lo que deseas, por lo que necesitas un indexado incremental:
Document updated
│
▼
new document version
│
▼
parse changed document
│
▼
generate new chunks
│
▼
embed
│
▼
write new index entries
│
▼
mark previous version inactive
La versionación lógica es mejor que el reemplazo destructivo inmediato. Por ejemplo, en el caso de un documento:
document_id = D17
version 41 → status=inactive
version 42 → status=active
Filtro de consultas por
status = active
Y las entradas antiguas pueden ser recogidas como basura en segundo plano. Desde el punto de vista de la aplicación, la actualización se realiza de forma atómica, sin necesidad de que todos los sistemas de almacenamiento cambien al mismo instante.
Versione también los modelos
Pronto o tarde cambiará de modelos de incrustación, y nunca debe almacenar vectores sin saber qué los generó. Como mínimo, mantenga estos elementos rastreables:
embedding_model
embedding_version
embedding_dimensions
chunking_version
parser_version
De lo contrario, seis meses después, es posible que descubra que el 15% de un índice fue creado con un pipeline más antiguo y no tenga forma sencilla de determinar qué 15%. La migración puede realizarse en índices paralelos:
Documents
│
┌─────────┴─────────┐
▼ ▼
embedding model V3 embedding model V4
│ │
▼ ▼
Index V3 Index V4
│
▼
shadow traffic
│
▼
evaluate
│
▼
switch alias
No mueva miles de millones de vectores con la esperanza de que la nueva recuperación sea mejor. Mídala antes de hacer el cambio.
Caché en varios niveles
No toda consulta merece todo el proceso. Si miles de empleados preguntan “¿Cuántos días de vacaciones tenemos?”, ejecutar
embedding
→ distributed retrieval
→ reranking
→ large LLM
Cada vez que se hace es un desperdicio. Tiene más sentido utilizar un conjunto de cachés en capas:
User Query
│
▼
Exact Response Cache
│ miss
▼
Semantic Cache
│ miss
▼
Retrieval Cache
│ miss
▼
Search
Un caché de incrustaciones es el complemento obvio:
hash(normalized_query)
↓
query embedding
Las cosas se complican cuando el contenido cambia. Una respuesta almacenada en caché contra
Policy version 17
puede torcerse una vez.
Policy version 18
Se pone en marcha. Por lo tanto, la clave de caché o el esquema de invalidación deben conocer los límites del corpus y las versiones. Es peligroso almacenar en caché resultados sin reglas de actualización.
Planifique para situaciones de fallo, no solo para el escenario ideal
Los sistemas grandes fallan de forma gradual. Espere situaciones como:
embedding provider unavailable
one vector shard unavailable
search cluster degraded
reranker timeout
LLM rate limited
LLM provider outage
queue backlog growing
parser crashing on malformed PDF
OCR worker running out of memory
Cada fallo requiere una política explícita. Quizás:
Vector search fails
↓
Can lexical search answer?
↓
yes
↓
return degraded retrieval mode
o:
Primary LLM unavailable
↓
Fallback model
↓
Lower quality but service continues
o:
Document parser fails 5 times
↓
Dead-letter queue
↓
record failure reason
↓
operator / automated remediation
Intentar de nuevo una y otra vez no es resiliencia. A veces es simplemente un bucle infinito con una factura de nube asociada.
Contrapresión
Imagínese que la ingestión recibe normalmente
20M chunks/hour
y la capacidad de incrustación puede manejar
25M chunks/hour
Todo está bien. Luego un cliente ejecuta una migración y genera
200M chunks/hour
Si no hay contrapresión, el fallo ocurre de la siguiente manera:
embedding service overloaded
↓
timeouts
↓
retries
↓
more load
↓
more failures
↓
retry storm
Con un amortiguador delante de los trabajadores:
incoming workload
↓
durable queue
↓
workers consume at safe rate
↓
backlog temporarily grows
La capacidad ahora puede escalar de forma independiente de la tasa de llegada. Ese es otro motivo por el cual el plano de ingestión merece su propia arquitectura.
La observabilidad debe abarcar la calidad de la recuperación
Los informes de monitoreo habituales indican cosas como:
HTTP 200 rate
CPU
memory
disk
p95 latency
error rate
requests/second
Todos son útiles, pero un servicio RAG puede devolver HTTP 200 en 800 ms y aún así dar una respuesta completamente incorrecta. También se necesita telemetría específica para la IA. Un seguimiento útil por solicitud podría verse así:
request_id: rq_912
query rewrite:
"parental policy?" → "parental leave policy"
vector retrieval:
200 candidates
145 ms
lexical retrieval:
200 candidates
91 ms
fusion:
278 unique candidates
reranking:
278 → 20
212 ms
context:
13 chunks
8,420 tokens
LLM:
input 9,104 tokens
output 681 tokens
1.8 sec
citations:
3 documents
total:
2.4 sec
La evaluación sin conexión requiere métricas que respondan a preguntas como: ¿se recuperó la evidencia adecuada?, ¿tuvo un ranking lo suficientemente alto?, ¿la respuesta está respaldada por esa evidencia?, ¿mejoró lo suficiente el reordenamiento de las consultas como para justificar su latencia?, y ¿qué clases de consultas fallan con mayor frecuencia? Esto es más útil para tomar acciones que seguir un porcentaje global de alucinaciones.
Evaluar la recuperación por separado de la generación
Cuando la respuesta final es incorrecta, existen al menos dos clases distintas de fallos. En un falla en la recuperación, la evidencia necesaria nunca llegó al contexto:
Correct document exists
↓
retriever misses it
↓
LLM never sees it
↓
wrong answer
Ninguna cantidad de edición rápida puede solucionar eso. En un falla de generación, la evidencia correcta sí llegó:
correct evidence
↓
context
↓
LLM
↓
incorrect interpretation
Ahora, el diseño de las instrucciones, la elección del modelo, la organización del contexto o la estrategia de razonamiento pueden ser importantes. Si solo se evalúan las respuestas finales, ambas parecen idénticas, por lo que un proceso de evaluación riguroso debe examinar las etapas intermedias.
Asignar un límite a la latencia
“El sistema debe ser rápido” no es un requisito. En su lugar, asigna un límite a cada etapa. Por ejemplo, supongamos que el objetivo es obtener una primera respuesta en unos 2,5 segundos, con una distribución como esta:
API + auth 50 ms
query understanding 100 ms
retrieval 300 ms
fusion 20 ms
reranking 250 ms
context construction 30 ms
LLM time-to-first-token 1,200 ms
network / safety margin 550 ms
-----------------------------------
target ~2,500 ms
Estos no son números universales. El ejercicio es importante porque muestra hacia dónde realmente va el tiempo. Si la recuperación de datos consume 1,8 segundos, cambiar de proveedor de LLM no afectará al cuello de botella principal. Si el LLM representa el 90 % de la demora, dedicar un mes a reducir 40 ms en la búsqueda vectorial probablemente no sea el mejor uso de los esfuerzos.
El costo también requiere la misma disciplina
Un gran sistema RAG tiene muchos centros de costo separados:
object storage
document parsing
OCR
embedding generation
vector storage
vector replicas
lexical search
metadata database
network transfer
reranking
LLM input tokens
LLM output tokens
observability
backups
Por lo tanto, mida el costo por unidad útil, por ejemplo:
cost / 1,000 indexed documents
cost / million chunks
cost / query
cost / successful answer
cost / tenant
El último de esos aspectos es el más importante en SaaS. Es posible que descubra:
Tenant A
5% of revenue
42% of LLM spend
Sin medición por inquilino, lo único que se ve es una factura mensual de infraestructura inesperadamente alta.
Conserva el almacenamiento costoso para quienes realmente lo necesitan
Una imagen mental útil es una gradación que va de lo costoso y rápido a lo barato y de gran capacidad:
expensive / fast
▲
│
vector indexes
search indexes
Redis caches
│
metadata DB
│
object storage
│
▼
cheap / large
El PDF completo no tiene razón para estar dentro del índice vectorial. Reserva almacenamiento costoso solo para aquello que requiera recuperación; deja que el almacenamiento de objetos almacene los documentos originales, y que la infraestructura de búsqueda contenga las representaciones buscables. Cuando el corpus llega a cientos de terabytes, esta distinción se vuelve muy importante.
Datos calientes y fríos
No todos los 100 millones de documentos son consultados por igual. Quizás:
Last 30 days → 70% of queries
Last 12 months → 25%
Older archive → 5%
Tratar todo de la misma manera desperdicia infraestructura. Una opción es una división como esta:
Query
│
├────→ Hot index
│
└────→ Archive index when necessary
El enrutamiento basado en el tiempo también evita que una pregunta sobre los eventos de esta semana se relacione con diez años de fragmentos históricos. Los sistemas grandes son eficientes al evitar tareas, no solo al realizar trabajos innecesarios más rápido.
La multiarrendamiento cambia todo
Supongamos que su plataforma sirve a
10,000 organizations
Casi con certeza no lo deseas.
10,000 completely independent vector clusters
Pero concentrar a todos los inquilinos en un único espacio de nombres genera problemas de interferencia entre usuarios y dificultades en la autorización. Un enfoque viable consiste en agrupar a los inquilinos según su tamaño:
Small tenants
↓
shared shard groups
Medium tenants
↓
partitioned shared infrastructure
Very large tenants
↓
dedicated shard groups
Eso evita que un cliente importe 500 millones de fragmentos, lo que saturaría a miles de fragmentos más pequeños, y proporciona una vía de aislamiento para los clientes empresariales que lo necesitan. Ninguna estrategia de particionamiento por inquilino sirve para todos.
El LLM está cerca del final
Mira cuánto trabajo de ingeniería abordamos antes de llegar al modelo. Eso es intencional. Una solicitud RAG madura se procesa de esta manera:
User
↓
API Gateway
↓
Authentication
↓
Authorization
↓
Rate Limiter
↓
Query Understanding
↓
Shard Routing
↓
Hybrid Candidate Retrieval
↓
Rank Fusion
↓
Reranker
↓
Deduplication
↓
Context Builder
↓
LLM
↓
Citation Validation
↓
Response
El LLM es importante, pero con 100 millones de documentos es solo un componente entre muchos.
La imagen completa de la producción
Al combinarlo todo, aquí está el camino de la consulta, luego el camino de la ingesta, y finalmente los sistemas que soportan ambos:
CLIENTS
│
▼
┌──────────────┐
│ API Gateway │
└──────┬───────┘
│
┌──────▼───────┐
│ Auth / ACL │
│ Rate Limits │
└──────┬───────┘
│
▼
┌────────────────────┐
│ Query Orchestrator │
└─────────┬──────────┘
│
┌─────────────┼──────────────┐
│ │ │
▼ ▼ ▼
Cache Query Rewrite Metadata
Filters
│ │ │
└─────────────┼──────────────┘
▼
Shard Router
│
┌────────────┴────────────┐
▼ ▼
Lexical Search Vector Search
│ │
└───────────┬─────────────┘
▼
Rank Fusion
│
▼
Reranker
│
▼
Deduplicate
│
▼
Context Builder
│
▼
LLM
│
▼
Citation Validation
│
▼
RESPONSE
================================================================
INGESTION SYSTEM
Documents
│
▼
Object Store
│
▼
Event Queue
│
┌────────────┼────────────┐
▼ ▼ ▼
Parser Parser Parser
│ │ │
└────────────┼────────────┘
▼
Normalization
│
▼
Chunker
│
┌──────────────┴──────────────┐
▼ ▼
Embedding Queue Text Index Queue
│ │
┌─────┼─────┐ ▼
▼ ▼ ▼ Lexical Index
GPU GPU GPU
│ │ │
└─────┼─────┘
▼
Vector Index
================================================================
SUPPORTING SYSTEMS
Metadata DB → document state, versions, ownership
Object Storage → original source documents
Vector Cluster → semantic retrieval
Search Cluster → lexical retrieval
Redis → caches and rate limiting
Message Queue → asynchronous ingestion
Observability → traces, metrics, evaluation
Secrets / IAM → service authorization
Evaluation System → retrieval + answer-quality tests
Antipatrones que evitar
Al analizar una arquitectura de este tipo, lo siguiente generaría preocupación inmediata:
One giant vector collection with no routing strategy
Synchronous PDF ingestion
Only vector retrieval, no lexical search
ACL filtering after retrieval
No document versioning
No embedding model version
No reranking
Sending top-100 chunks directly to the LLM
Using the LLM for every trivial classification task
No dead-letter queue
No retrieval-level evaluation
No tenant-level cost attribution
Treating the vector DB as permanent document storage
Re-embedding the entire corpus for every small content change
Benchmarking only average latency instead of tail latency
Cualquiera de ellos puede presentar un prototipo. Juntos, a escala de mil millones de vectores, se vuelven extremadamente problemáticos.
El principio fundamental
La forma más útil de pensar en un RAG grande no es esta:
question
→ vector search
→ LLM
Es esto:
billions of possible evidence units
↓
routing constraints
↓
relevant partitions
↓
cheap candidate search
↓
hundreds of items
↓
expensive reranking
↓
tens of passages
↓
context optimization
↓
LLM
↓
grounded answer
El sistema funciona como un embudo. En cada etapa invertimos más cálculos en menos candidatos:
Cheap operation → huge search space
Moderate operation → smaller candidate set
Expensive operation → tiny candidate set
Most expensive model → final context only
Esa idea es la que mantiene a la arquitectura viable desde el punto de vista económico.
Qué llevarse como aprendizaje
Un sistema RAG con unos pocos miles de documentos es en su mayor parte un ejercicio de ingeniería de aplicaciones. Con **100 millones de documentos**, se convierte en una infraestructura de búsqueda, sistemas distribuidos, ingeniería de datos, seguridad y control de costos, todos ellos que requieren atención al mismo tiempo. La base de datos vectorial es importante, al igual que el modelo de embedding y el LLM, pero la arquitectura que los rodea es más crucial de lo que admiten la mayoría de los tutoriales.
El modelo mental escalable: almacenar la fuente de forma económica, procesar de manera asíncrona, versionar todo, particionar intencionadamente, aplicar autorización desde el principio, recuperar datos mediante varios indicadores, reordenar rigurosamente los resultados, proporcionar al modelo solo evidencias de alta calidad y medir cada etapa por separado. Una vez que el corpus alcanza miles de millones de fragmentos, la mejor optimización ya no consiste en acelerar la búsqueda, sino en organizar todo de tal manera que la mayor parte del sistema nunca tenga que interactuar con la mayor parte del corpus.
Lecturas relacionadas
- Ingestión de datos RAG para entornos de producción en bancos: PDFs, OCR, tablas y conectores — Cómo ingresar documentos bancarios para RAG: clasificar PDFs, ejecutar OCR con indicadores de revisión, mantener la estructura de Word, manejar tablas e imágenes y crear conectores robustos.
- Limpieza de datos para RAG en producción: codificación, OCR, eliminación de duplicados y PII — Cómo convertir documentos empresariales analizados en texto listo para la recuperación: reparación de codificación, eliminación de textos genéricos y duplicados, marcadores de OCR de riesgo, manejo de idiomas y enmascaramiento de PII.
- RAG en tiempo de consulta con diez millones de documentos: embudos, enrutadores, agentes y evaluaciones — Un embudo de recuperación en menos de 200 ms con filtros SQL, búsqueda híbrida y reclasificación, además de enrutamiento de consultas, orquestación de agentes, métricas de evaluación y un script de pipeline de Qdrant.