Meilisearch, fast search with typo tolerance
Meilisearch is a full text search engine written in Rust, with its own HTTP API, typo tolerance switched on by default and results sorted by relevance. Version 1.53.1 was released on 13 August 2026, the meilisearch/meilisearch repository has around 59 thousand stars, and the licence is mixed: MIT for the core and the Business Source License for the Enterprise modules.
What Meilisearch actually does
Meilisearch takes JSON documents, builds an index from them and exposes them through a REST API. It is not a database where you keep application state. It is a secondary index that you have to feed from a real source of truth and keep in sync. If you delete its data directory you lose no business data, only the time needed to reindex.
Writes are asynchronous. The call that adds documents returns a task with an identifier, and the actual indexing happens in a background queue. That has two consequences. First, a document is not searchable immediately after the write and your tests have to wait for the task to finish. Second, for large data sets it pays to send one big batch rather than a thousand small ones, because every settings change or document batch creates a separate task.
Relevance is driven by a list of ranking rules applied in order, like a bucket sort. The default list in 1.53.1 looks like this: words, typo, proximity, attributeRank, sort, wordPosition, exactness. That is a meaningful change from older write-ups, which list six rules with a single attribute. The attribute rule is still accepted when you save settings, but the default set now splits it into two: the rank of the attribute and the position of the word inside it. If you are copying a configuration from a two-year-old tutorial, check what GET /indexes/:uid/settings actually returns.
Typo tolerance is on by default and is driven by word length thresholds. By default one typo is allowed from five characters, and two typos from nine. You can disable it globally, for chosen words, for chosen attributes, or separately for strings containing digits.
What Meilisearch does not do: it performs no joins between indexes in the SQL sense, it does not replace a transactional database, and on its own it provides no high availability in the open version. Replication and sharding are a separate licensing story, described below.
Version, licence and project health
The project is alive. Version 1.53.1 shipped on 13 August 2026, the last change on the main branch is from 14 August, the repository is not archived, it has 2672 forks and 311 open issues. The getmeili/meilisearch image on Docker Hub has passed 48 million pulls.
The licence is the place where five sources give three different answers, so settle the difference before you write anything into your dependency register. The LICENSE file in the repository ends with the line SPDX-License-Identifier: MIT AND BUSL-1.1. The Cargo.toml in the root declares plainly license = "MIT" under workspace.package. The badge in the README also mentions MIT only. The GitHub API returns NOASSERTION for this repository, which is an admission that it cannot classify it. The registry clients, meaning meilisearch 0.60.0 on npm and meilisearch 0.43.0 on PyPI, are plain MIT, which the LICENSE file inside the published tarball confirms.
The factual position is this: the core is MIT, and files marked as Enterprise Edition fall under BSL 1.1 in a version adapted by Meili SAS. The additional use grant in that licence is unambiguous: non-production use only, meaning testing, development and evaluation. Production use requires a commercial agreement. The change licence, four years after a given version is published, is MIT.
Which files this covers can be checked precisely, because each of them carries the header This file is part of Meilisearch Enterprise Edition (EE). In 1.53.1 there are eight of them and they cover three areas: sharding in crates/milli/src/sharding/enterprise_edition.rs, the multi-node network together with federated search across it, and snapshots to S3-compatible storage. In other words, everything that is one process on one machine stays MIT. Horizontal scaling and replication are the paid area, even though the code sits publicly in the same repository.
Installation and the first index
The simplest start is one container and two curl calls.
# run with an admin key and a persistent data directory
docker run -d --name meili -p 7700:7700 \
-e MEILI_MASTER_KEY=change_this_key \
-v $PWD/meili_data:/meili_data \
getmeili/meilisearch:v1.53.1
# add documents, this returns a taskUid, not a ready index
curl -X POST 'http://localhost:7700/indexes/products/documents?primaryKey=id' \
-H 'Authorization: Bearer change_this_key' \
-H 'Content-Type: application/json' \
--data '[{"id":1,"name":"Down jacket","brand":"Nordkapp","price":499,"inStock":true}]'
# check whether the task has finished
curl 'http://localhost:7700/tasks?limit=1' \
-H 'Authorization: Bearer change_this_key'
# search with a typo in the query
curl -X POST 'http://localhost:7700/indexes/products/search' \
-H 'Authorization: Bearer change_this_key' \
-H 'Content-Type: application/json' \
--data '{"q":"donw jaket","limit":10,"showRankingScore":true}'The master key given in MEILI_MASTER_KEY is for administration and should never reach the browser. For client-side search you generate a separate key with the search action, and for multi-tenant setups an additional tenant token with a filter baked in.
The JavaScript client at 0.60.0 has no dependencies and requires Node ^20.19.0 || >=22.12.0. It also has a migration trap: the exported class is called Meilisearch, not MeiliSearch with a capital S as in older examples. There is no alias, so the old import simply yields undefined.
import { Meilisearch } from 'meilisearch'
const client = new Meilisearch({
host: 'http://localhost:7700',
apiKey: process.env.MEILI_MASTER_KEY
})
const index = client.index('products')
// wait for the task, otherwise the test searches an empty index
await index.addDocuments(documents, { primaryKey: 'id' }).waitTask()
const results = await index.search('jaket', {
limit: 20,
filter: 'inStock = true AND price < 600',
sort: ['price:asc'],
attributesToHighlight: ['name'],
showRankingScore: true
})Index settings that decide relevance
The default configuration gives decent results on a small data set and falls apart on a large one. Three settings make the biggest difference: the order of searchableAttributes, the filterableAttributes list, and the pagination limits.
{
"searchableAttributes": ["name", "brand", "description"],
"filterableAttributes": ["brand", "price", "inStock", "categories"],
"sortableAttributes": ["price", "createdAt"],
"rankingRules": [
"words",
"typo",
"proximity",
"attributeRank",
"sort",
"wordPosition",
"exactness"
],
"typoTolerance": {
"enabled": true,
"minWordSizeForTypos": { "oneTypo": 5, "twoTypos": 9 },
"disableOnWords": ["Nordkapp"],
"disableOnAttributes": ["sku"],
"disableOnNumbers": true
},
"pagination": { "maxTotalHits": 1000 },
"faceting": { "maxValuesPerFacet": 100 },
"searchCutoffMs": 150
}The order of searchableAttributes is not cosmetic. The attributeRank rule sorts results by which attribute the match landed in, so the attribute listed first carries the most weight. Putting a long description ahead of the product name is the single most common cause of complaints about result quality.
maxTotalHits defaults to 1000 and is a hard ceiling on the number of returned hits, regardless of pagination. When you build an export or a sitemap you have to raise it deliberately, otherwise the data simply stops halfway. maxValuesPerFacet defaults to 100, which on a facet with a thousand brands gives an incomplete filter list.
searchCutoffMs is unset by default, and the engine then uses a threshold of 1500 milliseconds. Once the threshold is crossed the search returns whatever it managed to compute and reports no error. If you are building search-as-you-type, set a lower value, say 100 to 200 milliseconds, because an approximate answer delivered quickly beats an exact one delivered after a second. Treat claims about response times below fifty milliseconds as a design goal rather than a guarantee. Measure them on your data, your hardware and your query distribution.
disableOnNumbers set to true deserves separate attention in shops and parts catalogues. Without it a query for part number 14520 will match 14521, because that is one typo away.
Vector and hybrid search
Semantic search is built in and needs no separate vector database. You configure it through the embedders setting, where each entry has a name and a source field with one of these values: openAi, huggingFace, ollama, userProvided, rest or composite.
{
"embedders": {
"descriptions": {
"source": "openAi",
"model": "text-embedding-3-small",
"apiKey": "sk-...",
"dimensions": 1536,
"documentTemplate": "Product {{doc.name}} by {{doc.brand}}. {{doc.description}}",
"binaryQuantized": false
},
"own": {
"source": "userProvided",
"dimensions": 384
}
}
}With source set to openAi, huggingFace, ollama or rest, Meilisearch computes embeddings itself during indexing, which means it calls an external API and you pay for every document it processes. With userProvided you supply the vectors yourself in the reserved _vectors field and the engine never computes them. That is the only variant where you keep full control over the cost and over which model produces the embeddings.
Hybrid search is triggered by a hybrid object in the query. The embedder field is required, and semanticRatio accepts values from zero to one, where zero means pure keyword matching and one means pure semantics. The default is 0.5.
{
"q": "warm jacket for winter in the mountains",
"hybrid": { "embedder": "descriptions", "semanticRatio": 0.7 },
"limit": 20,
"filter": "inStock = true",
"showRankingScore": true,
"retrieveVectors": false
}This shifts where Meilisearch sits relative to vector databases. If you need to search products or articles while matching on both words and meaning, one process handles it and you never merge results from two systems. If instead you are building a pipeline with millions of vectors, quantisation, many collections and tuning of approximate index parameters, a dedicated store such as Qdrant or Pinecone gives you far more control. Meilisearch aims at product search enriched with semantics, not at being the vector store behind a RAG system.
When Postgres is enough and when it is not
This is the important question in this area, and the answer is not "always a separate engine". PostgreSQL has built-in full text search and at reasonable requirements it genuinely suffices.
-- a generated column with weights, a GIN index and sorting by relevance
ALTER TABLE products ADD COLUMN search_vector tsvector
GENERATED ALWAYS AS (
setweight(to_tsvector('simple', coalesce(name, '')), 'A') ||
setweight(to_tsvector('simple', coalesce(description, '')), 'B')
) STORED;
CREATE INDEX products_search_idx ON products USING GIN (search_vector);
SELECT id, name, ts_rank(search_vector, query) AS rank
FROM products, websearch_to_tsquery('simple', 'down jacket') AS query
WHERE search_vector @@ query AND in_stock
ORDER BY rank DESC
LIMIT 20;That code works, it is transactional, it needs no synchronisation and it adds no service to operate. Stay with it if queries are whole words, users spell them correctly, and results can be sorted by a simple relevance measure.
Three things push you towards a separate engine. The first is typos. Postgres tolerates none by default, because tsquery compares lexemes exactly. The pg_trgm extension lets you compute trigram similarity, but that is a separate mechanism you have to weave into tsvector by hand, and the resulting query with two indexes and an artificially combined score tends to be hard to maintain. The second is search-as-you-type. Prefixes in Postgres are done through to_tsquery('jack:*'), which stops behaving sensibly once you combine it with typos and multiple words. Meilisearch builds prefix structures during indexing, controlled by the prefixSearch setting whose default is indexingTime. The third is latency. A query with ts_rank has to score every matching row before sorting, so for broad queries the cost grows with the number of hits rather than the number of returned results.
There is also a middle road. If you already run on Supabase or another hosted Postgres, adding pg_trgm next to pgvector is a decent compromise without a new service. The cost of a separate engine covers more than the server: the sync pipeline, handling data drift, and reindexing after every schema change.
Meilisearch against the alternatives
| Solution | Deployment model | Typos | Vectors | When to choose |
|---|---|---|---|---|
| Meilisearch | one process, MIT plus BSL modules | on by default, configurable thresholds | built in, hybrid mode | in-product search, search-as-you-type |
| PostgreSQL with tsvector | extension in the existing database | only via pg_trgm, by hand | via pgvector | single source of truth, simple queries |
| Elasticsearch | JVM cluster, AGPLv3 or ELv2 or SSPL | fuzzy query, by hand | dense field and kNN | logs, analytics, complex aggregations |
| Qdrant | separate service, Apache 2.0 | no full text with tolerance | the core of the product | large vector scale, RAG systems |
| Algolia | closed service only | on by default | built in | no team to run infrastructure |
Against Elasticsearch the difference comes down to purpose. Elasticsearch is a distributed system for several jobs at once, with aggregations and log analytics, and it demands JVM heap tuning and shard planning. Meilisearch is a single binary that gives you good catalogue search within an hour, but it will not become your event warehouse. Against Redis with its search module the difference lies in durability and the data model, since Redis remains primarily an in-memory store.
Cloud, the Enterprise edition and costs
Running the core yourself is free and carries no feature restrictions, as long as you do not touch the Enterprise modules. Meilisearch Cloud is the managed service, and here it pays to read the pricing page carefully, because it gives two different numbers in two places.
The Cloud plan headline says "starting at 20 dollars a month". The cost estimator on the same page, in its initial position, shows something else: resource-based billing from 23 dollars a month for an XS instance with half a virtual core and one gigabyte of memory, made up of 18 dollars for the instance and 5 dollars for 32 gibibytes of disk, plus usage-based billing from 30 dollars a month for a base plan covering 100 thousand documents and 50 thousand searches. I cannot reconcile the 20 with the 23, so I quote both and suggest running the estimator on your own numbers before you plan a budget. The trial lasts 14 days and requires no card.
The Enterprise plan carries no published price. The features listed against it are an availability guarantee of up to 99.999 percent, a dedicated support channel, single sign-on through SAML, SOC 2 Type II compliance, advanced click and conversion analytics, result personalisation, dynamic search rules, and sharding together with replication. That last item is the same functionality you will find in the repository as Enterprise Edition files under BSL. The practical conclusion: if high availability of search is a requirement, the free version will not give it to you, and working around it by running the code from the Enterprise directories yourself would be production use forbidden by the licence.
That is a real lock-in risk and it is better named openly. You can run the core on your own hardware forever, but the growth path beyond a single machine leads either to a commercial agreement or to the vendor's cloud.
Common mistakes
Treating writes as synchronous. A test that adds a document and searches immediately will flake. Always wait for the task to complete.
Exposing the master key in client code. The key from MEILI_MASTER_KEY can delete indexes. Only a key with the search action belongs in the browser, and for multiple clients a tenant token with a filter baked in.
Leaving maxTotalHits at its default for exports and sitemaps. A thousand hits is a ceiling, not a suggestion.
Copying the rankingRules list from old material. The default set in 1.53.1 has seven entries with attributeRank and wordPosition, not six with attribute.
Enabling typo tolerance on identifier fields. Part numbers and SKU codes need disableOnAttributes or disableOnNumbers, otherwise search will suggest the neighbouring number.
Recording plain MIT in the dependency register. The correct description is MIT AND BUSL-1.1, per the licence file, even though Cargo.toml and the README badge say otherwise.
Assuming the hybrid mode is free in every sense. The code is MIT, but with any source other than userProvided every indexing run generates calls to a paid embeddings API.
FAQ
Is Meilisearch MIT licensed?
The core is, but the whole project is not. The licence file in the repository declares MIT AND BUSL-1.1. Eight files marked as Enterprise Edition, covering sharding, the multi-node network and S3 snapshots, fall under Business Source License 1.1, which permits non-production use only. Four years after a given version is published those files convert to MIT.
Will Meilisearch replace Postgres for me?
No, because it is a secondary index with no transactions and no role as the source of truth. It does replace search inside Postgres when you need typo tolerance, prefix matching while the user types, and predictably low latency that does not grow with the number of hits.
Is there vector and hybrid search?
Yes, both. Embeddings are configured through the embedders setting, and a hybrid query through the hybrid object with the embedder and semanticRatio fields. A value of 0 gives pure keyword matching, 1 gives pure semantics, and the default is 0.5.
Which features are paid only?
High availability through replication and sharding, snapshots to S3 storage, and on the cloud side additionally SAML sign-on, click analytics, personalisation and dynamic search rules. Everything else, including hybrid search and multi-tenancy, is in the open version.
What does Meilisearch Cloud cost?
The pricing page headline says 20 dollars a month, while its estimator in the initial position shows 23 dollars for the smallest instance under resource-based billing and 30 dollars for the base plan under usage-based billing. The discrepancy is in the pricing page itself, so run your own case through the estimator before deciding.
Can I run Meilisearch across several nodes for free?
Not in a way that complies with the licence. The multi-node network and sharding code is public but covered by BSL, which forbids production use. For the free version the realistic plan is a single instance with snapshots and fast index rebuilds from the source of truth.