Chatsy logoChatsy logo
Precios
Iniciar sesiónEmpieza gratis
Volver al blog
Ingeniería

Crear para escala: manejar millones de docs

Cómo escalamos nuestro sistema RAG de 50 a 2 millones de documentos usando particionado de pgvector, jobs en segundo plano y caché de respuestas, recortando costes 84%.

Chatsy Team
Ingeniería
28 de noviembre de 2024Actualizado: 15 de enero de 2026
17 min de lectura
Compartir:

Cuando lanzamos Chatsy, nuestro primer cliente tenía 50 documentos. Hoy, nuestro mayor cliente enterprise tiene más de 2 millones. Así construimos un sistema que escala sin fricción a través de ese rango.

Resumen rápido:

  • Escalar un sistema RAG significa resolver tres cuellos de botella a la vez: ingestión de documentos, búsqueda vectorial y generación con IA.
  • Índices particionados por tenant mantienen la búsqueda vectorial en ~50 ms constantes sin importar el conteo total de documentos, y el procesamiento con jobs en segundo plano maneja ingestión a 50 docs/seg.
  • Caché de respuestas, streaming y model routing inteligente recortaron el tiempo de respuesta API de 8 s a 2 s y los costes de infraestructura de $5,000 a $800/mes.
  • Lección clave: particiona temprano, manda todo lo pesado a background, cachea agresivamente y haz streaming de respuestas para minimizar la latencia percibida.
Cómo se construyó esta guía

Esta guía se basa en tres fuentes:

  • Patrones observados en múltiples despliegues de soporte al cliente con IA (desde pequeñas tiendas ecommerce hasta SaaS mid-market) enviados en 2024-2026 y aplicables directamente a escalar infraestructura de soporte con IA
  • Casos de estudio públicos de empresas que documentaron su arquitectura y métricas operativas
  • Debates de profesionales en r/SaaS, r/CustomerSuccess y r/devops donde equipos reportaron qué escaló y qué se rompió

Cuando citamos números, enlazamos al caso de estudio fuente o indicamos la metodología detrás de la cifra. Las afirmaciones genéricas de vendors sin matemática de soporte se marcan con etiquetas VERIFY. Revisado por última vez: abril de 2026.

El reto

Escalar un sistema RAG (Retrieval-Augmented Generation) es especialmente difícil porque estás escalando tres cosas simultáneamente:

  1. Ingestión de documentos: procesar documentos y generar embeddings
  2. Búsqueda vectorial: encontrar contenido relevante rápido
  3. Generación con IA: producir respuestas de alta calidad

Cada una tiene características de escalado y cuellos de botella distintos. Una implementación ingenua que funciona con 1,000 documentos se romperá con 100,000. Y lo que funciona con 100K se frenará por completo con 2M. Las estrategias que necesitas cambian en cada orden de magnitud.

Vista general de arquitectura

Nuestra arquitectura de producción separa responsabilidades en tres capas: manejo de solicitudes, persistencia de datos y procesamiento en segundo plano. Cada capa escala de forma independiente.

┌─────────────────────────────────────────────────────┐
│                    Load Balancer                    │
│              (health checks, SSL term)              │
└─────────────────────────────────────────────────────┘
                          │
        ┌─────────────────┼─────────────────┐
        ▼                 ▼                 ▼
┌──────────────┐  ┌──────────────┐  ┌──────────────┐
│   API Pod    │  │   API Pod    │  │   API Pod    │
│ (stateless)  │  │ (stateless)  │  │ (stateless)  │
└──────────────┘  └──────────────┘  └──────────────┘
        │                 │                 │
        └─────────────────┼─────────────────┘
                          │
        ┌─────────────────┼─────────────────┐
        ▼                 ▼                 ▼
┌──────────────┐  ┌──────────────┐  ┌──────────────┐
│  PostgreSQL  │  │    Redis     │  │   Trigger    │
│  + pgvector  │  │    Cache     │  │    .dev      │
└──────────────┘  └──────────────┘  └──────────────┘

