Migrate from Meilisearch to Medusa Search

In this guide, you'll migrate a Medusa project that indexes and searches products in Meilisearch to use Medusa Search.

Not a Cloud user yet? Sign up for Cloud to use Medusa Search and other Cloud features.

Migrate with an AI Agent#

If you use an AI agent, such as Claude Code or Codex, ask it to fetch this page:

Terminal
Fetch https://docs.medusajs.com/cloud/search/migrate-from-meilisearch and follow the instructions.

It will receive the following prompt with instructions on performing the full migration:

Migration Prompt
<role>You are a senior Medusa developer migrating an existingMedusa project from a custom Meilisearch integration toMedusa Search, the managed search service of Cloud.</role>
<task>Replace this project's custom Meilisearch integration withthe Search Module and Medusa Search, keeping the storefront'ssearch behavior intact.</task>
<context>- Medusa Search is a provider of the Search Module, which  ships with Medusa v2.21.1 and later. Cloud registers and  configures it for every environment, so it needs no  credentials or configuration in the project.- Medusa declares no index by default, so a project  declares its own `product` index definition under  `src/search`. A project installed after v2.21.1 ships with  one at `src/search/product.ts`, and any other project may  or may not have it.- An index definition lives in a file under `src/search`,  declares the fields the engine holds, fills the index with  a `seed` async generator, and keeps it current with its  `events` and `consume` properties.- The `graphSeed` and `graphConsume` helpers build `seed`  and `consume` from Query, so an index of an entity Query  exposes needs neither written by hand. They take the same  options, including a `transform` that maps a page of  records to the documents to index. Every document must  carry the record's primary key as `id`, and a record you  leave out of the returned array stays out of the index.- Medusa's Store API has a `POST /store/search` route that  searches any index of the application, so a project  doesn't need its own product search route. The body is a  search query naming the index in `entity`, or a batch of  them under `queries`.- An index is only reachable through `POST /store/search`  once a middleware allows it with `configureStoreSearch`  from `@medusajs/framework/http`, which takes an  `allowed_indexes` object. The route narrows a product  index to published products in the publishable API key's  sales channels, and any further constraint is that  middleware's `filters` option.- Queries run through `query.search` in an API route. Medusa  Search has no browser-side querying and no search-only  key, so the storefront searches through the Medusa backend  rather than the engine.- The `@medusajs/instantsearch-adapter` package is a search  client for InstantSearch widgets, so a storefront built on  `react-instantsearch` keeps its widgets and swaps the  client.- Medusa Search does not support synonyms, custom stop-word  lists, geo search, configurable ranking rules, engine-side  embedders, or sorting alongside a text query. An index or a  query that relies on them fails.- Medusa Search supports typo tolerance and highlighting, but  unlike Meilisearch both are opt-in per query through  `search_options`. Pass them in the search query to keep the  storefront's current behavior.</context>
<steps>1. Inspect the project and report what it has before you   change anything: the Meilisearch module, the sync   workflows and their steps, the subscribers, the admin sync   route and UI route, the storefront's search client and   search modal, and the Medusa version in `package.json`.2. If the Medusa version is below v2.21.1, stop and tell the   user to upgrade first.3. List the fields the project indexes in Meilisearch, taken   from the `fields` array of `syncProductsWorkflow`, and ask   the user for the index settings of their Meilisearch   instance. They can read them from   `GET /indexes/{index}/settings`.4. Check whether the project already declares a `product`   index, which ships at `src/search/product.ts`. If it   does and every indexed field and setting is covered by   it, skip to step 8 and say why. Otherwise, the project   needs an index definition of its own.5. Create the index definition under `src/search`. Map   Meilisearch's `searchableAttributes` to `searchable()`   fields with a weight, `filterableAttributes` to   `facetable()` or `filterable()`, `sortableAttributes` to   `sortable()`, and the attributes missing from   `displayedAttributes` to `retrievable(false)`. Build   `seed` and `consume` with `graphSeed` and `graphConsume`,   sharing one options object between them, and reproduce   the project's indexing rules in its `transform`, such as   leaving a product that isn't published out of the   documents it returns. List   the entity's create, update, and delete events in   `events`.6. Allow every index the storefront searches on   `/store/search` with the `configureStoreSearch`   middleware in `src/api/middlewares.ts`. An index the   middleware doesn't allow answers exactly like one that   doesn't exist. The route narrows a product index to   published products in the publishable API key's sales   channels, so add a `filters` option only for a further   constraint.7. Don't create a product search route, since   `POST /store/search` serves the storefront. Only write a   route with `query.search` for a result the built-in route   can't answer with, such as one reshaped for the   storefront. In such a route, respond with the hydrated   records from `data` and the metadata from `search_result`,   and don't also return `search_result.hits`, since each   hit's `document` repeats a record already returned.8. Replace any manual sync trigger with a workflow step that   calls the Search Module's `reindex` method.9. Delete the Meilisearch module, the sync workflows and   steps, the product subscribers, the module's entry in   `medusa-config.ts`, and the `meilisearch` dependency.   Do this here and not earlier, since the application fails   to boot while the module is still registered and the   package is gone. Leave the Meilisearch environment   variables in place and tell the user to remove them after   the cutover.10. Update the storefront to search through Medusa. If it    uses InstantSearch widgets, install    `@medusajs/instantsearch-adapter`, create its search    client with the storefront's JS SDK instance and the    path `/store/search`, pass the client and the index    name to the existing `InstantSearch` provider, and    change every component that reads a hit's `_formatted`    fields to read its `highlights` instead. Otherwise, post    to `/store/search` with the JS SDK and read the `hits`    and `metadata` it returns. Remove the    `@meilisearch/instant-meilisearch` package, the    Meilisearch `searchClient` export, and the    `NEXT_PUBLIC_MEILISEARCH_*` variables. Keep    `react-instantsearch`, which the adapter works with.11. Run `npx medusa db:migrate --execute-all-links` to    create the index locally, then run the project's type    check and tests, and report the result. Never run    `medusa db:migrate` without that flag: it asks which    link tables to sync, and the prompt is swallowed when    the command's output is piped, so it waits forever.</steps>
<constraints>- Do not add a feature that Medusa Search does not support.  When the project relies on one, report it and propose the  closest alternative instead of implementing it silently.- Do not delete the user's Meilisearch instance, indexes, or  keys, and do not call the Meilisearch API.- Do not expose the search index to the browser. Every query  goes through the Medusa backend.- Do not modify `POST /store/search` or recreate it in the  project.- Do not refine a widget or a filter on a field the index  definition doesn't mark `searchable()`, `filterable()`,  `facetable()`, or `sortable()`.- Do not run a deployment or push to any branch.- Never run a Medusa CLI command that can prompt without the  flag that skips its prompts. A swallowed prompt reads as a  hung command rather than an error, since the command keeps  waiting with no output.- Keep every package the current integration imports  installed until step 9, so the application boots at every  point before it.- Consult the Medusa documentation at  https://docs.medusajs.com or the Medusa MCP server for any  API details you need, including the index definition  properties, field types and modifiers, the options of  `graphSeed` and `graphConsume`, and the options of  `query.search`.</constraints>
<error_handling>- If the project has no Meilisearch integration, report that  and stop.- If the integration differs from the files listed in step  1, map each responsibility you find to its Search Module  equivalent and report the mapping before you change code.- If you cannot tell whether a field is searched, filtered,  faceted, or only displayed, ask the user rather than  guessing its modifiers.- If a storefront feature has no Medusa Search equivalent,  list it under manual follow-ups instead of removing the  feature. The adapter doesn't support `geoSearch`, Insights  and Analytics widgets, Query Rules, related-items widgets,  autocomplete, or Algolia-style `filters` strings.- If the storefront's search can't be expressed with the  built-in route's parameters, ask the user before adding a  custom search route for products.</error_handling>
<output_format>Respond with the following markdown sections:
## ChangesA table of every file you created, modified, or deleted,with one sentence on what changed in it.
## Behavior differencesEach Meilisearch behavior the project relied on that MedusaSearch does not support, and what you did about it.
## Manual follow-upsThe steps the user has to take themselves, such as removingenvironment variables, validating relevance, and deploying.</output_format>
<success_criteria>- The project builds and type checks with no reference to  `meilisearch` or `@meilisearch/instant-meilisearch` left  in the backend or the storefront.- Every field the project indexed in Meilisearch is either  held by the new index definition, covered by the default  product index, or listed as an intentional removal.- Every index the storefront searches is allowed in a  `configureStoreSearch` middleware on `/store/search`.- The storefront searches products through  `POST /store/search`, and no Meilisearch key reaches the  browser.- Every remaining search route uses `query.search` and  returns each record once.- Every unsupported Meilisearch behavior appears under  "Behavior differences".</success_criteria>

