Integrate Meilisearch with Medusa

In this tutorial, you'll learn how to integrate Meilisearch with Medusa to enable advanced search capabilities in your storefront.

When you install a Medusa application, you get a fully-fledged commerce platform with a Framework for customization. Medusa's architecture supports integrating third-party services, such as search engines, allowing you to build custom features around core commerce flows.

Meilisearch is an open-source, fast, and relevant search engine that you can integrate with Medusa to enhance your storefront's search functionality.

Summary#

By following this tutorial, you'll learn how to:

  • Install and set up Medusa.
  • Integrate Meilisearch into Medusa.
  • Trigger Meilisearch reindexing when a product is created, updated, or deleted, or when an admin manually triggers a reindex.
  • Customize the Next.js Starter Storefront to search for products through Meilisearch.

You can follow this tutorial whether you're new to Medusa or an advanced Medusa developer.

Diagram illustrating the integration of Meilisearch with Medusa

Example Repository
Find the full code of the guide in this repository.

Step 1: Install a Medusa Application#

Start by installing the Medusa application on your machine with the following command:

Terminal
npx create-medusa-app@latest

You'll first be asked for the project's name. Then, when asked whether you want to install the Next.js Starter Storefront, choose "Yes."

Afterwards, the installation process will start, which will install the Medusa application in a directory with your project's name and the Next.js Starter Storefront in a separate directory named {project-name}-storefront.

Why is the storefront installed separately: The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called API routes. Learn more in Medusa's Architecture documentation.

Once the installation finishes successfully, the Medusa Admin dashboard will open with a form to create a new user. Enter the user's credentials and submit the form. Afterwards, you can log in with the new user and explore the dashboard.

Ran into Errors: Check out the troubleshooting guides for help.

Step 2: Create Meilisearch Module#

To integrate third-party services into Medusa, you create a custom module. A module is a reusable package with functionalities related to a single feature or domain. Medusa integrates the module into your application without side effects on your setup.

In this step, you'll create a custom module that provides the necessary functionalities to integrate Meilisearch with Medusa.

Note: Refer to the Modules documentation to learn more.

Before building the module, you need to install Meilisearch's JavaScript client. Run the following command in your Medusa application's root directory:

Create Module Directory#

A module is created under the src/modules directory of your Medusa application. So, create the directory src/modules/meilisearch.

Create Service#

You define a module's functionalities in a service. A service is a TypeScript or JavaScript class that the module exports. In the service's methods, you can connect to the database, which is useful if your module defines tables in the database, or connect to third-party services.

In this section, you'll create the Meilisearch Module's service and the methods necessary to manage indexed products in Meilisearch and search through them.

To create the Meilisearch Module's service, create the file src/modules/meilisearch/service.ts with the following content:

