← Back to articles

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

LevelApproachQualityComplexity
1. Vector searchEmbed query + docs, find similarGoodLow
2. Hybrid searchVector + keyword (BM25)BetterMedium
3. Hybrid + re-rankHybrid results re-ranked by AIBestMedium

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

ToolTypeBest ForPrice
Supabase pgvectorDatabase extensionAlready on SupabaseIncluded
PineconeManaged vector DBScale, simplicityFree/$70/mo
MeilisearchSearch engine + vectorsFull-featured searchFree/Cloud
TypesenseSearch engine + vectorsPerformanceFree/Cloud
AlgoliaSearch-as-a-serviceE-commerce, instant$0-$1/1K requests
OramaEdge-native searchClient-side searchFree/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.

Get AI tool guides in your inbox

Weekly deep-dives on the best AI coding tools, automation platforms, and productivity software.