How to Add MeiliSearch to Your Medusa 2.0 Project
This guide assumes you already have a working Medusa 2.0 project with both a backend and storefront set up. We'll walk you through implementing MeiliSearch step by step. If you'd rather skip the manual setup, you can use my Railway template for an effortless deployment where everything is preconfigured:
Railway caps free users to 5 services. The template above has 7. I also have a TRIAL version that does not include MeiliSearch.
Video Instructions
If you're ready to dive into the implementation yourself, or if you just want to understand how it is implemented in the template above, this guide will cover it all, in three simple steps:
- Run a MeiliSearch instance
- Implement MeiliSearch in the Medusa backend
- Configure MeiliSearch in the storefront
1. Run a MeiliSearch Instance
MeiliSearch is a powerful, open-source search engine that runs as a separate service, not just an npm dependency. Below are three methods to run it: Railway, Docker, or MeiliSearch Cloud. We'll cover the first two in detail.
Method 1: Railway
- Go to your project canvas on Railway and create a new service.
- Choose the Docker Image option and search for
getmeili/meilisearch:latest
.
- Once deployed:
- Rename the service to
MeiliSearch
. - Navigate to the Variables tab and add the following environment variables:
- Rename the service to
MEILI_DB_PATH="/meili_data/data.ms"
MEILI_ENV="production"
MEILI_HTTP_ADDR=":::3331"
MEILI_MASTER_KEY="some-long-random-string"
MEILI_PUBLIC_URL="${{RAILWAY_PUBLIC_DOMAIN}}"
PORT="3331"
- Attach storage volume, right click on the MeiliSearch container and choose Attach Volume from the menu
- Set mount path to
/meili_data
- Enable networking, go to Settings -> Networking and turn on HTTP
That’s it! Your MeiliSearch instance is now ready. You can skip the Docker section now that you’ve used Railway instead. Use image below for reference your finished result.
Method 2: Docker (Local or VPS)
Use the following Docker Compose configuration to set up MeiliSearch locally or on a VPS:
docker-compose.yml
services:
meilisearch:
image: getmeili/meilisearch:latest
ports:
- "7700:7700"
volumes:
- ~/data.ms:/meili_data/data.ms
environment:
MEILI_MASTER_KEY: your_master_key_here
healthcheck:
test: ["CMD-SHELL", "wget --spider http://localhost:7700 || exit 1"]
interval: 10s
timeout: 5s
retries: 5
If you are not familiar with Docker compose, you may refer to the quick start guide. Additionally, more information about local instance of MeiliSearch can be found here.
Method 3: MeiliSearch Cloud
If you prefer a managed solution or want to support the developers, consider using MeiliSearch Cloud. It provides seamless deployment and additional features like facet search, semantic search, and geosearch.
2. Backend Implementation
Since the Medusa core team hasn’t released an official MeiliSearch module for version 2.0 yet, we’ll use the community-created @rokmohar/medusa-plugin-meilisearch
. Make sure to ⭐ their repository!
Step 1: Install the Plugin
Run one of these commands in your Medusa backend directory:
npm install --save @rokmohar/medusa-plugin-meilisearch
# or
yarn add @rokmohar/medusa-plugin-meilisearch
Step 2: Configure Environment Variables
Add these variables to your backend’s .env
file:
MEILISEARCH_HOST=http://localhost:7700 # Or public cloud URL if you are using cloud an online MeiliSearch instance (Railway)
MEILISEARCH_API_KEY=your_master_key_here
And, if you are also hosting your project on Railway add the following two variables to your Medusa 2.0 backend Railway service:
MEILISEARCH_API_KEY="${{MeiliSearch.MEILI_MASTER_KEY}}"
MEILISEARCH_HOST="https://${{MeiliSearch.MEILI_PUBLIC_URL}}"
Step 3: Update Medusa Configuration
In your medusa-config.js
, add the plugin configuration:
modules: [
// ... all your other modules,
{
resolve: '@rokmohar/medusa-plugin-meilisearch',
options: {
config: {
host: process.env.MEILISEARCH_HOST,
apiKey: process.env.MEILISEARCH_API_KEY
},
settings: {
products: {
indexSettings: {
searchableAttributes: ['title', 'description', 'variant_sku'],
displayedAttributes: ['id', 'title', 'description', 'variant_sku', 'thumbnail', 'handle']
},
primaryKey: 'id'
}
}
}
}
]
// rest of your config...
See how I added it: https://github.com/rpuls/medusajs-2.0-for-railway-boilerplate/blob/master/backend/medusa-config.js
Step 4: Create Product Indexing Subscribers
We are going to be indexing products during 3 events; creation, updating, deletion. This can be done by creating two simple subscribers. All credits for these two subscribers goes to https://github.com/rokmohar. Create two files in your backend's /subscribers
directory:
src/subscribers/product-upsert.ts
(create and update)
import type { SubscriberArgs, SubscriberConfig } from '@medusajs/framework'
import { IProductModuleService } from '@medusajs/framework/types'
import { Modules } from '@medusajs/framework/utils'
import { ProductEvents, SearchUtils } from '@medusajs/framework/utils'
import { MeiliSearchService } from '@rokmohar/medusa-plugin-meilisearch'
export default async function productUpsertHandler({ event: { data }, container }: SubscriberArgs<{ id: string }>) {
const productId = data.id
const productModuleService: IProductModuleService = container.resolve(Modules.PRODUCT)
const meiliSearchService: MeiliSearchService = container.resolve('meilisearch')
const product = await productModuleService.retrieveProduct(productId)
await meiliSearchService.addDocuments('products', [product], SearchUtils.indexTypes.PRODUCTS)
}
export const config: SubscriberConfig = {
event: [ProductEvents.PRODUCT_CREATED, ProductEvents.PRODUCT_UPDATED]
}
src/subscribers/product-delete.ts
import type { SubscriberArgs, SubscriberConfig } from '@medusajs/framework'
import { ProductEvents } from '@medusajs/framework/utils'
import { MeiliSearchService } from '@rokmohar/medusa-plugin-meilisearch'
export default async function productDeleteHandler({ event: { data }, container }: SubscriberArgs<{ id: string }>) {
const productId = data.id
const meiliSearchService: MeiliSearchService = container.resolve('meilisearch')
await meiliSearchService.deleteDocument('products', productId)
}
export const config: SubscriberConfig = {
event: ProductEvents.PRODUCT_DELETED
}
Note: if you are using older version of the module (lower than 0.2) you need to resolve it differently; const meilisearch = container.resolve('@rokmohar/medusa-plugin-meilisearch')
in both files.
That's all for the backend! 🚀
3. Storefront Implementation
If you are using the official Next.js storefront from Medusa—launched before they removed search capabilities—there’s not much for you to implement. The same applies to anyone who has launched a project using my starter template at any point in time. However, if you are working with a different storefront and need to add search capabilities, I’ve included an ultra-simple example at the end of this article. It demonstrates how to fetch search results in a React component using the standard web APIs provided by MeiliSearch.
Step 1: Fetch search API key
You can see how to do this in the video above at timestamp: 22:00
The request you need to make is:
GET <your_meilisearch_public_url>/keys
Authorization: Bearer <YOUR_MASTER_KEY>
Content-Type: application/json
From the response, locate the "key"
field where the "actions"
array includes "search"
. Copy this value—it is your search-only API key. This key should only be used on the frontend as it doesn't give any sensitive permissions.
Step 2: Configure Environment Variables
Add these variables to .env.local
in your storefront:
# MeiliSearch Configuration
NEXT_PUBLIC_SEARCH_ENDPOINT=http://localhost:7700
NEXT_PUBLIC_SEARCH_API_KEY=<your_search_key>
NEXT_PUBLIC_INDEX_NAME=products
And, if you are also hosting your project on Railway add the following two variables to your Storefront Railway service:
NEXT_PUBLIC_SEARCH_ENDPOINT="https://${{MeiliSearch.MEILI_PUBLIC_URL}}"
NEXT_PUBLIC_SEARCH_API_KEY="<your_search_key>"
NEXT_PUBLIC_INDEX_NAME="products"
All done!
You should now have a fully functional MeiliSearch integration with your Medusa project! Test it out by using the search field in your storefront 🚀
Alternative storefront implementation
Below is a simple React component with an input field and a search button that fetches search results from your MeiliSearch container via its search endpoint:
import React, { useState } from 'react';
const SearchComponent = () => {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
// MeiliSearch URL with environment variable fallback
const meiliSearchUrl = process.env.NEXT_PUBLIC_SEARCH_ENDPOINT || 'http://localhost:7700';
const searchApiKey = process.env.NEXT_PUBLIC_SEARCH_API_KEY; // Ensure this is set in your environment
const handleSearch = async () => {
try {
const response = await fetch(`${meiliSearchUrl}/indexes/products/search`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${searchApiKey}` // Use the search-only API key
},
body: JSON.stringify({ q: query })
});
if (!response.ok) {
throw new Error('Failed to fetch search results');
}
const data = await response.json();
setResults(data.hits);
} catch (error) {
console.error(error);
}
};
return (
<div>
<input
type='text'
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder='Search for products...'
/>
<button onClick={handleSearch}>Search</button>
<ul>
{results.map((result) => (
<li key={result.id}>{result.title}</li> // Adjust based on your index structure
))}
</ul>
</div>
);
};
export default SearchComponent;
In this example, I directly import the API key and MeiliSearch URL from process.env
within the component body. However, in real-world React development, it's better to pass these values through a centralized configuration or context. This approach ensures better maintainability and makes it easier to manage environment-specific configurations.