How to Build an AI-Powered Search Engine (2026)
Traditional keyword search fails when users don't use exact terms. AI-powered search understands meaning — "affordable laptop for students" finds results about "budget notebooks for college" even without matching keywords.
How AI Search Works
Traditional: "affordable laptop for students"
→ Matches documents containing those exact words
→ Misses: "budget notebook for college", "cheap computer for school"
AI-Powered: "affordable laptop for students"
→ Understands meaning: low-cost + portable computer + education use
→ Finds: all relevant results regardless of exact wording
Three Levels
| Level | Approach | Quality | Complexity |
|---|---|---|---|
| 1. Vector search | Embed query + docs, find similar | Good | Low |
| 2. Hybrid search | Vector + keyword (BM25) | Better | Medium |
| 3. Hybrid + re-rank | Hybrid results re-ranked by AI | Best | Medium |
Level 1: Vector Search
Step 1: Embed Your Content
import { embed, embedMany } from 'ai';
import { openai } from '@ai-sdk/openai';
// Embed all documents (run once, store results)
const documents = [
{ id: '1', title: 'MacBook Air M3', content: 'Lightweight laptop perfect for students...' },
{ id: '2', title: 'ThinkPad E-series', content: 'Budget-friendly business notebook...' },
// ... thousands of documents
];
const { embeddings } = await embedMany({
model: openai.embedding('text-embedding-3-small'),
values: documents.map(d => `${d.title} ${d.content}`),
});
// Store embeddings in vector database
for (let i = 0; i < documents.length; i++) {
await vectorDB.upsert({
id: documents[i].id,
values: embeddings[i],
metadata: { title: documents[i].title },
});
}
Step 2: Search
async function search(query: string, topK = 10) {
// Embed the query
const { embedding } = await embed({
model: openai.embedding('text-embedding-3-small'),
value: query,
});
// Find similar documents
const results = await vectorDB.query({
vector: embedding,
topK,
includeMetadata: true,
});
return results;
}
Step 3: Store in a Vector Database
Supabase pgvector:
create extension vector;
create table documents (
id serial primary key,
title text,
content text,
embedding vector(1536)
);
create index on documents using hnsw (embedding vector_cosine_ops);
-- Search function
create function search_documents(query_embedding vector(1536), match_count int)
returns table (id int, title text, content text, similarity float)
as $$
select id, title, content, 1 - (embedding <=> query_embedding) as similarity
from documents
order by embedding <=> query_embedding
limit match_count;
$$ language sql;
Pinecone:
import { Pinecone } from '@pinecone-database/pinecone';
const pc = new Pinecone({ apiKey: process.env.PINECONE_API_KEY });
const index = pc.index('search');
// Upsert
await index.upsert([{ id: '1', values: embedding, metadata: { title } }]);
// Query
const results = await index.query({ vector: queryEmbedding, topK: 10 });
Level 2: Hybrid Search
Vector search alone misses exact matches. "iPhone 15 Pro Max" should exactly match that product — not just semantically similar phones.
Hybrid search combines:
- Semantic search (vector similarity) — understands meaning
- Keyword search (BM25) — matches exact terms
async function hybridSearch(query: string, topK = 10) {
// 1. Vector search
const vectorResults = await semanticSearch(query, topK * 2);
// 2. Keyword search (BM25)
const keywordResults = await bm25Search(query, topK * 2);
// 3. Combine with Reciprocal Rank Fusion
const combined = reciprocalRankFusion(vectorResults, keywordResults);
return combined.slice(0, topK);
}
function reciprocalRankFusion(
...resultSets: SearchResult[][]
): SearchResult[] {
const k = 60; // constant
const scores = new Map<string, number>();
for (const results of resultSets) {
results.forEach((result, rank) => {
const current = scores.get(result.id) || 0;
scores.set(result.id, current + 1 / (k + rank + 1));
});
}
return [...scores.entries()]
.sort((a, b) => b[1] - a[1])
.map(([id, score]) => ({ id, score }));
}
Hybrid with Supabase
-- Full-text search + vector search
create function hybrid_search(
query_text text,
query_embedding vector(1536),
match_count int,
keyword_weight float default 0.3,
semantic_weight float default 0.7
) returns table (id int, title text, score float)
as $$
with keyword as (
select id, title, ts_rank(to_tsvector(content), plainto_tsquery(query_text)) as rank
from documents
where to_tsvector(content) @@ plainto_tsquery(query_text)
order by rank desc limit match_count * 2
),
semantic as (
select id, title, 1 - (embedding <=> query_embedding) as rank
from documents
order by embedding <=> query_embedding limit match_count * 2
)
select coalesce(k.id, s.id), coalesce(k.title, s.title),
(coalesce(k.rank, 0) * keyword_weight + coalesce(s.rank, 0) * semantic_weight) as score
from keyword k full outer join semantic s on k.id = s.id
order by score desc limit match_count;
$$ language sql;
Level 3: Re-Ranking
After hybrid search, re-rank results with a cross-encoder model for maximum relevance:
import Anthropic from '@anthropic-ai/sdk';
async function rerank(query: string, results: SearchResult[], topK = 5) {
// Use Cohere's reranker or Claude for re-ranking
const reranked = await cohere.rerank({
model: 'rerank-v3.5',
query,
documents: results.map(r => r.content),
topN: topK,
});
return reranked.results.map(r => ({
...results[r.index],
relevanceScore: r.relevanceScore,
}));
}
AI-Generated Answers
Combine search with LLM for direct answers:
async function searchAndAnswer(query: string) {
// 1. Find relevant documents
const results = await hybridSearch(query, 5);
// 2. Generate answer using top results as context
const context = results.map(r => r.content).join('\n\n---\n\n');
const { text } = await generateText({
model: anthropic('claude-sonnet-4-20250514'),
system: 'Answer the question using the provided search results. Cite sources.',
prompt: `Search results:\n${context}\n\nQuestion: ${query}`,
});
return { answer: text, sources: results };
}
Search Tools Compared
| Tool | Type | Best For | Price |
|---|---|---|---|
| Supabase pgvector | Database extension | Already on Supabase | Included |
| Pinecone | Managed vector DB | Scale, simplicity | Free/$70/mo |
| Meilisearch | Search engine + vectors | Full-featured search | Free/Cloud |
| Typesense | Search engine + vectors | Performance | Free/Cloud |
| Algolia | Search-as-a-service | E-commerce, instant | $0-$1/1K requests |
| Orama | Edge-native search | Client-side search | Free/Cloud |
Meilisearch (Recommended for Most)
Full-featured search engine with hybrid search built in:
import { MeiliSearch } from 'meilisearch';
const client = new MeiliSearch({ host: 'http://localhost:7700' });
// Index documents (Meilisearch auto-generates vectors with AI)
await client.index('products').addDocuments(products);
// Configure hybrid search
await client.index('products').updateSettings({
embedders: {
default: {
source: 'openAi',
model: 'text-embedding-3-small',
apiKey: process.env.OPENAI_API_KEY,
},
},
});
// Hybrid search
const results = await client.index('products').search('affordable laptop for students', {
hybrid: { semanticRatio: 0.7 },
});
Why Meilisearch: Typo-tolerant keyword search + semantic search in one engine. Fast. Easy to deploy. Open source.
FAQ
How much does AI search cost to run?
Embedding queries: ~$0.0001 per search (text-embedding-3-small). Vector DB: $0-70/month. Re-ranking: ~$0.001 per search. For 10K searches/month: ~$5-75 total.
How do I handle updates?
Re-embed documents when they change. For real-time updates: use a vector DB that supports upserts (Pinecone, Supabase). Batch re-indexing works for less frequent changes.
Do I need a vector database?
For < 10K documents: in-memory search or SQLite with a vector extension works fine. For 10K-1M documents: pgvector or Pinecone. For 1M+: dedicated vector database (Pinecone, Qdrant, Weaviate).
How does this compare to Algolia/Elasticsearch?
Traditional search engines are keyword-first. AI search understands meaning. Meilisearch and Typesense now combine both. Algolia is adding AI features. For new projects: start with hybrid search from day one.
Can I run this on the edge?
Orama runs entirely client-side (in the browser or at the edge). For small datasets (<50K docs), edge-native search eliminates server round-trips. For larger datasets: server-side vector search with edge API routes.
Bottom Line
Start simple: Supabase pgvector for basic vector search if you're already on Supabase. Level up: Meilisearch for full-featured hybrid search with minimal setup. Go advanced: Add Cohere re-ranking on top of hybrid results for maximum relevance.
Build it today: Index your content in Meilisearch with the OpenAI embedder (30 min setup). Enable hybrid search. You'll immediately see better results than keyword-only search. Total cost: free for self-hosted Meilisearch + ~$1/month for embeddings.