Codea Bien Logo
RAG con Angular, embeddings y Gemini
Angular

RAG con Angular, embeddings y Gemini

Kevin Dávila

angularembeddingsgemini

RAG en Angular: Búsqueda inteligente con embeddings y Gemini

La mayoría de los desarrolladores Angular sigue pensando que RAG es territorio exclusivo de backenders y data engineers. No lo es. Si tu app necesita buscar información relevante y responder con precisión, RAG es tu mejor aliado, y lo puedes integrar sin salir de tu stack Angular. Con la llegada de Gemini Embedding 2, además, el proceso se volvió bastante más simple de lo que era hace un año.

En este artículo vamos a construir un pipeline RAG completo, desde generar embeddings con la API de Gemini hasta crear una búsqueda semántica en tu aplicación Angular.

Abstract neural network visualization

Photo by Google DeepMind on Unsplash

¿Qué es RAG?

RAG (Retrieval Augmented Generation) es una técnica que permite a los modelos de lenguaje generar respuestas basadas en información específica y actualizada, en lugar de depender únicamente de los datos con los que fueron entrenados.

El flujo es simple:

  1. Retrieval: Buscas documentos relevantes usando embeddings

  2. Augmentation: Pasas esos documentos como contexto al modelo

  3. Generation: El modelo genera una respuesta fundamentada

¿Por qué importa para un desarrollador Angular? Porque tus usuarios esperan que tu app "entienda" lo que buscan. Una búsqueda por palabras clave ya no es suficiente cuando puedes ofrecer búsqueda semántica.

"RAG systems significantly improve factual accuracy and contextual richness in AI-generated responses" - Source: Google AI for Developers - Embeddings

Gemini Embedding 2

En abril de 2026, Google lanzó Gemini Embedding 2, su primer modelo de embeddings multimodal. Esto significa que puedes generar vectores no solo de texto, sino también de imágenes, video, audio y PDFs en un mismo espacio semántico.

Especificaciones clave:

  • Modelo: gemini-embedding-2

  • Input tokens: 8,192 máximo

  • Dimensiones de salida: 128 a 3072 (recomendadas: 768, 1536, 3072)

  • Modalidades: Texto, imagen, video, audio, PDF

  • Idiomas: 100+

"The latest model, gemini-embedding-2, is the first multimodal embedding model in the Gemini API" - Source: Gemini API Embeddings Documentation

Task types para RAG

Gemini Embedding 2 usa un sistema de prefijos para optimizar los embeddings según el caso de uso. Para RAG, el formato es:

Para queries (búsquedas):

task: search result | query: {tu pregunta aquí}

Para documentos (a indexar):

title: {título del documento} | text: {contenido}

Esto es importante: si no usas el formato correcto, tus embeddings pierden calidad.

(Es como ir al gimnasio y hacer ejercicios al azar, algo haces, pero no lo óptimo)

Arquitectura del pipeline RAG

Antes de escribir código, veamos la arquitectura completa:

┌─────────────┐     ┌──────────────────┐     ┌─────────────────┐
│   Angular   │────▶│  Backend (API)   │────▶│  Gemini API     │
│   Frontend  │     │  NestJS/Express  │     │  Embeddings     │
└─────────────┘     └──────────────────┘     └─────────────────┘
                           │                         │
                           ▼                         ▼
                    ┌──────────────────┐     ┌─────────────────┐
                    │  Vector DB       │     │  Gemini API     │
                    │  (ChromaDB)      │     │  Generación     │
                    └──────────────────┘     └─────────────────┘

El frontend Angular envía la pregunta al backend. El backend genera el embedding de la pregunta, busca en la base de vectores los documentos más similares, y luego envía todo a Gemini para generar la respuesta.

Paso 1: Generar embeddings con Gemini API

Primero necesitas instalar el SDK de Google GenAI:

npm install @google/genai

Ahora, el servicio para generar embeddings:

