How to Add Search to Your App (2026)
Users expect instant, typo-tolerant search. Here's how to add it — from simple PostgreSQL full-text search to dedicated search engines like Meilisearch and Algolia.
The Options
| Option | Best For | Cost | Latency |
|---|---|---|---|
| PostgreSQL full-text | Simple search, <100K docs | Free (with your DB) | 10-50ms |
| Meilisearch | Self-hosted, fast search | Free (self-host) | 1-20ms |
| Algolia | Managed, enterprise | $0+ (free tier) | 1-10ms |
| Typesense | Self-hosted alternative | Free (self-host) | 1-20ms |
| Pagefind | Static sites | Free | <5ms |
Option 1: PostgreSQL Full-Text Search (Simplest)
Already using Postgres? You already have search.
-- Add a search column
ALTER TABLE articles ADD COLUMN search_vector tsvector;
-- Update it
UPDATE articles SET search_vector =
setweight(to_tsvector('english', title), 'A') ||
setweight(to_tsvector('english', content), 'B');
-- Create an index
CREATE INDEX idx_articles_search ON articles USING gin(search_vector);
-- Search
SELECT title, ts_rank(search_vector, query) as rank
FROM articles, to_tsquery('english', 'typescript & react') as query
WHERE search_vector @@ query
ORDER BY rank DESC
LIMIT 20;
With Drizzle
const results = await db.execute(sql`
SELECT id, title, ts_rank(search_vector, to_tsquery('english', ${query})) as rank
FROM articles
WHERE search_vector @@ to_tsquery('english', ${query})
ORDER BY rank DESC
LIMIT 20
`)
Pros: No extra service. Free. Works with your existing database. Cons: No typo tolerance. No instant search. Limited relevance tuning.
Use when: <100K documents and basic search is fine.
Option 2: Meilisearch (Recommended)
Fast, typo-tolerant, easy to set up. Open source.
Setup
# Docker
docker run -p 7700:7700 getmeili/meilisearch:latest
# Or cloud: cloud.meilisearch.com
Index Documents
import { MeiliSearch } from 'meilisearch'
const client = new MeiliSearch({ host: 'http://localhost:7700', apiKey: 'masterKey' })
// Add documents
await client.index('articles').addDocuments([
{ id: 1, title: 'Getting Started with Next.js', content: '...', category: 'tutorial' },
{ id: 2, title: 'React Server Components', content: '...', category: 'guide' },
])
// Configure searchable attributes and ranking
await client.index('articles').updateSettings({
searchableAttributes: ['title', 'content', 'category'],
rankingRules: ['words', 'typo', 'proximity', 'attribute', 'sort', 'exactness'],
filterableAttributes: ['category'],
sortableAttributes: ['created_at'],
})
Search
const results = await client.index('articles').search('nxtjs tutrial', {
limit: 20,
filter: 'category = tutorial',
})
// Returns results even with typos: "nxtjs tutrial" → "Next.js tutorial"
React Component
'use client'
import { useState, useEffect } from 'react'
import { MeiliSearch } from 'meilisearch'
const client = new MeiliSearch({ host: '/api/search', apiKey: 'searchKey' })
function SearchBar() {
const [query, setQuery] = useState('')
const [results, setResults] = useState([])
useEffect(() => {
if (!query) return setResults([])
const timer = setTimeout(async () => {
const { hits } = await client.index('articles').search(query, { limit: 10 })
setResults(hits)
}, 200) // Debounce
return () => clearTimeout(timer)
}, [query])
return (
<div>
<input value={query} onChange={e => setQuery(e.target.value)} placeholder="Search..." />
<ul>
{results.map(hit => (
<li key={hit.id}>{hit.title}</li>
))}
</ul>
</div>
)
}
Pros: Typo-tolerant. Instant (<20ms). Easy setup. Free (self-host). Cons: Extra service to manage. Needs data sync from your DB.
Option 3: Algolia (Managed)
The premium managed option. Fastest search with zero ops.
import algoliasearch from 'algoliasearch'
const client = algoliasearch('APP_ID', 'ADMIN_KEY')
const index = client.initIndex('articles')
// Add documents
await index.saveObjects(articles, { autoGenerateObjectIDIfNotExist: true })
// Search
const { hits } = await index.search('next.js tutorial')
Pros: Fastest. Best relevance. Zero ops. Analytics. A/B testing. Cons: Expensive at scale ($1/1K searches on paid).
Option 4: Pagefind (Static Sites)
For Astro, Hugo, Jekyll, and other static sites:
npx pagefind --site dist
Generates a search index from your static HTML. Client-side search with zero server. ~100KB JavaScript.
<link href="/pagefind/pagefind-ui.css" rel="stylesheet">
<script src="/pagefind/pagefind-ui.js"></script>
<div id="search"></div>
<script>
new PagefindUI({ element: "#search" })
</script>
Keeping Search in Sync
Your database is the source of truth. Search indexes need syncing:
// After creating/updating a document in your DB
await db.insert(articles).values(newArticle)
await meiliClient.index('articles').addDocuments([newArticle])
// Or use webhooks/events
events.on('article.created', async (article) => {
await meiliClient.index('articles').addDocuments([article])
})
Decision Framework
Static site → Pagefind (free, client-side)
< 100K docs, basic search → PostgreSQL full-text (free)
< 1M docs, good search → Meilisearch (free self-host, $30/mo cloud)
Enterprise, analytics needed → Algolia ($$$)
FAQ
Do I need a separate search engine?
If PostgreSQL full-text search meets your needs (no typo tolerance, <100K docs), no. Add Meilisearch when users complain about search quality.
Meilisearch vs Algolia?
Meilisearch: free to self-host, good enough for most apps. Algolia: managed, faster, better relevance tuning, expensive. Start with Meilisearch, upgrade to Algolia if search is revenue-critical.
How do I handle search on mobile?
Same approach. API calls to your search endpoint. Debounce input to avoid excessive requests.
Bottom Line
Start with PostgreSQL full-text search (free, no extra service). Upgrade to Meilisearch when you need typo tolerance and instant results. Use Algolia only when search is business-critical and budget allows. For static sites, Pagefind is perfect.