Module 6: Vector Search & AI
Vector search enables semantic understanding - finding content by meaning, not just keywords.
What Are Embeddings?
Embeddings convert text into numerical vectors that capture meaning:
"How do I reset my password?"
↓ (embedding model)
[0.123, -0.456, 0.789, ..., 0.234] // 1536 dimensions
"I forgot my login credentials"
↓ (embedding model)
[0.125, -0.452, 0.791, ..., 0.231] // Similar vector!Similar meanings = Similar vectors
pgvector: Vector Search in PostgreSQL
pgvector adds vector operations to Postgres:
sql
-- Enable extension
CREATE EXTENSION vector;
-- Create table with vector column
CREATE TABLE documents (
id SERIAL PRIMARY KEY,
content TEXT,
embedding VECTOR(1536) -- OpenAI ada-002 dimension
);
-- Create index for fast search
CREATE INDEX ON documents
USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100);Setting Up with Prisma
prisma
// prisma/schema.prisma
model Document {
id String @id @default(cuid())
content String
embedding Unsupported("vector(1536)")?
@@index([embedding])
}Generating Embeddings
typescript
// lib/embeddings.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
}Storing Documents
typescript
// Store document with embedding
async function storeDocument(content: string) {
const embedding = await generateEmbedding(content)
await db.$executeRaw`
INSERT INTO documents (id, content, embedding)
VALUES (${crypto.randomUUID()}, ${content}, ${embedding}::vector)
`
}Semantic Search
typescript
// Search for similar documents
async function searchDocuments(query: string, limit = 5) {
const queryEmbedding = await generateEmbedding(query)
const results = await db.$queryRaw`
SELECT id, content,
1 - (embedding <=> ${queryEmbedding}::vector) as similarity
FROM documents
ORDER BY embedding <=> ${queryEmbedding}::vector
LIMIT ${limit}
`
return results
}RAG (Retrieval Augmented Generation)
RAG combines search with AI generation:
User Question
↓
[1. Generate Embedding]
↓
[2. Search Vector DB] → Relevant Documents
↓
[3. Build Prompt with Context]
↓
[4. Send to LLM]
↓
AI-Generated Answer (with sources)Implementation
typescript
// lib/rag.ts
import OpenAI from 'openai'
const openai = new OpenAI()
export async function answerQuestion(question: string) {
// 1. Search for relevant documents
const relevantDocs = await searchDocuments(question, 5)
// 2. Build context from documents
const context = relevantDocs
.map((doc: any) => doc.content)
.join('\n\n---\n\n')
// 3. Generate answer with context
const response = await openai.chat.completions.create({
model: 'gpt-4-turbo-preview',
messages: [
{
role: 'system',
content: `You are a helpful assistant. Answer questions based on the provided context. If the answer isn't in the context, say "I don't have information about that."
Context:
${context}`,
},
{
role: 'user',
content: question,
},
],
})
return {
answer: response.choices[0].message.content,
sources: relevantDocs,
}
}API Route
typescript
// app/api/ask/route.ts
import { answerQuestion } from '@/lib/rag'
export async function POST(request: Request) {
const { question } = await request.json()
if (!question) {
return Response.json({ error: 'Question required' }, { status: 400 })
}
const result = await answerQuestion(question)
return Response.json(result)
}Chunking Strategies
Large documents need to be split into chunks:
typescript
// Simple chunking by character count
function chunkText(text: string, chunkSize = 1000, overlap = 200): string[] {
const chunks: string[] = []
let start = 0
while (start < text.length) {
const end = start + chunkSize
chunks.push(text.slice(start, end))
start = end - overlap // Overlap prevents cutting context
}
return chunks
}
// Better: Chunk by paragraphs/sentences
function chunkByParagraph(text: string, maxChunkSize = 1000): string[] {
const paragraphs = text.split('\n\n')
const chunks: string[] = []
let currentChunk = ''
for (const paragraph of paragraphs) {
if (currentChunk.length + paragraph.length > maxChunkSize) {
if (currentChunk) chunks.push(currentChunk.trim())
currentChunk = paragraph
} else {
currentChunk += '\n\n' + paragraph
}
}
if (currentChunk) chunks.push(currentChunk.trim())
return chunks
}Supabase Vector Search
Supabase has built-in pgvector support:
typescript
// Store with Supabase
const { error } = await supabase.from('documents').insert({
content: 'Document content here',
embedding: embedding, // Array of numbers
})
// Search with Supabase RPC
const { data } = await supabase.rpc('match_documents', {
query_embedding: queryEmbedding,
match_threshold: 0.7,
match_count: 5,
})sql
-- Create search function in Supabase SQL editor
CREATE OR REPLACE FUNCTION match_documents(
query_embedding VECTOR(1536),
match_threshold FLOAT,
match_count INT
)
RETURNS TABLE (
id UUID,
content TEXT,
similarity FLOAT
)
LANGUAGE plpgsql
AS $$
BEGIN
RETURN QUERY
SELECT
documents.id,
documents.content,
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;
$$;Hybrid Search
Combine vector + keyword search for best results:
sql
-- Hybrid search: vectors + full-text
WITH vector_results AS (
SELECT id, content,
1 - (embedding <=> $1::vector) as vector_score
FROM documents
ORDER BY embedding <=> $1::vector
LIMIT 20
),
text_results AS (
SELECT id, content,
ts_rank(to_tsvector('english', content), plainto_tsquery($2)) as text_score
FROM documents
WHERE to_tsvector('english', content) @@ plainto_tsquery($2)
LIMIT 20
)
SELECT
COALESCE(v.id, t.id) as id,
COALESCE(v.content, t.content) as content,
(COALESCE(v.vector_score, 0) * 0.7 + COALESCE(t.text_score, 0) * 0.3) as combined_score
FROM vector_results v
FULL OUTER JOIN text_results t ON v.id = t.id
ORDER BY combined_score DESC
LIMIT 10;Use Cases
| Use Case | Description |
|---|---|
| Semantic Search | Find documents by meaning, not keywords |
| Q&A Chatbots | Answer questions from your knowledge base |
| Recommendations | Find similar products/content |
| Deduplication | Find near-duplicate content |
| Classification | Categorize content by comparing to examples |
Summary
- Embeddings - Convert text to vectors capturing meaning
- pgvector - Vector operations in PostgreSQL
- Semantic Search - Find by meaning, not keywords
- RAG - Combine search + LLM for grounded answers
- Chunking - Split large docs for better retrieval
- Hybrid Search - Combine vector + keyword for best results