Por qué funciona esta topología

  • Los pods API son stateless. Cualquier pod puede manejar cualquier solicitud. Escalar horizontalmente significa añadir pods detrás del load balancer.
  • PostgreSQL con pgvector maneja datos relacionales y embeddings vectoriales en una sola base de datos, eliminando la necesidad de sincronizar un vector store separado.
  • Redis se sitúa entre la API y tanto PostgreSQL como OpenAI, cacheando embeddings, resultados de búsqueda y respuestas generadas.
  • Trigger.dev gestiona todo el trabajo en segundo plano: procesamiento de documentos, embeddings por lote, limpieza programada y entrega de webhooks.

La decisión arquitectónica clave fue mantener todo en PostgreSQL en vez de ejecutar una base de datos vectorial separada. Esto simplifica operaciones, reduce costes de infraestructura y nos permite usar herramientas estándar de base de datos para backups, migraciones y monitoring. Nuestra migración de Pinecone a pgvector cubre el razonamiento en profundidad.

Escalar ingestión de documentos

El cuello de botella de ingestión

El procesamiento de documentos consume CPU y memoria:

  • Parsing de PDF y extracción de texto
  • Limpieza y normalización de contenido
  • Chunking inteligente con solapamiento
  • Generación de embeddings (llamadas API)
  • Escrituras en base de datos con indexación vectorial

Un solo PDF de 100 páginas puede tardar más de 30 segundos en procesarse. Cuando un cliente sube 500 documentos a la vez, eso supera 4 horas de procesamiento secuencial. Los usuarios esperan que su contenido sea buscable en minutos, no horas.

Nuestra solución: jobs en segundo plano con Trigger.dev

Usamos Trigger.dev para procesamiento en segundo plano fiable y escalable:

