npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@rokmohar/medusa-plugin-meilisearch

v1.3.5

Published

Meilisearch plugin for Medusa 2

Downloads

11,162

Readme

MedusaJS v2 MeiliSearch Plugin with i18n Support

This plugin integrates MeiliSearch with your Medusa e-commerce store and adds support for internationalization (i18n) of your product catalog.

Features

  • Full-text search for your Medusa store
  • Real-time indexing
  • Typo-tolerance
  • Faceted search
  • Support for both products and categories
  • Internationalization (i18n) support with multiple strategies:
    1. Separate index per language
    2. Language-specific fields with suffix
  • Flexible translation configuration
  • Custom field transformations
  • Automatic field detection

Installation

Run the following command to install the plugin with npm:

npm install --save @rokmohar/medusa-plugin-meilisearch

Or with yarn:

yarn add @rokmohar/medusa-plugin-meilisearch

Upgrade to v1.0

This step is required only if you are upgrading from previous version to v1.0.

  • The plugin now supports new MedusaJS plugin system.
  • Subscribers are included in the plugin.
  • You don't need custom subscribers anymore, you can remove them.

⚠️ MedusaJS v2.4.0 or newer

This plugin is only for MedusaJS v2.4.0 or newer.

If you are using MedusaJS v2.3.1 or older, please use the older version of this plugin.

Configuration

Add the plugin to your medusa-config.ts file:

import { loadEnv, defineConfig } from '@medusajs/framework/utils'
import { MeilisearchPluginOptions } from '@rokmohar/medusa-plugin-meilisearch'

loadEnv(process.env.NODE_ENV || 'development', process.cwd())

module.exports = defineConfig({
  // ... other config
  plugins: [
    // ... other plugins
    {
      resolve: '@rokmohar/medusa-plugin-meilisearch',
      options: {
        config: {
          host: process.env.MEILISEARCH_HOST ?? '',
          apiKey: process.env.MEILISEARCH_API_KEY ?? '',
        },
        settings: {
          // The key is used as the index name in Meilisearch
          products: {
            // Required: Index type
            type: 'products',
            // Optional: Whether the index is enabled. When disabled:
            // - Index won't be created or updated
            // - Documents won't be added or removed
            // - Index won't be included in searches
            // - All operations will be silently skipped
            enabled: true,
            // Optional: Specify which fields to include in the index
            // If not specified, all fields will be included
            fields: ['id', 'title', 'description', 'handle', 'variant_sku', 'thumbnail'],
            indexSettings: {
              searchableAttributes: ['title', 'description', 'variant_sku'],
              displayedAttributes: ['id', 'handle', 'title', 'description', 'variant_sku', 'thumbnail'],
              filterableAttributes: ['id', 'handle'],
            },
            primaryKey: 'id',
            // Create your own transformer
            /*transformer: (product) => ({
              id: product.id,
              // other attributes...
            }),*/
          },
          categories: {
            // Required: Index type
            type: 'categories',
            // Optional: Whether the index is enabled
            enabled: true,
            // Optional: Specify which fields to include in the index
            // If not specified, all fields will be included
            fields: ['id', 'name', 'description', 'handle', 'is_active', 'parent_id'],
            indexSettings: {
              searchableAttributes: ['name', 'description'],
              displayedAttributes: ['id', 'name', 'description', 'handle', 'is_active', 'parent_id'],
              filterableAttributes: ['id', 'handle', 'is_active', 'parent_id'],
            },
            primaryKey: 'id',
            // Create your own transformer
            /*transformer: (category) => ({
              id: category.id,
              name: category.name,
              // other attributes...
            }),*/
          },
        },
        i18n: {
          // Choose one of the following strategies:

          // 1. Separate index per language
          // strategy: 'separate-index',
          // languages: ['en', 'fr', 'de'],
          // defaultLanguage: 'en',

          // 2. Language-specific fields with suffix
          strategy: 'field-suffix',
          languages: ['en', 'fr', 'de'],
          defaultLanguage: 'en',
          translatableFields: ['title', 'description'],
        },
      } satisfies MeilisearchPluginOptions,
    },
  ],
})

⚠️ Worker Mode Considerations

Important: Product events and background tasks will not work if your Medusa instance is running in server 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 MEDUSA_WORKER_MODE or WORKER_MODE environment variable. By default, Medusa will use shared 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.

