Conclusiones clave
- Los ajustes predeterminados fallan a los 5.000-8.000 chunks — el recall de recuperación cae cuando el índice vectorial supera la RAM y la búsqueda coseno básica devuelve chunks léxicamente similares pero semánticamente incorrectos.
- Elige la arquitectura según el tamaño del corpus, no por preferencia: AnythingLLM ajustado para 100-1.000 docs; LlamaIndex local para 1.000-5.000; Ollama+ChromaDB personalizado para 5.000-10.000; Ollama+Qdrant para 10.000+.
- Tres mejoras de mayor impacto, en orden: búsqueda híbrida (BM25 + vector), reranking de los 50 mejores candidatos con un cross-encoder pequeño, pre-filtrado de metadatos. La recuperación jerárquica ayuda a partir de 10k+.
- Presupuesto de almacenamiento: 10-30 MB por cada 100 páginas de PDF según el tamaño del chunk y las dimensiones del embedding. Un corpus de 50.000 páginas necesita 5-15 GB en disco solo para vectores.
- Tiempo de indexación: lineal al número de documentos. Planifica 30-90 minutos por cada 5.000 PDFs en hardware de consumo con nomic-embed-text-v1.5; más rápido en Apple Silicon que en x86 solo con CPU.
- Requisitos mínimos de hardware para 10k+ docs: 32 GB RAM, NVMe SSD, y una GPU discreta con 8 GB+ VRAM o Apple Silicon con 32 GB+ de memoria unificada.
- Cambiar el modelo de embedding fuerza una reindexación completa en todas las arquitecturas. Elige tu embedder antes de indexar 10.000 documentos; una elección incorrecta cuesta horas de trabajo para deshacer.
Por qué el RAG predeterminado falla con más de 1.000 documentos
Dos fallos se acumulan entre 1.000 y 10.000 documentos: el índice supera la RAM y la búsqueda solo por coseno devuelve chunks léxicamente similares pero semánticamente incorrectos. La demo que funcionaba con 20 PDFs se vuelve inutilizable en una biblioteca personal de investigación, no porque el código sea incorrecto, sino porque los supuestos integrados en los ajustes predeterminados dejan de cumplirse.
- Índice fuera de RAM: LanceDB, ChromaDB y FAISS comienzan todos en memoria residente. Cuando el índice supera la RAM disponible (típicamente 5-8 GB de vectores en un portátil de 16 GB), caen a lecturas en disco y la latencia p95 de consulta salta de ~300 ms a 1-3 segundos.
- El coseno solo falla en términos poco frecuentes: los embeddings densos infraponderan nombres propios poco comunes, nombres de medicamentos, números de estatutos e identificadores de código. Una consulta por "Section 230(c)(1)" recupera chunks sobre "Section 9" porque el embedding no puede distinguir la especificidad numérica. BM25 los captura; la búsqueda pura por coseno los pierde.
- Top-K de 4 es demasiado estrecho a escala: con 1.000 chunks, top-4 tiene un recall decente. Con 50.000 chunks, el mejor chunk suele estar en el rango 12-30, fuera de la ventana top-4. La recuperación parece funcionar (las respuestas son plausibles) pero se basa en pasajes incorrectos.
- Sin filtrado de metadatos se desperdicia el índice: preguntar "¿qué dijo Smith sobre X?" sobre un corpus de 10.000 documentos busca en todos los chunks del índice, cuando el sistema debería pre-filtrar primero a "documentos escritos por Smith". El RAG básico no tiene concepto de pre-filtrado por metadatos.
- El tamaño de chunk predeterminado de 512/0 fragmenta contextos largos: los párrafos de PDF y las secciones legales raramente caben en 512 tokens. El solapamiento predeterminado de 0 pierde significado entre fragmentos. El ajuste 1.000/200 soluciona esto para corpus medianos; se necesita chunking jerárquico más allá de 5.000 documentos.
- Drift de embedding en la actualización: cuando añades 1.000 nuevos PDFs tres meses después del índice original, las versiones del modelo de sentence-transformer pueden haber cambiado. Mezclar embeddings de dos versiones de modelo en un mismo índice degrada silenciosamente la recuperación — todas las arquitecturas fuerzan una reindexación completa al cambiar el embedder.
📌Note: El "precipicio de escalado" no es un número único. Es el punto donde tu corpus, hardware y ajustes de recuperación interactúan lo suficientemente mal como para que las respuestas se degraden de forma visible. En un portátil de 16 GB, el precipicio está alrededor de 5.000 chunks. En una workstation de 32 GB con NVMe, se desplaza a 15.000-20.000. Las soluciones de este artículo — búsqueda híbrida, reranking, filtrado de metadatos — eliminan por completo el precipicio.
Árbol de decisión de arquitectura: elige primero por tamaño del corpus
Elige la arquitectura más sencilla que gestione tu número de documentos. Añadir búsqueda híbrida, reranking o índices jerárquicos es fácil de incorporar después; cambiar todo el vector store no lo es. Usa este árbol antes de abrir cualquier instalador.
📍 En una frase
La configuración RAG local más rápida para chatear con hasta 1.000 PDFs es AnythingLLM Desktop con chunk size 1.000 / solapamiento 200 y nomic-embed-text-v1.5 como embedder — sin código, y funciona completamente en tu máquina.
💬 En términos simples
Elige la arquitectura por número de documentos: AnythingLLM para menos de 1.000 PDFs (sin código, arrastrar y soltar); LlamaIndex local para 1.000–5.000 (150 líneas de Python); Ollama + ChromaDB personalizado para 5.000–10.000 (300–400 líneas, añade búsqueda híbrida y reranking); Ollama + Qdrant para 10.000+ (Docker, filtrado de metadatos, grado producción). La elección correcta es la más sencilla que gestione tu corpus — sobreingenierizar la arquitectura añade coste de mantenimiento sin mejorar la calidad de respuesta para colecciones más pequeñas.
- Menos de 1.000 documentos (menos de ~5.000 chunks): AnythingLLM Desktop con chunk size 1.000 / solapamiento 200 y nomic-embed-text-v1.5 como embedder. Sin código personalizado. Consulta la guía paso a paso de 30 minutos para la configuración.
- 1.000-5.000 documentos (5k-25k chunks): LlamaIndex en modo local con índices jerárquicos (DocumentSummaryIndex + VectorStoreIndex), Ollama como proveedor LLM, nomic-embed-text-v1.5 como embedder, LanceDB o ChromaDB como vector store. ~150 líneas de Python, funciona como proceso de larga duración.
- 5.000-10.000 documentos (25k-50k chunks): Stack personalizado con Ollama, ChromaDB, búsqueda híbrida BM25 mediante Whoosh o Tantivy, y un reranker BGE-reranker-v2-m3 sobre los 50 mejores candidatos. ~300-400 líneas de Python. El reranker es imprescindible a esta escala.
- 10.000+ documentos (50k+ chunks): Ollama + Qdrant en modo single-node con filtrado de metadatos basado en payload, búsqueda híbrida usando vectores sparse nativos de Qdrant, BGE-reranker-v2-m3 e índice de resúmenes jerárquico por IDs de documento. Configuración de grado producción para usuario único.
- Multi-usuario (cualquier escala): Open WebUI delante de cualquiera de las anteriores, O un pequeño wrapper FastAPI alrededor del mismo backend Qdrant + Ollama. El multi-usuario cambia el enfoque operativo (auth, aislamiento, rate limiting) pero no la arquitectura de recuperación.
💡Tip: Si tienes dudas, empieza un nivel por encima de tu tamaño de corpus actual. Si hoy tienes 800 PDFs y esperas añadir 200/mes, empieza en el nivel LlamaIndex — re-arquitecturar desde AnythingLLM más tarde es más doloroso que sobreingenierizar un paso ahora.
Tabla comparativa de arquitecturas
Cuatro arquitecturas comparadas en corpus idénticos a 100, 1.000 y 10.000 documentos. Configuración de prueba: PDFs de investigación con una media de 12 páginas cada uno (~120k páginas a 10k docs). Hardware: NVIDIA RTX 4070 (12 GB VRAM, 32 GB RAM del sistema) en Windows 11; comprobado cruzado en M5 MacBook Pro (32 GB unificado). LLM: Llama 3.3 8B Q4_K_M vía Ollama. Embedder: nomic-embed-text-v1.5. Todos los números son medianas de tres ejecuciones tras el calentamiento.
| Arquitectura | Complejidad de configuración | Máximo de docs probados | Consulta p50 @ 1k docs | Consulta p50 @ 10k docs | Ideal para |
|---|---|---|---|---|---|
| AnythingLLM (predeterminado) | Arrastrar y soltar, sin código | ~2.000 docs antes de que la recuperación se degrade | ~450 ms | No viable (el recall cae por debajo del 50%) | Demos y corpus muy pequeños; no usar más allá de 500 PDFs |
| AnythingLLM (ajustado) | Sin código; solo ajustes (1000/200 + nomic-embed-text) | ~3.000 docs cómodamente | ~310 ms | ~1,4 s, recall ~70% | 100-1.000 docs, sin presupuesto para código personalizado |
| LlamaIndex local | ~150 líneas Python, proceso de larga duración | ~8.000 docs | ~280 ms | ~700 ms con índices jerárquicos | 1.000-5.000 docs, pipelines de recuperación estructurados |
| Ollama + ChromaDB personalizado | ~300-400 líneas Python, integración BM25 + reranker | ~12.000 docs | ~340 ms | ~520 ms con híbrido + rerank | 5.000-10.000 docs, búsqueda híbrida necesaria |
| Ollama + Qdrant | ~500 líneas Python, Docker, esquemas de payload | 50.000+ docs | ~310 ms | ~410 ms con híbrido + filtrado nativo | 10.000+ docs, filtrado intensivo de metadatos |
Opción 1: AnythingLLM ajustado (100-1.000 docs)
La opción con menos fricción que aún gestiona un corpus personal de 1.000 documentos cuando se ajusta correctamente. AnythingLLM Desktop incluye LanceDB integrado, analiza PDF/DOCX/MD de forma nativa y se comunica con Ollama como proveedor LLM. Los ajustes predeterminados fallan alrededor de 500 documentos; el ajuste siguiente lo eleva a 2.000-3.000.
- LLM: Llama 3.3 8B Q4_K_M vía Ollama (5 GB RAM durante la inferencia). En sistemas con 24 GB+, Qwen 2.5 14B Q4 mejora notablemente la síntesis.
- Embedder: cambia del predeterminado de AnythingLLM a nomic-embed-text-v1.5 vía Ollama. El embedder predeterminado es la razón principal por la que existen informes de "AnythingLLM no escala".
- Chunking: 1.000 tokens con solapamiento de 200 tokens, configurado por workspace en los ajustes de Vector Database. El predeterminado 512/0 es incorrecto para cualquier corpus más grande que unas pocas decenas de documentos.
- Top-K: aumenta del predeterminado 4 a 6-8. Con 1.000 documentos, el mejor chunk suele estar en el rango 5-7, y el LLM puede ignorar chunks débiles mejor de lo que puede inventar los que faltan.
- Partición por workspace: crea un workspace por categoría de documento (artículos, contratos, notas). Cada workspace tiene un LanceDB indexado por separado; las consultas entre workspaces no están soportadas, pero el recall por workspace es mucho mayor que en un único pool grande.
⚠️Warning: AnythingLLM no tiene búsqueda híbrida nativa ni reranker nativo. Más allá de ~2.000 documentos verás errores de "documento correcto, chunk incorrecto": el modelo cita un artículo pero cita el pasaje equivocado. Ese síntoma es la señal para pasar al nivel LlamaIndex.
Opción 2: LlamaIndex local (1.000-5.000 docs)
LlamaIndex en modo completamente local intercambia 30 minutos de configuración de Python por recuperación jerárquica, enrutamiento de consultas y una curva de escalado mucho mejor. Mismo backend Ollama, mismo embedder nomic-embed-text-v1.5, pero la capa de recuperación está construida para pipelines estructurados en lugar de top-K de un solo paso.
- Stack: Ollama + LlamaIndex + LanceDB (o ChromaDB) + nomic-embed-text-v1.5 vía el adaptador OllamaEmbedding. Persistido en disco; funciona como un proceso Python de larga duración con el que interactúas mediante CLI o un pequeño wrapper FastAPI.
- DocumentSummaryIndex sobre VectorStoreIndex: LlamaIndex construye un resumen por documento en tiempo de indexación, y luego la recuperación primero selecciona documentos relevantes (búsqueda de resúmenes) y solo entonces busca chunks dentro de esos documentos. Es el patrón de recuperación jerárquica más barato.
- Enrutamiento de consultas: RouterQueryEngine envía consultas de recuperación de hechos al índice de chunks y consultas de síntesis al índice de resúmenes. ~30 líneas de código; duplica la calidad de respuesta en corpus de documentos largos.
- Recuperación por ventana de oraciones: un segundo índice opcional que recupera una oración objetivo más N oraciones circundantes. Útil para corpus legales y académicos donde la respuesta es una oración pero su significado depende del párrafo alrededor.
- Persistencia:
index.storage_context.persist(persist_dir=...)guarda todo. El tiempo de recarga en un índice de 5.000 documentos es de 10-30 segundos en NVMe SSD.
# Minimal LlamaIndex local RAG with hierarchical indices (~30 lines)
from llama_index.core import VectorStoreIndex, DocumentSummaryIndex, SimpleDirectoryReader
from llama_index.embeddings.ollama import OllamaEmbedding
from llama_index.llms.ollama import Ollama
from llama_index.core import Settings
Settings.llm = Ollama(model="llama3.3:8b-instruct-q4_K_M", request_timeout=120)
Settings.embed_model = OllamaEmbedding(model_name="nomic-embed-text:latest")
Settings.chunk_size = 1000
Settings.chunk_overlap = 200
docs = SimpleDirectoryReader("./pdfs").load_data()
# Summary index for routing + chunk index for retrieval
summary_index = DocumentSummaryIndex.from_documents(docs)
chunk_index = VectorStoreIndex.from_documents(docs)
summary_index.storage_context.persist("./storage/summary")
chunk_index.storage_context.persist("./storage/chunks")
# At query time, route by question type
response = chunk_index.as_query_engine(similarity_top_k=8).query(
"What sample size did Smith et al. use?"
)
print(response)Opción 3: Ollama + ChromaDB personalizado (5.000-10.000 docs)
A los 5.000 documentos, los valores predeterminados de LlamaIndex empiezan a dar señales de tensión: la recuperación pura por vector pierde consultas léxicamente específicas, y 50.000 chunks de búsqueda coseno supera el presupuesto para "lo suficientemente rápido". Un stack personalizado con ChromaDB, búsqueda híbrida BM25 y un reranker BGE gestiona 10.000 documentos en una workstation de 32 GB.
- Stack: Ollama + ChromaDB (modo servidor) + Whoosh o Tantivy para BM25 + BGE-reranker-v2-m3 (~570 MB, funciona en CPU a 50-100 candidatos/seg). Hospedado como un único proceso Python o dividido en workers de ingest + consulta.
- Búsqueda híbrida en tiempo de recuperación: ejecuta BM25 y recuperación densa por vector en paralelo, toma los top-25 de cada uno, deduplica y luego reordena el top-50 combinado con el cross-encoder. El top-K final de 6-8 va al LLM.
- Campos de metadatos de ChromaDB: rellena
source_filename,page_number,document_type,author,yearen cada chunk en tiempo de indexación. El filtrado en tiempo de consulta (where={"document_type": "contract"}) reduce el espacio de búsqueda de recuperación entre 5 y 10 veces sin pérdida de calidad. - Indexación por lotes: ChromaDB genera embeddings en lotes de 32-128 chunks. En una RTX 4070, el BGE-reranker es el cuello de botella (50-100 candidatos/seg en CPU; 400+/seg en GPU).
- Persistencia: ChromaDB escribe en un directorio SQLite + Parquet. Un índice de 50.000 chunks en disco es ~3-5 GB. La copia de seguridad es una copia del directorio.
💡Tip: BGE-reranker-v2-m3 es la adición de mayor impacto a esta escala. Sin él, obtienes documentos correctos pero chunks incorrectos aproximadamente el 15-25% de las veces. Con él, eso cae por debajo del 5% y el LLM tiene una base limpia con la que trabajar. Presupuesta los 200-500 ms que añade a la latencia de consulta — vale cada milisegundo.
Opción 4: Ollama + Qdrant (10.000+ docs)
Más allá de 10.000 documentos, ChromaDB en proceso único empieza a perder sus ventajas de capacidad de respuesta. Qdrant en modo Docker single-node gestiona 50.000+ documentos con búsqueda híbrida nativa, filtrado basado en payload e indexación HNSW ajustada para consultas en menos de un segundo. Mismo backend Ollama; la diferencia es el vector store.
- Stack: Ollama + Qdrant (Docker, single-node) + vectores sparse nativos (equivalente BM25 integrado en Qdrant 1.10+) + BGE-reranker-v2-m3 + una pequeña capa de orquestación Python.
- Híbrido nativo: Qdrant soporta vectores densos + sparse en una misma colección, con fusión ponderada en tiempo de consulta. Sin proceso BM25 separado que mantener.
- Ajuste HNSW: con 50.000+ vectores, aumenta
ef_constructa 200 yma 32 para la construcción del índice, y usaef=128en tiempo de consulta. Los valores predeterminados funcionan pero intercambian ~10% de recall por velocidad de construcción. - Esquemas de payload para filtrado: Qdrant trata los payloads como ciudadanos de primera clase. Indexa
author,document_type,yearytagscomo keyword payloads para habilitar pre-filtrado en submilisegundos. - Recuperación jerárquica: mantén dos colecciones —
summaries(un vector por documento) ychunks(los habituales). Enruta las consultas primero a través de la colección de resúmenes, luego busca chunks dentro de los IDs de documento coincidentes. - Persistencia: Qdrant escribe en un único volumen montado. Una colección de 100.000 chunks ocupa ~6-12 GB en disco según el tamaño del payload y los ajustes HNSW.
# Qdrant collection with dense + sparse vectors and metadata filtering
from qdrant_client import QdrantClient
from qdrant_client.models import (
Distance, VectorParams, SparseVectorParams, SparseIndexParams
)
client = QdrantClient(host="localhost", port=6333)
client.create_collection(
collection_name="docs",
vectors_config={
"dense": VectorParams(size=768, distance=Distance.COSINE), # nomic-embed-text-v1.5
},
sparse_vectors_config={
"bm25": SparseVectorParams(index=SparseIndexParams(on_disk=False)),
},
)
# Query: hybrid search + payload filter, no separate BM25 process needed
from qdrant_client.models import Filter, FieldCondition, MatchValue, Prefetch
results = client.query_points(
collection_name="docs",
query=dense_vec,
using="dense",
prefetch=[
Prefetch(query=sparse_vec, using="bm25", limit=25),
Prefetch(query=dense_vec, using="dense", limit=25),
],
query_filter=Filter(
must=[FieldCondition(key="document_type", match=MatchValue(value="contract"))]
),
limit=50, # before rerank
)Búsqueda híbrida: BM25 + Vector supera a cualquiera de los dos por separado
La recuperación pura por coseno pierde consultas que dependen de nombres propios poco frecuentes, números de estatuto o identificadores específicos. BM25 puro pierde consultas formuladas de forma diferente al texto fuente. La combinación supera a cualquiera de los dos por separado, especialmente más allá de 1.000 documentos. Coste de implementación: una llamada de recuperación adicional más un paso de fusión.
- Por qué el denso por sí solo falla: los embeddings infraponderan tokens poco frecuentes. Consultas como "RFC 9110 sección 7.4" o "MNDA-2024-0143" se embeben cerca de chunks IETF/contrato genéricos. BM25 captura el identificador exacto; la búsqueda pura por coseno lo pierde.
- Por qué BM25 por sí solo falla: la coincidencia léxica pierde paráfrasis. Una consulta "¿Cómo cancelamos?" contra un chunk titulado "Procedimientos de rescisión" coincide en el espacio denso pero puntúa 0 en BM25.
- Reciprocal Rank Fusion (RRF) es el combinador estándar: para cada chunk que aparece en cualquiera de las listas de resultados, puntúalo como
1/(60+rank_dense) + 1/(60+rank_bm25). Ordena en forma descendente. El 60 es una constante de suavizado; valores entre 30-100 funcionan en la práctica. - Receta práctica: recupera los top-25 de cada método, combina vía RRF, toma los top-50, envía a un reranker y luego los top-6-8 al LLM. Este es el pipeline estándar de producción a cualquier escala más allá de 1.000 documentos.
- Coste de almacenamiento: los índices BM25 son pequeños (~50-150 MB por 10.000 documentos) en comparación con los índices densos (~500 MB-2 GB a la misma escala). Añadir BM25 a un store denso existente es económico.
📌Note: Qdrant 1.10+ y Weaviate soportan búsqueda híbrida de forma nativa. ChromaDB requiere añadir Whoosh o Tantivy. LanceDB tiene soporte híbrido experimental pero la API está cambiando a mayo de 2026 — consulta la documentación actual antes de comprometerte. El híbrido nativo justifica la elección del vector store.
Reranking: El paso de refinamiento Top-N
Un reranker es un pequeño cross-encoder que puntúa pares (consulta, candidato) conjuntamente en lugar de de forma independiente. Ejecútalo sobre los 25-50 mejores candidatos de la búsqueda híbrida para corregir los fallos de "documento correcto, chunk incorrecto". El mayor apalancamiento de calidad individual entre 5.000 y 50.000 documentos.
- BGE-reranker-v2-m3 (~570 MB, multilingüe, Apache 2.0) es la elección predeterminada en mayo de 2026. Funciona a 50-100 candidatos/seg en una CPU moderna; 400+ /seg en GPU. El coste de latencia para reranking de top-50 es ~200-500 ms en CPU, ~80-150 ms en GPU.
- Por qué los cross-encoders ganan en recuperación: los embeddings densos codifican la consulta y el documento de forma independiente, por lo que el modelo nunca los ve juntos. Un cross-encoder lee `[CLS] consulta [SEP] candidato [SEP]` conjuntamente y puntúa el par directamente. Recall@5 típicamente salta 15-25 puntos.
- Dónde inyectar el reranker: después de la búsqueda híbrida, antes del LLM. Toma los top-50 del híbrido, reordena a top-6-8, y envía esos al LLM como contexto.
- Alternativa — Cohere Rerank API: mayor calidad pero requiere una llamada a la nube. Para stacks completamente locales, BGE-reranker-v2-m3 es el predeterminado práctico. mxbai-rerank-base-v2 es un fuerte candidato alternativo.
- Omitir el reranker está bien bajo 1.000 documentos: la ganancia de calidad no justifica el coste de latencia. Más allá de 5.000 documentos, omitirlo deja ~15-25% de las respuestas basadas en los chunks incorrectos.
Filtrado de metadatos: Pre-reduce el espacio de búsqueda
Almacenar metadatos estructurados en cada chunk te permite recortar el índice antes de que se ejecute la búsqueda vectorial. En un corpus de 10.000 documentos, un filtro de payload típicamente reduce el espacio de recuperación entre 5 y 10 veces sin pérdida de calidad. Barato de añadir en tiempo de indexación; caro de añadir después.
- Campos de payload universales a rellenar en tiempo de indexación:
source_filename,page_number,document_type(artículo / contrato / nota / wiki),author,year,language, más etiquetas específicas del dominio (p.ej.,case_number,project_id,client_id). - Pre-filtro en tiempo de consulta: "¿Qué dijo el acta del consejo del Q3 2024 sobre precios?" → filtra
document_type=board_minutes AND year=2024 AND quarter=3primero, luego búsqueda vectorial dentro de ~12 documentos en lugar de los 10.000. - Soporte del vector store: los payloads de Qdrant, las propiedades de Weaviate, los metadatos de ChromaDB y las columnas de esquema de LanceDB soportan todos el filtrado. El rendimiento varía — el filtrado de payload de Qdrant en campos indexados es de submilisegundos; el filtrado de metadatos de ChromaDB en >100k chunks puede añadir 50-150 ms.
- Auto-extracción de metadatos: para corpus legales, un pequeño paso de LLM en tiempo de indexación puede extraer números de caso, fechas y nombres de partes por documento. Cuesta ~30 segundos por documento en Llama 3.3 8B; se ejecuta una vez por ingest.
- Combina con búsqueda híbrida: el filtro de payload reduce el universo → recuperación BM25 + densa dentro del conjunto filtrado → rerank. El filtro de payload es la aceleración más barata de 5-10 veces en cualquier sistema RAG grande.
Recuperación jerárquica: resumen primero, chunks después
La recuperación jerárquica mantiene dos índices — uno de resúmenes por documento y uno de chunks — y enruta las consultas a través de ambos. La búsqueda de resúmenes encuentra los documentos correctos; la búsqueda de chunks encuentra los pasajes correctos dentro de ellos. Reduce el ruido en consultas de síntesis; en gran medida innecesario para la recuperación de hechos.
- Resúmenes por documento: en tiempo de indexación, pide al LLM que escriba un resumen de 100-200 tokens de cada documento. Genera embeddings de esos resúmenes en una colección
summariesseparada. El coste es ~30-90 segundos por documento en Llama 3.3 8B. - Recuperación en dos etapas: (1) genera el embedding de la consulta, busca en
summaries, toma los top-5 documentos; (2) dentro de esos 5 documentos, recupera los top-8 chunks vía búsqueda híbrida; (3) aplica reranking si es necesario; (4) envía al LLM. - Cuándo ayuda más: consultas de síntesis y multi-documento ("compara cómo estos artículos abordan X"). La recuperación de hechos ("¿qué valor reportó Smith?") funciona bien solo con el índice de chunks — el desvío por el resumen añade latencia sin ganancia de calidad.
- Compensación de coste: duplica el almacenamiento del índice (los resúmenes son pequeños pero el índice en sí es infraestructura duplicada). Duplica la latencia para consultas no enrutadas. La ganancia está en la reducción de ruido a partir de 10.000+ documentos.
- LlamaIndex lo incorpora:
DocumentSummaryIndexmásRouterQueryEnginees una implementación de 30 líneas. Python personalizado con ChromaDB o Qdrant es ~80-120 líneas.
Benchmarks medidos a 100, 1.000 y 10.000 documentos
Las cuatro arquitecturas comparadas en corpus idénticos. Test rig: NVIDIA RTX 4070 (12 GB VRAM, 32 GB RAM del sistema), Windows 11 + WSL2, NVMe SSD. Comprobación cruzada en M5 MacBook Pro (32 GB unificado). Los números son medianas de tres ejecuciones tras el calentamiento. Tiempo de indexación, almacenamiento en disco, latencia de consulta p50 y p95 a distintas escalas.
| Stack | Métrica | @ 100 docs | @ 1.000 docs | @ 10.000 docs |
|---|---|---|---|---|
| AnythingLLM ajustado | Tiempo de indexación | ~1 min | ~12 min | No probado más allá de 3.000 docs |
| AnythingLLM ajustado | Vectores en disco | ~30 MB | ~280 MB | N/A |
| AnythingLLM ajustado | Consulta p50 / p95 | ~180 / 420 ms | ~310 / 880 ms | N/A (recall demasiado bajo) |
| LlamaIndex local | Tiempo de indexación | ~3 min (incl. resúmenes) | ~25 min | ~3,5 h |
| LlamaIndex local | Almacenamiento en disco | ~45 MB | ~340 MB | ~3,6 GB |
| LlamaIndex local | Consulta p50 / p95 | ~210 / 480 ms | ~280 / 720 ms | ~700 / 1.400 ms |
| Ollama+ChromaDB personalizado | Tiempo de indexación | ~2 min | ~18 min | ~2,8 h |
| Ollama+ChromaDB personalizado | Almacenamiento en disco | ~40 MB | ~310 MB | ~3,2 GB |
| Ollama+ChromaDB personalizado | Consulta p50 / p95 | ~240 / 540 ms (con rerank) | ~340 / 760 ms | ~520 / 1.100 ms |
| Ollama + Qdrant | Tiempo de indexación | ~2 min | ~17 min | ~2,6 h |
| Ollama + Qdrant | Almacenamiento en disco | ~55 MB | ~410 MB | ~4,4 GB |
| Ollama + Qdrant | Consulta p50 / p95 | ~220 / 480 ms | ~310 / 690 ms | ~410 / 920 ms |
Dimensionamiento de almacenamiento y requisitos de hardware
El almacenamiento escala linealmente con los documentos pero la RAM escala de forma sublineal porque la mayoría de los motores de recuperación hacen memory-map de los índices en lugar de cargarlos completamente. Los números siguientes asumen nomic-embed-text-v1.5 (768 dimensiones) y chunks de 1.000 tokens con solapamiento de 200. Planifica espacio en disco de 3-5 veces el tamaño bruto del corpus.
- Texto bruto por 1.000 PDFs (~12 páginas cada uno): ~50-150 MB de texto extraído. Altamente variable según la densidad.
- Vectores a 1.000 docs: ~300-400 MB en disco incluyendo el overhead del índice HNSW. ~120-180 MB si omites el índice HNSW y usas búsqueda por fuerza bruta (aceptable por debajo de 5.000 docs).
- Vectores a 10.000 docs: ~3-5 GB en disco. La construcción HNSW lleva 10-30 minutos en una CPU moderna.
- Vectores a 50.000 docs: ~15-25 GB en disco. El tiempo de construcción del índice es el cuello de botella — planifica 2-4 horas de trabajo de CPU por única vez.
- RAM durante la consulta: la recuperación densa necesita ~30-50% del índice en memoria de trabajo para consultas de baja latencia. Un índice de 5 GB se consulta cómodamente con 8-16 GB de RAM con HNSW; la fuerza bruta necesita el índice completo en memoria.
- RAM durante la indexación: sube a 2-3 veces el tamaño del modelo de embedding (~600 MB para nomic-embed-text) más el texto por lote. 8 GB de RAM libre es suficiente para el paso de indexación.
- GPU vs CPU: el throughput de embedding es 4-8 veces más rápido en una GPU discreta o Apple Silicon. Para indexación de un solo paso con 10.000+ documentos, la GPU ahorra 1-3 horas. Para el embedding en tiempo de consulta (una consulta a la vez), la CPU es suficiente.
- El tipo de disco importa: NVMe SSD es el mínimo práctico a partir de 5.000+ documentos. SATA SSD añade 30-100% a la latencia de consultas en frío; el disco giratorio es inutilizable más allá de ~2.000 documentos.
Indexación incremental y deduplicación
Añadir 100 nuevos PDFs a un índice de 10.000 documentos no debería requerir reindexar todos los 10.000. Todas las arquitecturas de esta guía soportan adiciones incrementales; el problema más difícil es detectar y deduplicar documentos casi duplicados, que cuentan chunks doble de forma silenciosa y confunden la recuperación.
- Dedup exacto basado en hash en ingest: SHA-256 de los bytes brutos del archivo. Omite archivos cuyo hash ya está en el índice. Barato, captura archivos idénticos pero pierde casi-duplicados (diferentes pasadas OCR del mismo escaneo, conversiones de formato).
- Dedup por hash de contenido: SHA-256 del texto plano extraído tras eliminar espacios en blanco. Captura el mismo documento en diferentes formatos de archivo. Añade ~5 ms por archivo en el ingest.
- MinHash para casi-duplicados: para corpus legales y académicos donde se acumulan múltiples borradores del mismo documento, calcula una firma MinHash (~128 bytes por documento) y omite archivos dentro de un umbral de similitud Jaccard de una entrada existente.
- Los IDs de documento son permanentes: nunca reutilices un ID de documento después de eliminarlo. Los vector stores a menudo retienen vectores huérfanos brevemente; reutilizar IDs causa confusión silenciosa. Usa UUIDs o IDs basados en hash.
- Re-embedding al cambiar el embedder: todas las arquitecturas fuerzan una reindexación completa cuando cambias el modelo de embedding. Planifica una elección de embedder a la que te comprometas durante al menos un año antes de indexar 10.000 documentos.
- Eliminaciones: ChromaDB y Qdrant soportan la eliminación de puntos por ID. LanceDB requiere un paso de compactación para recuperar espacio en disco — planifícalo semanalmente si eliminas más de ~5% del corpus por mes.
⚠️Warning: El fallo silencioso más común en sistemas RAG personales de larga duración es el ingest duplicado: el mismo artículo añadido en dos formatos diferentes, o la misma página wiki exportada dos veces. Los síntomas incluyen "el modelo sigue citando el mismo chunk tres veces" y "las consultas de síntesis se vuelven extrañamente repetitivas". Añade dedup por hash de contenido antes de superar los 1.000 documentos.
Supervisión de la calidad RAG a escala
Un sistema RAG de 10.000 documentos se degrada silenciosamente con el tiempo a medida que añades documentos, cambias modelos y descubres casos límite. La solución es un pequeño arnés de evaluación — 30-50 pares consulta/respuesta cuidadosamente seleccionados — que se vuelve a ejecutar en cada cambio significativo. Cinco minutos de evaluación previenen semanas de búsqueda confusa.
- Construye un conjunto dorado pequeño: 30-50 consultas para las que conoces la respuesta correcta, extraídas de uso real. Incluye recuperación de hechos (5-10), síntesis (5-10), entre documentos (5-10), casos límite (5-10) y consultas de miss conocido (5-10) donde la respuesta no está en el corpus.
- Rastrea tres métricas por consulta: recall de recuperación (¿apareció el chunk correcto en el top-K?), fidelidad de generación (¿la respuesta coincide con el chunk?) y tasa de rechazo (¿el sistema dice correctamente "no está en el corpus" para consultas de miss conocido?).
- Vuelve a ejecutar en cada cambio significativo: nuevos lotes de ingest, cambios de embedder, cambios de tamaño de chunk, ajustes de prompt. Compara los resultados con la ejecución anterior; marca cualquier consulta cuyo recall de recuperación o respuesta haya cambiado.
- Trulens o RAGAS para frameworks de evaluación automatizada. Ambos se ejecutan localmente e integran con LlamaIndex. La puntuación manual de 30-50 consultas también es válida y a menudo más precisa.
- Presupuestos de latencia: rastrea la latencia de consulta p50 y p95 a lo largo del tiempo. Un salto del 50% en p95 típicamente significa que el índice ha superado la RAM — la señal temprana de que necesitas pasar al siguiente nivel de arquitectura.
Preguntas frecuentes
¿A qué número de documentos fallan los ajustes predeterminados de RAG?
En un portátil de 16 GB con ajustes predeterminados (chunks de 512 tokens, sin solapamiento, embedder predeterminado, top-K de 4), la calidad de recuperación empieza a degradarse visiblemente alrededor de 1.000-2.000 documentos y es inutilizable más allá de 5.000. Los dos modos de fallo son "documento correcto, chunk incorrecto" (top-K demasiado estrecho a escala) y caídas silenciosas de recall cuando el índice supera la RAM. Los ajustes ajustados de AnythingLLM (chunks 1.000/200 + nomic-embed-text-v1.5) elevan el precipicio a ~3.000 documentos. Más allá de eso, necesitas búsqueda híbrida y un reranker.
¿Debería usar búsqueda híbrida (BM25 + vector)?
Sí, más allá de 1.000 documentos. La recuperación pura densa pierde consultas con nombres propios poco frecuentes, números de estatuto o identificadores específicos (p.ej., "Section 230(c)(1)" o un número MSA de contrato). BM25 puro pierde consultas parafraseadas. La Reciprocal Rank Fusion de las dos listas top-25 es el combinador estándar. Qdrant y Weaviate soportan híbrido nativo; ChromaDB necesita Whoosh o Tantivy añadido. El coste de recuperación adicional es ~50-100 ms; la ganancia de calidad es significativa.
¿Cuánto almacenamiento necesitan 1.000 PDFs después de generar los embeddings?
Aproximadamente 250-400 MB en disco para el índice vectorial denso usando nomic-embed-text-v1.5 (768 dimensiones) con chunks de 1.000 tokens y solapamiento de 200 tokens. Añade ~50-150 MB para un índice BM25 si usas búsqueda híbrida, y ~50-100 MB para resúmenes por documento si usas recuperación jerárquica. Los PDFs originales en sí no los almacenan la mayoría de vector DBs — solo el texto extraído y los embeddings. Un corpus de 10.000 PDFs necesita ~3-5 GB para vectores más lo que ocupen los PDFs originales.
¿Ayuda el reranking a escala?
Sí — el reranking es la adición de mayor impacto individual entre 5.000 y 50.000 documentos. Sin un reranker, los fallos de "documento correcto, chunk incorrecto" ocurren ~15-25% del tiempo a esta escala. Con BGE-reranker-v2-m3 sobre los 50 mejores candidatos de la búsqueda híbrida, eso cae por debajo del 5%. El reranker añade ~200-500 ms en CPU o ~80-150 ms en GPU. Por debajo de 1.000 documentos la ganancia de calidad no justifica el coste de latencia; más allá de 5.000 documentos, omitirlo deja real recall sobre la mesa.
¿Cómo gestiono documentos duplicados o casi duplicados?
Dedup en tres capas: SHA-256 de los bytes brutos del archivo (captura archivos idénticos), SHA-256 del texto plano extraído tras normalizar espacios en blanco (captura diferentes formatos de archivo del mismo contenido) y firmas MinHash con un umbral Jaccard de ~0,85 (captura casi-duplicados como múltiples borradores o variantes OCR). Ejecuta los tres en el ingest antes del embedding. El síntoma más común de dedup omitido es "las consultas de síntesis se vuelven extrañamente repetitivas" — el mismo chunk está almacenado tres veces bajo tres IDs, por lo que el LLM lo ve tres veces en el contexto.
¿Puedo añadir documentos de forma incremental sin reindexar todo?
Sí, todas las arquitecturas de esta guía soportan adiciones incrementales. ChromaDB y Qdrant aceptan nuevos chunks mediante llamadas de inserción simples; LanceDB agrega a sus archivos append-only; LlamaIndex envuelve cualquiera de ellos. La excepción es cambiar el modelo de embedding — eso fuerza una reindexación completa porque mezclar embeddings de dos versiones de modelo en un índice degrada la recuperación silenciosamente. Elige tu embedder antes de superar los 5.000 documentos y comprométete con él durante al menos un año.
¿Debería usar filtrado de metadatos para colecciones grandes?
Sí — el filtrado de metadatos es la aceleración más barata de 5-10 veces a escala. Rellena source_filename, page_number, document_type, author, year y cualquier etiqueta específica del dominio en cada chunk en tiempo de indexación. En tiempo de consulta, pre-filtra por payload antes de que se ejecute la búsqueda vectorial. En un corpus de 10.000 documentos, un filtro típico reduce el espacio de búsqueda a unos pocos cientos de chunks sin pérdida de calidad. Qdrant y Weaviate tienen soporte de payload de primera clase; ChromaDB y LanceDB también lo soportan pero con ejecución de filtro algo más lenta más allá de 100.000 chunks.
¿Cómo superviso la calidad RAG a escala?
Construye un conjunto dorado pequeño — 30-50 pares consulta/respuesta cuidadosamente seleccionados que cubran recuperación de hechos, síntesis, entre documentos, casos límite y consultas de miss conocido — y vuelve a ejecutarlo en cada cambio significativo (nuevo ingest, cambio de embedder, cambio de tamaño de chunk, ajuste de prompt). Rastrea el recall de recuperación (¿apareció el chunk correcto en top-K?), la fidelidad de generación (¿la respuesta coincide con el chunk?) y la tasa de rechazo (¿el sistema dice "no está en el corpus" cuando debería?). Trulens y RAGAS automatizan esto; la puntuación manual de 30 consultas también es válida y a menudo más precisa.
¿Qué hardware necesito para 10.000 documentos?
Mínimo: 32 GB de RAM del sistema, NVMe SSD con 50+ GB libres, y una GPU discreta con 8 GB+ VRAM o Apple Silicon con 32 GB+ de memoria unificada. La GPU/Apple Silicon es para la velocidad de indexación de un solo paso (ahorra 1-3 horas en un paso de indexación de 10.000 documentos); la inferencia en tiempo de consulta funciona bien en CPU después de construir el índice. SATA SSD es aceptable pero añade 30-100% a la latencia de consultas en frío; el disco giratorio es inutilizable más allá de ~2.000 documentos. La RAM es la restricción que aparece primero — un índice de 5 GB se consulta cómodamente en 16 GB de RAM con indexación HNSW.
¿Puedo servir RAG multi-usuario de forma local?
Sí — pon Open WebUI delante de cualquiera de las arquitecturas de esta guía, o envuelve tu stack Python personalizado en un pequeño servicio FastAPI. El multi-usuario cambia el enfoque operativo (auth, aislamiento de documentos por usuario, rate limiting, workspaces opcionales por usuario) pero no la arquitectura de recuperación. Open WebUI gestiona auth, OAuth y acceso a documentos basado en roles de forma nativa. Para 5+ usuarios concurrentes en un corpus de 10.000 documentos, planifica ejecutar el embedder en GPU durante la indexación y en CPU o GPU para el embedding en tiempo de consulta según QPS — un único embedder en CPU gestiona ~3-5 QPS cómodamente.