Complete Guide on How to Integrate Meilisearch with Medusa.js

Viktor Holik
Rigby News
Published in
4 min readMay 1, 2024

About a year ago, my team and I embarked on a Medusa e-commerce project that required advanced product filtering capabilities. We needed functionalities like predictive filtering based on categories or attributes, full-text search, typo tolerance, and relevant search results. Developing these features from scratch in PostgreSQL or similar databases is time-consuming and requires considerable expertise. Fortunately, there’s an open-source search solution that fits the bill perfectly — Meilisearch.

In this guide, I will walk you through how Meilisearch works and provide some practical examples of its integration with Medusa.js. Let’s dive in!

Integration with Medusa.js

First, let’s explore how Medusa integrates with the Meilisearch API. When the backend starts, Medusa uses a loader that bulk adds products using the Meilisearch API. Subsequently, any product changes, deletions, or creations are managed by a Medusa subscriber that handles these updates appropriately.

To start using Meilisearch in your Medusa project, include the medusa-plugin-meilisearch in your medusa-config.js:

const plugins = [
// ...
{
resolve: `medusa-plugin-meilisearch`,
options: {
config: {
host: process.env.MEILISEARCH_HOST,
apiKey: process.env.MEILISEARCH_API_KEY,
},
settings: {
// index settings...
},
},
},
]

Index configuration

In this guide, I’m using the plugin’s default configuration, except for filterableAttributes and sortableAttributes.

An index in Meilisearch is a group of documents with associated settings. By specifying an index key, you can configure specific settings for that index:

{
resolve: `medusa-plugin-meilisearch`,
options: {
config: {
host: process.env.MEILISEARCH_HOST,
apiKey: process.env.MEILISEARCH_API_KEY,
},
settings: {
products: {
indexSettings: {
filterableAttributes: ["categories.handle", "variants.prices.amount", "variants.prices.currency_code"],
sortableAttributes: ["title", "variants.prices.amount"],
searchableAttributes: ["title", "description", "variant_sku"],
displayedAttributes: ["*"],
primaryKey: "id",
transformer: (product) => ({
// Custom transformation logic
})
},
},
},
},
},

By setting filterableAttributes, you enable document filtering using those keys. Remember, this also adds faceted search (filtering predictions). The transformer function allows you to modify data in your preferred format before updating it in Meilisearch. Each key in indexSettings is optional.

Using on a storefront

Let’s initialize our Meilisearch client. Install the Meilisearch package and add the following in your utility folder:

Keep in mind you should not expose API key publicly.

// lib/meilisearch
import { MeiliSearch } from 'meilisearch'

export const meilisearchClient = new MeiliSearch({
host: process.env.MEILISEARCH_HOST!,
apiKey: process.env.MEILISEARCH_API_KEY!,
})

Now, let’s create a utility function that handles product filtering:

// lib/meilisearch
type FiltersQuery = {
categories?: string[]
orderBy?: string
order?: 'asc' | 'desc'
page?: number
minPrice?: number
maxPrice?: number
query?: string
currencyCode?: string
}

const PAGE_SIZE = 15

export async function getProductsFromMeilisearch({
categories,
maxPrice,
minPrice,
orderBy,
order = 'asc',
page = 1,
query,
currencyCode = 'usd',
}: FiltersQuery) {
// To implement...
}

Meilisearch employs an SQL-like syntax for product filtering, making it straightforward to use. Let’s develop a function that filters products based on specified parameters.

export async function getProductsFromMeilisearch({
categories,
maxPrice,
minPrice,
orderBy,
order = 'asc',
page = 1,
query,
currencyCode = 'usd',
}: FiltersQuery) {
const offset = (page - 1) * PAGE_SIZE

const queries: string[] = []

if (categories) {
queries.push(`categories.handle IN [${categories.join(', ')}]`)
}

if (minPrice) {
queries.push(
`(variants.prices.amount >= ${minPrice} AND variants.prices.currency_code = "${currencyCode}")`
)
}

if (maxPrice) {
queries.push(
`(variants.prices.amount <= ${maxPrice} AND variants.prices.currency_code = "${currencyCode}")`
)
}

const result = await meilisearchClient.index('products').search(query, {
limit: PAGE_SIZE,
offset: offset,
sort: orderBy ? [`${orderBy}:${order}`] : undefined,
filter: queries.join(' AND '),
facets: ['categories.handle'],
})

return result
}

Now, you should be able to see your filtered products and facets in the responses. With this function, you can seamlessly implement pagination, filtering, predictive filtering, and more.

{
hits: [
{
id: 'cool-t-shirt',
title: 'Medusa T-Shirt',
description: 'Reimagine the feeling of a classic T-shirt. With our cotton T-shirts, everyday essentials no longer have to be ordinary.',
categories: [Array],
handle: 't-shirt',
subtitle: null,
is_giftcard: false,
weight: 400,
images: [Array],
options: [Array],
variants: [Array],
tags_value: [],
variant_sku: [],
variant_title: [Array],
variant_upc: [],
variant_ean: [],
variant_mid_code: [],
variant_hs_code: [],
variant_options: [],
variant_options_value: [Array]
}
],
query: 'T shirt',
processingTimeMs: 0,
limit: 15,
offset: 0,
estimatedTotalHits: 1,
facetDistribution: { 'categories.handle': { 't-shirt': 1 } },
facetStats: {}
}

Summary

This article aims to provide you with a fundamental understanding of how to use Meilisearch with Medusa.js. I recommend starting with your project needs and experimenting with these integrations. I hope you find this guide helpful.

If you have any questions about Medusa.js or want to see specific guides, leave your requests in the comments section below!

--

--