Supabase + AI: pgvector, embeddings y búsqueda semántica
Kevin Dávila
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 anteriorBú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.