Who This Guide Is For#

This guide assumes your project follows the Integrate Meilisearch with Medusa guide, which is the most common Meilisearch setup in Medusa projects. That setup has the following pieces:

  • A Meilisearch Module in src/modules/meilisearch that wraps Meilisearch's client.
  • A syncProductsWorkflow and its steps that index and delete products in Meilisearch.
  • Subscribers on product events and a custom meilisearch.sync event.
  • An admin UI route and API route that trigger a full sync.
  • A storefront that queries Meilisearch directly from the browser with @meilisearch/instant-meilisearch and a search key.

If your integration differs, the mapping in the Mapping MeiliSearch Settings to Medusa Search section still applies, since every Meilisearch integration owns the same responsibilities.

Not sure whether to migrate? Refer to Medusa Search vs Meilisearch for a feature-by-feature comparison, including the Meilisearch features that Medusa Search doesn't offer.

What Changes in Your Project#

Medusa Search isn't a drop-in replacement for the Meilisearch client. It's a provider of the Search Module, which owns the parts of the integration you wrote by hand. So the migration deletes more code than it adds.

Responsibility

Your Meilisearch integration

Medusa Search

Running the engine

A self-hosted instance or a Meilisearch Cloud project, which you upgrade and monitor.

Cloud provisions the service and passes the credentials, so nothing to run.

