Meilisearch
"Meilisearch: self-hosted search engine, typo tolerance, faceting, filtering, sorting, REST API, JavaScript SDK"
Meilisearch is an open-source, self-hosted search engine designed for instant, relevant results with minimal configuration. Its guiding principles are: ## Key Points - **Instant search out of the box** — sub-50ms responses with typo tolerance, prefix matching, and ranking baked in. - **Simple REST API** — every operation is an HTTP call. No query DSL to learn. - **Convention over configuration** — sensible defaults for ranking, typo tolerance, and tokenization. Override only when needed. - **Developer experience first** — a single binary to deploy, a dashboard UI included, and official SDKs for every major language. - **Document-oriented** — data is stored as JSON documents with a primary key. Think of each index as a collection. - **Use `waitForTask` in scripts, not in request handlers.** Indexing is asynchronous. In web servers, return the task UID and let clients poll or use webhooks. - **Set `filterableAttributes` before indexing.** Adding filterable attributes later triggers a full re-index. - **Use tenant tokens for multi-tenant apps.** They embed filters server-side so the frontend cannot bypass tenant isolation. - **Keep documents under 1 MB each.** Large documents slow indexing. Store blobs externally and index only searchable/filterable fields. - **Batch document additions.** Send thousands of documents per call instead of one at a time. - **Set `displayedAttributes`** to avoid returning internal fields (like tenant IDs) in search results. - **Pin the Meilisearch version** in production. Major versions can include breaking changes to ranking or API behavior.
skilldb get search-services-skills/MeilisearchFull skill: 291 linesMeilisearch
Core Philosophy
Meilisearch is an open-source, self-hosted search engine designed for instant, relevant results with minimal configuration. Its guiding principles are:
- Instant search out of the box — sub-50ms responses with typo tolerance, prefix matching, and ranking baked in.
- Simple REST API — every operation is an HTTP call. No query DSL to learn.
- Convention over configuration — sensible defaults for ranking, typo tolerance, and tokenization. Override only when needed.
- Developer experience first — a single binary to deploy, a dashboard UI included, and official SDKs for every major language.
- Document-oriented — data is stored as JSON documents with a primary key. Think of each index as a collection.
Meilisearch excels at end-user-facing search (e-commerce, docs, content sites) where speed and tolerance for messy input matter more than complex analytics or aggregation.
Setup
Run Meilisearch and connect from TypeScript:
// npm install meilisearch
import { MeiliSearch } from "meilisearch";
const client = new MeiliSearch({
host: "http://127.0.0.1:7700",
apiKey: "YOUR_MASTER_KEY",
});
interface Movie {
id: number;
title: string;
overview: string;
genres: string[];
releaseDate: string;
rating: number;
poster: string;
}
// Create an index and add documents
async function setupIndex() {
// Create or get the index with a primary key
const index = client.index<Movie>("movies");
// Add documents — Meilisearch infers the schema
const task = await index.addDocuments(movies, { primaryKey: "id" });
// Tasks are asynchronous; wait for completion
await client.waitForTask(task.taskUid);
console.log("Documents indexed");
}
Configure index settings for filtering, sorting, and faceting:
async function configureIndex() {
const index = client.index<Movie>("movies");
await index.updateSettings({
// Fields that can appear in filter/facet expressions
filterableAttributes: ["genres", "rating", "releaseDate"],
// Fields that can be used for sorting
sortableAttributes: ["rating", "releaseDate"],
// Control which fields are searchable and their priority
searchableAttributes: ["title", "overview"],
// Fields returned in search results
displayedAttributes: ["id", "title", "overview", "poster", "rating"],
// Ranking rules in order of priority
rankingRules: [
"words",
"typo",
"proximity",
"attribute",
"sort",
"exactness",
"rating:desc", // custom ranking rule
],
// Define synonyms
synonyms: {
film: ["movie", "picture"],
sci-fi: ["science fiction"],
},
});
}
Key Techniques
Basic Search with Filters
async function searchMovies(query: string, genre?: string) {
const index = client.index<Movie>("movies");
const results = await index.search(query, {
limit: 20,
offset: 0,
filter: genre ? `genres = "${genre}"` : undefined,
sort: ["rating:desc"],
attributesToHighlight: ["title", "overview"],
highlightPreTag: "<mark>",
highlightPostTag: "</mark>",
});
console.log(`Found ${results.estimatedTotalHits} results in ${results.processingTimeMs}ms`);
return results.hits;
}
Faceted Search
async function facetedSearch(query: string, filters?: string) {
const index = client.index<Movie>("movies");
const results = await index.search(query, {
filter: filters,
facets: ["genres", "rating"],
limit: 20,
});
// facetDistribution shows count per facet value
// e.g. { genres: { Action: 42, Comedy: 31 }, rating: { ... } }
const { facetDistribution, hits } = results;
return { facetDistribution, hits };
}
// Complex filter expressions
async function complexFilter() {
const index = client.index<Movie>("movies");
return index.search("", {
filter: [
"genres = Action OR genres = Thriller",
"rating >= 7.5",
"releaseDate > 2020-01-01",
],
// Array items are ANDed together; use OR within a string
sort: ["releaseDate:desc"],
});
}
Multi-Index Search
async function multiSearch(query: string) {
const results = await client.multiSearch({
queries: [
{ indexUid: "movies", q: query, limit: 5 },
{ indexUid: "actors", q: query, limit: 5 },
{ indexUid: "directors", q: query, limit: 3 },
],
});
return {
movies: results.results[0].hits,
actors: results.results[1].hits,
directors: results.results[2].hits,
};
}
Document Management
async function manageDocuments() {
const index = client.index<Movie>("movies");
// Add or replace documents (upsert by primary key)
const addTask = await index.addDocuments(newMovies);
await client.waitForTask(addTask.taskUid);
// Partial update — only specified fields are changed
const updateTask = await index.updateDocuments([
{ id: 42, rating: 8.1 },
{ id: 99, rating: 7.3 },
]);
await client.waitForTask(updateTask.taskUid);
// Delete specific documents
const deleteTask = await index.deleteDocuments([42, 99]);
await client.waitForTask(deleteTask.taskUid);
// Delete documents matching a filter
const filterDeleteTask = await index.deleteDocuments({
filter: "rating < 3.0",
});
await client.waitForTask(filterDeleteTask.taskUid);
}
API Key Management
async function setupApiKeys() {
// Create a search-only key for the frontend
const searchKey = await client.createKey({
description: "Frontend search key",
actions: ["search"],
indexes: ["movies", "actors"],
expiresAt: new Date("2027-01-01"),
});
// Create an admin key for indexing
const adminKey = await client.createKey({
description: "Backend indexing key",
actions: ["documents.add", "documents.delete", "settings.update"],
indexes: ["*"],
expiresAt: null, // never expires
});
// Generate a tenant token for multi-tenant search
const tenantToken = client.generateTenantToken(
searchKey.uid,
{
movies: { filter: `tenantId = ${tenantId}` },
},
{ apiKey: searchKey.key, expiresAt: new Date(Date.now() + 3600_000) }
);
return { searchKey: searchKey.key, adminKey: adminKey.key, tenantToken };
}
Express Integration
import express from "express";
const app = express();
app.use(express.json());
// Proxy search for cases where direct frontend access is not desired
app.get("/api/search", async (req, res) => {
const { q = "", genre, page = "0" } = req.query as Record<string, string>;
const index = client.index<Movie>("movies");
const results = await index.search(q, {
filter: genre ? `genres = "${genre}"` : undefined,
facets: ["genres"],
limit: 20,
offset: parseInt(page) * 20,
});
res.json({
hits: results.hits,
total: results.estimatedTotalHits,
facets: results.facetDistribution,
});
});
// Webhook to sync data changes into Meilisearch
app.post("/api/webhooks/movie-updated", async (req, res) => {
const movie: Movie = req.body;
const index = client.index<Movie>("movies");
const task = await index.addDocuments([movie]);
res.json({ taskUid: task.taskUid });
});
Best Practices
- Use
waitForTaskin scripts, not in request handlers. Indexing is asynchronous. In web servers, return the task UID and let clients poll or use webhooks. - Set
filterableAttributesbefore indexing. Adding filterable attributes later triggers a full re-index. - Use tenant tokens for multi-tenant apps. They embed filters server-side so the frontend cannot bypass tenant isolation.
- Keep documents under 1 MB each. Large documents slow indexing. Store blobs externally and index only searchable/filterable fields.
- Batch document additions. Send thousands of documents per call instead of one at a time.
- Set
displayedAttributesto avoid returning internal fields (like tenant IDs) in search results. - Pin the Meilisearch version in production. Major versions can include breaking changes to ranking or API behavior.
Anti-Patterns
- Using Meilisearch for analytics or aggregation. It is not a database. Use it for user-facing search only.
- Polling task status in a tight loop. Use
waitForTaskwith a reasonable interval or check task status on demand. - Indexing raw HTML or Markdown. Strip tags before indexing. Markup fragments pollute search results and highlighting.
- Storing the master key in frontend code. Use a search-only API key or tenant token for client-side access.
- Relying on
estimatedTotalHitsfor exact pagination. The count is an estimate. Do not display exact page counts. - Skipping
filterableAttributesconfiguration. Filters on unconfigured attributes silently return no results, causing confusing bugs. - Running without authentication in production. Always start Meilisearch with
--master-keyset and create scoped keys.
Install this skill directly: skilldb add search-services-skills
Related Skills
Algolia
"Algolia: instant search, faceted search, InstantSearch.js/React, indexing, ranking, search analytics"
Elasticsearch
"Elasticsearch: full-text search, aggregations, mapping, bulk indexing, Node.js client, relevance tuning"
Fuse Js
Fuse.js is a lightweight, powerful fuzzy-search library for JavaScript that runs entirely client-side. It's ideal for quickly adding flexible, typo-tolerant search capabilities to web applications without server-side infrastructure.
Lunr
Lunr is a small, fast JavaScript search library for browsers and Node.js. It allows you to build a search index directly within your application, providing full-text search capabilities without a backend API or external service. It's ideal for static sites, documentation, or client-side applications requiring offline-capable search.
Manticore Search
"Manticore Search: open-source full-text search, SQL-based queries, real-time indexes, columnar storage, Elasticsearch-compatible API"
Opensearch
OpenSearch is a community-driven, open-source search and analytics suite derived from Elasticsearch. It's ideal for powering full-text search, log analytics, security monitoring, and real-time application monitoring, offering powerful scalability and flexibility for diverse data needs.