Adding search to a site sounds simple until you realise there are a dozen architecturally different approaches and picking the wrong one means re-implementing it six months later. The right answer depends on whether your content is static or dynamic, how many documents you have, and whether you need semantic (meaning-based) results or keyword results. This is the 2026 decision framework.
What changed in 2026
- Pagefind 2.0 added vector search on top of its static index — you can ship semantic search on a Jamstack site with no server.
- Meilisearch 1.x stabilized its vector search and hybrid mode, making it a credible alternative to Algolia for self-hosters.
- pgvector 0.7 added HNSW indexing, which makes approximate nearest-neighbor search fast enough for production at moderate scale.
- Algolia added AI-based relevance tuning; the free tier still caps at 10,000 records.
- Full-text search in Postgres (
tsvector / ts_query) remains underrated — for apps already on Postgres, it's often the right answer with no extra infrastructure.
Tool comparison
| Tool |
Type |
Self-host |
Free tier |
Best for |
| Pagefind |
Static WASM |
Yes (build output) |
Free |
Docs, blogs, static sites |
| Algolia |
SaaS |
No |
10k records |
Product/content with budget |
| Meilisearch |
Open source / cloud |
Yes |
Cloud free tier |
Self-hosted alternative to Algolia |
| Typesense |
Open source / cloud |
Yes |
Cloud free tier |
Self-hosted, exact Algolia parity |
| Postgres FTS |
DB built-in |
Yes (your DB) |
Free |
Apps already on Postgres |
| pgvector |
Postgres extension |
Yes |
Free |
Semantic/hybrid search in Postgres |
| Elasticsearch |
Open source / cloud |
Yes |
None free |
Very large scale, complex queries |
Pagefind (static sites)
npx pagefind --site dist --output-path dist/pagefind
<!-- Add to your page -->
<link rel="stylesheet" href="/pagefind/pagefind-ui.css" />
<div id="search"></div>
<script>
window.addEventListener('load', () => {
new PagefindUI({ element: '#search', showImages: true })
})
</script>
<script src="/pagefind/pagefind-ui.js"></script>
Run pagefind after every build. It indexes HTML, generates a WASM binary, and everything runs in the browser — no API key, no server, no cost.
Algolia / Typesense (instant search)
// Push records to Algolia
import algoliasearch from 'algoliasearch'
const client = algoliasearch(process.env.ALGOLIA_APP_ID!, process.env.ALGOLIA_WRITE_KEY!)
const index = client.initIndex('products')
await index.saveObjects(products.map(p => ({ objectID: p.id, ...p })))
// React InstantSearch
import { InstantSearch, SearchBox, Hits } from 'react-instantsearch'
<InstantSearch indexName="products" searchClient={searchClient}>
<SearchBox />
<Hits hitComponent={ProductCard} />
</InstantSearch>
Typesense has a drop-in instantsearch-adapter package — swap the client, keep the UI components.
Postgres full-text search
-- Add a tsvector column
ALTER TABLE articles ADD COLUMN search_vector tsvector;
UPDATE articles
SET search_vector = to_tsvector('english', title || ' ' || body);
CREATE INDEX articles_search_idx ON articles USING GIN(search_vector);
-- Query
SELECT id, title
FROM articles
WHERE search_vector @@ plainto_tsquery('english', 'query terms')
ORDER BY ts_rank(search_vector, plainto_tsquery('english', 'query terms')) DESC
LIMIT 20;
For semantic search, add pgvector:
-- Hybrid: BM25 + vector similarity
SELECT id, title,
ts_rank(search_vector, query) * 0.5 +
(1 - (embedding <=> $1::vector)) * 0.5 AS score
FROM articles, plainto_tsquery('english', $2) query
WHERE search_vector @@ query
ORDER BY score DESC
LIMIT 20;
How to pick
| Your situation |
Recommendation |
| Static site / docs |
Pagefind |
| App on Postgres, < 1M rows |
Postgres FTS or pgvector |
| Need facets + instant UX |
Algolia (paid) or Typesense |
| Self-host, Algolia-like features |
Meilisearch or Typesense |
| Millions of documents, complex |
Elasticsearch / OpenSearch |
Common mistakes
Building search before content exists — don't over-engineer a search solution for 50 articles. Pagefind or Ctrl+F is fine until you have real scale.
Not debouncing the search input — fire one API call per keystroke and you'll hit rate limits and slow down the UI. Debounce to ~250 ms.
Forgetting to re-index on data changes — search indices go stale. Trigger re-indexing in your deployment pipeline or via webhooks on content changes.
No fallback for zero results — an empty results page frustrates users. Show "did you mean?", recent searches, or popular pages.
What to skip
- Building your own inverted index — there are a dozen excellent open-source tools; reinventing this is a waste of engineering time.
- Elasticsearch for a blog — the operational overhead (JVM heap, index shards, snapshot management) is massive for small content sets.
- Client-side Fuse.js for large datasets — Fuse.js is fine for a small in-memory search over <1,000 items; beyond that, use a proper search tool.
FAQ
Can I add search to a Next.js app?
Yes. For static content use Pagefind at build time. For dynamic content, use the Algolia, Typesense, or Meilisearch JavaScript client in an API route or server action.
How do I handle multilingual search?
Algolia and Typesense support per-language analyzers. Postgres FTS uses language-specific dictionaries (to_tsvector('french', ...)). Pagefind supports multiple languages via the --language flag.
What about search analytics?
Algolia has first-class analytics in the dashboard. For self-hosted tools, log search queries and result click-through to your own analytics; Posthog works well.
Do I need a vector database for semantic search?
Not necessarily. If you are already on Postgres, pgvector handles vectors and SQL queries in the same database. A standalone vector DB (Pinecone, Qdrant) is worth it only at large scale or if you need features pgvector does not cover.
Where to go next