TL;DR
Meilisearch is the open-source search engine — instant search, typo tolerance, faceted filtering, easy setup, Rust-powered, great developer experience. Typesense is the open-source search engine built for speed — typo tolerance, geo search, vector search, high availability, also Rust/C++. Algolia is the hosted search-as-a-service — instant search UI widgets, analytics, A/B testing, AI search, the industry standard for commercial search. In 2026: Meilisearch for easy self-hosted search, Typesense for fast open-source search with vector support, Algolia for enterprise search-as-a-service.
Key Takeaways
- Meilisearch: meilisearch ~50K weekly downloads — open-source, Rust, instant search, easy setup
- Typesense: typesense ~20K weekly downloads — open-source, fast, vector search, geo search
- Algolia: algoliasearch ~500K weekly downloads — hosted, analytics, AI, InstantSearch widgets
- Meilisearch and Typesense are open-source (self-hosted or cloud)
- Algolia has the richest UI component library (InstantSearch)
- All three handle typo tolerance out of the box
Meilisearch
Meilisearch — open-source instant search:
Setup and indexing
import { MeiliSearch } from "meilisearch"
const client = new MeiliSearch({
host: "http://localhost:7700",
apiKey: process.env.MEILI_MASTER_KEY,
})
// Create index and add documents:
const index = client.index("packages")
await index.addDocuments([
{ id: 1, name: "react", description: "UI library", downloads: 25000000, tags: ["frontend", "ui"] },
{ id: 2, name: "vue", description: "Progressive framework", downloads: 5000000, tags: ["frontend", "ui"] },
{ id: 3, name: "express", description: "Web framework", downloads: 30000000, tags: ["backend", "http"] },
{ id: 4, name: "fastify", description: "Fast web framework", downloads: 5000000, tags: ["backend", "http"] },
])
// Configure searchable attributes and ranking:
await index.updateSettings({
searchableAttributes: ["name", "description", "tags"],
filterableAttributes: ["tags", "downloads"],
sortableAttributes: ["downloads", "name"],
rankingRules: [
"words", "typo", "proximity", "attribute", "sort", "exactness",
],
})
Search
const index = client.index("packages")
// Basic search (typo-tolerant by default):
const results = await index.search("reac")
// Finds "react" despite typo
console.log(results.hits)
// [{ id: 1, name: "react", ... }]
console.log(results.estimatedTotalHits)
console.log(results.processingTimeMs) // ~1ms
// With filters:
const filtered = await index.search("framework", {
filter: ["tags = frontend", "downloads > 1000000"],
sort: ["downloads:desc"],
limit: 10,
offset: 0,
})
// Faceted search:
const faceted = await index.search("", {
facets: ["tags"],
})
console.log(faceted.facetDistribution)
// { tags: { frontend: 2, backend: 2, ui: 2, http: 2 } }
Highlighted results
const results = await index.search("web framework", {
attributesToHighlight: ["name", "description"],
highlightPreTag: "<mark>",
highlightPostTag: "</mark>",
attributesToCrop: ["description"],
cropLength: 30,
})
results.hits.forEach((hit) => {
console.log(hit._formatted?.name)
// "express" or "<mark>web</mark> <mark>framework</mark>"
console.log(hit._formatted?.description)
// "<mark>Web</mark> <mark>framework</mark>"
})
React InstantSearch
import { InstantSearch, SearchBox, Hits, RefinementList } from "react-instantsearch"
import { instantMeiliSearch } from "@meilisearch/instant-meilisearch"
const { searchClient } = instantMeiliSearch(
"http://localhost:7700",
process.env.MEILI_SEARCH_KEY
)
function PackageSearch() {
return (
<InstantSearch searchClient={searchClient} indexName="packages">
<SearchBox placeholder="Search packages..." />
<RefinementList attribute="tags" />
<Hits hitComponent={PackageHit} />
</InstantSearch>
)
}
function PackageHit({ hit }: { hit: any }) {
return (
<div>
<h3>{hit.name}</h3>
<p>{hit.description}</p>
<span>{hit.downloads.toLocaleString()} downloads/week</span>
</div>
)
}
Typesense
Typesense — fast open-source search:
Setup and indexing
import Typesense from "typesense"
const client = new Typesense.Client({
nodes: [{ host: "localhost", port: 8108, protocol: "http" }],
apiKey: process.env.TYPESENSE_API_KEY!,
connectionTimeoutSeconds: 2,
})
// Create collection with schema:
await client.collections().create({
name: "packages",
fields: [
{ name: "name", type: "string" },
{ name: "description", type: "string" },
{ name: "downloads", type: "int64" },
{ name: "tags", type: "string[]", facet: true },
{ name: "version", type: "string" },
],
default_sorting_field: "downloads",
})
// Index documents:
await client.collections("packages").documents().import([
{ name: "react", description: "UI library", downloads: 25000000, tags: ["frontend", "ui"], version: "19.0.0" },
{ name: "vue", description: "Progressive framework", downloads: 5000000, tags: ["frontend", "ui"], version: "3.5.0" },
{ name: "express", description: "Web framework", downloads: 30000000, tags: ["backend", "http"], version: "5.0.0" },
])
Search
// Basic search:
const results = await client.collections("packages")
.documents()
.search({
q: "reac", // Typo-tolerant
query_by: "name,description",
sort_by: "downloads:desc",
per_page: 10,
page: 1,
})
console.log(results.found) // Total matches
console.log(results.search_time_ms) // ~0.5ms
results.hits?.forEach((hit) => {
console.log(hit.document.name, hit.text_match)
})
// Filtered search:
const filtered = await client.collections("packages")
.documents()
.search({
q: "framework",
query_by: "name,description",
filter_by: "tags:=[frontend] && downloads:>1000000",
sort_by: "downloads:desc",
facet_by: "tags",
})
console.log(filtered.facet_counts)
// [{ field_name: "tags", counts: [{ value: "frontend", count: 2 }] }]
Vector search (semantic)
// Create collection with vector field:
await client.collections().create({
name: "packages-semantic",
fields: [
{ name: "name", type: "string" },
{ name: "description", type: "string" },
{ name: "embedding", type: "float[]", num_dim: 384 },
],
})
// Index with embeddings:
await client.collections("packages-semantic").documents().import([
{
name: "react",
description: "A JavaScript library for building user interfaces",
embedding: [0.1, 0.2, ...], // 384-dim vector
},
])
// Vector search:
const results = await client.collections("packages-semantic")
.documents()
.search({
q: "*",
vector_query: "embedding:([0.1, 0.2, ...], k:10)",
})
// Hybrid search (keyword + vector):
const hybrid = await client.collections("packages-semantic")
.documents()
.search({
q: "UI library",
query_by: "name,description",
vector_query: "embedding:([0.1, 0.2, ...], k:10, alpha:0.5)",
})
Geo search
// Collection with geo field:
await client.collections().create({
name: "events",
fields: [
{ name: "name", type: "string" },
{ name: "location", type: "geopoint" },
{ name: "date", type: "string" },
],
})
// Search within radius:
const nearby = await client.collections("events")
.documents()
.search({
q: "javascript meetup",
query_by: "name",
filter_by: "location:(37.7749, -122.4194, 10 km)", // 10km from SF
sort_by: "location(37.7749, -122.4194):asc", // Nearest first
})
Algolia
Algolia — search-as-a-service:
Setup and indexing
import algoliasearch from "algoliasearch"
const client = algoliasearch(
process.env.ALGOLIA_APP_ID!,
process.env.ALGOLIA_ADMIN_KEY!
)
const index = client.initIndex("packages")
// Index documents:
await index.saveObjects([
{ objectID: "1", name: "react", description: "UI library", downloads: 25000000, tags: ["frontend", "ui"] },
{ objectID: "2", name: "vue", description: "Progressive framework", downloads: 5000000, tags: ["frontend", "ui"] },
{ objectID: "3", name: "express", description: "Web framework", downloads: 30000000, tags: ["backend", "http"] },
])
// Configure index settings:
await index.setSettings({
searchableAttributes: ["name", "description", "tags"],
attributesForFaceting: ["tags", "filterOnly(downloads)"],
customRanking: ["desc(downloads)"],
typoTolerance: true,
minWordSizefor1Typo: 3,
minWordSizefor2Typos: 7,
})
Search
const searchClient = algoliasearch(
process.env.ALGOLIA_APP_ID!,
process.env.ALGOLIA_SEARCH_KEY! // Search-only key
)
const index = searchClient.initIndex("packages")
// Basic search:
const { hits, nbHits, processingTimeMS } = await index.search("reac")
console.log(nbHits) // Total matches
console.log(processingTimeMS) // ~1ms
hits.forEach((hit) => {
console.log(hit.name)
console.log(hit._highlightResult?.name?.value)
// "<em>reac</em>t" — auto-highlighted
})
// Filtered search:
const filtered = await index.search("framework", {
filters: "tags:frontend AND downloads > 1000000",
facets: ["tags"],
hitsPerPage: 10,
page: 0,
})
console.log(filtered.facets)
// { tags: { frontend: 2, backend: 2 } }
React InstantSearch
import algoliasearch from "algoliasearch/lite"
import {
InstantSearch, SearchBox, Hits,
RefinementList, Pagination, Stats,
} from "react-instantsearch"
const searchClient = algoliasearch(
process.env.NEXT_PUBLIC_ALGOLIA_APP_ID!,
process.env.NEXT_PUBLIC_ALGOLIA_SEARCH_KEY!
)
function PackageSearch() {
return (
<InstantSearch searchClient={searchClient} indexName="packages">
<div className="flex gap-8">
<aside className="w-64">
<h3>Tags</h3>
<RefinementList attribute="tags" />
</aside>
<main className="flex-1">
<SearchBox placeholder="Search packages..." />
<Stats />
<Hits hitComponent={PackageHit} />
<Pagination />
</main>
</div>
</InstantSearch>
)
}
function PackageHit({ hit }: { hit: any }) {
return (
<article>
<h3>{hit.name}</h3>
<p>{hit.description}</p>
<div className="flex gap-2">
{hit.tags?.map((tag: string) => (
<span key={tag} className="badge">{tag}</span>
))}
</div>
</article>
)
}
Analytics and A/B testing
import algoliasearch from "algoliasearch"
const client = algoliasearch(appId, adminKey)
// Enable analytics:
const index = client.initIndex("packages")
await index.setSettings({
enablePersonalization: true,
enableRules: true,
})
// Query rules (merchandising):
await index.saveRule({
objectID: "promote-react",
conditions: [{
anchoring: "contains",
pattern: "ui library",
}],
consequence: {
promote: [{
objectID: "1", // react
position: 0,
}],
},
})
// A/B test:
const response = await client.addABTest({
name: "ranking-test",
variants: [
{ index: "packages", trafficPercentage: 50, description: "Current" },
{ index: "packages_v2", trafficPercentage: 50, description: "New ranking" },
],
endAt: "2026-04-01T00:00:00Z",
})
Feature Comparison
| Feature | Meilisearch | Typesense | Algolia |
|---|---|---|---|
| Open-source | ✅ | ✅ | ❌ (proprietary) |
| Self-hosted | ✅ | ✅ | ❌ (cloud only) |
| Cloud offering | Meilisearch Cloud | Typesense Cloud | ✅ (primary) |
| Typo tolerance | ✅ | ✅ | ✅ |
| Faceted search | ✅ | ✅ | ✅ |
| Geo search | ✅ | ✅ | ✅ |
| Vector search | ✅ (experimental) | ✅ | ✅ (NeuralSearch) |
| InstantSearch UI | ✅ (via adapter) | ✅ (via adapter) | ✅ (native) |
| Analytics | ❌ | ❌ | ✅ |
| A/B testing | ❌ | ❌ | ✅ |
| Query rules | ❌ | ✅ (overrides) | ✅ |
| Multi-tenancy | ✅ (tenant tokens) | ✅ (scoped keys) | ✅ (API keys) |
| Written in | Rust | C++ | C++ |
| Indexing speed | Fast | Very fast | Fast |
| Search latency | ~1-5ms | ~0.5-2ms | ~1-5ms |
| Free tier | Self-hosted | Self-hosted | 10K searches/mo |
When to Use Each
Use Meilisearch if:
- Want easy-to-setup open-source search
- Need instant search with great developer experience
- Building search for small to medium datasets
- Want self-hosted search with minimal configuration
Use Typesense if:
- Need the fastest open-source search engine
- Want vector search for semantic/AI-powered search
- Need geo search capabilities
- Building search for large datasets with strict latency requirements
Use Algolia if:
- Want fully managed search-as-a-service
- Need analytics, A/B testing, and query rules
- Building commercial search for e-commerce or SaaS
- Want the richest InstantSearch UI component library
Methodology
Download data from npm registry (weekly average, February 2026). Feature comparison based on Meilisearch v1.x, Typesense v0.25.x/v27.x, and Algolia v4.x/v5.x.
Indexing Strategy and Real-Time Updates
How each engine handles index updates directly affects the search experience for applications with frequently changing data. Meilisearch uses an optimistic indexing strategy — documents added via addDocuments() are available for search within a few hundred milliseconds for small batches. The index update is performed asynchronously and returns a task ID you can poll to confirm completion. For applications that update documents frequently (e-commerce inventory, news articles, npm package metadata), this async approach means search results can be slightly stale. Meilisearch's task queue ensures updates are eventually consistent, and for most search use cases, sub-second staleness is acceptable.
Typesense's indexing is similarly asynchronous but exposes more direct control over when changes are committed. Its importDocuments endpoint with action: "upsert" efficiently handles bulk updates, and the strict schema definition means Typesense validates documents against the collection schema at index time — you'll get an error immediately if a document doesn't match the expected types, rather than discovering the mismatch when search returns malformed results. For search applications where data quality is critical, Typesense's schema enforcement is a meaningful operational advantage.
Algolia processes index updates synchronously from the API client's perspective — saveObjects returns only after the index is updated and searchable. This makes it easier to reason about consistency: after the API call returns successfully, the documents are immediately searchable with no polling required. For high-volume update scenarios (price changes on thousands of products), Algolia's batch operations can handle thousands of documents in a single API call, and their infrastructure is designed for this use pattern at scale.
Tenant Isolation and Multi-Tenant Search
Applications that serve multiple customers from a single index — SaaS platforms, marketplaces, multi-team tools — need to ensure one tenant can't see another's data in search results. All three engines support this through tenant tokens or scoped API keys, but the implementation patterns differ.
Meilisearch's tenant tokens embed filter conditions into a short-lived JWT. When your server generates a tenant token for user X, you include a filter clause like tenantId = "X" in the token. The search SDK uses this token for all searches, and Meilisearch enforces the filter server-side — the user cannot remove it. This is secure without requiring per-tenant indexes.
Typesense achieves the same via scoped API keys that can embed filter conditions: filter_by: "tenant_id:=user_123". The scoped key is generated server-side, passed to the client, and Typesense enforces the filter on every search using that key. This is the same security model as Meilisearch's tenant tokens but without the JWT structure.
Algolia uses secured API keys — API keys generated server-side with embedded filter restrictions. A secured key with filters: "tenantId:user123" ensures all searches using that key automatically apply the filter. Algolia's advantage is that generating and revoking secured keys can be done programmatically via their API, and the key expiry can be set at generation time without maintaining a key-to-tenant mapping on your server.
For high-scale multi-tenant deployments (thousands of tenants), all three engines support per-tenant indexes as an alternative — creating a separate index per tenant provides complete isolation and allows per-tenant configuration but increases operational complexity. Per-tenant indexes are generally only appropriate when tenants have meaningfully different search configuration needs (custom ranking, different searchable attributes).
TypeScript SDK Quality and Developer Experience
The quality of the JavaScript/TypeScript SDK significantly affects day-to-day development productivity. Meilisearch's meilisearch npm package ships comprehensive TypeScript definitions that cover the full API surface. The MeiliSearch client class, index operations, search parameters, and response types are all fully typed. A particularly useful detail is that the search() method is generic — index.search<PackageDocument>(query, params) returns SearchResponse<PackageDocument> where hits is PackageDocument[], giving type-safe access to your document fields without casting.
Typesense's TypeScript SDK is similarly complete. The SearchResponse type is generic over the document type, and the collection schema definition uses TypeScript types to ensure your field definitions match your document structure at compile time. The TypesenseInstantsearchAdapter — used to connect Typesense to the InstantSearch UI library — also ships TypeScript definitions, enabling type-safe configuration of the search adapter without runtime surprises.
Performance Tuning and Search Relevance Configuration
Out-of-the-box relevance is one dimension; tuning relevance for your specific data and user behavior is another. Meilisearch exposes six ranking rules in a configurable order: words, typo, proximity, attribute, sort, and exactness. The order determines priority — if two documents match the query equally on words, the typo rule breaks the tie by favoring the document with fewer typos in the matching terms. Teams with domain-specific ranking requirements (for example, a package search where download count should rank higher than attribute match position) can reorder these rules and add custom sort criteria. This configurability covers the majority of product search use cases without requiring machine learning or external training data.
Typesense's relevance model is similar to Meilisearch's but with more explicit control over the search algorithm. The num_typos parameter per query field lets you configure how aggressively typos are tolerated for each searchable attribute independently — a package name field might allow one typo while a short description field allows two. Typesense's drop_tokens_threshold and typo_tokens_threshold settings handle edge cases where very short queries would otherwise match too broadly or too narrowly. For high-stakes search implementations where small relevance improvements meaningfully affect user behavior, Typesense's granular parameter surface is the most useful of the three self-hosted options.
Algolia's relevance is the deepest and most commercially mature. Beyond the standard ranking formula, Algolia's Query Rules (formerly "query rules and merchandising") let you define if-then logic: if the query contains "react", boost records tagged framework to position 0. If the query is empty, return the most downloaded packages. These rules are managed through Algolia's dashboard or the API and require no re-indexing. Algolia's AI Ranking feature uses click and conversion data to automatically adjust ranking for popular queries, surfacing items users historically click on. This closed-loop optimization is unavailable in self-hosted search engines without building a custom ML pipeline.
Compare search tooling and backend libraries on PkgPulse →
See also: AVA vs Jest and Payload CMS vs Strapi vs Directus, amqplib vs KafkaJS vs Redis Streams.