Connecting to the engine

The Meilisearch Module's service, its options, and three environment variables.

Nothing to write.

Index schema and settings

Index settings stored on the Meilisearch instance, outside your repository.

An index definition in src/search, which deploys with your code.

Initial and full sync

syncProductsWorkflow, the meilisearch.sync subscriber, the admin API route, and the admin UI route.

The index definition's seed generator, which Medusa runs on deployment, plus the reindex method for a manual rebuild.

Incremental sync

The product-sync and product-delete subscribers, and the delete workflow.

The events and consume properties of the index definition.

Rollback on a failed write

Compensation functions that reindex the previous documents.

Not needed. A failed consume leaves the index behind, and a reindex repairs it.

Waiting on a write

Meilisearch queues every write as a task, so a document isn't searchable the moment the call returns.

Medusa Search doesn't defer its writes, so there's no task to poll.

Querying

The storefront's Meilisearch client, calling the engine from the browser with a search key.

Medusa provides the /store/search route out-of-the-box, and you can search as well with query.search.

Per-environment isolation

An index name or an instance per environment, provisioned and configured by you.

One set of indexes per environment, created for you.

Behavior Differences to Settle First#

Some Meilisearch behaviors have no equivalent in Medusa Search, and an index or a query that relies on them fails rather than degrading. Decide what to do about each one before you write any code:

Behavior

What to do

Synonyms and stop words

Medusa Search supports neither, and an index that declares them fails at startup. Expand a query's terms in your API route, or index the alternative terms as a searchable field.

Ranking rules

Medusa Search ranks by the weight you set on each searchable field. Meilisearch's rankingRules order, including a custom attribute:asc rule, has no equivalent, so reproduce the ordering you need in the API route.

Sorting by an attribute

Medusa Search sorts by any number of sortable fields, but not alongside a text query, which Meilisearch allows. A sorted product listing therefore runs as a separate query from a search, without a q filter.

Hybrid search and embedders

On the Scale and Enterprise plans, Medusa Search runs vector search on embeddings you compute and store in the index, and it blends text and vector results by a semantic ratio the same way. On the Enterprise plan it can also create the embeddings for you, so you don't configure an embedder yourself.

