Overview
Icon for MeiliSearch

MeiliSearch

Open-source search engine for your storefront

MedusaJS v2 Meilisearch plugin

Meilisearch for Medusa v2 catalogs: full-text and hybrid search over products and categories, plus store endpoints you can swap in for Copy to clipboard/store/products and Copy to clipboard/store/product-categories without your storefront noticing.

How it fits together

Medusa 2.19.0 introduced a Search Module of its own, and from v2.0.0 this plugin plugs into it as the Meilisearch engine behind it. The division of labour:

Who Does what Medusa's Search Module Creates and migrates indexes, seeds them, batches writes, routes catalog events, rebuilds on drift This plugin's provider Turns index declarations into Meilisearch settings and queries into Meilisearch requests This plugin's factories Ship ready-made product and category declarations you can extend This plugin's routes and admin page Native-parity store search, plus a settings screen for indexes and reindexing

Practically, that means you no longer run any indexing code yourself. You declare what an index holds; Medusa keeps it filled.

What you get

  • Every method of the provider contract, including atomic index swaps for rebuilds without downtime, batched multi-search, and write-task tracking so seeding knows when a batch has actually landed
  • Declared field weights compiled into Meilisearch relevance ordering, with filterable, sortable and facetable attributes derived from the same declaration
  • Value, range and statistical facets; highlighting and cropped snippets; estimated or exhaustive counts
  • Hybrid search over Meilisearch embedders — OpenAI, Ollama, Hugging Face, a REST endpoint, or vectors you compute
  • One index per locale, translated through Medusa's Translation Module
  • An escape hatch at every level: raw Meilisearch settings per index, raw Meilisearch parameters per query
  • Store endpoints that keep native pricing, tax, Copy to clipboardvariants.inventory_quantity, sales-channel scoping, filters and sorts

Compatibility

Plugin Medusa Meilisearch server Node Copy to clipboard^2.0.0 Copy to clipboard^2.19.0 Copy to clipboard>= 1.20 Copy to clipboard>= 22 Copy to clipboard^1.4.1 Copy to clipboard>= 2.15 < 2.19 Copy to clipboard>= 1.5 Copy to clipboard>= 22 Copy to clipboard^1.3.7 Copy to clipboard^2.13.4 Copy to clipboard>= 1.5 Copy to clipboard>= 20 Copy to clipboard^1.0.1 Copy to clipboard^2.4.0 Copy to clipboard>= 1.5 Copy to clipboard>= 20

The 2.19 release removed the search interface the v1 line was written against, so the two lines do not overlap: v1 stops at Medusa 2.18, v2 starts at 2.19. Coming from v1, work through the upgrade guide.

The Copy to clipboard>= 1.20 server floor comes from index swapping: the plugin's Copy to clipboardswap reindex strategy uses the Copy to clipboardrename field that Meilisearch 1.20 added to Copy to clipboardPOST /swap-indexes. Medusa 2.19 itself still runs on Node 20.19+, but this plugin is built and tested on Node 22 only.

The Meilisearch JS client stays on Copy to clipboard^0.56.0, the last version published as CommonJS.

Installation

npm install --save @rokmohar/medusa-plugin-meilisearch
# or
yarn add @rokmohar/medusa-plugin-meilisearch

Configuration

Register the plugin (for its API routes and admin page) and the Search Module with this package as its provider:

