Tutorial: Construyendo una Aplicación RAG con Nuxt y Gemini File Search
La Generación Aumentada por Recuperación, conocida por su acrónimo en inglés RAG (Retrieval-Augmented Generation), representa uno de los paradigmas más sólidos y prácticos dentro del ecosistema actual de la inteligencia artificial aplicada al desarrollo de software. A diferencia de los modelos de lenguaje de gran escala que operan exclusivamente sobre sus datos de preentrenamiento, una arquitectura RAG permite que las aplicaciones indexen, recuperen y utilicen documentación propia para generar respuestas contextualizadas con una precisión notablemente superior. En este tutorial, exploraremos cómo construir una aplicación RAG funcional combinando el framework Nuxt con la API Gemini File Search de Google, una integración que resulta especialmente potente para equipos que buscan una solución moderna, mantenible y eficiente.
El interés por implementar RAG en proyectos reales ha crecido exponencialmente en los últimos dos años, impulsado por la necesidad de las organizaciones de dotar a sus sistemas conversacionales de conocimiento específico del dominio. Manuales técnicos, bases de conocimiento internas, contratos legales o documentación de productos son ejemplos de corpus que un modelo genérico simplemente no puede conocer con fiabilidad. Al construir una capa de recuperación que selecciona los fragmentos más relevantes antes de pasarlos al modelo, se reduce drásticamente la tasa de alucinaciones y se obtiene un control granular sobre las fuentes de información que alimentan cada respuesta.
Nuxt, el meta-framework construido sobre Vue.js, ofrece una base arquitectónica ideal para este tipo de proyectos gracias a su modelo híbrido de renderizado y, sobre todo, a su sistema de server functions o rutas de API del lado del servidor. Esta capacidad permite mantener las credenciales de la API de Google de forma completamente segura, sin exponerlas nunca al cliente, al tiempo que se dispone de una capa de lógica de negocio cohesionada con la interfaz de usuario. La elección de Nuxt no es trivial: su ecosistema maduro, el soporte nativo para TypeScript y la integración con herramientas de estado como Pinia hacen que el mantenimiento y la escalabilidad del proyecto sean significativamente más sencillos que en soluciones ad-hoc.
Antes de adentrarnos en el código, conviene establecer con precisión el vocabulario técnico que articulará todo el tutorial. Tres conceptos son pilares fundamentales de cualquier implementación RAG:
- Chunks (fragmentos): Segmentos de texto de tamaño controlado en los que se divide un documento antes de su indexación. El tamaño del chunk influye directamente en la relevancia de los resultados recuperados; chunks demasiado pequeños pierden contexto, mientras que los excesivamente grandes introducen ruido.
- Embeddings: Representaciones vectoriales de alta dimensionalidad que capturan el significado semántico de cada chunk. La similaridad entre el vector de una consulta y los vectores indexados determina qué fragmentos se recuperan.
- Tools (herramientas): En el contexto de la API Gemini, las tools son capacidades declarativas que se exponen al modelo para que pueda invocarlas durante la generación. La herramienta de búsqueda en archivo (File Search) permite al modelo consultar el almacén de vectores de forma autónoma antes de formular su respuesta.
Configuración Inicial del Proyecto y Entorno de Desarrollo
El punto de partida es la creación de un proyecto Nuxt limpio. Utilizando la CLI oficial, se inicializa la aplicación con soporte para TypeScript y se instala el SDK oficial de Google Generative AI. Es fundamental gestionar las variables de entorno de forma segura desde el primer momento; Nuxt proporciona el archivo .env junto con la directiva runtimeConfig en nuxt.config.ts para exponer únicamente las variables necesarias al servidor, jamás al bundle del cliente.
# Crear el proyecto Nuxt
npx nuxi@latest init rag-gemini-app
cd rag-gemini-app
# Instalar el SDK de Google Generative AI
npm install @google/generative-ai
# Instalar dependencias adicionales recomendadas
npm install @pinia/nuxt pinia
En el archivo nuxt.config.ts, la configuración de runtimeConfig queda estructurada de la siguiente manera:
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@pinia/nuxt'],
runtimeConfig: {
// Solo accesible en el servidor
geminiApiKey: process.env.GEMINI_API_KEY,
// Accesible en cliente y servidor (evitar datos sensibles)
public: {
appName: 'RAG Gemini App'
}
}
})
El archivo .env en la raíz del proyecto almacenará la clave de API obtenida desde Google AI Studio. Es imprescindible añadir este archivo al .gitignore para evitar comprometer credenciales en el repositorio. Esta separación entre configuración pública y privada es una práctica de seguridad no negociable en cualquier aplicación que interactúe con servicios externos de pago o acceso restringido.
Gestión del Almacén de Búsqueda y Subida de Documentos
La API Gemini File Search organiza los documentos en estructuras denominadas corpus o almacenes de búsqueda. El flujo operativo comienza con la creación de un almacén, continúa con la subida e indexación de documentos y culmina con la capacidad de realizar consultas semánticas contra ese corpus. Nuxt permite encapsular toda esta lógica en rutas de servidor ubicadas en el directorio server/api/, manteniendo una separación clara entre la capa de presentación y la capa de integración con servicios externos.
// server/api/corpus/create.post.ts
import { GoogleGenerativeAI } from '@google/generative-ai'
export default defineEventHandler(async (event) => {
const config = useRuntimeConfig()
const body = await readBody(event)
const genAI = new GoogleGenerativeAI(config.geminiApiKey)
// Crear un nuevo corpus en Gemini File Search
const corpus = await genAI.createCorpus({
name: body.name,
displayName: body.displayName
})
return {
corpusId: corpus.name,
status: 'created'
}
})
La subida de documentos requiere un manejo especial, ya que los archivos deben procesarse en el servidor antes de ser enviados a la API. Nuxt facilita la lectura de archivos multipart mediante las utilidades de H3, el motor HTTP subyacente. Una vez recibido el archivo, se transmite directamente a la API de Gemini, que se encarga de la segmentación en chunks, la generación de embeddings y la indexación en el corpus especificado. Este proceso es asíncrono por naturaleza: la API devuelve inmediatamente un identificador de documento con estado PROCESSING, y se requiere un mecanismo de polling o webhooks para confirmar la disponibilidad del documento.
// server/api/documents/upload.post.ts
export default defineEventHandler(async (event) => {
const config = useRuntimeConfig()
const formData = await readMultipartFormData(event)
if (!formData) throw createError({ statusCode: 400, message: 'No file provided' })
const fileData = formData.find(f => f.name === 'file')
const corpusId = formData.find(f => f.name === 'corpusId')?.data.toString()
if (!fileData || !corpusId) {
throw createError({ statusCode: 400, message: 'Missing required fields' })
}
const genAI = new GoogleGenerativeAI(config.geminiApiKey)
// Subir el documento al corpus
const document = await genAI.uploadDocument({
corpusName: corpusId,
document: {
displayName: fileData.filename || 'document',
content: fileData.data.toString('base64'),
mimeType: fileData.type || 'text/plain'
}
})
return {
documentId: document.name,
state: document.state // PROCESSING | ACTIVE | FAILED
}
})
Implementación del Pipeline de Consulta RAG
El corazón de la aplicación es el endpoint de consulta, donde se integran la recuperación y la generación en un único flujo coherente. La estrategia consiste en configurar el modelo Gemini con acceso a la herramienta de búsqueda semántica apuntando al corpus previamente indexado. Cuando el usuario formula una pregunta, el modelo evalúa automáticamente si necesita consultar el corpus, ejecuta la búsqueda, obtiene los chunks más relevantes y los incorpora como contexto para la generación de la respuesta final.
// server/api/query/index.post.ts
import { GoogleGenerativeAI } from '@google/generative-ai'
export default defineEventHandler(async (event) => {
const config = useRuntimeConfig()
const { question, corpusId } = await readBody(event)
if (!question || !corpusId) {
throw createError({ statusCode: 400, message: 'Question and corpusId are required' })
}
const genAI = new GoogleGenerativeAI(config.geminiApiKey)
// Configurar el modelo con la herramienta de búsqueda
const model = genAI.getGenerativeModel({
model: 'gemini-1.5-pro',
tools: [{
retrieval: {
vertexAiSearch: {
datastore: corpusId
}
}
}]
})
const result = await model.generateContent(question)
const response = result.response
return {
answer: response.text(),
groundingMetadata: response.candidates?.[0]?.groundingMetadata,
sources: extractSources(response.candidates?.[0]?.groundingMetadata)
}
})
function extractSources(metadata: any): string[] {
return metadata?.groundingChunks?.map((chunk: any) =>
chunk.retrievedContext?.uri || chunk.retrievedContext?.title
).filter(Boolean) || []
}
Un aspecto diferencial de esta implementación es la extracción y presentación de los metadatos de grounding. La API Gemini devuelve, junto a la respuesta generada, información sobre qué fragmentos específicos del corpus fundamentaron esa respuesta. Exponer estas fuentes en la interfaz de usuario no solo incrementa la transparencia y la confianza del usuario final, sino que también permite auditar el sistema e identificar posibles deficiencias en la cobertura del corpus.
Construcción de la Interfaz de Usuario
La capa de presentación se articula mediante componentes Vue 3 con la Composition API y un store Pinia que centraliza el estado de la aplicación: lista de corpus disponibles, documentos indexados, historial de consultas y estados de carga. La interfaz se divide en dos áreas funcionales principales: el panel de administración de documentos y el panel de consulta interactiva.
// stores/rag.ts
import { defineStore } from 'pinia'
interface Document {
id: string
displayName: string
state: 'PROCESSING' | 'ACTIVE' | 'FAILED'
}
interface QueryResult {
question: string
answer: string
sources: string[]
timestamp: Date
}
export const useRagStore = defineStore('rag', {
state: () => ({
corpusId: '' as string,
documents: [] as Document[],
queryHistory: [] as QueryResult[],
isLoading: false,
error: null as string | null
}),
actions: {
async uploadDocument(file: File) {
this.isLoading = true
this.error = null
try {
const formData = new FormData()
formData.append('file', file)
formData.append('corpusId', this.corpusId)
const result = await $fetch('/api/documents/upload', {
method: 'POST',
body: formData
})
this.documents.push({
id: result.documentId,
displayName: file.name,
state: result.state
})
} catch (err: any) {
this.error = err.message
} finally {
this.isLoading = false
}
},
async queryCorpus(question: string) {
this.isLoading = true
this.error = null
try {
const result = await $fetch('/api/query', {
method: 'POST',
body: { question, corpusId: this.corpusId }
})
this.queryHistory.unshift({
question,
answer: result.answer,
sources: result.sources,
timestamp: new Date()
})
} catch (err: any) {
this.error = err.message
} finally {
this.isLoading = false
}
}
}
})
El componente de consulta implementa un formulario reactivo que deshabilita el botón de envío durante el procesamiento, muestra un indicador de carga y renderiza la respuesta junto a las fuentes referenciadas. La visualización de fuentes se implementa como una lista colapsable que permite al usuario explorar qué fragmentos específicos del corpus sustentaron la respuesta generada, cerrando el ciclo de transparencia que caracteriza a una implementación RAG bien diseñada.
Consideraciones de Producción y Buenas Prácticas
Antes de llevar esta aplicación a un entorno productivo, es necesario abordar varios aspectos críticos que van más allá de la funcionalidad básica. En primer lugar, la gestión del estado de procesamiento de documentos requiere un mecanismo robusto: dado que la indexación puede tardar desde segundos hasta varios minutos dependiendo del tamaño del archivo, se recomienda implementar un sistema de polling con backoff exponencial o, en entornos más avanzados, Server-Sent Events para notificar al cliente cuando un documento pasa al estado ACTIVE.
- Limitación de velocidad (Rate Limiting): Implementar middleware en Nuxt para limitar el número de consultas por usuario y período de tiempo, evitando costes inesperados por uso abusivo de la API.
- Validación de archivos: Verificar el tipo MIME, el tamaño máximo y el contenido de los documentos subidos antes de enviarlos a la API, rechazando formatos no soportados o archivos potencialmente maliciosos.
- Caché de respuestas: Para consultas frecuentes e idénticas, considerar el cacheo de respuestas usando Nitro (el servidor de Nuxt) con un TTL razonable, reduciendo latencia y costes operativos.
- Monitorización: Integrar herramientas de observabilidad para registrar latencias, tasas de error y consumo de tokens, permitiendo optimizaciones iterativas del sistema.
- Gestión del ciclo de vida del corpus: Implementar rutinas de limpieza periódica para eliminar documentos obsoletos o corpus sin uso, controlando los costes de almacenamiento en la plataforma de Google.
Una arquitectura RAG bien implementada no se mide únicamente por la calidad de las respuestas que genera, sino por la capacidad del sistema para explicar por qué generó esa respuesta, qué fragmentos la sustentaron y con qué grado de confianza. La trazabilidad es la diferencia entre una herramienta experimental y una solución empresarial.
La combinación de Nuxt y la API Gemini File Search ofrece un equilibrio excepcional entre velocidad de desarrollo, seguridad y potencia. El framework maneja con elegancia la separación entre lógica de servidor y presentación al cliente, mientras que la API de Google abstrae la complejidad de la generación de embeddings, la indexación vectorial y la recuperación semántica, tareas que de otro modo requerirían la orquestación de múltiples servicios especializados. El resultado es un stack coherente que permite a equipos de desarrollo de tamaño reducido construir sistemas RAG de calidad profesional en un tiempo razonablemente corto, sentando las bases para iteraciones más avanzadas como el soporte multimodal, la búsqueda híbrida o la personalización por usuario.