typescript
export const processDocument = task({ id: "process-document", retry: { maxAttempts: 3 }, run: async ({ documentId }) => { // 1. Extraer texto const text = await extractText(documentId); // 2. Chunking inteligente const chunks = await chunkText(text, { maxTokens: 512, overlap: 50, }); // 3. Generar embeddings en lotes for (const batch of chunk(chunks, 100)) { const embeddings = await openai.embeddings.create({ model: "text-embedding-3-small", input: batch.map(c => c.text), }); // 4. Guardar en base de datos await prisma.chunk.createMany({ data: batch.map((chunk, i) => ({ documentId, content: chunk.text, embedding: embeddings.data[i].embedding, })), }); } }, });

Este enfoque nos da:

  • Escalado horizontal: añadir más workers según haga falta
  • Fiabilidad: retries automáticos ante fallos
  • Visibilidad: seguimiento de progreso en tiempo real

Procesamiento por lotes para cargas masivas

Cuando los clientes importan cientos de documentos a la vez, usamos una tarea de orquestación por lotes que distribuye trabajo entre varios workers:

typescript
export const batchImport = task({ id: "batch-import", run: async ({ chatbotId, documentIds }) => { // Procesar en lotes paralelos de 10 const batches = chunk(documentIds, 10); for (const batch of batches) { await Promise.all( batch.map((docId) => processDocument.triggerAndWait({ documentId: docId }) ) ); // Actualizar progreso para la UI await updateImportProgress(chatbotId, { processed: batch.length, total: documentIds.length, }); } }, });

Con 10 workers concurrentes, una carga de 500 documentos termina en aproximadamente 25 minutos en lugar de 4 horas. Las actualizaciones de progreso alimentan una UI en tiempo real para que los clientes vean exactamente dónde está su importación.

Gestión de queue y backpressure

A escala, necesitas manejar picos sin saturar servicios downstream. Nuestra estrategia de queue usa tres niveles de prioridad:

  1. Alta prioridad: actualizaciones de documentos en tiempo real (cliente edita un artículo y espera reindexación inmediata)
  2. Prioridad normal: cargas e importaciones estándar
  3. Baja prioridad: reindexación programada, tareas de limpieza y migraciones masivas

Cuando se acerca el rate limit de la API de embeddings, aplicamos backpressure reduciendo la tasa de dequeue en vez de fallar jobs. Los controles de concurrencia integrados de Trigger.dev manejan esto limpiamente.

Escalar búsqueda vectorial

El problema de latencia de búsqueda

A medida que crece el conteo de documentos, aumenta la latencia de búsqueda. Sin particionado, en un solo índice plano:

  • 10K chunks: ~20 ms
  • 100K chunks: ~50 ms
  • 1M chunks: ~200 ms
  • 10M chunks: ~2000 ms (inaceptable para chat en tiempo real)

La razón es directa: los índices IVFFlat escanean un porcentaje fijo de clusters. Más vectores significa más datos que escanear por consulta.

Nuestra solución: índices particionados

Particionamos vectores por tenant (chatbot), una estrategia refinada durante nuestra migración de Pinecone a pgvector:

sql
-- Crear tabla particionada CREATE TABLE chunks ( id UUID PRIMARY KEY, chatbot_id UUID NOT NULL, content TEXT, embedding vector(1536) ) PARTITION BY HASH (chatbot_id); -- Crear particiones CREATE TABLE chunks_p0 PARTITION OF chunks FOR VALUES WITH (MODULUS 16, REMAINDER 0); -- ... repetir para 16 particiones -- Indexar cada partición CREATE INDEX ON chunks_p0 USING ivfflat (embedding vector_cosine_ops);

Ahora las consultas solo escanean particiones relevantes:

sql
SELECT content, embedding <=> $1 AS distance FROM chunks WHERE chatbot_id = $2 -- Solo escanea la partición relevante ORDER BY distance LIMIT 10;

Resultado: consultas consistentes de ~50 ms sin importar el conteo total de documentos.

Estrategia de sharding de base de datos

Con más de 2M documentos, incluso los índices particionados se benefician de ajuste cuidadoso. Nuestro enfoque de sharding sigue tres principios:

  1. Aislamiento de tenant mediante particionado hash. Los datos de cada chatbot caen en una de 16 particiones hash. Esto evita que tenants grandes degraden la búsqueda de tenants pequeños.
  2. Ajuste de índice por partición. El parámetro lists de IVFFlat escala con el número de vectores en cada partición. Ejecutamos un job nocturno que revisa tamaños de partición y reconstruye índices cuando el conteo de vectores cruza un umbral (por ejemplo, 100K vectores dispara un aumento de 100 a 250 lists).
  3. Pooling de conexiones. PgBouncer se sitúa entre los pods API y PostgreSQL, manteniendo manejable el conteo de conexiones incluso bajo alta concurrencia. Sin esto, 50 pods API concurrentes agotarían las conexiones máximas de PostgreSQL.
┌──────────┐     ┌────────────┐     ┌────────────────┐
│ API Pods │ ──▶ │ PgBouncer  │ ──▶ │  PostgreSQL    │
│ (50+)    │     │ (pool: 20) │     │  (max_conn: 50)│
└──────────┘     └────────────┘     └────────────────┘

HNSW vs IVFFlat: cuándo cambiar

Empezamos con IVFFlat porque es más rápido de construir y funciona bien a escala moderada. A medida que crecen los tamaños de partición, HNSW se vuelve atractivo por su mejor tradeoff recall-latencia:

FactorIVFFlatHNSW
Tiempo de buildRápido (minutos)Lento (horas en 1M+)
Latencia de consultaBuena con ajusteConsistentemente rápida
Uso de memoriaMenorMayor (~2x)
Rendimiento de insertRápidoModerado
Ideal para< 500K vectores/partición> 500K vectores/partición

Para la mayoría de nuestros tenants, IVFFlat es la elección correcta. Reservamos HNSW para clientes enterprise con 500K+ chunks donde la mejora marginal de latencia de consulta justifica el coste de memoria.

La capa de caché

La caché es el mayor reductor de coste individual en nuestro stack. Cacheamos en tres niveles:

Nivel 1: caché de embeddings (Redis)

Generar embeddings cuesta dinero y añade latencia. Cuando el mismo chunk de texto se reindexa (por ejemplo, un documento se reimporta sin cambios), omitimos la llamada API:

typescript
async function getEmbedding(text: string): Promise<number[]> { const cacheKey = `emb:${hash(text)}`; const cached = await redis.get(cacheKey); if (cached) return JSON.parse(cached); const result = await openai.embeddings.create({ model: "text-embedding-3-small", input: text, }); const embedding = result.data[0].embedding; await redis.setex(cacheKey, 86400 * 7, JSON.stringify(embedding)); // 7 días return embedding; }

Solo esto recortó nuestros costes de API de embeddings en 35% porque muchos clientes reimportan documentos durante pruebas y configuración.

Nivel 2: caché de resultados de búsqueda (Redis)

Consultas idénticas al mismo chatbot suelen devolver los mismos resultados. Cacheamos los chunks recuperados por un TTL corto:

typescript
const searchCacheKey = `search:${chatbotId}:${hash(queryEmbedding)}`; const cachedResults = await redis.get(searchCacheKey); if (cachedResults) return JSON.parse(cachedResults); const results = await vectorSearch(chatbotId, queryEmbedding); await redis.setex(searchCacheKey, 300, JSON.stringify(results)); // 5 min

El TTL corto (5 minutos) asegura frescura mientras captura el patrón común de varios usuarios preguntando lo mismo en una ventana corta.

Nivel 3: caché de respuestas (Redis)

La caché más impactante. Respuestas LLM completas para combinaciones idénticas de pregunta + contexto:

typescript
const responseCacheKey = `resp:${hash(question + contextChunks)}`; const cached = await redis.get(responseCacheKey); if (cached) return cached; const response = await generateResponse(question, contextChunks); await redis.setex(responseCacheKey, 3600, response); // 1 hora return response;

Las tasas de acierto varían según el caso de uso: chatbots con muchas FAQ ven 40-60% de cache hits. Bots de soporte con preguntas únicas específicas de cuenta ven 10-15%. Incluso 10% ahorra coste significativo a escala.

Escalar generación con IA

El problema de coste de generación

Las llamadas API a LLM son:

  • Caras (~$0.01-0.10 por solicitud)
  • Lentas (1-10 segundos)
  • Limitadas por rate limits

Nuestras soluciones

1. Streaming de respuestas No esperes la respuesta completa: transmite tokens al cliente:

typescript
const stream = await openai.chat.completions.create({ model: "gpt-4o", messages, stream: true, }); for await (const chunk of stream) { controller.enqueue(chunk.choices[0]?.delta?.content || ""); }

El streaming reduce drásticamente la latencia percibida. El usuario ve el primer token en ~300 ms en lugar de esperar 2-5 segundos a la respuesta completa. Esto marca más diferencia para la satisfacción del usuario que mejoras de velocidad cruda.

2. Model routing Usa modelos más baratos para preguntas simples:

typescript
const complexity = await classifyComplexity(question); const model = complexity === "simple" ? "gpt-4o-mini" : "gpt-4o";

En la práctica, 60-70% de preguntas de soporte son lo bastante directas para GPT-4o-mini. El routing ahorra aproximadamente 50% en costes LLM frente a usar GPT-4o para todo.

3. Gestión de rate limits A escala, alcanzarás rate limits de OpenAI. Lo manejamos con una implementación token bucket que encola solicitudes al acercarse a límites y cae a un proveedor de modelo secundario si el primario está limitado.

Resultados

MétricaAntesDespués
Velocidad de ingestión1 doc/seg50 docs/seg
Latencia de búsqueda (P99)2000 ms80 ms
Tiempo de respuesta API8 s2 s
Infraestructura mensual$5,000$800

Monitoring y alertas

No puedes optimizar lo que no puedes medir. Nuestra configuración de monitoring rastrea cuatro categorías de métricas:

Métricas de infraestructura

MétricaHerramientaUmbral de alerta
Tiempo de respuesta API (P50, P95, P99)Logs de aplicaciónP99 > 3 s
Uso del pool de conexiones de base de datosStats de PgBouncer> 80% utilización
Uso de memoria RedisRedis INFO> 75% de memoria máxima
Profundidad de queue de jobs en segundo planoDashboard Trigger.dev> 1,000 jobs pendientes

Métricas de negocio

MétricaQué te dice
Docs procesados por minutoSalud del pipeline de ingestión
Latencia de búsqueda por tenantSi un tenant grande está degradando rendimiento
Tasa de acierto de caché por nivelSi tu estrategia de caché es eficaz
Coste LLM por conversaciónSi model routing funciona

Estrategia de alertas

Usamos un enfoque de alertas por niveles para evitar fatiga:

  • P1 (page inmediato): latencia de búsqueda P99 > 3 s, tasa de error API > 5%, agotamiento de conexiones de base de datos
  • P2 (alerta Slack, responder en 1 hora): profundidad de queue de ingestión > 5,000, tasa de acierto de caché baja de 20%, tasa de error LLM > 2%
  • P3 (revisión diaria): anomalías de coste, builds lentos de índices de partición, tasas elevadas de retry

La alerta más útil que añadimos fue sobre latencia de búsqueda por tenant. Cuando un cliente importa un set masivo de documentos, el índice de su partición puede quedar mal ajustado. La alerta lo detecta antes de que el cliente lo note.

Benchmarks reales

Así se ven nuestros números de producción en distintos niveles de escala:

Nivel de escalaDocsChunksBúsqueda promedio (P50)Búsqueda (P99)Tasa de ingestión
Starter< 1K< 10K8 ms25 ms5 docs/seg
Growth1K-50K10K-500K15 ms45 ms20 docs/seg
Scale50K-500K500K-5M25 ms65 ms50 docs/seg
Enterprise500K-2M+5M-20M+35 ms80 ms50 docs/seg

Estos números se mantienen porque los vectores de cada tenant están aislados en su propia partición. Un solo tenant enterprise con 2M documentos no afecta la latencia de búsqueda de un tenant starter con 500 documentos.

Ideas clave

  1. Particiona temprano: adaptar particionado después duele. Diseña aislamiento de tenant desde el primer día.
  2. Todo lo pesado a background: nunca bloquees al usuario con operaciones pesadas. Procesamiento de documentos, generación de embeddings y reconstrucciones de índice pertenecen a jobs en segundo plano.
  3. Cachea en cada capa: caché de embeddings, caché de búsqueda y caché de respuestas atacan cuellos de botella distintos. Combinadas, recortan costes 60-70%.
  4. Haz streaming de respuestas: la latencia percibida importa tanto como la latencia real. Tiempo al primer token de 300 ms se siente instantáneo aunque la respuesta completa tarde 3 segundos.
  5. Monitorea por tenant: las métricas agregadas ocultan problemas. Un solo tenant grande puede degradar su propia experiencia sin afectar promedios.
  6. Dimensiona bien tus índices: IVFFlat funciona para la mayoría de tenants. Reserva HNSW para las particiones más grandes donde el tradeoff recall-latencia justifica el coste de memoria.
  7. Planifica pooling de conexiones: PgBouncer (o equivalente) no es opcional a escala. Cincuenta pods API abriendo conexiones directas agotarán PostgreSQL.

Crear para escala no es manejar la carga de hoy, sino manejar la de mañana sin reescribir todo. Nuestra adopción de técnicas como búsqueda híbrida juega un rol clave en esa escalabilidad.

Verlo en acción ->


Cuándo esta arquitectura de escala no encaja

Omite el patrón de índices particionados, workers en segundo plano y caché de dos niveles si estás ejecutando un sistema RAG single-tenant que sirve menos de ~5,000 documentos y unos pocos miles de consultas al día: una sola instancia PostgreSQL con pgvector e ingestión síncrona es más simple, barata y fácil de depurar. Omítelo si tu tráfico de recuperación es bursty pero tu set de documentos es pequeño y estable: cachea las respuestas, no los embeddings. Y omítelo si tu equipo no tiene rotación on-call: la arquitectura de este post asume que alguien recibe page cuando una worker queue se atasca o un índice de tenant se degrada. Sin ese músculo operativo, un proveedor RAG gestionado (Vertex AI Search, Pinecone Assistant, Anthropic Claude con file search) encaja mejor hasta que lo superes.


Preguntas frecuentes

¿Cuándo deberías planificar para escala?

Planifica para escala desde el inicio si esperas crecimiento significativo. Adaptar particionado después duele: la arquitectura de Chatsy usa índices particionados por tenant desde el primer día, lo que mantiene búsqueda vectorial en ~50 ms sin importar el conteo de documentos. Si estás creando un sistema RAG que podría crecer de cientos a millones de documentos, diseña particionado, jobs en segundo plano y caché desde el principio.

¿Cuáles son los mayores retos de escala para sistemas RAG?

Los tres cuellos de botella principales son ingestión de documentos (intensiva en CPU y memoria: un solo PDF de 100 páginas puede tardar más de 30 segundos), latencia de búsqueda vectorial (que degrada de ~20 ms en 10K docs a ~2000 ms en 10M docs sin particionado) y coste y latencia de generación con IA (las llamadas LLM son caras y lentas). Cada uno requiere soluciones distintas: jobs en segundo plano para ingestión, índices particionados para búsqueda, y caché más model routing para generación.

¿Qué infraestructura se requiere para escala?

La arquitectura escalada de Chatsy usa pods API con load balancing, PostgreSQL con pgvector para almacenamiento vectorial particionado, Redis para caché de respuestas y Trigger.dev para procesamiento de jobs en segundo plano. La clave es escalado horizontal de workers para ingestión (50 docs/seg), tablas particionadas para que las consultas solo escaneen datos relevantes del tenant, y Redis para cachear respuestas LLM y evitar llamadas API redundantes.

¿Cuáles son las implicaciones de coste de escalar?

Escalar correctamente puede reducir costes drásticamente. Chatsy recortó infraestructura mensual de $5,000 a $800 implementando caché de respuestas, streaming y model routing inteligente, reduciendo el tiempo de respuesta API de 8 s a 2 s. La caché de respuestas reutiliza resultados para preguntas idénticas; model routing usa modelos más baratos (por ejemplo, GPT-4o-mini) para consultas simples. El tradeoff es inversión de ingeniería inicial para ahorro a largo plazo.

¿Cómo monitoreas sistemas a escala?

No puedes optimizar lo que no puedes medir. Monitorea throughput de ingestión (docs/seg), latencia de búsqueda (P99), tiempo de respuesta API y costes de infraestructura. Los resultados de Chatsy muestran el valor: rastrear estas métricas reveló el cuello de botella de ingestión (1 doc/seg → 50 docs/seg con jobs en segundo plano), pico de latencia de búsqueda (2000 ms → 80 ms con particionado) y coste API (8 s → 2 s con caché y routing).


Artículos relacionados

  • De Pinecone a pgvector: una reducción de costes del 97%
  • Búsqueda vectorial: cómo los chatbots de IA encuentran respuestas
  • Búsqueda híbrida explicada: lo mejor de ambos mundos
  • Cómo entrenar tu chatbot de IA con documentación
  • Seguridad de chatbots de IA: cómo Chatsy protege tus datos
#architecture#scale#performance#engineering
AnteriorBúsqueda híbrida explicada: lo mejor de ambos mundos
Más reciente El futuro del soporte al cliente: IA que entiende
Relacionado

Artículos relacionados

Ingeniería

De Pinecone a pgvector: una reducción de costes del 97%

Cómo recortamos 97% los costes de base de datos vectorial migrando de Pinecone a pgvector. Una guía técnica detallada sobre la migración.

Técnico

Orquestación multiagente para soporte al cliente: guía de arquitectura

Aprende a diseñar sistemas multiagente para soporte al cliente donde agentes especializados manejan facturación, problemas técnicos, envíos y devoluciones, con un router que orquesta conversaciones.

Chatbots con IA

12 métricas de chatbots de IA que deberías seguir (y por qué)

Mide lo que importa. Aprende qué KPIs de chatbot realmente indican éxito y cómo crear un dashboard que impulse mejoras.

¿Listo para probar Chatsy?

Crea tu propio agente de soporte al cliente con IA en minutos, sin código.

Comenzar prueba gratis

¿Listo para transformar tu
soporte al cliente?

Implementa agentes de soporte de IA que resuelven problemas, actúan y encantan a tus clientes.

Empieza gratisNo se requiere tarjeta de crédito
Chatsy logoChatsy logo

Plataforma de soporte al cliente con IA, chat en vivo, transferencia humana, base de conocimiento y tickets.

Producto

  • Funciones
  • Precios
  • Integraciones

Soluciones

  • Ecommerce
  • SaaS
  • Salud
  • Servicios financieros

Recursos

  • Blog
  • Estadísticas
  • Comparar
  • Alternativas
  • Plantillas
  • Glosario
  • Calculadora de ROI
  • Feed RSS

Empresa

  • Acerca de
  • Contacto
  • Política de privacidad
  • Términos de servicio

© 2026 Chatsy. Todos los derechos reservados.

Idioma
EnglishEspañol

10685-B Hazelhurst Dr. # 21148, Houston, TX 77043, USA