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 clipboard
variants.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
123npm install --save @rokmohar/medusa-plugin-meilisearch# oryarn 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:
1234567891011121314151617181920// medusa-config.tsimport { 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:
1234// src/search/products.tsimport { defineProductSearchIndex } from '@rokmohar/medusa-plugin-meilisearch/indexes'export default defineProductSearchIndex()
1234// src/search/categories.tsimport { defineCategorySearchIndex } from '@rokmohar/medusa-plugin-meilisearch/indexes'export default defineCategorySearchIndex()
Create the indexes, then start the app:
12npx medusa db:migratenpx 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:
1234567891011121314import { 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:
1234export 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.
Semantic search
Configure Meilisearch embedders on the provider and query them with Copy to clipboardsemanticSearch:
123456789101112options: {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
123456789101112131415import { 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:
1await 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.
12curl 'http://localhost:9000/store/meilisearch/products?query=shirt&limit=10®ion_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.
12curl '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
12MEILISEARCH_HOST=http://localhost:7700MEILISEARCH_API_KEY=your_master_key
docker-compose
12345678910111213services:meilisearch:image: getmeili/meilisearch:v1.53ports:- '7700:7700'environment:MEILI_MASTER_KEY: your_master_keyMEILI_NO_ANALYTICS: 'true'volumes:- meilisearch:/meili_datavolumes: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.
12345yarn installyarn lintyarn typecheckyarn testyarn build