i18n Configuration

The plugin supports two main strategies for handling translations, with flexible configuration options for each.

Basic Configuration

{
  i18n: {
    // Choose strategy: 'separate-index' or 'field-suffix'
    strategy: 'field-suffix',
    // List of supported languages
    languages: ['en', 'fr', 'de'],
    // Default language to fall back to
    defaultLanguage: 'en',
    // Optional: List of translatable fields
    translatableFields: ['title', 'description', 'handle']
  }
}

Advanced Field Configuration

You can provide detailed configuration for each translatable field:

{
  i18n: {
    strategy: 'field-suffix',
    languages: ['en', 'fr', 'de'],
    defaultLanguage: 'en',
    translatableFields: [
      // Simple field name
      'title',

      // Field with different target name
      {
        source: 'description',
        target: 'content'  // Will be indexed as content_en, content_fr, etc.
      },

      // Field with transformation
      {
        source: 'handle',
        transform: (value) => value.toLowerCase().replace(/\s+/g, '-')
      }
    ]
  }
}

Custom Translation Transformer

The plugin provides a flexible way to transform your products with custom translations. Instead of relying on specific storage formats, you can provide translations directly to the transformer:

import { transformProduct } from '@rokmohar/medusa-plugin-meilisearch'

const getProductTranslations = async (productId: string) => {
  // Example: fetch from your translation service/database
  return {
    title: [
      { language_code: 'en', value: 'Blue T-Shirt' },
      { language_code: 'fr', value: 'T-Shirt Bleu' },
    ],
    description: [
      { language_code: 'en', value: 'A comfortable blue t-shirt' },
      { language_code: 'fr', value: 'Un t-shirt bleu confortable' },
    ],
  }
}

// Example usage in your custom transformer
const customTransformer = async (product, options) => {
  const translations = await getProductTranslations(product.id)

  return transformProduct(product, {
    ...options,
    translations,
  })
}

Integration with Tolgee

For production environments, you'll often want to integrate with external translation management platforms. Here's an example of integrating with Tolgee, a popular translations management platform:

import { default as axios } from 'axios'
import { logger } from '@medusajs/framework'
import { TranslationMap } from '@rokmohar/medusa-plugin-meilisearch'

type TranslationsHttpResponse = {
  [lang: string]: {
    [id: string]: Record<string, string>
  }
}

const options = {
  apiKey: process.env.TOLGEE_API_KEY ?? '',
  baseURL: process.env.TOLGEE_API_URL ?? '',
  projectId: process.env.TOLGEE_PROJECT_ID ?? '',
}

const httpClient = axios.create({
  baseURL: `${options.baseURL}/v2/projects/${options.projectId}`,
  headers: {
    Accept: 'application/json',
    'X-API-Key': options.apiKey,
  },
  maxBodyLength: Infinity,
})

export const getTranslations = async (id: string, langs: string[]) => {
  const translations: TranslationMap = {}

  try {
    const response = await httpClient.get<TranslationsHttpResponse>(`/translations/${langs.join(',')}?ns=${id}`)
    Object.entries(response.data).forEach(([language_code, { [id]: values }]) => {
      Object.entries(values).forEach(([key, value]) => {
        if (!(key in translations)) {
          translations[key] = []
        }
        translations[key].push({ language_code, value })
      })
    })
  } catch (e) {
    logger.error(e)
  }

  return translations
}

Usage in your transformer configuration:

{
  settings: {
    products: {
      type: 'products',
      // ... other config
      transformer: async (product, defaultTransformer, options) => {
        const translations = await getTranslations(product.id, ['sl', 'en'])
        return defaultTransformer(product, {
          ...options,
          translations,
          includeAllTranslations: true
        })
      },
    }
  }
}

This integration fetches translations from Tolgee's API and transforms them into the format expected by this plugin. For complete storefront translation management, consider using the medusa-plugin-tolgee by SteelRazor47, which provides comprehensive translation management features including admin widgets.

i18n Strategies

1. Separate Index per Language

This strategy creates a separate MeiliSearch index for each language. For example, if your base index is named "products", it will create:

  • products_en
  • products_fr
  • products_de

Benefits:

  • Better performance for language-specific searches
  • Language-specific settings and ranking rules
  • Cleaner index structure

2. Language-specific Fields with Suffix

