Getting Started with Meilisearch
Every database can search. Run WHERE title LIKE '%redemption%' and you'll find The Shawshank Redemption. But what happens when a user types "shawshnk rdmption"? Nothing. Traditional databases match characters, not intent. One typo, zero results.
Meilisearch fills that gap. It's a search engine built for the moment a user starts typing: typo-tolerant, prefix-aware, and fast enough to return results on every keystroke. Search for "interst" and get Interstellar. Search for "shawshnk rdmption" and get exactly the right movie. No special syntax, no fuzzy matching operators, no configuration.
Everything below fits in one TypeScript file. Run it against a local Meilisearch instance, or point it at Layerbase Cloud if you'd rather skip the local install.
Contents
- Create a Meilisearch Instance
- Set Up the Project
- The Movie Dataset
- Create an Index and Add Documents
- Configure Filterable and Sortable Attributes
- Typo-Tolerant Search
- Faceted Search
- When to Reach for Meilisearch
Create a Meilisearch Instance
Local with SpinDB
SpinDB is the quickest path to a local Meilisearch instance: one CLI, no Docker, no manual downloads. (What is SpinDB?)
Install SpinDB globally:
npm i -g spindb # npm
pnpm add -g spindb # pnpmOr run it directly without installing:
npx spindb create meili1 -e meilisearch --start # npm
pnpx spindb create meili1 -e meilisearch --start # pnpmIf you installed globally, create and start a Meilisearch instance:
spindb create meili1 -e meilisearch --startSpinDB downloads the Meilisearch binary for your platform, configures it, and starts the server. Verify it's running:
spindb url meili1http://127.0.0.1:7700Leave the server running. We'll connect to it from TypeScript in the next section.
Layerbase Cloud
Prefer managed? Layerbase Cloud spins one up in seconds. You'll get a connection URL and API key from the dashboard's Quick Connect panel.
Cloud instances use TLS, so the connection code is slightly different:
const client = new Meilisearch({
host: 'https://cloud.layerbase.dev:PORT',
apiKey: 'YOUR_API_KEY',
})Replace PORT and YOUR_API_KEY with the values from your Quick Connect panel. Everything else in this guide works the same locally or on Cloud.
Set Up the Project
mkdir meilisearch-movie-search && cd meilisearch-movie-search
pnpm init
pnpm add meilisearch
pnpm add -D tsx typescriptCreate a file called search.ts. All the code in this post goes into that one file.
The Movie Dataset
Here are 15 movies with genres and descriptions. In production this would come from your database or API. The titles and descriptions are what Meilisearch will index and search against:
const movies = [
{
id: 1,
title: 'Alien',
year: 1979,
genres: ['sci-fi', 'horror'],
description:
'The crew of a deep-space mining ship encounters a deadly extraterrestrial creature that stalks them through narrow corridors.',
},
{
id: 2,
title: 'The Shawshank Redemption',
year: 1994,
genres: ['drama'],
description:
'A banker wrongly imprisoned for murder befriends a fellow inmate and uses patience, cunning, and a rock hammer to find freedom.',
},
{
id: 3,
title: 'Inception',
year: 2010,
genres: ['sci-fi', 'thriller'],
description:
"A thief who enters people's dreams takes on the job of planting an idea in a CEO's mind, navigating increasingly unstable layers of consciousness.",
},
{
id: 4,
title: 'The Intouchables',
year: 2011,
genres: ['comedy', 'drama'],
description:
'A wealthy quadriplegic hires an unlikely caretaker from the projects, and they form a life-changing bond through humor and honesty.',
},
{
id: 5,
title: 'Spirited Away',
year: 2001,
genres: ['animation', 'fantasy'],
description:
'A young girl navigates a world of spirits and gods to rescue her parents from a witch who runs a magical bathhouse.',
},
{
id: 6,
title: 'Mad Max: Fury Road',
year: 2015,
genres: ['action', 'sci-fi'],
description:
"In a post-apocalyptic wasteland, a war captain defects with the tyrant's captive wives and drives across the desert with a lone drifter.",
},
{
id: 7,
title: 'Interstellar',
year: 2014,
genres: ['sci-fi', 'drama'],
description:
"A former pilot travels through a wormhole near Saturn to find a habitable planet as Earth's crops fail and humanity faces extinction.",
},
{
id: 8,
title: 'The Grand Budapest Hotel',
year: 2014,
genres: ['comedy', 'drama'],
description:
'A legendary hotel concierge and his protégé become entangled in theft, murder, and the upheaval of a continent between two wars.',
},
{
id: 9,
title: 'Parasite',
year: 2019,
genres: ['thriller', 'drama'],
description:
'A poor family infiltrates a wealthy household one job at a time, until a hidden secret in the basement throws everything into chaos.',
},
{
id: 10,
title: 'Finding Nemo',
year: 2003,
genres: ['animation', 'comedy'],
description:
"An overprotective clownfish crosses the ocean to rescue his son, who was captured by a diver and placed in a dentist's fish tank.",
},
{
id: 11,
title: 'The Matrix',
year: 1999,
genres: ['sci-fi', 'action'],
description:
'A computer hacker discovers that reality is a simulation built by machines, and joins a rebellion to free humanity from digital enslavement.',
},
{
id: 12,
title: 'WALL-E',
year: 2008,
genres: ['animation', 'sci-fi'],
description:
"A lonely trash-compacting robot on an abandoned Earth falls in love with a sleek probe robot and follows her into space, inadvertently deciding humanity's future.",
},
{
id: 13,
title: 'Jaws',
year: 1975,
genres: ['thriller', 'horror'],
description:
'A police chief, a marine biologist, and a grizzled fisherman hunt a great white shark that is terrorizing a summer beach community.',
},
{
id: 14,
title: 'Coco',
year: 2017,
genres: ['animation', 'fantasy'],
description:
"A boy who dreams of being a musician accidentally ends up in the Land of the Dead and discovers the truth about his family's musical past.",
},
{
id: 15,
title: 'The Silence of the Lambs',
year: 1991,
genres: ['thriller', 'horror'],
description:
'An FBI trainee seeks the help of an imprisoned cannibal psychiatrist to catch a serial killer who is kidnapping women.',
},
]Create an Index and Add Documents
An index in Meilisearch is like a table. Each index stores documents (JSON objects with a primary key), and Meilisearch automatically indexes every field for search.
import { Meilisearch } from 'meilisearch'
const client = new Meilisearch({ host: 'http://localhost:7700' })
const INDEX = 'movies'
// Delete the index if it exists from a previous run
try {
const deleteTask = await client.index(INDEX).delete()
await client.waitForTask(deleteTask.taskUid)
} catch {
// Index doesn't exist yet, that's fine
}
// Create the index with 'id' as the primary key
const createTask = await client.createIndex(INDEX, { primaryKey: 'id' })
await client.waitForTask(createTask.taskUid)
// Add all movies
const addTask = await client.index(INDEX).addDocuments(movies)
await client.waitForTask(addTask.taskUid)
console.log(`Added ${movies.length} movies to "${INDEX}" index`)Meilisearch processes tasks asynchronously. The waitForTask calls ensure each operation completes before moving on. In production you'd handle task status differently, but for a script like this, waiting is fine.
Configure Filterable and Sortable Attributes
Meilisearch indexes all fields for full-text search by default but doesn't enable filtering or sorting. To filter by genre or sort by year, you opt in:
const settingsTask = await client.index(INDEX).updateSettings({
filterableAttributes: ['genres', 'year'],
sortableAttributes: ['year'],
})
await client.waitForTask(settingsTask.taskUid)
console.log('Configured filterable and sortable attributes')This builds the internal data structures for filtering and sorting on those fields. You only do this once per index.
Typo-Tolerant Search
Now the fun part. Let's throw some mangled queries at it:
const queries = [
'shawshnk rdmption',
'interst',
'the matix',
'spirted away',
]
for (const query of queries) {
const results = await client.index(INDEX).search(query)
console.log(`\n"${query}"`)
for (const hit of results.hits) {
console.log(` ${hit.title} (${hit.year})`)
}
}Run it:
npx tsx search.tsExpected output:
"shawshnk rdmption"
The Shawshank Redemption (1994)
"interst"
Interstellar (2014)
"the matix"
The Matrix (1999)
Mad Max: Fury Road (2015)
"spirted away"
Spirited Away (2001)Every query has typos or is incomplete. Meilisearch nails it anyway. "shawshnk rdmption" is missing letters in both words. "interst" is barely a fragment of "Interstellar." A traditional database would return nothing for any of these.
Why SQL Can't Do This
To make the contrast concrete, here's what a LIKE query would return for the same inputs:
function sqlLikeSearch(query: string) {
const pattern = query.toLowerCase()
return movies.filter(
(m) =>
m.title.toLowerCase().includes(pattern) ||
m.description.toLowerCase().includes(pattern),
)
}
for (const query of queries) {
const matches = sqlLikeSearch(query)
console.log(`\nSQL LIKE "%${query}%": ${matches.length} results`)
}SQL LIKE "%shawshnk rdmption%": 0 results
SQL LIKE "%interst%": 0 results
SQL LIKE "%the matix%": 0 results
SQL LIKE "%spirted away%": 0 resultsZero across the board. LIKE requires an exact substring match. One wrong character and the row vanishes. PostgreSQL's pg_trgm extension can handle some fuzzy matching, but it requires manual configuration, trigram indexes, and a similarity threshold. Meilisearch does this out of the box.
Faceted Search
Meilisearch can combine full-text search with attribute filters. This is the "filter by category, then search within results" pattern you see on every e-commerce site:
Find all sci-fi movies, then search within that subset:
// All sci-fi movies
const sciFi = await client.index(INDEX).search('', {
filter: ['genres = "sci-fi"'],
})
console.log('\nAll sci-fi movies:')
for (const hit of sciFi.hits) {
console.log(` ${hit.title} (${hit.year})`)
}
// Sci-fi movies matching "space"
const sciFiSpace = await client.index(INDEX).search('space', {
filter: ['genres = "sci-fi"'],
})
console.log('\nSci-fi movies matching "space":')
for (const hit of sciFiSpace.hits) {
console.log(` ${hit.title} (${hit.year})`)
}
// Dramas from 2010 or later, sorted by year
const recentDramas = await client.index(INDEX).search('', {
filter: ['genres = "drama"', 'year >= 2010'],
sort: ['year:desc'],
})
console.log('\nDramas from 2010+, newest first:')
for (const hit of recentDramas.hits) {
console.log(` ${hit.title} (${hit.year})`)
}All sci-fi movies:
Alien (1979)
Inception (2010)
Mad Max: Fury Road (2015)
Interstellar (2014)
The Matrix (1999)
WALL-E (2008)
Sci-fi movies matching "space":
Alien (1979)
Interstellar (2014)
WALL-E (2008)
Dramas from 2010+, newest first:
Parasite (2019)
Interstellar (2014)
The Grand Budapest Hotel (2014)
The Intouchables (2011)Filters use a simple string syntax. Array elements are ANDed, comparison operators work on numeric fields, and you can sort on any attribute you've marked as sortable. Meilisearch narrows by filter first, then ranks by relevance within those results.
You can also request facet distributions to build dynamic filter UIs:
const faceted = await client.index(INDEX).search('', {
facets: ['genres'],
})
console.log('\nGenre distribution:')
for (const [genre, count] of Object.entries(
faceted.facetDistribution?.genres ?? {},
)) {
console.log(` ${genre}: ${count}`)
}Genre distribution:
action: 2
animation: 4
comedy: 3
drama: 5
fantasy: 2
horror: 3
sci-fi: 6
thriller: 3That's exactly the data you need for a sidebar with genre checkboxes and counts. I love that this comes for free.
When to Reach for Meilisearch
Meilisearch shines when users are typing into a search box and expect it to just work:
- Search bars and autocomplete: any UI where users type and expect instant, forgiving results
- Product catalogs: typo-tolerant search combined with faceted filtering by category, price, or attributes
- Documentation search: find the right page even when users don't remember the exact term
- Content platforms: search across blog posts, articles, or media libraries with prefix matching
- Internal tools: search employees, tickets, or records without forcing users to type exact names
If your search needs to feel instant and forgive sloppy typing, Meilisearch is probably what you want.
Where it's not the right fit: if you need to search by meaning rather than text (e.g., "scary movie set in space" matching a description about a "deadly creature in corridors"), that's semantic search. A vector database like Qdrant is built for that.
Wrapping Up
The full script is under 80 lines of real code. You indexed a dataset, ran typo-tolerant and faceted searches that traditional databases can't touch, and got results in milliseconds. The same pattern scales from 15 movies to millions of documents.
The Meilisearch documentation covers advanced features like synonyms, stop words, ranking rules, multi-index search, geo search, and tenant tokens for multi-tenant applications.
To manage your local Meilisearch instance:
spindb stop meili1 # Stop the server
spindb start meili1 # Start it again
spindb list # See all your database instancesSpinDB handles 20+ database engines, so Meilisearch can sit alongside Qdrant, CockroachDB, or whatever else your project needs. Layerbase Desktop wraps the same thing in a GUI on macOS.