Ir al contenido

Lab: ingestión

Hasta acá fue conceptual. Ahora vamos a ejecutar la fase de indexación del pipeline RAG: leer los 4 markdowns de data/, partirlos en chunks, generar un embedding por chunk con Ollama, y guardar todo en Qdrant.

El comando es uno solo:

Ventana de terminal
pnpm vercel:ingest

Si todo está bien configurado del Setup, en menos de un minuto vas a ver tu primera collection vectorizada. Vamos a abrir la caja y mirar qué hace ese comando exactamente.

El código vive en packages/01-vercel-ai-sdk/src/ingest.ts — 76 líneas honestas, sin abstracciones que escondan nada. Vamos por partes.

packages/01-vercel-ai-sdk/src/ingest.ts
const docs = await loadMarkdownDir(DATA_DIR);
console.log(`Loaded ${docs.length} documents`);
const chunks = chunkDocs(docs);
console.log(`Generated ${chunks.length} chunks`);

loadMarkdownDir y chunkDocs viven en @rag-lab/shared — código compartido que los 4 frameworks reutilizan. La idea: que la única variable cambiante entre los packages 01-04 sea el framework, no la lógica de chunking ni la carga.

chunkDocs aplica el chunker estándar (800 chars, 100 de overlap). Vas a obtener entre 20 y 30 chunks para el corpus de ejemplo.

ingest.ts: recreateCollection
async function recreateCollection(): Promise<void> {
const { collections } = await qdrant.getCollections();
const exists = collections.some((c) => c.name === COLLECTION);
if (exists) {
await qdrant.deleteCollection(COLLECTION);
}
await qdrant.createCollection(COLLECTION, {
vectors: { size: EMBED_DIM, distance: 'Cosine' },
});
}

Cada vez que corrés pnpm vercel:ingest, la collection rag_vercel se borra y recrea. Es la opción más simple para un curso: ingestión idempotente, sin estado raro entre runs.

Dos detalles que importan:

  • size: EMBED_DIM — la dimensión de los vectores (768 para nomic-embed-text). Tiene que matchear con el modelo de embeddings o Qdrant tira error al primer upsert.
  • distance: 'Cosine' — la métrica que usa Qdrant para comparar vectores. Cosine es el default de la industria para texto.
ingest.ts: embed
const embeddingModel = ollama.embeddingModel(OLLAMA_EMBED);
const { embeddings } = await embedMany({
model: embeddingModel,
values: chunks.map((c) => c.content),
});
console.log(`Embedded ${embeddings.length} chunks`);

embedMany es la función estrella de Vercel AI SDK acá. Le mandás todos los chunks en un solo llamado, y devuelve un array paralelo de vectores. Internamente puede paralelizarlo o batchearlo — vos no te enterás.

ollama.embeddingModel(...) viene de @ai-sdk/openai-compatible, apuntando al endpoint /v1 de Ollama (el provider lo configuramos en las primeras líneas del archivo).

ingest.ts: build points + upsert
const points = chunks.map((chunk, i) => ({
id: i,
vector: embeddings[i]!,
payload: {
docId: chunk.docId,
filename: chunk.filename,
index: chunk.index,
content: chunk.content,
},
}));
await qdrant.upsert(COLLECTION, { wait: true, points });

Cada chunk se convierte en un point de Qdrant: id numérico, vector con el embedding, y payload con la metadata original (incluyendo el content para poder recuperarlo en query).

wait: true hace que el upsert espere a que Qdrant confirme la escritura. En un curso es lo correcto. En producción con miles de chunks, querés batchear y no bloquear.

Ventana de terminal
pnpm vercel:ingest

¿Querés ver las 76 líneas enteras? Acá está el código tal como vive en main:

packages/01-vercel-ai-sdk/src/ingest.ts en GitHub →

Tu corpus está indexado. Ahora viene la parte divertida: hacer preguntas. En la próxima página vas a ver el lab de query — cómo el sistema toma tu pregunta, encuentra los chunks relevantes y le pide al LLM que responda.