This strategy adds language suffixes to translatable fields in the same index. For example:

  • title_en, title_fr, title_de
  • description_en, description_fr, description_de

Benefits:

  • Single index to maintain
  • Ability to search across all languages at once
  • Smaller storage requirements

Custom Translatable Fields

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:

{
  i18n: {
    strategy: 'field-suffix',
    languages: ['en', 'fr'],
    defaultLanguage: 'en',
    // Only these fields will be translatable
    translatableFields: ['title', 'description']
  }
}

Product API Endpoints

Search for Product Hits

GET /store/meilisearch/products-hits

Query Parameters:

  • query: Search query string
  • limit: (Optional) Limit results from search
  • offset: (Optional) Offset results from search
  • language: (Optional) Language code
  • semanticSearch - Enable AI-powered semantic search (boolean)
  • semanticRatio - Semantic vs keyword search ratio (0-1)

Search for Products

GET /store/meilisearch/products

Query Parameters:

  • fields - MedusaJS fields expression
  • limit: (Optional) Limit results from search
  • offset: (Optional) Offset results from search
  • region_id: (Optional, but required for calculated_price) Current region ID
  • currency_code: (Optional, but required for calculated_price) Current currency code
  • query: Search query string
  • language: (Optional) Language code
  • semanticSearch - Enable AI-powered semantic search (boolean)
  • semanticRatio - Semantic vs keyword search ratio (0-1)

Category Support

This plugin provides full support for MedusaJS v2 categories, including:

  • Real-time indexing of category changes
  • i18n support for category names and descriptions
  • Hierarchical category structure support with parent-child relationships
  • Custom category field transformations

Category Configuration Example

{
  settings: {
    categories: {
      type: 'categories',
      enabled: true,
      fields: ['id', 'name', 'description', 'handle', 'is_active', 'parent_id'],
      indexSettings: {
        searchableAttributes: ['name', 'description'],
        displayedAttributes: ['id', 'name', 'description', 'handle', 'is_active', 'parent_id'],
        filterableAttributes: ['id', 'handle', 'is_active', 'parent_id'],
      },
      primaryKey: 'id',
    },
  },
  i18n: {
    strategy: 'field-suffix',
    languages: ['en', 'fr', 'de'],
    defaultLanguage: 'en',
    translatableFields: ['name', 'description'], // Category-specific translatable fields
  },
}

Category API Endpoints

Search for Category Hits

GET /store/meilisearch/categories-hits

Query Parameters:

  • query: Search query string
  • limit: (Optional) Limit results from search
  • offset: (Optional) Offset results from search
  • language: (Optional) Language code
  • semanticSearch - Enable AI-powered semantic search (boolean)
  • semanticRatio - Semantic vs keyword search ratio (0-1)

Search for Categories

GET /store/meilisearch/categories

Query Parameters:

  • fields - MedusaJS fields expression
  • limit: (Optional) Limit results from search
  • offset: (Optional) Offset results from search
  • query: Search query string
  • language: (Optional) Language code
  • semanticSearch - Enable AI-powered semantic search (boolean)
  • semanticRatio - Semantic vs keyword search ratio (0-1)

AI-Powered Semantic Search

This plugin supports AI-powered semantic search using vector embeddings. See docs/semantic-search.md for detailed configuration and usage instructions.

ENV variables

Add the environment variables to your .env and .env.template file:

# ... others vars
MEILISEARCH_HOST=
MEILISEARCH_API_KEY=

If you want to use with the docker-compose from this README, use the following values:

# ... others vars
MEILISEARCH_HOST=http://127.0.0.1:7700
MEILISEARCH_API_KEY=ms

docker-compose

You can add the following configuration for Meilisearch to your docker-compose.yml:

services:
  # ... other services

  meilisearch:
    image: getmeili/meilisearch:latest
    ports:
      - '7700:7700'
    volumes:
      - ~/data.ms:/data.ms
    environment:
      - MEILI_MASTER_KEY=ms
    healthcheck:
      test: ['CMD', 'curl', '-f', 'http://localhost:7700']
      interval: 10s
      timeout: 5s
      retries: 5

Add search to Medusa NextJS starter

You can find instructions on how to add search to a Medusa NextJS starter inside the nextjs folder.

FAQ

Contributing

Feel free to open issues and pull requests!