Vector Embeddings Setup
Enable pgvector Extension
-- Enable the pgvector extension
create extension if not exists vector;
-- Create a table with vector column
create table documents (
id bigserial primary key,
content text,
embedding vector(1536)
);
-- Create an index for fast similarity search
create index on documents using ivfflat (embedding vector_cosine_ops)
with (lists = 100);
Alternative: HNSW Index (Better Performance)
-- Create HNSW index for better performance
create index on documents using hnsw (embedding vector_cosine_ops);
Generate Embeddings
Using OpenAI
import { createClient } from '@supabase/supabase-js'
import OpenAI from 'openai'
const supabase = createClient(url, key)
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY })
async function generateEmbedding(text: string) {
const response = await openai.embeddings.create({
model: 'text-embedding-3-small',
input: text,
})
return response.data[0].embedding
}
async function storeDocument(content: string) {
// Generate embedding
const embedding = await generateEmbedding(content)
// Store in database
const { data, error } = await supabase
.from('documents')
.insert({
content,
embedding,
})
.select()
.single()
if (error) throw error
return data
}
Using Hugging Face
import { HfInference } from '@huggingface/inference'
const hf = new HfInference(process.env.HUGGINGFACE_API_KEY)
async function generateEmbeddingHF(text: string) {
const response = await hf.featureExtraction({
model: 'sentence-transformers/all-MiniLM-L6-v2',
inputs: text,
})
return response
}
Similarity Search
Cosine Similarity
async function searchSimilarDocuments(query: string, matchCount: number = 5) {
// Generate embedding for query
const queryEmbedding = await generateEmbedding(query)
// Search for similar documents
const { data, error } = await supabase.rpc('match_documents', {
query_embedding: queryEmbedding,
match_count: matchCount,
})
if (error) throw error
return data
}
// SQL function for similarity search
/*
create or replace function match_documents(
query_embedding vector(1536),
match_count int default 5
)
returns table (
id bigint,
content text,
similarity float
)
language plpgsql
as $$
begin
return query
select
documents.id,
documents.content,
1 - (documents.embedding <=> query_embedding) as similarity
from documents
order by documents.embedding <=> query_embedding
limit match_count;
end;
$$;
*/
L2 Distance (Euclidean)
create or replace function match_documents_l2(
query_embedding vector(1536),
match_count int default 5
)
returns table (
id bigint,
content text,
distance float
)
language plpgsql
as $$
begin
return query
select
documents.id,
documents.content,
documents.embedding <-> query_embedding as distance
from documents
order by documents.embedding <-> query_embedding
limit match_count;
end;
$$;
Inner Product
create or replace function match_documents_inner(
query_embedding vector(1536),
match_count int default 5
)
returns table (
id bigint,
content text,
score float
)
language plpgsql
as $$
begin
return query
select
documents.id,
documents.content,
(documents.embedding <#> query_embedding) * -1 as score
from documents
order by documents.embedding <#> query_embedding
limit match_count;
end;
$$;
Semantic Search with Metadata
interface Document {
id: number
content: string
metadata: {
title: string
author: string
category: string
created_at: string
}
embedding: number[]
}
async function semanticSearch(
query: string,
filters?: {
category?: string
author?: string
},
limit: number = 10
) {
const queryEmbedding = await generateEmbedding(query)
const { data, error } = await supabase.rpc('semantic_search', {
query_embedding: queryEmbedding,
match_count: limit,
filter_category: filters?.category,
filter_author: filters?.author,
})
if (error) throw error
return data
}
// SQL function with filters
/*
create or replace function semantic_search(
query_embedding vector(1536),
match_count int default 10,
filter_category text default null,
filter_author text default null
)
returns table (
id bigint,
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
(filter_category is null or documents.metadata->>'category' = filter_category)
and (filter_author is null or documents.metadata->>'author' = filter_author)
order by documents.embedding <=> query_embedding
limit match_count;
end;
$$;
*/
Hybrid Search (Vector + Full-Text)
create or replace function hybrid_search(
query_text text,
query_embedding vector(1536),
match_count int default 10
)
returns table (
id bigint,
content text,
similarity float,
fts_rank float,
combined_rank float
)
language plpgsql
as $$
begin
return query
select
documents.id,
documents.content,
1 - (documents.embedding <=> query_embedding) as similarity,
ts_rank(documents.fts, plainto_tsquery(query_text)) as fts_rank,
(1 - (documents.embedding <=> query_embedding)) * 0.7 +
ts_rank(documents.fts, plainto_tsquery(query_text)) * 0.3 as combined_rank
from documents
where documents.fts @@ plainto_tsquery(query_text)
order by combined_rank desc
limit match_count;
end;
$$;
RAG (Retrieval Augmented Generation)
import OpenAI from 'openai'
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY })
async function ragQuery(question: string) {
// 1. Generate embedding for the question
const questionEmbedding = await generateEmbedding(question)
// 2. Find relevant documents
const { data: documents } = await supabase.rpc('match_documents', {
query_embedding: questionEmbedding,
match_count: 5,
})
// 3. Build context from documents
const context = documents
.map((doc: any) => doc.content)
.join('\n\n')
// 4. Generate answer using GPT
const completion = await openai.chat.completions.create({
model: 'gpt-4',
messages: [
{
role: 'system',
content: 'You are a helpful assistant. Answer the question based on the provided context. If you cannot answer based on the context, say so.',
},
{
role: 'user',
content: `Context:\n${context}\n\nQuestion: ${question}`,
},
],
})
return {
answer: completion.choices[0].message.content,
sources: documents,
}
}
Streaming RAG Responses
async function* streamRAG(question: string) {
// Get relevant context
const questionEmbedding = await generateEmbedding(question)
const { data: documents } = await supabase.rpc('match_documents', {
query_embedding: questionEmbedding,
match_count: 5,
})
const context = documents.map((doc: any) => doc.content).join('\n\n')
// Stream response from OpenAI
const stream = await openai.chat.completions.create({
model: 'gpt-4',
messages: [
{
role: 'system',
content: 'Answer based on the provided context.',
},
{
role: 'user',
content: `Context:\n${context}\n\nQuestion: ${question}`,
},
],
stream: true,
})
for await (const chunk of stream) {
const content = chunk.choices[0]?.delta?.content || ''
if (content) {
yield content
}
}
}
// Usage
for await (const chunk of streamRAG('What is Supabase?')) {
process.stdout.write(chunk)
}
Image Embeddings
import OpenAI from 'openai'
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY })
async function generateImageEmbedding(imageUrl: string) {
// Use CLIP or similar model for image embeddings
// This is a simplified example
const response = await fetch('https://api.openai.com/v1/embeddings', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.OPENAI_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: 'clip-vit-base-patch32',
input: imageUrl,
}),
})
const data = await response.json()
return data.data[0].embedding
}
async function searchSimilarImages(imageUrl: string) {
const embedding = await generateImageEmbedding(imageUrl)
const { data, error } = await supabase.rpc('match_images', {
query_embedding: embedding,
match_count: 10,
})
if (error) throw error
return data
}
Batch Processing
async function batchGenerateEmbeddings(texts: string[]) {
const batchSize = 100
const results: Array<{ text: string; embedding: number[] }> = []
for (let i = 0; i < texts.length; i += batchSize) {
const batch = texts.slice(i, i + batchSize)
const embeddings = await Promise.all(
batch.map((text) => generateEmbedding(text))
)
results.push(
...batch.map((text, index) => ({
text,
embedding: embeddings[index],
}))
)
}
return results
}
async function batchInsertDocuments(
documents: Array<{ content: string; embedding: number[] }>
) {
const { data, error } = await supabase
.from('documents')
.insert(documents)
.select()
if (error) throw error
return data
}
Clustering
-- K-means clustering function
create or replace function kmeans_cluster(
k int,
max_iterations int default 100
)
returns table (
id bigint,
cluster_id int
)
language plpgsql
as $$
declare
iteration int := 0;
changed boolean := true;
begin
-- Initialize cluster assignments randomly
drop table if exists temp_clusters;
create temp table temp_clusters as
select id, floor(random() * k)::int as cluster_id
from documents;
while changed and iteration < max_iterations loop
-- Update centroids and reassign
-- (Simplified - actual implementation would be more complex)
iteration := iteration + 1;
end loop;
return query select * from temp_clusters;
end;
$$;
Deduplication
async function findDuplicates(threshold: number = 0.95) {
const { data: allDocs } = await supabase
.from('documents')
.select('id, content, embedding')
const duplicates: Array<{ id1: number; id2: number; similarity: number }> = []
for (let i = 0; i < allDocs.length; i++) {
for (let j = i + 1; j < allDocs.length; j++) {
const similarity = cosineSimilarity(
allDocs[i].embedding,
allDocs[j].embedding
)
if (similarity >= threshold) {
duplicates.push({
id1: allDocs[i].id,
id2: allDocs[j].id,
similarity,
})
}
}
}
return duplicates
}
function cosineSimilarity(a: number[], b: number[]): number {
const dotProduct = a.reduce((sum, val, i) => sum + val * b[i], 0)
const magnitudeA = Math.sqrt(a.reduce((sum, val) => sum + val * val, 0))
const magnitudeB = Math.sqrt(b.reduce((sum, val) => sum + val * val, 0))
return dotProduct / (magnitudeA * magnitudeB)
}
Edge Function for AI
// supabase/functions/generate-answer/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 { query } = await req.json()
const supabase = createClient(
Deno.env.get('SUPABASE_URL') ?? '',
Deno.env.get('SUPABASE_ANON_KEY') ?? ''
)
// Generate embedding
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-3-small',
input: query,
}),
})
const embeddingData = await embeddingResponse.json()
const embedding = embeddingData.data[0].embedding
// Search documents
const { data: documents } = await supabase.rpc('match_documents', {
query_embedding: embedding,
match_count: 5,
})
const context = documents.map((d: any) => d.content).join('\n\n')
// Generate answer
const completionResponse = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${Deno.env.get('OPENAI_API_KEY')}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: 'gpt-4',
messages: [
{
role: 'system',
content: 'Answer based on the context provided.',
},
{
role: 'user',
content: `Context:\n${context}\n\nQuestion: ${query}`,
},
],
}),
})
const completion = await completionResponse.json()
return new Response(
JSON.stringify({
answer: completion.choices[0].message.content,
sources: documents,
}),
{ headers: { 'Content-Type': 'application/json' } }
)
})
Example Apps
Vector Hello World
Get started with vectors (Jupyter)
Image Search
Semantic image search app
Semantic Search
Full-text + vector search
Chatbot with RAG
AI chatbot with context
Next Steps
AI Documentation
Learn about AI features
pgvector Guide
Deep dive into pgvector
Vector Embeddings
Understanding embeddings
Similarity Search
Advanced search techniques
