Semantic Search With Vercel AI SDK and Vector Embeddings
Build a semantic search feature that understands meaning, not just keywords. Learn to generate embeddings with the Vercel AI SDK and query them with pgvector for intelligent search results.
Why Semantic Search Beats Keyword Search
Traditional keyword search matches exact terms. Semantic search understands meaning — a search for "car" finds documents about "vehicle", "automobile", and "driving". This is powered by vector embeddings: numerical representations of text that encode semantic meaning.
The Vercel AI SDK makes it straightforward to generate embeddings and the pgvector Postgres extension stores and queries them efficiently.
Setting Up pgvector
-- Enable pgvector extension
CREATE EXTENSION IF NOT EXISTS vector;
-- Add embedding column to your content table
ALTER TABLE articles ADD COLUMN embedding vector(1536);
-- Create index for fast similarity search
CREATE INDEX ON articles USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100);
Generating Embeddings With the AI SDK
import { embed, embedMany } from 'ai';
import { openai } from '@ai-sdk/openai';
// Embed a single text
async function embedText(text: string) {
const { embedding } = await embed({
model: openai.embedding('text-embedding-3-small'),
value: text,
});
return embedding; // number[]
}
// Embed multiple texts in one API call (more efficient)
async function embedArticles(articles: { id: string; content: string }[]) {
const { embeddings } = await embedMany({
model: openai.embedding('text-embedding-3-small'),
values: articles.map(a => a.content),
});
return articles.map((a, i) => ({ id: a.id, embedding: embeddings[i] }));
}
Storing Embeddings in Postgres
import { db } from './db';
async function indexArticle(articleId: string, content: string) {
const embedding = await embedText(content);
// Store as pgvector format
await db.$executeRaw`
UPDATE articles
SET embedding = ${JSON.stringify(embedding)}::vector
WHERE id = ${articleId}
`;
}
Building the Search API Route
// app/api/search/route.ts
import { embed } from 'ai';
import { openai } from '@ai-sdk/openai';
import { db } from '@/lib/db';
export async function POST(request: Request) {
const { query } = await request.json();
// Embed the search query
const { embedding } = await embed({
model: openai.embedding('text-embedding-3-small'),
value: query,
});
// Find the 5 most similar articles using cosine similarity
const results = await db.$queryRaw`
SELECT
id,
title,
slug,
summary,
1 - (embedding <=> ${JSON.stringify(embedding)}::vector) AS similarity
FROM articles
WHERE status = 'PUBLISHED'
ORDER BY embedding <=> ${JSON.stringify(embedding)}::vector
LIMIT 5
`;
return Response.json({ results });
}
Building the Search UI
'use client';
import { useState } from 'react';
import { useDebounce } from 'use-debounce';
export function SemanticSearch() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const [debouncedQuery] = useDebounce(query, 400);
useEffect(() => {
if (!debouncedQuery.trim()) return;
fetch('/api/search', {
method: 'POST',
body: JSON.stringify({ query: debouncedQuery }),
})
.then(r => r.json())
.then(d => setResults(d.results));
}, [debouncedQuery]);
return (
<div>
<input
value={query}
onChange={e => setQuery(e.target.value)}
placeholder="Search by meaning..."
className="w-full p-3 border rounded-lg"
/>
<ul className="mt-4 space-y-2">
{results.map(r => (
<li key={r.id}>
<a href={`/posts/${r.slug}`} className="text-blue-600 hover:underline">
{r.title}
</a>
<span className="text-sm text-gray-500 ml-2">
{Math.round(r.similarity * 100)}% match
</span>
</li>
))}
</ul>
</div>
);
}
Cost Considerations
text-embedding-3-small costs $0.02 per million tokens. For a site with 10,000 articles averaging 500 tokens each, initial indexing costs about $0.10. Queries cost fractions of a cent. Semantic search is one of the most cost-effective AI features you can add.
Admin
Cal.com
Open source scheduling — tự host booking system, thay thế Calendly. Free & privacy-first.
Vercel
Deploy Next.js app trong 30 giây. Free tier rộng rãi cho side projects.
Bình luận (0)
Đăng nhập để bình luận
Chưa có bình luận nào. Hãy là người đầu tiên!