Geo search

Medusa Search doesn't support geo fields, and an index that declares one fails at startup. Filter on a region or a country field instead of a _geo attribute.

Tenant tokens

Medusa Search has no equivalent, since the engine never sees a request from the browser. Apply the tenant's filter in your API route, where you already know the authenticated customer.


Step 1: Upgrade Your Medusa Application#

Medusa Search requires Medusa v2.21.1 or later. Upgrade your application, then confirm it starts locally before you change any search code.

Refer to the Update Medusa guide for the upgrade steps.

Once you upgrade, the Search Module is registered by default with the PostgreSQL Search Module Provider locally, and with Medusa Search on Cloud. So you don't have to register anything in medusa-config.ts.


Step 2: Declare Your Index#

Medusa declares no index by default, so your application declares its own product index in a file under src/search. If you installed your Medusa application after v2.21.1, it already has that definition at src/search/product.ts. Otherwise, check whether the file is there, and declare the index if it isn't.

Customize your product index definition, or declare another index, in the following cases:

  • Your Meilisearch documents hold custom fields that your product index doesn't. You can then add those fields to the definition.
  • You need index settings or field options that your definition doesn't apply, such as a different weight, a facetable field, or a Medusa Search setting.
  • You indexed data models other than products in Meilisearch, such as product categories. You then declare an index for each of them.

Otherwise, skip to Step 3 and query the product index you already have.

An index definition replaces your Meilisearch index's settings, holding the schema, the initial fill, and the event handling in one file that deploys with your code. The definition alone decides what the index holds, so customize src/search/product.ts rather than adding a second definition named product.

Tip: The example index below is simplified to show one feature at a time. For examples of indexing prices in multiple currencies, option values, categories, and other product data, refer to the Product Index Examples guide.

For example, the integration guide indexes each product's id, title, description, handle, thumbnail, categories, and tags, and it indexes published products only. To reproduce that, create the file src/search/product.ts with the following content:

src/search/product.ts
1import {2  defineSearchIndex,3  graphConsume,4  graphSeed,5  search,6} from "@medusajs/framework/utils"7
8const source = {9  fields: [10    "id",11    "title",12    "description",13    "handle",14    "thumbnail",15    "status",16    "categories.id",17    "categories.name",18    "categories.handle",19    "tags.id",20    "tags.value",21  ],22  transform: (products) => {23    return products24      .filter((p) => p.status === "published")25      .map(({ status, ...product }) => product)26  },27}28
29export const productIndex = defineSearchIndex({30  name: "product",31  entity: "product",32  fields: search.define({33    id: search.keyword().filterable(),34    title: search.text().searchable({ weight: 3 }),35    description: search.text().searchable(),36    handle: search.keyword().filterable(),37    thumbnail: search.keyword(),38    categories: search39      .object({40        id: search.keyword().filterable(),41        name: search42          .keyword()43          .searchable()44          .facetable(),45        handle: search.keyword().filterable(),46      })47      .array(),48    tags: search49      .object({50        id: search.keyword().filterable(),51        value: search52          .keyword()53          .searchable()54          .facetable(),55      })56      .array(),57  }),58  events: [59    "product.created",60    "product.updated",61    "product.deleted",62  ],63  consume: graphConsume(source),64  seed: graphSeed(source),65})

The definition declares the fields the engine holds, then lets the graphSeed and graphConsume helpers read those fields from Query. Note the following about it:

  • graphSeed fills the index for the first time, reading the catalog in batches and resuming an interrupted run, so you don't paginate the read yourself.
  • graphConsume replaces your product-sync and product-delete subscribers. Medusa routes each event in events to it, and it upserts or deletes the affected product's document.
  • Both helpers share the same source, so an event writes the same document the seed does. Its transform indexes published products only, which is the check the syncProductsWorkflow performs in its transform function, and leaving any other product out of the documents it returns deletes that product's document, the way deleteProductsFromAlgoliaStep did.

If you're creating a custom index for Medusa Search, you'll need to map each Meilisearch setting to its equivalent in Medusa Search:

