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

Frontend Developer: Master of Efficiency and UX — Key Strategies for Optimal Application Performance

In today's ever-evolving digital world, where users expect speed, performance and a seamless web application experience, the role of the Frontend Developer becomes extremely important...

We are backed by Medusa investors - Catch The Tornado

Piotr and Tomasz Karwatka have joined our ranks.

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