Supabase + AI: pgvector, embeddings y búsqueda semántica
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
- Ve a Database > Extensions
- Busca
vector - 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:
- Buscar documentos relevantes (pgvector)
- Agregarlos como contexto al prompt
- 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.