// embedding.service.ts
import { GoogleGenAI } from '@google/genai';

export class EmbeddingService {
  private ai: GoogleGenAI;

  constructor() {
    this.ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });
  }

  async generateEmbedding(text: string, isQuery: boolean = false): Promise<number[]> {
    const formattedText = isQuery
      ? `task: search result | query: ${text}`
      : `title: none | text: ${text}`;

    const response = await this.ai.models.embedContent({
      model: 'gemini-embedding-2',
      contents: formattedText,
      config: { outputDimensionality: 768 },
    });

    return response.embeddings[0].values;
  }

  async generateDocumentEmbedding(title: string, content: string): Promise<number[]> {
    const formattedText = `title: ${title} | text: ${content}`;

    const response = await this.ai.models.embedContent({
      model: 'gemini-embedding-2',
      contents: formattedText,
      config: { outputDimensionality: 768 },
    });

    return response.embeddings[0].values;
  }
}

Nota que uso outputDimensionality: 768 en lugar de los 3072 por defecto. ¿Por qué? Porque según los benchmarks de MTEB, 768 dimensiones logran un score de 67.99 vs 68.16 de 2048 dimensiones. La diferencia es marginal, pero el ahorro en almacenamiento y velocidad es significativo.

"Selecting a smaller output dimensionality can save storage space and increase computational efficiency" - Source: Gemini API Embeddings Documentation

Paso 2: Almacenar embeddings en ChromaDB

ChromaDB es una base de datos de vectores open-source que funciona perfectamente para empezar. En producción podrías usar Pinecone, QDrant o Weaviate.

npm install chromadb
// vector-store.service.ts
import { ChromaClient, Collection } from 'chromadb';
import { EmbeddingService } from './embedding.service';

export class VectorStoreService {
  private client: ChromaClient;
  private collection: Collection;
  private embeddingService: EmbeddingService;

  constructor(embeddingService: EmbeddingService) {
    this.client = new ChromaClient();
    this.embeddingService = embeddingService;
  }

  async initialize(): Promise<void> {
    this.collection = await this.client.getOrCreateCollection({
      name: 'knowledge_base',
      metadata: { 'hnsw:space': 'cosine' },
    });
  }

  async addDocument(id: string, title: string, content: string): Promise<void> {
    const embedding = await this.embeddingService.generateDocumentEmbedding(title, content);

    await this.collection.add({
      ids: [id],
      embeddings: [embedding],
      documents: [content],
      metadatas: [{ title }],
    });
  }

  async search(query: string, topK: number = 5): Promise<any[]> {
    const queryEmbedding = await this.embeddingService.generateEmbedding(query, true);

    const results = await this.collection.query({
      queryEmbeddings: [queryEmbedding],
      nResults: topK,
    });

    return results.documents[0].map((doc, index) => ({
      content: doc,
      title: results.metadatas[0][index].title,
      distance: results.distances[0][index],
    }));
  }
}

Usamos cosine como métrica de distancia porque se enfoca en la dirección del vector, no en su magnitud. Esto refleja mejor la similitud conceptual entre textos.

Paso 3: El servicio RAG completo

Ahora juntamos todo. El servicio RAG busca documentos relevantes y genera una respuesta con Gemini:

// rag.service.ts
import { GoogleGenAI } from '@google/genai';
import { VectorStoreService } from './vector-store.service';

export class RagService {
  private ai: GoogleGenAI;
  private vectorStore: VectorStoreService;