// medusa-config.ts
import { defineConfig, Modules } from '@medusajs/framework/utils'
export default defineConfig({
plugins: [
{
resolve: '@rokmohar/medusa-plugin-meilisearch',
options: {},
},
],
modules: [
{
resolve: '@medusajs/medusa/search',
options: {
providers: [
{
resolve: '@rokmohar/medusa-plugin-meilisearch/providers/meilisearch',
id: 'meilisearch',
options: {
config: {

Then declare your indexes under Copy to clipboardsrc/search. The application loads every file in that directory and hands the declarations to the Search Module:

// src/search/products.ts
import { defineProductSearchIndex } from '@rokmohar/medusa-plugin-meilisearch/indexes'
export default defineProductSearchIndex()
// src/search/categories.ts
import { defineCategorySearchIndex } from '@rokmohar/medusa-plugin-meilisearch/indexes'
export default defineCategorySearchIndex()

Create the indexes, then start the app:

npx medusa db:migrate
npx medusa develop

Copy to clipboarddb:migrate creates and migrates the physical Meilisearch indexes. On boot the Search Module seeds any index that was just created, was emptied, or whose declaration changed; from then on it keeps them current from events. Indexes live in Meilisearch and are never recreated at startup.

Provider options

Option Type Description Copy to clipboardconfig Copy to clipboardConfig Meilisearch client config. Copy to clipboardhost is required; Copy to clipboardapiKey is optional for keyless instances. Copy to clipboardembedders Copy to clipboardEmbedders Meilisearch embedders applied to every index this provider manages. Keys are the embedder names. Copy to clipboardsettings Copy to clipboardSettings Meilisearch settings applied below the settings derived from each declaration, e.g. Copy to clipboardrankingRules. Copy to clipboardtask_timeout_ms Copy to clipboardnumber How long to wait for a deferred write. Default Copy to clipboard120000. Copy to clipboardtask_polling_interval_ms Copy to clipboardnumber How often to poll for a write to land. Default Copy to clipboard500.

Index factory options

Both Copy to clipboarddefineProductSearchIndex() and Copy to clipboarddefineCategorySearchIndex() take the same options and always return an array of declarations (one per locale).

Option Default Description Copy to clipboardname Copy to clipboardproducts / Copy to clipboardcategories Base index name. Copy to clipboardprovider the module's default provider Provider identifier this index binds to. Copy to clipboardprimary_key Copy to clipboardid Document primary key. Copy to clipboardfields the default schema Replaces the field schema. Use Copy to clipboardsearch.define({ ... }). Copy to clipboardsettings Copy to clipboard{} Index settings (synonyms, stop words, typo tolerance, …). Copy to clipboardgraph_fields the default selection Extra Copy to clipboardquery.graph paths to fetch while seeding and ingesting. Copy to clipboardfilters Copy to clipboard{ status: 'published' } / Copy to clipboard{ is_active: true, is_internal: false } Copy to clipboardquery.graph filters. Copy to clipboardtransform pick of the declared paths Maps an entity to a search document. Synchronous. Copy to clipboardbatch_size Copy to clipboard200 Seed page size. Copy to clipboardevents product / category events, both namespaces Events this index reacts to. Copy to clipboardconsume the default routing table Turns an event into index mutations. Copy to clipboardlocales – BCP-47 locales; emits one index per locale. Copy to clipboarddefault_locale first entry of Copy to clipboardlocales Which locale keeps the bare index name.

Extending the default schema:

import { search } from '@medusajs/framework/utils'
import { defineProductSearchIndex, productSearchSchema } from '@rokmohar/medusa-plugin-meilisearch/indexes'
export default defineProductSearchIndex({
fields: search.define({
...productSearchSchema(),
brand: search.text().searchable({ weight: 3 }).facetable(),
}),
graph_fields: ['brand'],
settings: {
synonyms: { trousers: ['pants'] },
stop_words: ['the'],
},
})

Meilisearch has no per-attribute weights — relevance follows the order of Copy to clipboardsearchableAttributes — so declared weights become that ordering.

Worker mode

Seeding and event ingestion run outside Copy to clipboardworker_mode: 'server'. In a split deployment, install and configure the plugin on the worker instance too, or its indexes will stay empty.

Internationalization

Pass Copy to clipboardlocales to a factory to get one index per locale. The default locale keeps the bare name, the others are suffixed:

export default defineProductSearchIndex({
locales: ['en-US', 'fr-FR', 'de-DE'],
default_locale: 'en-US',
})

This registers Copy to clipboardproducts, Copy to clipboardproducts-fr-FR and Copy to clipboardproducts-de-DE. Each index declares its locale, seeds and ingests through Copy to clipboardquery.graph(..., { locale }) so documents carry the translated values, and tells Meilisearch which analyzer to use. Localized reads require Medusa's Translation Module (Copy to clipboardfeatureFlags: { translation: true }).

Store requests select an index by locale: Copy to clipboard?locale=fr-FR (or the Copy to clipboardx-medusa-locale header). Region variants fall back to the same language, so Copy to clipboardfr-CA uses the Copy to clipboardfr-FR index when no Copy to clipboardfr-CA index exists, and an unknown locale falls back to the default index. Copy to clipboard?index=products-fr-FR addresses one index directly.

Configure Meilisearch embedders on the provider and query them with Copy to clipboardsemanticSearch:

options: {
config: { host: process.env.MEILISEARCH_HOST!, apiKey: process.env.MEILISEARCH_API_KEY },
embedders: {
default: {
source: 'openAi',
apiKey: process.env.OPENAI_API_KEY,
model: 'text-embedding-3-small',
dimensions: 1536,
documentTemplate: '{{doc.title}} {{doc.description}}',
},
},
}

Declare embedders under Copy to clipboardsettings.provider_options.meilisearch.embedders on an index to scope them to that index — the admin status card reads declarations, so per-index embedders also show up there. Pre-computed embeddings are supported by declaring a Copy to clipboardsearch.vector(dimensions) field. See docs/semantic-search.md for Ollama and OpenAI walkthroughs.

Querying from code

import { Modules } from '@medusajs/framework/utils'
const search = container.resolve(Modules.SEARCH)
const { hits, facets, metadata } = await search.search({
entity: 'products',
fields: ['id', 'title'],
filters: { q: 'shirt', status: 'published' },
pagination: { skip: 0, take: 20 },
search_options: {
facets: ['categories.name'],
highlight: { fields: ['title'] },
count: 'exact',
},
})

Copy to clipboardquery.search(...) does the same and additionally hydrates fields the index does not hold. Anything Meilisearch cannot express — Copy to clipboard$like and Copy to clipboard$prefix filters, cursor pagination, the Copy to clipboardany matching strategy, query-time typo tolerance, ascending relevance — raises an error instead of silently returning different results. Raw Meilisearch parameters go through Copy to clipboardsearch_options.provider_options.meilisearch.

Reindex on demand:

await search.reindex({ index: 'products', strategy: 'swap' })

Store API endpoints

All four endpoints accept the Meilisearch-specific parameters Copy to clipboardquery, Copy to clipboardindex, Copy to clipboardlanguage, Copy to clipboardsemanticSearch, Copy to clipboardsemanticRatio, Copy to clipboardembedder and Copy to clipboardfilter (a raw Meilisearch filter expression).

Copy to clipboardGET /store/meilisearch/products

Everything the native Copy to clipboard/store/products accepts, plus the parameters above. Without Copy to clipboardquery it behaves exactly like the native route. With one, Meilisearch supplies the matching product ids and their ranking, and the response is hydrated natively — calculated prices, tax, Copy to clipboardvariants.inventory_quantity, sales-channel scoping.

curl 'http://localhost:9000/store/meilisearch/products?query=shirt&limit=10&region_id=reg_1&fields=id,title,*variants.calculated_price' \
-H 'x-publishable-api-key: pk_...'

Response: Copy to clipboard{ products, count, limit, offset }.

Copy to clipboardGET /store/meilisearch/categories

Same idea against the native Copy to clipboard/store/product-categories. Response: Copy to clipboard{ categories, count, limit, offset }.

Copy to clipboardGET /store/meilisearch/products-hits and Copy to clipboardGET /store/meilisearch/categories-hits

Raw engine hits, with no database read: Copy to clipboard{ hits, query, processingTimeMs, estimatedTotalHits, limit, offset }, plus Copy to clipboardfacets when requested and Copy to clipboardhybridSearch / Copy to clipboardsemanticRatio for a hybrid query. Each hit carries the index's retrievable fields and Copy to clipboard_score. Additional parameters: Copy to clipboardlimit, Copy to clipboardoffset, Copy to clipboardsort, Copy to clipboardfacets, Copy to clipboardfields.

curl 'http://localhost:9000/store/meilisearch/products-hits?query=shirt&limit=5&facets=categories.name' \
-H 'x-publishable-api-key: pk_...'

Admin API endpoints

Endpoint Description Copy to clipboardGET /admin/meilisearch/indexes Registered indexes with their entity, locales and retrievable fields. Copy to clipboardPOST /admin/meilisearch/sync Starts a reindex (Copy to clipboard{ index?, strategy? }) and returns immediately. Copy to clipboardPOST /admin/meilisearch/products-hits Raw product hits, same body as the store endpoint. Copy to clipboardPOST /admin/meilisearch/categories-hits Raw category hits. Copy to clipboardGET /admin/meilisearch/vector-status Semantic-search status derived from the registered declarations.

Medusa's own dashboard search uses the core Copy to clipboard/admin/search endpoint and picks up these indexes automatically.

Environment variables

MEILISEARCH_HOST=http://localhost:7700
MEILISEARCH_API_KEY=your_master_key

docker-compose

services:
meilisearch:
image: getmeili/meilisearch:v1.53
ports:
- '7700:7700'
environment:
MEILI_MASTER_KEY: your_master_key
MEILI_NO_ANALYTICS: 'true'
volumes:
- meilisearch:/meili_data
volumes:
meilisearch:

Add search to the Medusa Next.js starter

See nextjs/README.md.

FAQ

  • Product categories and tags
  • Product variant prices
  • Product search prices
  • Semantic search
  • Migrating from v1

Contributing

Issues and pull requests are welcome at github.com/rokmohar/medusa-plugin-meilisearch.

yarn install
yarn lint
yarn typecheck
yarn test
yarn build

Build your own

Develop your own custom integration

Build your own integration with our API to speed up your processes. Make your integration available via npm for it to be shared in our Library with the broader Medusa community.

gift card interface

Ready to build your custom commerce setup?