Meilisearch

Medusa Search

searchableAttributes

A search.text() field with the searchable() modifier. The attribute's position in Meilisearch's ordered list becomes a weight.

filterableAttributes

The facetable() modifier for an attribute the storefront shows counts for, and filterable() for one you only filter on. Meilisearch uses one setting for both, so check which attributes your storefront facets on.

sortableAttributes

The sortable() modifier, with the caveat that a sort doesn't run alongside a text query.

displayedAttributes

The retrievable() modifier set to false on every attribute you left out of the list.

rankingRules

A weight per searchable field. Medusa Search has no equivalent for the rest of Meilisearch's ranking criteria.

distinctAttribute

The distinct_attribute index setting, or the distinct search option for a single query.

faceting.maxValuesPerFacet

The faceting.max_values_per_facet index setting, alongside faceting.sort_by.

pagination.maxTotalHits

The pagination.max_total_hits index setting. Medusa Search caps a query at the 10,000th result regardless.

typoTolerance

The typo_tolerance index setting, which carries the same min_word_size_for_one_typo and min_word_size_for_two_typos thresholds. disableOnAttributes becomes disabled_on_attributes, and disableOnWords has no equivalent. A query then opts in with the typo_tolerance search option, which Meilisearch didn't require.

synonyms and stopWords

No equivalent. Settle them as explained in Behavior Differences to Settle First.

Primary key

The index's primary_key, which defaults to id. Since the integration guide indexes each product under its own ID, you can keep the default.

One index or instance per environment

One index definition. Cloud scopes the physical indexes per environment, so the MEILISEARCH_PRODUCT_INDEX_NAME variable goes away.

Test the Index Locally#

Locally, the PostgreSQL provider holds the index in your database, so run the migrations that create it:

Then start your application. The Search Module fills the index when the application starts in worker or shared mode. You no longer need a Meilisearch instance running on your machine to develop against search.


Step 3: Replace the Search API Route#

Medusa's Store API has a POST /store/search API route that searches any index of your application, including the product index you declared in Step 2.

An index isn't searchable through the route until a middleware allows it, so add the configureStoreSearch middleware in src/api/middlewares.ts:

src/api/middlewares.ts
1import {2  configureStoreSearch,3  defineMiddlewares,4} from "@medusajs/framework/http"5
6export default defineMiddlewares({7  routes: [8    {9      matcher: "/store/search",10      middlewares: [11        configureStoreSearch({12          allowed_indexes: {13            product: true,14          },15        }),16      ],17    },18  ],19})

The route narrows a product index to published products in the publishable API key's sales channels. Learn more in the Store Search API Route guide.

Then, you can delete the route you previously created for searching products, and use the built-in route instead:

Terminal
MEDUSA_URL=https://your-project.medusajs.app
curl -X POST "$MEDUSA_URL/store/search" \  -H "x-publishable-api-key: {your_publishable_api_key}" \  -H "Content-Type: application/json" \  --data '{    "entity": "product",    "filters": { "q": "sweatshirt" },    "pagination": { "take": 20 }  }'

The route returns a JSON response like the following:

