Open-source поисковый движок для вашей витрины
This plugin integrates MeiliSearch with your Medusa e-commerce store and adds support for internationalization (i18n) of your product catalog.
Run the following command to install the plugin with npm:
npm install --save @rokmohar/medusa-plugin-meilisearchOr with yarn:
yarn add @rokmohar/medusa-plugin-meilisearchThis step is required only if you are upgrading from previous version to v1.0.
| Plugin version | Medusa version |
|---|---|
Note: This plugin is only compatible with MedusaJS v2. For MedusaJS v1 / v2.3.x and older, use the legacy version.
Add the plugin to your file:
1import { loadEnv, defineConfig } from '@medusajs/framework/utils'2import { MeilisearchPluginOptions } from '@rokmohar/medusa-plugin-meilisearch'3
4loadEnv(process.env.NODE_ENV || 'development', process.cwd())5
6module.exports = defineConfig({7 // ... other config8 plugins: [9 // ... other plugins10 {11 resolve: '@rokmohar/medusa-plugin-meilisearch',12 options: {13 config: {14 host: process.env.MEILISEARCH_HOST ?? '',15 apiKey: process.env.MEILISEARCH_API_KEY ?? '',16 },17 settings: {18 // The key is used as the index name in Meilisearch19 products: {20 // Required: Index type21 type: 'products',22 // Optional: Whether the index is enabled. When disabled:23 // - Index won't be created or updated24 // - Documents won't be added or removed25 // - Index won't be included in searches26 // - All operations will be silently skipped27 enabled: true,28 // Optional: Specify which fields to include in the index29 // If not specified, all fields will be included30 fields: ['id', 'title', 'description', 'handle', 'variant_sku', 'thumbnail'],31 indexSettings: {32 searchableAttributes: ['title', 'description', 'variant_sku'],33 displayedAttributes: ['id', 'handle', 'title', 'description', 'variant_sku', 'thumbnail'],34 filterableAttributes: ['id', 'handle'],35 },36 primaryKey: 'id',37 // Create your own transformer38 /*transformer: (product) => ({39 id: product.id,40 // other attributes...41 }),*/42 },43 categories: {44 // Required: Index type45 type: 'categories',46 // Optional: Whether the index is enabled47 enabled: true,48 // Optional: Specify which fields to include in the index49 // If not specified, all fields will be included50 fields: ['id', 'name', 'description', 'handle', 'is_active', 'parent_id'],51 indexSettings: {52 searchableAttributes: ['name', 'description'],53 displayedAttributes: ['id', 'name', 'description', 'handle', 'is_active', 'parent_id'],54 filterableAttributes: ['id', 'handle', 'is_active', 'parent_id'],55 },56 primaryKey: 'id',57 // Create your own transformer58 /*transformer: (category) => ({59 id: category.id,60 name: category.name,61 // other attributes...62 }),*/63 },64 },65 i18n: {66 // Choose one of the following strategies:67
68 // 1. Separate index per language69 // strategy: 'separate-index',70 // languages: ['en', 'fr', 'de'],71 // defaultLanguage: 'en',72
73 // 2. Language-specific fields with suffix74 strategy: 'field-suffix',75 languages: ['en', 'fr', 'de'],76 defaultLanguage: 'en',77 translatableFields: ['title', 'description'],78 },79 } satisfies MeilisearchPluginOptions,80 },81 ],82})Important: Product events and background tasks will not work if your Medusa instance is running in mode, because the server instance does not process subscribers or background jobs.
Depending on your setup:
Monolithic architecture (only one backend instance):
Ensure you do not set the or environment variable. By default, Medusa will use mode, which supports both background processing and serving HTTP requests from the same instance.
Split architecture (separate server and worker instances):
Follow the official Medusa documentation on worker mode.
In this case, you must add this plugin in the worker instance, as the server instance does not handle event subscribers or background tasks.
The plugin supports two main strategies for handling translations, with flexible configuration options for each.
1{2 i18n: {3 // Choose strategy: 'separate-index' or 'field-suffix'4 strategy: 'field-suffix',5 // List of supported languages6 languages: ['en', 'fr', 'de'],7 // Default language to fall back to8 defaultLanguage: 'en',9 // Optional: List of translatable fields10 translatableFields: ['title', 'description', 'handle']11 }12}You can provide detailed configuration for each translatable field:
1{2 i18n: {3 strategy: 'field-suffix',4 languages: ['en', 'fr', 'de'],5 defaultLanguage: 'en',6 translatableFields: [7 // Simple field name8 'title',9
10 // Field with different target name11 {12 source: 'description',13 target: 'content' // Will be indexed as content_en, content_fr, etc.14 },15
16 // Field with transformation17 {18 source: 'handle',19 transform: (value) => value.toLowerCase().replace(/\s+/g, '-')20 }21 ]22 }23}The plugin provides a flexible way to transform your products with custom translations. Translations are passed directly to the default transformer via :
1{2 settings: {3 products: {4 type: 'products',5 // ... other config6 transformer: async (product, defaultTransformer, options) => {7 const translations = {8 title: [9 { language_code: 'en', value: 'Blue T-Shirt' },10 { language_code: 'fr', value: 'T-Shirt Bleu' },11 ],12 description: [13 { language_code: 'en', value: 'A comfortable blue t-shirt' },14 { language_code: 'fr', value: 'Un t-shirt bleu confortable' },15 ],16 }17
18 return defaultTransformer(product, {19 ...options,20 translations,21 includeAllTranslations: true,22 })23 },24 }25 }26}Pass to emit all language suffixes (e.g. , ). Without it only the current language suffix is written.
The recommended approach for production is to use the Medusa Translation module, which is built into Medusa v2.
1. Enable the translation feature flag and module in :
1module.exports = defineConfig({2 featureFlags: {3 translation: true,4 },5 // ... other config6 modules: [7 // ... other modules8 {9 resolve: '@medusajs/medusa/translation',10 },11 ],12})2. Create a translations utility ():
1import { ContainerRegistrationKeys } from '@medusajs/utils'2import type { MedusaContainer } from '@medusajs/framework'3import { TranslationMap } from '@rokmohar/medusa-plugin-meilisearch'4
5// Maps Medusa locale codes (e.g. 'sl-SI') to index field suffixes (e.g. 'sl')6export const LOCALE_MAP: Record<string, string> = {7 'sl-SI': 'sl',8 'en-US': 'en',9 // add more as needed10}11
12export const getTranslations = async (13 id: string,14 langs: string[],15 container: MedusaContainer,16): Promise<TranslationMap> => {17 const query = container.resolve(ContainerRegistrationKeys.QUERY)18
19 const { data: rows } = await query.graph({20 entity: 'translation',21 fields: ['reference_id', 'locale_code', 'translations'],22 filters: { reference_id: id, locale_code: langs },23 })24
25 const result: TranslationMap = {}26
27 for (const row of rows) {28 const langCode = LOCALE_MAP[row.locale_code] ?? row.locale_code29
30 for (const [field, value] of Object.entries(row.translations as Record<string, string>)) {31 if (!result[field]) {32 result[field] = []33 }34 result[field].push({ language_code: langCode, value })35 }36 }37
38 return result39}3. Use in your transformer:
The transformer receives — the real forwarded by workflow steps. It is when no container is available (e.g. during a manual index rebuild without a workflow context), so always guard with a fallback:
1import { getTranslations, LOCALE_MAP } from './src/utils/translations'2
3{4 settings: {5 products: {6 type: 'products',7 indexSettings: {8 searchableAttributes: ['title', 'title_sl', 'title_en'],9 displayedAttributes: ['id', 'handle', 'title', 'title_sl', 'title_en', 'thumbnail'],10 filterableAttributes: ['id'],11 },12 transformer: async (product, defaultTransformer, options) => {13 if (!options?.container) {14 // No container available — index without translations15 return defaultTransformer(product, options)16 }17
18 const raw = await getTranslations(product.id, ['sl-SI', 'en-US'], options.container)19
20 // Remap locale codes to match index field suffixes21 const translations = Object.fromEntries(22 Object.entries(raw).map(([field, values]) => [23 field,24 values.map((t) => ({25 language_code: LOCALE_MAP[t.language_code] ?? t.language_code,26 value: t.value,27 })),28 ]),29 )30
31 return defaultTransformer(product, {32 ...options,33 translations,34 includeAllTranslations: true,35 })36 },37 }38 }39}How reaches the transformer: Medusa plugin modules receive the awilix cradle proxy at construction time, not the real — so the service cannot self-supply it. Workflow steps (triggered by product/category events) have the real container from their context and forward it via . The transformer then resolves services (e.g. ) from it.
This strategy creates a separate MeiliSearch index for each language. For example, if your base index is named "products", it will create:
Benefits:
This strategy adds language suffixes to translatable fields in the same index. For example:
Benefits:
If no translatable fields are specified and using the field-suffix strategy, the plugin will automatically detect string fields as translatable. You can override this by explicitly specifying the fields:
1{2 i18n: {3 strategy: 'field-suffix',4 languages: ['en', 'fr'],5 defaultLanguage: 'en',6 // Only these fields will be translatable7 translatableFields: ['title', 'description']8 }9}GET /store/meilisearch/products-hitsQuery Parameters:
GET /store/meilisearch/productsQuery Parameters:
This plugin provides full support for MedusaJS v2 categories, including:
1{2 settings: {3 categories: {4 type: 'categories',5 enabled: true,6 fields: ['id', 'name', 'description', 'handle', 'is_active', 'parent_id'],7 indexSettings: {8 searchableAttributes: ['name', 'description'],9 displayedAttributes: ['id', 'name', 'description', 'handle', 'is_active', 'parent_id'],10 filterableAttributes: ['id', 'handle', 'is_active', 'parent_id'],11 },12 primaryKey: 'id',13 },14 },15 i18n: {16 strategy: 'field-suffix',17 languages: ['en', 'fr', 'de'],18 defaultLanguage: 'en',19 translatableFields: ['name', 'description'], // Category-specific translatable fields20 },21}GET /store/meilisearch/categories-hitsQuery Parameters:
GET /store/meilisearch/categoriesQuery Parameters:
This plugin supports AI-powered semantic search using vector embeddings. See docs/semantic-search.md for detailed configuration and usage instructions.
Add the environment variables to your and file:
1# ... others vars2MEILISEARCH_HOST=3MEILISEARCH_API_KEY=If you want to use with the from this README, use the following values:
1# ... others vars2MEILISEARCH_HOST=http://127.0.0.1:77003MEILISEARCH_API_KEY=msYou can add the following configuration for Meilisearch to your :
1services:2 # ... other services3
4 meilisearch:5 image: getmeili/meilisearch:latest6 ports:7 - '7700:7700'8 volumes:9 - ~/data.ms:/data.ms10 environment:11 - MEILI_MASTER_KEY=ms12 healthcheck:13 test: ['CMD', 'curl', '-f', 'http://localhost:7700']14 interval: 10s15 timeout: 5s16 retries: 5You can find instructions on how to add search to a Medusa NextJS starter inside the nextjs folder.
Feel free to open issues and pull requests!