src/modules/meilisearch/service.ts
1const { Meilisearch } = require("meilisearch")2import { MedusaError } from "@medusajs/framework/utils"3
4type MeilisearchOptions = {5  host: string;6  apiKey: string;7  productIndexName: string;8}9
10export type MeilisearchIndexType = "product"11
12export default class MeilisearchModuleService {13  private client: typeof Meilisearch14  private options: MeilisearchOptions15
16  constructor({}, options: MeilisearchOptions) {17    if (!options.host || !options.apiKey || !options.productIndexName) {18      throw new MedusaError(19        MedusaError.Types.INVALID_ARGUMENT, 20        "Meilisearch options are required"21      )22    }23    this.client = new Meilisearch({24      host: options.host,25      apiKey: options.apiKey,26    })27    this.options = options28  }29
30  // TODO: Add methods31}

You export a class that serves as the Meilisearch Module's service. In the service, you define two properties:

  • client: An instance of the Meilisearch Client, which you'll use to perform actions with Meilisearch's API.
  • options: An object of options that the module receives when it's registered, which you'll learn about later. The options contain:
    • apiKey: The Meilisearch API key.
    • host: The Meilisearch host.
    • productIndexName: The name of the index where products are stored.
Tip: If you want to index other types of data, such as product categories, you can add new properties for their index names in the MeilisearchOptions type.

A module's service receives the module's options as a second parameter in its constructor. In the constructor, you initialize the Meilisearch client using the module's options.

What is the first constructor parameter? A module has a container that holds all resources registered in that module, and you can access those resources in the first parameter of the constructor. Learn more about it in the Module Container documentation.

Index Data Method

The first method you need to add to the service is a method that receives an array of data to add or update in Meilisearch's index.

Add the following methods to the MeilisearchModuleService class:

src/modules/meilisearch/service.ts
1export default class MeilisearchModuleService {2  // ...3  async getIndexName(type: MeilisearchIndexType) {4    switch (type) {5      case "product":6        return this.options.productIndexName7      default:8        throw new Error(`Invalid index type: ${type}`)9    }10  }11
12  async indexData(data: Record<string, unknown>[], type: MeilisearchIndexType = "product") {13    const indexName = await this.getIndexName(type)14    const index = this.client.index(indexName)15    16    // Transform data to include id as primary key for Meilisearch17    const documents = data.map((item) => ({18      ...item,19      id: item.id,20    }))21
22    await index.addDocuments(documents)23  }24}

You define two methods:

  1. getIndexName: A method that receives an MeilisearchIndexType (defined in the previous snippet) and returns the index name for that type. In this case, you only have one type, product, so you return the product index name.
    • If you want to index other types of data, you can add more cases to the switch statement.
  2. indexData: A method that receives an array of data and an MeilisearchIndexType. The method indexes the data in the Meilisearch index for the given type.

Retrieve and Delete Methods#

The next methods you'll add to the service are methods to retrieve and delete data from the Meilisearch index. You'll use these later to keep the Meilisearch index in sync with Medusa.

Add the following methods to the MeilisearchModuleService class:

src/modules/meilisearch/service.ts
1export default class MeilisearchModuleService {2  // ...3
4  async retrieveFromIndex(documentIds: string[], type: MeilisearchIndexType = "product") {5    const indexName = await this.getIndexName(type)6    const index = this.client.index(indexName)7    8    const results = await Promise.all(9      documentIds.map(async (id) => {10        try {11          return await index.getDocument(id)12        } catch (error) {13          // Document not found, return null14          return null15        }16      })17    )18
19    return results.filter(Boolean)20  }21
22  async deleteFromIndex(documentIds: string[], type: MeilisearchIndexType = "product") {23    const indexName = await this.getIndexName(type)24    const index = this.client.index(indexName)25    26    await index.deleteDocuments(documentIds)27  }28}

You define two methods:

  1. retrieveFromIndex: A method that receives an array of document IDs and a MeilisearchIndexType. The method retrieves the documents with the given IDs from the Meilisearch index.
  2. deleteFromIndex: A method that receives an array of document IDs and a MeilisearchIndexType. The method deletes the documents with the given IDs from the Meilisearch index.

Search Method

The last method you'll implement is a method to search through the Meilisearch index. This method lets you expose search functionality to clients through Medusa's API routes.

Add the following method to the MeilisearchModuleService class:

src/modules/meilisearch/service.ts
1export default class MeilisearchModuleService {2  // ...3
4  async search(query: string, type: MeilisearchIndexType = "product") {5    const indexName = await this.getIndexName(type)6    const index = this.client.index(indexName)7    8    return await index.search(query)9  }10}

The search method receives a query string and a MeilisearchIndexType. The method searches through the Meilisearch index for the given type, such as products, and returns the results.

Export Module Definition#

The final piece of a module is its definition, which you export in an index.ts file at its root directory. This definition tells Medusa the name of the module and its service.

So, create the file src/modules/meilisearch/index.ts with the following content:

src/modules/meilisearch/index.ts
1import { Module } from "@medusajs/framework/utils"2import MeilisearchModuleService from "./service"3
4export const MEILISEARCH_MODULE = "meilisearch"5
6export default Module(MEILISEARCH_MODULE, {7  service: MeilisearchModuleService,8})

You use the Module function from the Modules SDK to create the module's definition. It accepts two parameters:

  1. The module's name, which is meilisearch.
  2. An object with a required property service indicating the module's service.

You also export the module's name as MEILISEARCH_MODULE so you can reference it later.

Add Module to Medusa's Configurations#

Once you finish building the module, add it to Medusa's configurations to start using it.

In medusa-config.ts, add a modules property and pass an array with your custom module:

medusa-config.ts
1module.exports = defineConfig({2  // ...3  modules: [4    {5      resolve: "./src/modules/meilisearch",6      options: {7        host: process.env.MEILISEARCH_HOST!,8        apiKey: process.env.MEILISEARCH_API_KEY!,9        productIndexName: process.env.MEILISEARCH_PRODUCT_INDEX_NAME!,10      },11    },12  ],13})

Each object in the modules array has a resolve property, whose value is either a path to the module's directory, or an npm package’s name.

You also pass an options property with the module's options, including the Meilisearch host, API Key, and the product index name.

Add Environment Variables#

Before you can start using the Meilisearch Module, you need to set the environment variables for the Meilisearch host, API Key, and the product index name.

Add the following environment variables to your .env file:

Code
1MEILISEARCH_HOST=your-meilisearch-host2MEILISEARCH_API_KEY=your-meilisearch-api-key3MEILISEARCH_PRODUCT_INDEX_NAME=your-product-index-name
  • your-meilisearch-host is the host of your Meilisearch instance. If you're running Meilisearch locally, it should be http://127.0.0.1:7700.
  • your-meilisearch-api-key is the master key of your Meilisearch instance. If you're running Meilisearch locally, you should have set it when starting Meilisearch. If you're using Meilisearch Cloud, you can find it in the dashboard under "API Keys." Learn more in the Meilisearch documentation.
  • your-product-index-name is the name of the index where you'll store products. You can choose any name you want. Even if the index doesn't exist, Meilisearch will create it when you add documents to it.

Your module is now ready for use. You'll see how to use it in the next steps.


Step 3: Sync Products to Meilisearch Workflow#

To keep the Meilisearch index in sync with Medusa, you need to trigger indexing when products are created, updated, or deleted in Medusa. You can also allow admins to manually trigger a reindex.

To implement the indexing functionality, you need to create a workflow. A workflow is a series of actions, called steps, that complete a task. You construct a workflow like you construct a function, but it's a special function that allows you to track its executions' progress, define roll-back logic, and configure other advanced features.

Note: Learn more about workflows in the Workflows documentation.

In this step, you'll create a workflow that indexes products in Meilisearch. In the next steps, you'll learn how to use the workflow when products are created, updated, or deleted, or when admins manually trigger a reindex.

The workflow has the following steps:

Medusa provides the useQueryGraphStep in its @medusajs/medusa/core-flows package. You only need to implement the second step.

syncProductsStep#

In the second step of the workflow, you create or update indexes in Meilisearch for the products retrieved in the first step.

To create the step, create the file src/workflows/steps/sync-products.ts with the following content:

src/workflows/steps/sync-products.ts
1import { ProductDTO } from "@medusajs/framework/types"2import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"3import { MEILISEARCH_MODULE } from "../../modules/meilisearch"4
5export type SyncProductsStepInput = {6  products: ProductDTO[]7}8
9export const syncProductsStep = createStep(10  "sync-products",11  async ({ products }: SyncProductsStepInput, { container }) => {12    const meilisearchModuleService = container.resolve(13      MEILISEARCH_MODULE14    )15    const existingProducts = await meilisearchModuleService.retrieveFromIndex(16      products.map((product) => product.id),17      "product"18    )19    const newProducts = products.filter((product) => !existingProducts.some(20      (p) => p.id === product.id)21    )22    await meilisearchModuleService.indexData(23      products as unknown as Record<string, unknown>[], 24      "product"25    )26
27    return new StepResponse(undefined, {28      newProducts: newProducts.map((product) => product.id),29      existingProducts,30    })31  }32  // TODO add compensation33)

You create a step with createStep from the Workflows SDK. It accepts two parameters:

  1. The step's unique name, which is sync-products.
  2. An async function that receives two parameters:
    • The step's input, which is an object holding an array of products to sync into Meilisearch.
    • An object that has properties including the Medusa container, which is a registry of Framework and commerce tools that you can access in the step.

In the step function, you resolve the Meilisearch Module's service from the Medusa container using the name you exported in the module definition's file.

Then, you retrieve the products that are already indexed in Meilisearch and determine which products are new. You'll learn why this is useful in a bit.

Finally, you pass the products you received in the input to Meilisearch to create or update its indices.

A step function must return a StepResponse instance. The StepResponse constructor accepts two parameters:

  1. The step's output, which in this case is undefined.
  2. Data to pass to the step's compensation function.

Compensation Function

The compensation function undoes the actions performed in a step. Then, if an error occurs during the workflow's execution, the compensation functions of executed steps are called to roll back the changes. This mechanism ensures data consistency in your application, especially as you integrate external systems.

To add a compensation function to a step, pass it as a third parameter to createStep:

src/workflows/steps/sync-products.ts
1export const syncProductsStep = createStep(2  // ...3  async (input, { container }) => {4    if (!input) {5      return6    }7
8    const meilisearchModuleService = container.resolve(9      MEILISEARCH_MODULE10    )11    12    if (input.newProducts) {13      await meilisearchModuleService.deleteFromIndex(14        input.newProducts,15        "product"16      )17    }18
19    if (input.existingProducts) {20      await meilisearchModuleService.indexData(21        input.existingProducts,22        "product"23      )24    }25  }26)

The compensation function receives two parameters:

  1. The data you passed as a second parameter of StepResponse in the step function.
  2. A context object similar to the step function that holds the Medusa container.

In the compensation function, you resolve the Meilisearch Module's service from the container. Then, you delete from Meilisearch the products that were newly indexed and revert the existing products to their original data.

Add Sync Products Workflow#

You can now create the workflow that syncs products to Meilisearch.

To create the workflow, create the file src/workflows/sync-products.ts with the following content:

src/workflows/sync-products.ts
1import { createWorkflow, WorkflowResponse } from "@medusajs/framework/workflows-sdk"2import { useQueryGraphStep } from "@medusajs/medusa/core-flows"3import { syncProductsStep, SyncProductsStepInput } from "./steps/sync-products"4
5type SyncProductsWorkflowInput = {6  filters?: Record<string, unknown>7  limit?: number8  offset?: number9}10
11export const syncProductsWorkflow = createWorkflow(12  "sync-products",13  ({ filters, limit, offset }: SyncProductsWorkflowInput) => {14    const { data, metadata } = useQueryGraphStep({15      entity: "product",16      fields: [17        "id", 18        "title", 19        "description", 20        "handle", 21        "thumbnail", 22        "categories.id",23        "categories.name",24        "categories.handle",25        "tags.id",26        "tags.value",27      ],28      pagination: {29        take: limit,30        skip: offset,31      },32      filters: {33        status: "published",34        ...filters,35      },36    })37
38    syncProductsStep({39      products: data,40    } as SyncProductsStepInput)41
42    return new WorkflowResponse({43      products: data,44      metadata,45    })46  }47)

You create a workflow using createWorkflow from the Workflows SDK. It accepts the workflow's unique name as a first parameter.

It accepts as a second parameter a constructor function, which is the workflow's implementation. The function can accept input, which in this case is pagination and filter parameters for the products to retrieve.

In the workflow's constructor function, you:

  1. Retrieve products from Medusa's database using useQueryGraphStep. This step uses Medusa's Query tool to retrieve data across modules. You pass it the pagination and filter parameters you received in the input.
  2. Index the products in Meilisearch using syncProductsStep. You pass it the products you retrieved in the previous step.

A workflow must return an instance of WorkflowResponse. The WorkflowResponse constructor accepts the workflow's output as a parameter, which is an object holding the retrieved products and their pagination details.

In the next step, you'll learn how to execute this workflow.


Step 4: Trigger Meilisearch Sync Manually#

As mentioned earlier, you'll trigger the Meilisearch sync automatically when product events occur. You also want to allow admins to manually trigger a reindex.

In this step, you'll add the functionality to trigger the syncProductsWorkflow manually from the Medusa Admin dashboard. This requires:

  1. Creating a subscriber that listens to a custom meilisearch.sync event to trigger syncing products to Meilisearch.
  2. Creating an API route that the Medusa Admin dashboard can call to emit the meilisearch.sync event, which triggers the subscriber.
  3. Adding a new page or UI route to the Medusa Admin dashboard to allow admins to trigger the reindex.

Create Products Sync Subscriber#

A subscriber is an asynchronous function that listens to one or more events and performs actions when these events are emitted. A subscriber is useful when syncing data across systems, as the operation can be time-consuming and should be performed in the background.

Note: Learn more about subscribers in the Events and Subscribers documentation.

You create a subscriber in a TypeScript or JavaScript file under the src/subscribers directory. So, to create the subscriber that listens to the meilisearch.sync event, create the file src/subscribers/meilisearch-sync.ts with the following content:

src/subscribers/meilisearch-sync.ts
1import {2  SubscriberArgs,3  type SubscriberConfig,4} from "@medusajs/framework"5import { syncProductsWorkflow } from "../workflows/sync-products"6
7export default async function meilisearchSyncHandler({ 8  container,9}: SubscriberArgs) {10  const logger = container.resolve("logger")11  12  let hasMore = true13  let offset = 014  const limit = 5015  let totalIndexed = 016
17  logger.info("Starting product indexing...")18
19  while (hasMore) {20    const { result: { products, metadata } } = await syncProductsWorkflow(container)21      .run({22        input: {23          limit,24          offset,25        },26      })27
28    hasMore = offset + limit < (metadata?.count ?? 0)29    offset += limit30    totalIndexed += products.length31  }32
33  logger.info(`Successfully indexed ${totalIndexed} products`)34}35
36export const config: SubscriberConfig = {37  event: "meilisearch.sync",38}

A subscriber file must export:

  1. An asynchronous function, which is the subscriber that is executed when the event is emitted.
  2. A configuration object that holds the name of the event the subscriber listens to, which is meilisearch.sync in this case.

The subscriber function receives an object as a parameter that has a container property, which is the Medusa container.

In the subscriber function, you initialize variables to keep track of pagination and the total number of products indexed.

Then, you start a loop that retrieves products in batches of 50. It indexes them in Meilisearch using the syncProductsWorkflow. Finally, you log the total number of products indexed.

You'll learn how to emit the meilisearch.sync event next.

Tip: If you want to sync other data types, you can do it in this subscriber as well.

Create API Route to Trigger Sync#

To allow the Medusa Admin dashboard to trigger the meilisearch.sync event, you need to create an API route that emits the event.

An API Route is an endpoint that exposes commerce features to external applications and clients, such as storefronts.

Note: Learn more about API routes in this documentation.

An API route is created in a route.ts file under a subdirectory of the src/api directory. The path of the API route is the file's path relative to src/api.

So, to create an API route at the path /admin/meilisearch/sync, create the file src/api/admin/meilisearch/sync/route.ts with the following content:

src/api/admin/meilisearch/sync/route.ts
1import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"2import { Modules } from "@medusajs/framework/utils"3
4export async function POST(5  req: MedusaRequest,6  res: MedusaResponse7) {8  const eventModuleService = req.scope.resolve(Modules.EVENT_BUS)9  await eventModuleService.emit({10    name: "meilisearch.sync",11    data: {},12  })13  res.send({14    message: "Syncing data to Meilisearch",15  })16}

Since you export a POST route handler function, you expose a POST API route at /admin/meilisearch/sync. The route handler function accepts two parameters:

  1. A request object with details and context on the request, such as body parameters or authenticated user details.
  2. A response object to manipulate and send the response.

In the route handler, you use the Medusa container that is available in the request object. You resolve the Event Module. This module manages events and their subscribers.

Then, you emit the meilisearch.sync event using the Event Module's emit method. You pass it the event name.

Finally, you send a response with a message indicating that data is being synced to Meilisearch.

Add Meilisearch Sync Page to Admin Dashboard#

The last step is to add a new page to the admin dashboard. This page allows admins to trigger the reindex. You add a new page using a UI Route.

A UI route is a React component that specifies the content to be shown in a new page in the Medusa Admin dashboard. You'll create a UI route to display a button that triggers the reindex when clicked.

Note: Learn more about UI routes in the UI Routes documentation.

Configure JS SDK

Before creating the UI route, you'll configure Medusa's JS SDK. You can use it to send requests to the Medusa server from any client application, including your Medusa Admin customizations.

The JS SDK is installed by default in your Medusa application. To configure it, create the file src/admin/lib/sdk.ts with the following content:

src/admin/lib/sdk.ts
1import Medusa from "@medusajs/js-sdk"2
3export const sdk = new Medusa({4  baseUrl: import.meta.env.VITE_BACKEND_URL || "/",5  debug: import.meta.env.DEV,6  auth: {7    type: "session",8  },9})

You create an instance of the JS SDK using the Medusa class from the JS SDK. You pass it an object having the following properties:

  • baseUrl: The base URL of the Medusa server.
  • debug: A boolean indicating whether to log debug information into the console.
  • auth: An object specifying the authentication type. When using the JS SDK for admin customizations, you use the session authentication type.

Create UI Route

You'll now create the UI route that displays a button to trigger the reindex. You create a UI route in a page.tsx file under a subdirectory of src/admin/routes directory. The file's path relative to src/admin/routes determines its path in the dashboard.

So, to create a new page under the Settings section of the Medusa Admin, create the file src/admin/routes/settings/meilisearch/page.tsx with the following content:

src/admin/routes/settings/meilisearch/page.tsx
1import { Container, Heading, Button, toast } from "@medusajs/ui"2import { useMutation } from "@tanstack/react-query"3import { sdk } from "../../../lib/sdk"4import { defineRouteConfig } from "@medusajs/admin-sdk"5
6const MeilisearchPage = () => {7  const { mutate, isPending } = useMutation({8    mutationFn: () => 9      sdk.client.fetch("/admin/meilisearch/sync", {10        method: "POST",11      }),12    onSuccess: () => {13      toast.success("Successfully triggered data sync to Meilisearch") 14    },15    onError: (err) => {16      console.error(err)17      toast.error("Failed to sync data to Meilisearch") 18    },19  })20
21  const handleSync = () => {22    mutate()23  }24
25  return (26    <Container className="divide-y p-0">27      <div className="flex items-center justify-between px-6 py-4">28        <Heading level="h2">Meilisearch Sync</Heading>29      </div>30      <div className="px-6 py-8">31        <Button 32          variant="primary"33          onClick={handleSync}34          isLoading={isPending}35        >36          Sync Data to Meilisearch37        </Button>38      </div>39    </Container>40  )41}42
43export const config = defineRouteConfig({44  label: "Meilisearch",45})46
47export default MeilisearchPage

A UI route's file must export:

  1. A React component that defines the content of the page.
  2. A configuration object that specifies the route's label in the dashboard. This label is used to show a sidebar item for the new route.

In the React component, you use useMutation hook from @tanstack/react-query to create a mutation that sends a POST request to the API route you created earlier. In the mutation function, you use the JS SDK to send the request.

Then, in the return statement, you display a button that triggers the mutation when clicked, which sends a request to the API route you created earlier.

Test it Out#

You'll now test out the entire flow. Start by triggering the reindex manually from the Medusa Admin dashboard, then check the Meilisearch dashboard for the indexed products.

Run the following command to start the Medusa application:

Then, open the Medusa Admin at http://localhost:9000/app and log in with the credentials you set up in the first step.

Note: Can't remember the credentials? Learn how to create a user in the Medusa CLI reference.

After you log in, go to Settings from the sidebar. You'll find a new "Meilisearch" item in the Settings' sidebar. If you click on it, you'll find the page you created with the button to sync products to Meilisearch.

If you click on the button, the products will be synced to Meilisearch.

The Meilisearch Sync page in the Medusa Admin dashboard with a button to sync products to Meilisearch

You can check that the sync ran and was completed by checking the Medusa logs in the terminal where you started the Medusa application. You should find the following messages:

Terminal
info:    Processing meilisearch.sync which has 1 subscribersinfo:    Starting product indexing...info:    Successfully indexed 4 products

These messages indicate that the meilisearch.sync event was emitted, which triggered the subscriber you created to sync the products using the syncProductsWorkflow.

Finally, you can check the Meilisearch dashboard to see the indexed products. Open the Meilisearch dashboard, either Cloud or local, and choose the index you specified for products in the environment variable MEILISEARCH_PRODUCT_INDEX_NAME. You'll find your Medusa products indexed there.

The Meilisearch dashboard showing the indexed products


Step 5: Update Index on Product Changes#

You'll now automate the indexing of products whenever a change occurs. This includes when a product is created, updated, or deleted.

Similar to before, you'll create subscribers to listen to these events.

Handle Create and Update Products#

The action to perform when a product is created or updated is the same. You'll use the syncProductsWorkflow to sync the product to Meilisearch.

So, you only need one subscriber to handle these two events. To create the subscriber, create the file src/subscribers/product-sync.ts with the following content:

src/subscribers/product-sync.ts
1import {2  SubscriberArgs,3  type SubscriberConfig,4} from "@medusajs/framework"5import { syncProductsWorkflow } from "../workflows/sync-products"6
7export default async function handleProductEvents({8  event: { data },9  container,10}: SubscriberArgs<{ id: string }>) {11  await syncProductsWorkflow(container)12    .run({13      input: {14        filters: {15          id: data.id,16        },17      },18    })19}20
21export const config: SubscriberConfig = {22  event: ["product.created", "product.updated"],23}

The subscriber listens to the product.created and product.updated events. When either of these events is emitted, the subscriber triggers the syncProductsWorkflow to sync the product to Meilisearch.

When the product.created and product.updated events are emitted, the product's ID is passed in the event data payload. You can access this in the event.data property of the subscriber function's parameter.

So, you pass the product's ID to the syncProductsWorkflow as a filter to retrieve only the product that was created or updated.

Test it Out

To test it out, start the Medusa application:

Then, either create a product or update an existing one using the Medusa Admin dashboard. If you check the Meilisearch dashboard, you'll find that the product's index was created or updated.

Handle Product Deletion#

When a product is deleted, you need to remove it from the Meilisearch index. This requires a different action than creating or updating a product. You'll create a new workflow that deletes the product from Meilisearch, then create a subscriber that listens to the product.deleted event to trigger the workflow.

Create Delete Product Step

The workflow to delete a product from Meilisearch will have only one step that deletes products by their IDs from Meilisearch.

So, create the step at src/workflows/steps/delete-products-from-meilisearch.ts with the following content:

src/workflows/steps/delete-products-from-meilisearch.ts
1import {2  createStep,3  StepResponse,4} from "@medusajs/framework/workflows-sdk"5import { MEILISEARCH_MODULE } from "../../modules/meilisearch"6
7export type DeleteProductsFromMeilisearchStep = {8  ids: string[]9}10
11export const deleteProductsFromMeilisearchStep = createStep(12  "delete-products-from-meilisearch-step",13  async (14    { ids }: DeleteProductsFromMeilisearchStep,15    { container }16  ) => {17    const meilisearchModuleService = container.resolve(MEILISEARCH_MODULE)18    19    const existingRecords = await meilisearchModuleService.retrieveFromIndex(20      ids, 21      "product"22    )23    await meilisearchModuleService.deleteFromIndex(24      ids,25      "product"26    )27
28    return new StepResponse(undefined, existingRecords)29  },30  async (existingRecords, { container }) => {31    if (!existingRecords) {32      return33    }34    const meilisearchModuleService = container.resolve(MEILISEARCH_MODULE)35    36    await meilisearchModuleService.indexData(37      existingRecords,38      "product"39    )40  }41)

The step receives the IDs of the products to delete as an input.

In the step, you resolve the Meilisearch Module's service and retrieve the existing records from Meilisearch. This is useful to revert the deletion if an error occurs.

Then, you delete the products from Meilisearch and pass the existing records to the compensation function.

In the compensation function, you reindex the existing records if an error occurs.

Create Delete Product Workflow

You can now create the workflow that deletes products from Meilisearch. Create the file src/workflows/delete-products-from-meilisearch.ts with the following content:

src/workflows/delete-products-from-meilisearch.ts
1import { createWorkflow } from "@medusajs/framework/workflows-sdk"2import { deleteProductsFromMeilisearchStep } from "./steps/delete-products-from-meilisearch"3
4type DeleteProductsFromMeilisearchWorkflowInput = {5  ids: string[]6}7
8export const deleteProductsFromMeilisearchWorkflow = createWorkflow(9  "delete-products-from-meilisearch",10  (input: DeleteProductsFromMeilisearchWorkflowInput) => {11    deleteProductsFromMeilisearchStep(input)12  }13)

The workflow receives an object with the IDs of the products to delete. It then executes the deleteProductsFromMeilisearchStep to delete the products from Meilisearch.

Create Delete Product Subscriber

Finally, you'll create the subscriber that listens to the product.deleted event to trigger the above workflow.

Create the file src/subscribers/product-delete.ts with the following content:

src/subscribers/product-delete.ts
1import {2  SubscriberArgs,3  type SubscriberConfig,4} from "@medusajs/framework"5import { deleteProductsFromMeilisearchWorkflow } from "../workflows/delete-products-from-meilisearch"6
7export default async function productDeleteHandler({ 8  event: { data },9  container,10}: SubscriberArgs<{ id: string }>) {11  const logger = container.resolve("logger")12  13  logger.info(`Deleting product ${data.id} from Meilisearch`)14
15  await deleteProductsFromMeilisearchWorkflow(container)16    .run({17      input: {18        ids: [data.id],19      },20    })21}22
23export const config: SubscriberConfig = {24  event: "product.deleted",25}

The subscriber listens to the product.deleted event. When the event is emitted, the subscriber triggers the deleteProductsFromMeilisearchWorkflow, passing it the ID of the product to delete.

Test it Out

To test product deletion, start the Medusa application:

Then, delete a product from the Medusa Admin dashboard. If you check the Meilisearch dashboard, you'll find that the product index was deleted there as well.


Step 6: Search Products in Next.js Starter Storefront#

The last step is to provide search functionalities to customers on your storefront. In the first step, you installed the Next.js Starter Storefront along with the Medusa application.

In this step, you'll customize the Next.js Starter Storefront to add search functionality.

Reminder: 

The Next.js Starter Storefront was installed in a separate directory from Medusa. The directory's name is {your-project}-storefront.

So, if your Medusa application's directory is medusa-search, you can find the storefront by going back to the parent directory and changing to the medusa-search-storefront directory:

Terminal
cd ../medusa-search-storefront # change based on your project name

Install Meilisearch Packages#

Before adding the implementation of search functionality, you need to install the Meilisearch packages necessary to add search functionality in your storefront.

Run the following command in the directory of your Next.js Starter Storefront:

This installs the Meilisearch InstantSearch JavaScript library and the React InstantSearch library. You'll use these to build the search functionality.

Add Search Client Configuration#

Next, you need to configure the Meilisearch search client.

In src/lib/config.ts, add the following imports at the top of the file:

Storefront
src/lib/config.ts
1import { 2  instantMeiliSearch,3} from "@meilisearch/instant-meilisearch"

Then, add the following at the end of the file:

Storefront
src/lib/config.ts
1export const { searchClient } = instantMeiliSearch(2  process.env.NEXT_PUBLIC_MEILISEARCH_HOST || "",3  process.env.NEXT_PUBLIC_MEILISEARCH_API_KEY || ""4)

In the code above, you create a searchClient object that initializes the Meilisearch client with your Meilisearch host and API Key.

Set Environment Variables#

In the storefront's .env.local file, add the following Meilisearch-related environment variables:

Storefront
Code
1NEXT_PUBLIC_MEILISEARCH_HOST=your_meilisearch_host2NEXT_PUBLIC_MEILISEARCH_API_KEY=your_meilisearch_api_key3NEXT_PUBLIC_MEILISEARCH_INDEX_NAME=your-products-index-name

Where:

  • your_meilisearch_host is your Meilisearch host, as explained in the Add Environment Variables section earlier.
  • your_meilisearch_api_key is your Meilisearch API key with search permissions. You can retrieve it as explained in the Meilisearch Documentation.
  • your-products-index-name is the name of the index you created in Meilisearch to store the products. You can retrieve this as explained in the Add Environment Variables section earlier. You'll use this variable later.
Warning: Do not use the masterKey as the API key in the storefront, as it has all permissions, including write permissions. Only use an API key with search permissions.

Add Search Modal Component#

You'll now add a search modal component that customers can use to search for products. The search modal will display search results in real-time as the customer types in the search query.

Later, you'll add the search modal to the navigation bar. This allows customers to open the search modal from any page.

Create the file src/modules/search/components/modal/index.tsx with the following content:

Storefront
src/modules/search/components/modal/index.tsx
1"use client"2
3import React, { useEffect, useState } from "react"4import { Hits, InstantSearch, SearchBox } from "react-instantsearch"5import { searchClient } from "../../../../lib/config"6import Modal from "../../../common/components/modal"7import { Button } from "@medusajs/ui"8import Image from "next/image"9import Link from "next/link"10import { usePathname } from "next/navigation"11
12type Hit = {13  id: string;14  title: string;15  description: string;16  handle: string;17  thumbnail: string;18  categories: {19    id: string20    name: string21    handle: string22  }[]23  tags: {24    id: string25    value: string26  }[]27}28
29export default function SearchModal() {30  const [isOpen, setIsOpen] = useState(false)31  const pathname = usePathname()32
33  useEffect(() => {34    setIsOpen(false)35  }, [pathname])36
37  return (38    <>39      <div className="hidden small:flex items-center gap-x-6 h-full">40        <Button 41          onClick={() => setIsOpen(true)} 42          variant="transparent"43          className="hover:text-ui-fg-base text-small-regular px-0 hover:bg-transparent focus:!bg-transparent"44        >45          Search46        </Button>47      </div>48      <Modal isOpen={isOpen} close={() => setIsOpen(false)}>49        <InstantSearch 50          // @ts-expect-error - searchClient type issue51          searchClient={searchClient} 52          indexName={process.env.NEXT_PUBLIC_MEILISEARCH_INDEX_NAME}53        >54          <SearchBox className="w-full [&_input]:w-[94%] [&_input]:outline-none [&_button]:w-[3%]" />55          <Hits hitComponent={Hit} />56        </InstantSearch>57      </Modal>58    </>59  )60}61
62const Hit = ({ hit }: { hit: Hit }) => {63  return (64    <div className="flex flex-row gap-x-2 mt-4 relative" key={hit.id}>65      <Image src={hit.thumbnail} alt={hit.title} width={100} height={100} />66      <div className="flex flex-col gap-y-1">67        <h3>{hit.title}</h3>68        <p className="text-sm text-gray-500">{hit.description}</p>69      </div>70      <Link href={`/products/${hit.handle}`} className="absolute right-0 top-0 w-full h-full" aria-label={`View Product: ${hit.title}`} />71    </div>72  )73}

You create a SearchModal component that displays a search box and search results using widgets from the react-instantsearch library.

To display each result item (or hit), you create a Hit component. This component displays the product's title, description, and thumbnail. You also add a link to the product's page.

Finally, you show the search modal when the customer clicks a "Search" button. You'll add this button to the navigation bar next.

Add Search Button to Navigation Bar#

The last step is to show the search button in the navigation bar.

In src/modules/layout/templates/nav/index.tsx, add the following imports at the top of the file:

Storefront
src/modules/layout/templates/nav/index.tsx
import SearchModal from "@modules/search/components/modal"

Then, in the return statement of the Nav component, add the SearchModal component before the div surrounding the "Account" link:

Storefront
src/modules/layout/templates/nav/index.tsx
<SearchModal />

The search button will now appear in the navigation bar before the Account link.

Test it Out#

To test out the storefront changes and the search API route, start the Medusa application:

Then, start the Next.js Starter Storefront from its directory:

Next, go to localhost:8000. You'll find a Search button at the top right of the navigation bar. If you click on it, you can search through your products. You can also click on a product to view its page.

The Next.js Starter Storefront showing the search modal with search results


Next Steps#

You've now integrated Meilisearch with Medusa and added search functionality to your storefront. You can expand on these features to:

  • Add filters to the search results. You can do that using react-instantsearch widgets.
  • Support indexing other data types, such as product categories. You can create subscribers and workflows for categories similar to products.

Learn More about Medusa#

If you're new to Medusa, check out the main documentation, where you'll get a more in-depth understanding of all the concepts you've used in this guide and more.

To learn more about the commerce features that Medusa provides, check out Medusa's Commerce Modules.

Troubleshooting#

If you encounter issues during your development, check out the troubleshooting guides.

Getting Help#

If you encounter issues not covered in the troubleshooting guides:

  1. Visit the Medusa GitHub repository to report issues or ask questions.
  2. Join the Medusa Discord community for real-time support from community members.
Was this page helpful?
Ask Anything
FAQ
What is Medusa?
How can I create a module?
How can I create a data model?
How do I create a workflow?
How can I extend a data model in the Product Module?
Recipes
How do I build a marketplace with Medusa?
How do I build digital products with Medusa?
How do I build subscription-based purchases with Medusa?
What other recipes are available in the Medusa documentation?
Chat is cleared on refresh
Line break