Code
1{2  "results": [3    {4      "hits": [5        {6          "id": "prod_01KXR3J9J610DT161E2E4ZS6P1",7          "score": 1.42,8          "document": {9            "id": "prod_01KXR3J9J610DT161E2E4ZS6P1",10            "title": "Medusa Sweatshirt",11            "description": "A classic sweatshirt, reimagined.",12            "handle": "sweatshirt",13            "thumbnail": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatshirt-vintage-front.png"14          }15        }16      ],17      "metadata": {18        "skip": 0,19        "take": 20,20        "count": 12,21        "query": "sweatshirt",22        "processing_time_ms": 723      }24    }25  ]26}

The route answers with one result per posted query, each holding the matching hits ranked by relevance and the query's metadata. Where Meilisearch reported estimatedTotalHits, the count is in metadata.count. Step 4 provides guidance on updating your storefront to read the new response shape.

Search a Custom Index#

If you indexed data models other than products in Meilisearch, such as brands, create an API route that searches their index with query.search.

For example, to search a brand index, create the file src/api/store/brands/search/route.ts with the following content:

src/api/store/brands/search/route.ts
1import {2  MedusaRequest,3  MedusaResponse,4} from "@medusajs/framework/http"5import {6  ContainerRegistrationKeys,7} from "@medusajs/framework/utils"8import { z } from "@medusajs/framework/zod"9
10export const SearchSchema = z.object({11  query: z.string(),12  limit: z.number().optional().default(20),13  offset: z.number().optional().default(0),14})15
16type SearchRequest = z.infer<typeof SearchSchema>17
18export async function POST(19  req: MedusaRequest<SearchRequest>,20  res: MedusaResponse21) {22  const query = req.scope.resolve(23    ContainerRegistrationKeys.QUERY24  )25
26  const { query: q, limit, offset } = req.validatedBody27
28  const { data, search_result } = await query.search({29    entity: "brand",30    fields: ["id", "name", "country"],31    filters: { q },32    pagination: { skip: offset, take: limit },33  })34
35  res.json({36    brands: data,37    metadata: search_result.metadata,38  })39}

The route resolves Query from the Medusa container and searches the brand index, which is the name of its definition. query.search returns the hydrated entities in data and the engine's own result in search_result.

Add Filters, Facets, and Sorting#

You can apply filters, facets, and sorting directly in the API route using the filters and search_options parameters of query.search.

For example, to filter the brands by country and return the country counts:

src/api/store/brands/search/route.ts
1const { data, search_result } = await query.search({2  entity: "brand",3  fields: ["id", "name", "country"],4  filters: {5    q,6    country: req.validatedBody.country,7  },8  search_options: {9    facets: ["country"],10    disjunctive_facets: true,11  },12  pagination: { skip: offset, take: limit },13})

disjunctive_facets keeps the sibling country counts visible while a country filter is active, which is what a filter sidebar needs. Refer to Search Queries for every filter operator, facet type, and sorting option.

Note: Medusa Search can't sort by a field alongside a text query. If your storefront offers "sort by price" on a search results page, run the sorted listing as a separate query without a q filter.

Step 4: Update the Storefront#

Medusa provides an InstantSearch adapter for connecting your storefront to the Medusa backend and searching products. You can use the same widgets for displaying search results as you did with Meilisearch.

In your storefront, install the adapter:

You should also install InstantSearch packages specific for your frontend framework if you haven't already, such as react-instantsearch or vue-instantsearch.

Then, replace the Meilisearch searchClient with the adapter's client. For example, create the file src/lib/search-client.ts with the following content:

src/lib/search-client.ts
1import {2  createInstantSearchAdapter,3} from "@medusajs/instantsearch-adapter"4// JS SDK instance5import { sdk } from "./config"6
7export const PRODUCT_INDEX_NAME = "product"8
9export const { searchClient } = createInstantSearchAdapter({10  sdk,11  path: "/store/search",12})

The adapter sends the requests with the Medusa JS SDK instance your storefront already exports, so the publishable API key and the other headers are set for you.

Next, pass the client to your existing InstantSearch provider and set indexName to the name of your index definition:

src/components/search/index.tsx
1import {2  Configure,3  InstantSearch,4  SearchBox,5} from "react-instantsearch"6import {7  PRODUCT_INDEX_NAME,8  searchClient,9} from "../../lib/search-client"10
11const Search = () => (12  <InstantSearch13    indexName={PRODUCT_INDEX_NAME}14    searchClient={searchClient}15  >16    <Configure hitsPerPage={12} />17    <SearchBox />18    {/* your existing hit list, filters, and pagination widgets */}19  </InstantSearch>20)21
22export default Search

Finally, clean up what's left behind from the Meilisearch integration:

  1. Remove the Meilisearch searchClient from src/lib/config.ts, along with the @meilisearch/instant-meilisearch dependency and environment variables like NEXT_PUBLIC_MEILISEARCH_*.
  2. Change every component that reads a hit's objectID to read id instead, since the adapter builds a hit from the document your index holds.
  3. Confirm each field your widgets filter, facet, or sort on is marked filterable(), facetable(), or sortable() in the index definition you wrote in Step 2. A widget can't refine on a field the index doesn't hold that way.

Refer to the InstantSearch Adapter guide for the full setup, the adapter's configuration options, and the widgets it supports. For filters, facets, sorting, and pagination, refer to the Filtering, Sorting, and Pagination example.


Step 5: Replace the Manual Sync#

The integration guide's admin page triggers a full sync through the meilisearch.sync event. You no longer need that sync for everyday changes:

  1. Medusa fills the index on deployment.
  2. Medusa keeps the index current from the events and consume properties of the index definition, applying every product change as it happens. This applies for both the product index Medusa creates by default or the custom definition you wrote in Step 2.

A manual rebuild is only for repairing a diverged index, such as after a consume call failed or a change reached the database without emitting an event.

For a manual rebuild, you can drop the custom admin page entirely: admin users can rebuild any index from Settings -> Search in the Medusa Admin dashboard. Refer to the Manage Search Indexes user guide.

Triggering a Manual Rebuild in Code#

If you'd rather trigger the rebuild from your own code, replace the syncProductsWorkflow with a step that calls the Search Module's reindex method.

For example, create the file src/workflows/steps/reindex-products.ts with the following content:

src/workflows/steps/reindex-products.ts
1import { Modules } from "@medusajs/framework/utils"2import {3  createStep,4  StepResponse,5} from "@medusajs/framework/workflows-sdk"6
7export const reindexProductsStep = createStep(8  "reindex-products",9  async (_, { container }) => {10    const searchModuleService = container.resolve(11      Modules.SEARCH12    )13
14    const result = await searchModuleService.reindex({15      index: "product",16    })17
18    return new StepResponse(result)19  }20)

In this example, you use the reindex method from the Search Module to trigger a manual rebuild of the product index. You can then use this step in a workflow to programmatically rebuild the index whenever needed.

Note: reindex waits for the rebuild to finish before it resolves, so run it in a workflow rather than in the request handler of your admin API route. Refer to Long-Running Workflows if the rebuild outlasts your request timeout.

Step 6: Remove the Meilisearch Integration#

Once your searches run against the new index, delete the code that Medusa Search replaces. From a project that followed the integration guide, remove the following:

What to remove

Why

src/modules/meilisearch

The Search Module and its provider replace the client wrapper.

The module's entry in the modules array of medusa-config.ts

Removing the directory without this leaves your application failing to boot.

src/workflows/sync-products.ts, src/workflows/delete-products-from-meilisearch.ts, and their steps

The index definition's seed and consume functions replace them.

src/subscribers/meilisearch-sync.ts, src/subscribers/product-sync.ts, and src/subscribers/product-delete.ts

Medusa subscribes to the events in the definition's events array.

The meilisearch dependency

Nothing in the backend calls Meilisearch anymore.

The MEILISEARCH_HOST, MEILISEARCH_API_KEY, and MEILISEARCH_PRODUCT_INDEX_NAME variables

Remove them from every environment, including the Cloud environment variables. Delete them last, after the cutover.


Step 7: Deploy and Fill the Index to Preview#

With the backend and the storefront changes in place, push your changes to the branch of a Cloud environment. Use a preview or staging rather than production for the first deployment, so you can make sure everything works as expected.

During the deployment, Cloud runs the search migrations, which create the physical index for your definition. The Search Module then fills it with the documents that seed yields when the application starts.

Once your application is deployed, you can check the Medusa Admin dashboard for the status of the search index by going to Settings -> Search. Learn more in the User Guide.

If the index status is successful, it means the seeding has finished. You can test it out either by sending a request to the Store Search API route, or by using the storefront search interface to confirm that products appear as expected.


Step 8: Deploy Your Index#

Once the preview environment's search holds up, merge your changes into your production environment. Cloud builds and deploys your project, creates the production index, and fills it with the documents that seed yields.


Next Steps#

Was this guide helpful?
Ask Bloom
For assistance in your development, use Claude Code Plugins or Medusa MCP server in Cursor, VSCode, etc...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