Complete Guide on How to Integrate Meilisearch with Medusa.js

By Viktor Holik

Featured image

About a year ago, my team and I embarked on a Medusa eCommerce 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.

meiliserach-medusa-1

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.

Transform your online store with our expertise in Medusa.js

Let's talk about your project

Other blog posts

Building a Custom Order Mechanism for Trading Events in Medusa

In this article, we’ll show you how we created a custom order mechanism for trading events in Medusa...

Next.js + Vercel, the Best Solution for the Frontend of an Online Store

Finding the right frontend solution for your online store is crucial. Enter Next.js and Vercel, a dynamic duo that promises unmatched performance, scalability, and developer experience for frontend developers...

Tell us about your project

Got a project in mind? Let's make it happen!

By clicking “Send Message” you grant us, i.e., Rigby, consent for email marketing of our services as part of the communication regarding your project. You may withdraw your consent, for example via hello@rigbyjs.com.
More information
placeholder

Grzegorz Tomaka

Co-CEO & Co-founder

LinkedIn icon
placeholder

Jakub Zbaski

Co-CEO & Co-founder

LinkedIn icon