Codea Bien Logo
Supabase + AI: pgvector, embeddings y búsqueda semántica
Supabase

Supabase + AI: pgvector, embeddings y búsqueda semántica

Kevin Dávila

supabase pgvectorembeddingssemantic searchragvector database

Antes, montar búsqueda semántica era un proyecto de semanas: aprovisionar un cluster de Pinecone o Weaviate, mantenerlo sincronizado con tu base de datos, y sumar otra factura al stack. Ahora es una columna y una función. En la misma base de datos PostgreSQL.

pgvector en Supabase te da embeddings, similitud y RAG sin salir de SQL. En este post vas a habilitar la extensión, generar tus primeros embeddings y armar búsqueda semántica y recomendaciones que viven junto al resto de tus datos.

Qué es pgvector

pgvector es una extensión de PostgreSQL que agrega soporte para vectores (embeddings). Esto significa que puedes:

  • Almacenar embeddings de texto, imágenes, etc.

  • Hacer búsqueda por similitud (cosine, L2, inner product)

  • Construir pipelines de RAG (Retrieval Augmented Generation)

  • Todo dentro de tu misma base de datos PostgreSQL

Habilitar pgvector

Desde el dashboard

  1. Ve a Database > Extensions

  2. Busca vector

  3. Click en Enable

Desde SQL

-- Habilitar la extensión
CREATE EXTENSION IF NOT EXISTS vector;

Crear una tabla con vectores

-- Tabla para documentos con embeddings
CREATE TABLE documents (
  id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
  content TEXT NOT NULL,
  metadata JSONB DEFAULT '{}',
  embedding VECTOR(1536), -- Dimensión del embedding (1536 para OpenAI ada-002)
  created_at TIMESTAMPTZ DEFAULT now()
);

Índice para búsqueda rápida

-- Índice HNSW para búsqueda aproximada (más rápido)
CREATE INDEX ON documents USING hnsw (embedding vector_cosine_ops);

-- O índice IVFFlat (alternativa)
CREATE INDEX ON documents USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100);

Generar embeddings

Con OpenAI

// src/embeddings/openai.ts
import OpenAI from 'openai'

const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY })

export async function generateEmbedding(text: string): Promise<number[]> {
  const response = await openai.embeddings.create({
    model: 'text-embedding-ada-002',
    input: text
  })
  return response.data[0].embedding
}

Con Supabase Edge Function

// supabase/functions/generate-embedding/index.ts
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'

serve(async (req) => {
  const { text } = await req.json()

  // Llamar a la API de embeddings (OpenAI, Gemini, etc.)
  const embeddingResponse = await fetch('https://api.openai.com/v1/embeddings', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${Deno.env.get('OPENAI_API_KEY')}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      model: 'text-embedding-ada-002',
      input: text
    })
  })

  const { data } = await embeddingResponse.json()
  const embedding = data[0].embedding

  // Guardar en Supabase
  const supabase = createClient(
    Deno.env.get('SUPABASE_URL') ?? '',
    Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? ''
  )

  const { error } = await supabase
    .from('documents')
    .insert({
      content: text,
      embedding: JSON.stringify(embedding)
    })

  if (error) {
    return new Response(JSON.stringify({ error: error.message }), { status: 500 })
  }

  return new Response(JSON.stringify({ success: true }))
})

Búsqueda semántica

Buscar documentos similares

-- Función para búsqueda por similitud
CREATE OR REPLACE FUNCTION search_documents(
  query_embedding VECTOR(1536),
  match_count INT DEFAULT 5,
  match_threshold FLOAT DEFAULT 0.8
)
RETURNS TABLE (
  id UUID,
  content TEXT,
  metadata JSONB,
  similarity FLOAT
)
LANGUAGE plpgsql
AS $$
BEGIN
  RETURN QUERY
  SELECT
    documents.id,
    documents.content,
    documents.metadata,
    1 - (documents.embedding <=> query_embedding) AS similarity
  FROM documents
  WHERE 1 - (documents.embedding <=> query_embedding) > match_threshold
  ORDER BY documents.embedding <=> query_embedding
  LIMIT match_count;
END;
$$;

Usar desde el cliente

// src/search.ts
import { supabase } from './lib/supabase'
import { generateEmbedding } from './embeddings/openai'

