Skip to main content
Explore AI and vector examples covering embeddings, similarity search, semantic search, and AI integrations.

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
}

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