Building Search at Scale: Elasticsearch vs Typesense vs Meilisearch
When I built BirJob, I started with PostgreSQL's ILIKE queries for job search. It worked fine for the first 10,000 jobs. At 50,000 jobs, search took 400ms. At 100,000 jobs, typo tolerance was non-existent — searching "proqramist" returned nothing when the actual title was "proqramçı." Users were frustrated, and I was losing them.
Switching to a dedicated search engine dropped query times to 8ms, added typo tolerance, faceted filtering, and synonym support. But choosing which search engine was the hard part. Elasticsearch is the incumbent with 15 years of production battle scars. Typesense is the developer-friendly upstart with obsessive focus on simplicity. Meilisearch is the typo-tolerant darling that "just works" out of the box.
I've deployed all three in production workloads ranging from 10,000 documents to 50 million. This guide is my honest comparison — the good, the bad, and the "I wish someone had told me this before I chose."
When You Need a Search Engine (and When You Don't)
Not every application needs a dedicated search engine. PostgreSQL's full-text search (GIN indexes + ts_vector) handles many use cases well. Here's my decision framework:
| Requirement | PostgreSQL FTS | Dedicated Search Engine |
|---|---|---|
| < 100K documents | Usually sufficient | Optional |
| Typo tolerance | Not built-in | Native |
| Faceted filtering | Manual (slow) | Native (fast) |
| Relevance tuning | Basic (ts_rank) | Advanced (BM25, custom) |
| Instant search (as-you-type) | Slow for large datasets | Designed for this |
| Synonym support | Manual dictionary | Native |
| Multi-language | Limited stemmers | Better support |
According to Algolia's research, 43% of users go straight to the search bar when landing on a website, and 39% leave a site after a bad search experience. Search isn't just a feature — it's often the primary navigation mechanism.
The Three Contenders: Architecture Overview
Elasticsearch: The Swiss Army Knife
Elasticsearch is built on Apache Lucene and has been the dominant search engine since 2010. It's not just a search engine — it's a distributed analytics engine used for log analysis, APM, security analytics, and full-text search.
// Index a document
PUT /jobs/_doc/1
{
"title": "Senior Backend Developer",
"company": "TechCorp",
"location": "Baku",
"salary_min": 3000,
"salary_max": 5000,
"skills": ["Python", "PostgreSQL", "Docker"],
"posted_at": "2026-03-20T00:00:00Z"
}
// Search with typo tolerance, filtering, and facets
POST /jobs/_search
{
"query": {
"bool": {
"must": [
{
"multi_match": {
"query": "bacend develper",
"fields": ["title^3", "skills^2", "company"],
"fuzziness": "AUTO"
}
}
],
"filter": [
{ "term": { "location": "Baku" } },
{ "range": { "salary_min": { "gte": 2000 } } }
]
}
},
"aggs": {
"companies": { "terms": { "field": "company.keyword", "size": 10 } },
"salary_ranges": {
"range": {
"field": "salary_min",
"ranges": [
{ "to": 1000 },
{ "from": 1000, "to": 3000 },
{ "from": 3000 }
]
}
}
},
"highlight": {
"fields": { "title": {}, "skills": {} }
}
}
Typesense: The Developer-Friendly Alternative
Typesense is a C++-based search engine focused on simplicity, speed, and developer experience. It was built specifically as an open-source alternative to Algolia — with a similar API design but self-hosted.
// Create a collection (typed schema)
const schema = {
name: 'jobs',
fields: [
{ name: 'title', type: 'string' },
{ name: 'company', type: 'string', facet: true },
{ name: 'location', type: 'string', facet: true },
{ name: 'salary_min', type: 'int32' },
{ name: 'skills', type: 'string[]', facet: true },
{ name: 'posted_at', type: 'int64' }
],
default_sorting_field: 'posted_at'
};
await client.collections().create(schema);
// Index a document
await client.collections('jobs').documents().create({
title: 'Senior Backend Developer',
company: 'TechCorp',
location: 'Baku',
salary_min: 3000,
skills: ['Python', 'PostgreSQL', 'Docker'],
posted_at: 1710892800
});
// Search (typo tolerance is on by default)
const results = await client.collections('jobs').documents().search({
q: 'bacend develper',
query_by: 'title,skills,company',
query_by_weights: '3,2,1',
filter_by: 'location:Baku && salary_min:>=2000',
facet_by: 'company,location,skills',
sort_by: 'posted_at:desc'
});
Meilisearch: The Typo-Tolerant Default
Meilisearch is a Rust-based search engine that prioritizes instant search and typo tolerance. Its claim to fame: incredible search quality out of the box with zero configuration.
// Create index and add documents (no schema required)
const index = client.index('jobs');
await index.addDocuments([{
id: 1,
title: 'Senior Backend Developer',
company: 'TechCorp',
location: 'Baku',
salary_min: 3000,
skills: ['Python', 'PostgreSQL', 'Docker'],
posted_at: '2026-03-20'
}]);
// Configure searchable and filterable attributes
await index.updateSettings({
searchableAttributes: ['title', 'skills', 'company'],
filterableAttributes: ['location', 'salary_min', 'skills'],
sortableAttributes: ['posted_at', 'salary_min'],
typoTolerance: { enabled: true },
synonyms: {
'developer': ['engineer', 'programmer', 'coder'],
'frontend': ['front-end', 'front end', 'ui']
}
});
// Search
const results = await index.search('bacend develper', {
filter: 'location = Baku AND salary_min >= 2000',
facets: ['company', 'location', 'skills'],
sort: ['posted_at:desc']
});
The Comprehensive Comparison
| Feature | Elasticsearch | Typesense | Meilisearch |
|---|---|---|---|
| Language | Java (Lucene) | C++ | Rust |
| Latency (p50) | ~5-20ms | ~2-5ms | ~5-15ms |
| Typo tolerance | Fuzziness param | Default on | Default on (best) |
| Faceted search | Aggregations (powerful) | Native (simple) | Native (simple) |
| Max docs (practical) | Billions | ~100M per node | ~10M per node |
| Clustering | Native (complex) | Raft-based (built-in) | Experimental |
| RAM usage | High (JVM heap) | Low-Medium | High (in-memory index) |
| Setup complexity | High | Low | Very Low |
| Analytics/logs | Excellent (Kibana) | Not designed for | Not designed for |
| License | SSPL / ELv2 | GPL-3.0 | MIT |
| Cloud offering | Elastic Cloud ($$$) | Typesense Cloud ($) | Meilisearch Cloud ($) |
My Benchmarks: Real Numbers on Real Data
I indexed the same dataset (1 million job listings, ~2KB each) on a single-node setup (4 vCPU, 16GB RAM) and measured the following:
| Metric | Elasticsearch 8.x | Typesense 0.25 | Meilisearch 1.x |
|---|---|---|---|
| Indexing time (1M docs) | ~4 minutes | ~3 minutes | ~12 minutes |
| Index size on disk | ~3.2 GB | ~1.8 GB | ~4.1 GB |
| RAM usage (steady state) | ~6 GB | ~2.5 GB | ~8 GB |
| Search latency (p50) | 8ms | 3ms | 6ms |
| Search latency (p99) | 45ms | 12ms | 28ms |
| Typo handling ("bacend") | Good (with fuzziness) | Great | Excellent |
When to Use Each One
Choose Elasticsearch When...
You need more than just search. If you're building a log aggregation platform, an APM system, a SIEM, or any analytics workload alongside search, Elasticsearch (with Kibana) is unmatched. The ELK stack is the industry standard for observability.
You have billions of documents. Elasticsearch scales horizontally across dozens of nodes. Neither Typesense nor Meilisearch can match its scalability for truly massive datasets.
You need complex aggregations. Elasticsearch's aggregation framework supports nested aggregations, pipeline aggregations, and statistical calculations that Typesense and Meilisearch don't attempt.
Choose Typesense When...
You want Algolia-quality search without Algolia pricing. Typesense was explicitly designed as an open-source Algolia alternative. The API is similar, the search quality is comparable, and you can self-host. A Typesense benchmark showed comparable or better search relevance compared to Algolia on standard datasets.
You need low-latency, instant search. Typesense consistently delivers the lowest p50 and p99 latencies in my benchmarks. For as-you-type search experiences, this matters.
You want built-in high availability without ops complexity. Typesense uses Raft consensus for clustering. Setting up a 3-node cluster is a 10-minute operation. Elasticsearch clustering requires careful shard planning, master node election, and ongoing maintenance.
Choose Meilisearch When...
Typo tolerance is your #1 priority. Meilisearch's typo tolerance is the best in the business. It handles character transpositions, missing characters, extra characters, and phonetic similarities better than any other engine I've tested.
You want zero-config search quality. Meilisearch's default ranking rules produce excellent results without tuning. Index your data, point the frontend at it, and you get good search. Elasticsearch requires careful mapping, analyzer configuration, and relevance tuning to match.
You're building a content search UI (e-commerce, documentation, blog). Meilisearch's instant search SDK (for React, Vue, vanilla JS) makes building a polished search UI trivially easy.
My Opinionated Take
For most application search use cases, Typesense is the best choice in 2026. It combines the developer experience of Meilisearch with better scalability, Raft-based HA, and a lower memory footprint. Unless you need Elasticsearch's analytics capabilities or Meilisearch's specific typo tolerance algorithm, Typesense is the sweet spot.
Elasticsearch is overengineered for application search. I've seen too many teams deploy a 3-node Elasticsearch cluster with custom analyzers, synonym dictionaries, and dedicated DevOps time — all to power a search bar with 50,000 documents. That's using a tank to deliver pizza.
Meilisearch's RAM usage is its Achilles' heel. Meilisearch loads the entire index into memory. With 1M documents taking ~8 GB of RAM, scaling to 10M documents requires a beefy (and expensive) server. Typesense's hybrid disk/memory approach handles larger datasets more efficiently.
Don't use any of these for log search. If you need log aggregation, use Elasticsearch or (better yet) Grafana Loki. Typesense and Meilisearch are designed for application search, not log analysis.
Implementation: Keeping Search in Sync with Your Database
The hardest part of running a search engine isn't the search engine itself — it's keeping it in sync with your primary database. Here are three approaches:
Approach 1: Application-Level Sync (Simplest)
// After every database write, update the search index
async function createJob(jobData) {
// Write to PostgreSQL
const job = await db.query('INSERT INTO jobs (...) VALUES (...) RETURNING *', [...]);
// Index in search engine (fire-and-forget, with retry)
searchClient.collections('jobs').documents().create(job).catch(err => {
logger.error('Search indexing failed, will retry', { jobId: job.id, err });
retryQueue.add('index-job', { job });
});
return job;
}
Approach 2: Change Data Capture (CDC)
// Using Debezium to stream PostgreSQL changes to search
// docker-compose.yml (simplified)
services:
debezium:
image: debezium/connect
environment:
- BOOTSTRAP_SERVERS=kafka:9092
# Captures INSERT/UPDATE/DELETE from PostgreSQL WAL
# Publishes to Kafka topic: dbserver.public.jobs
search-sync-worker:
build: ./workers/search-sync
# Consumes Kafka events and updates Typesense/Meilisearch/ES
Approach 3: Periodic Re-indexing (Most Robust)
// Cron job that re-indexes all data periodically
// Simplest, most reliable, but introduces latency
async function fullReindex() {
const batchSize = 1000;
let offset = 0;
// Create a new collection (zero-downtime swap)
const tempCollection = `jobs_${Date.now()}`;
await searchClient.collections().create({ name: tempCollection, fields: [...] });
while (true) {
const jobs = await db.query('SELECT * FROM jobs ORDER BY id LIMIT $1 OFFSET $2', [batchSize, offset]);
if (jobs.rows.length === 0) break;
await searchClient.collections(tempCollection).documents().import(jobs.rows);
offset += batchSize;
}
// Atomic swap: alias the new collection as "jobs"
await searchClient.aliases().upsert('jobs', { collection_name: tempCollection });
}
Action Plan
Week 1: Evaluate
- Index your actual data in all three engines (Docker makes this trivial)
- Measure: indexing speed, search latency, relevance quality, RAM usage
- Test typo tolerance with your specific domain vocabulary
- Evaluate the SDKs for your language/framework
Week 2: Implement
- Choose your engine and deploy to staging
- Build the sync pipeline (start with approach 1 or 3)
- Implement the search API endpoint with filtering and facets
- Build the frontend search UI (InstantSearch.js or custom)
Week 3: Optimize
- Tune relevance settings based on user behavior
- Add synonyms for your domain
- Implement search analytics (what are users searching for?)
- Set up monitoring: index size, query latency, error rates
Sources and Further Reading
- Elasticsearch Official Documentation
- Typesense Documentation
- Meilisearch Documentation
- Algolia — What Makes a Great Search Experience
- Typesense — Comparison with Alternatives
- Meilisearch — Comparison with Elasticsearch
- Apache Lucene — The Engine Behind Elasticsearch
I'm Ismat, and I build BirJob — Azerbaijan's job aggregator scraping 80+ sources daily.