export async function semanticSearch(query: string) {
  // Generar embedding del query
  const queryEmbedding = await generateEmbedding(query)

  // Buscar documentos similares
  const { data, error } = await supabase.rpc('search_documents', {
    query_embedding: JSON.stringify(queryEmbedding),
    match_count: 5,
    match_threshold: 0.7
  })

  if (error) throw error
  return data
}

// Ejemplo de uso
const results = await semanticSearch('¿Cómo configuro autenticación?')
console.log(results)
// [{ content: "Supabase Auth permite...", similarity: 0.92 }, ...]

RAG (Retrieval Augmented Generation)

RAG combina búsqueda semántica con generación de texto. El flujo es:

  1. Buscar documentos relevantes (pgvector)

  2. Agregarlos como contexto al prompt

  3. Generar respuesta con LLM

Implementación completa

// src/rag.ts
import { supabase } from './lib/supabase'
import { generateEmbedding } from './embeddings/openai'
import OpenAI from 'openai'

const openai = new OpenAI()

export async function ragQuery(question: string) {
  // 1. Buscar documentos relevantes
  const queryEmbedding = await generateEmbedding(question)
  
  const { data: documents } = await supabase.rpc('search_documents', {
    query_embedding: JSON.stringify(queryEmbedding),
    match_count: 3,
    match_threshold: 0.7
  })

  if (!documents?.length) {
    return 'No encontré información relevante para tu pregunta.'
  }

  // 2. Construir el contexto
  const context = documents
    .map(doc => doc.content)
    .join('\n\n')

  // 3. Generar respuesta con contexto
  const completion = await openai.chat.completions.create({
    model: 'gpt-4',
    messages: [
      {
        role: 'system',
        content: `Responde la pregunta basándote SOLO en el siguiente contexto. Si el contexto no contiene la información, di que no tienes suficiente información.\n\nContexto:\n${context}`
      },
      { role: 'user', content: question }
    ]
  })

  return completion.choices[0].message.content
}

// Ejemplo
const answer = await ragQuery('¿Cómo implemento RLS en Supabase?')
console.log(answer)

Ejemplo completo: Chatbot con memoria

// src/chatbot.ts
import { supabase } from './lib/supabase'
import { generateEmbedding } from './embeddings/openai'
import OpenAI from 'openai'

const openai = new OpenAI()

export class RAGChatbot {
  private conversationHistory: Array<{ role: string; content: string }> = []

  async ask(question: string) {
    // Buscar documentos relevantes
    const queryEmbedding = await generateEmbedding(question)
    
    const { data: documents } = await supabase.rpc('search_documents', {
      query_embedding: JSON.stringify(queryEmbedding),
      match_count: 3
    })

    const context = documents?.map(d => d.content).join('\n') || ''

    // Agregar al historial
    this.conversationHistory.push({
      role: 'user',
      content: question
    })

    // Generar respuesta
    const completion = await openai.chat.completions.create({
      model: 'gpt-4',
      messages: [
        {
          role: 'system',
          content: `Eres un asistente experto en Supabase. Usa el siguiente contexto para responder:\n\n${context}`
        },
        ...this.conversationHistory
      ]
    })

    const answer = completion.choices[0].message.content || ''
    
    this.conversationHistory.push({
      role: 'assistant',
      content: answer
    })

    return answer
  }
}

// Uso
const chatbot = new RAGChatbot()
console.log(await chatbot.ask('¿Qué es RLS?'))
console.log(await chatbot.ask('¿Cómo lo implemento?')) // Recuerda el contexto anterior

Búsqueda multimodal

pgvector puede almacenar vectores de cualquier fuente - texto, imágenes, audio:

// src/multimodal.ts
// Embedding de imagen (con CLIP, por ejemplo)
const imageEmbedding = await generateImageEmbedding(imageUrl)

await supabase.from('documents').insert({
  content: 'Descripción de la imagen',
  embedding: JSON.stringify(imageEmbedding),
  metadata: { type: 'image', url: imageUrl }
})

Referencia de Codea Bien

Para más sobre embeddings y RAG en Angular, revisa: RAG en Angular: Búsqueda inteligente con embeddings y Gemini

Qué sigue

En el próximo artículo vamos con Supabase + AI Agents: cómo usar el MCP Server para conectar agentes de AI a tu base de datos, y construir automatizaciones inteligentes.