  constructor(vectorStore: VectorStoreService) {
    this.ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });
    this.vectorStore = vectorStore;
  }

  async query(question: string): Promise<string> {
    // 1. Retrieval: buscar documentos relevantes
    const relevantDocs = await this.vectorStore.search(question, 3);

    // 2. Augmentation: construir el contexto
    const context = relevantDocs
      .map((doc, i) => `[${i + 1}] ${doc.title}: ${doc.content}`)
      .join('\n\n');

    // 3. Generation: generar respuesta con Gemini
    const prompt = `
      Responde la siguiente pregunta usando SOLO el contexto proporcionado.
      Si el contexto no contiene información suficiente, di que no tienes información.

      Contexto:
      ${context}

      Pregunta: ${question}
    `;

    const response = await this.ai.models.generateContent({
      model: 'gemini-2.5-flash',
      contents: prompt,
    });

    return response.text;
  }
}

Paso 4: El componente Angular

Finalmente, el componente en Angular que consume nuestra API:

// search.component.ts
import { Component, signal } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { FormsModule } from '@angular/forms';

@Component({
  selector: 'app-search',
  standalone: true,
  imports: [FormsModule],
  template: `
    <div class="search-container">
      <h2>Búsqueda inteligente</h2>
      <div class="search-input">
        <input
          [(ngModel)]="query"
          placeholder="Haz una pregunta..."
          (keyup.enter)="search()"
        />
        <button (click)="search()" [disabled]="loading()">
          {{ loading() ? 'Buscando...' : 'Buscar' }}
        </button>
      </div>

      @if (result()) {
        <div class="result">
          <h3>Respuesta</h3>
          <p>{{ result() }}</p>
        </div>
      }

      @if (sources().length > 0) {
        <div class="sources">
          <h3>Fuentes consultadas</h3>
          @for (source of sources(); track source.title) {
            <div class="source-item">
              <strong>{{ source.title }}</strong>
              <span>Relevancia: {{ (1 - source.distance).toFixed(2) }}</span>
            </div>
          }
        </div>
      }
    </div>
  `,
})
export class SearchComponent {
  query = '';
  result = signal<string | null>(null);
  sources = signal<any[]>([]);
  loading = signal(false);

  constructor(private http: HttpClient) {}

  search() {
    if (!this.query.trim()) return;

    this.loading.set(true);
    this.http.post<any>('/api/rag/query', { question: this.query }).subscribe({
      next: (response) => {
        this.result.set(response.answer);
        this.sources.set(response.sources);
        this.loading.set(false);
      },
      error: () => this.loading.set(false),
    });
  }
}

(Código demostrativo, usa mejor códito en tus proyectos, por favor)

Consejos para producción

Algunas lecciones aprendidas después de implementar esto en proyectos reales:

1. Chunking inteligente: No metas documentos enteros. Divide el texto en chunks de 200-500 tokens con overlap de 50 tokens. Esto mejora significativamente la precisión de la búsqueda.

2. Cachea embeddings: Generar embeddings tiene costo. Si tu contenido no cambia frecuentemente, cachea los vectores en tu base de datos.

3. Monitorea la calidad: Usa métricas como precision@k y recall@k para evaluar si tu sistema de retrieval está encontrando los documentos correctos.

4. Usa File Search para prototipar: Google ofrece una herramienta managed llamada File Search que simplifica RAG. Es ideal para validar tu idea antes de construir un pipeline completo.

"We built the File Search tool which makes doing RAG easier to manage and more cost effective" - Source: Gemini API Embeddings Documentation

Conclusión

RAG no es exclusivo de equipos de ML. Como desarrollador Angular ya tienes casi todo lo que necesitas para integrar búsqueda semántica en tus aplicaciones: un backend liviano, una base de vectores y un modelo de embeddings. Gemini Embedding 2 se encarga de la parte más difícil, ofreciendo un modelo multimodal con una API sencilla.


Keywords: RAG Angular, Gemini Embedding 2, búsqueda semántica Angular, embeddings TypeScript, inteligencia artificial Angular, vector database, ChromaDB Angular, Gemini API tutorial, retrieval augmented generation, AI search Angular

Meta description: Aprende a implementar RAG en Angular con Gemini Embedding 2. Tutorial práctico con código TypeScript para búsqueda semántica usando embeddings y ChromaDB.