Migrate from Algolia to Medusa Search
In this guide, you'll migrate a Medusa project that indexes and searches products in Algolia to use Medusa Search.
Migrate with an AI Agent#
If you use an AI agent, such as Claude Code or Codex, ask it to fetch this page:
It will receive the following prompt with instructions on performing the full migration:
<role>You are a senior Medusa developer migrating an existingMedusa project from a custom Algolia integration to MedusaSearch, the managed search service of Cloud.</role> <task>Replace this project's custom Algolia search integrationwith the Search Module and Medusa Search, keeping thestorefront's search 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. There is no browser-side querying and no search-only key.- 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, merchandising rules, geo search, or search analytics. An index or a query that relies on them fails.- Medusa Search supports typo tolerance and highlighting, but both are opt-in per query through `search_options` and both require a text query.</context> <steps>1. Inspect the project and report what it has before you change anything: the Algolia module, the sync workflows and their steps, the subscribers, the admin sync route and UI route, the search API route, the storefront search client, 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 Algolia, taken from the sync workflow's `fields` array, and ask the user for the index settings from their Algolia dashboard.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 6 and say why. Otherwise, the project needs an index definition of its own.5. Create the index definition under `src/search`. Map Algolia's searchable attributes to `searchable()` fields with a weight, faceting attributes to `facetable()` or `filterable()`, sorting replicas to `sortable()`, and unretrievable attributes 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. Delete the project's product search API route and its validation middleware, since `POST /store/search` replaces it. 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 Algolia module, the sync workflows and steps, the product subscribers, the module's entry in `medusa-config.ts`, and the `algoliasearch` 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 Algolia 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 `objectID` to read `id`. Otherwise, post to `/store/search` with the JS SDK and read the `hits` and `metadata` it returns. Remove the Algolia packages, the Algolia search client, and the Algolia environment variables from the storefront.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 Algolia account, indexes, or credentials, and do not call the Algolia API.- 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 Algolia 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 `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 Algolia 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 `algoliasearch` left in the backend or the storefront.- Every field the project indexed in Algolia 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 renders the hits it returns.- Every remaining search route uses `query.search` and returns each record once.- Every unsupported Algolia behavior appears under "Behavior differences".</success_criteria>
Who This Guide Is For#
This guide assumes your project follows the Integrate Algolia with Medusa guide, which is the most common Algolia setup in Medusa projects. That setup has the following pieces:
- An Algolia Module in
src/modules/algoliathat wraps Algolia's client. - A
syncProductsWorkflowand its steps that index and delete products in Algolia. - Subscribers on product events and a custom
algolia.syncevent. - An admin UI route and API route that trigger a full sync.
- A
/store/searchAPI route that the storefront calls. - A storefront search client that sends every query to that route.
If your integration differs, the mapping in the Map Your Integration to Medusa Search section still applies, since every Algolia integration owns the same responsibilities.
What Changes in Your Project#
Medusa Search isn't a drop-in replacement for the Algolia 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 Algolia integration | Medusa Search |
|---|---|---|
Connecting to the engine | The Algolia Module's service, its options, and three environment variables. | Cloud provisions the service and passes the credentials, so nothing to write. |
Index schema | Algolia's dashboard settings, outside your repository. | An index definition in |
Initial and full sync |
| The index definition's |
Incremental sync | The | The |
Rollback on a failed write | Compensation functions that reindex the previous documents. | Not needed. A failed |
Querying | The module service's | Medusa provides the |
Per-environment isolation | An index name per environment, provisioned and configured by you. | One set of indexes per environment, created for you. |
Behavior Differences to Settle First#
Some Algolia 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 | Medusa Search doesn't support synonyms, 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 and merchandising | Medusa Search ranks by the weight you set on each searchable field. Reproduce pinned or boosted results with your own logic in the API route. |
Sorting by an attribute | Medusa Search sorts by any number of |
Search analytics and A/B testing | Medusa Search doesn't provide them. Track queries in your own analytics from the API route. |
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.
If you already have the product index definition, you can customize it, or declare a new one in the following cases:
- Your Algolia records hold custom fields that your
productindex 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 Algolia, 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 Algolia index's configuration, 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.
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:
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:
graphSeedfills the index for the first time, reading the catalog in batches and resuming an interrupted run, so you don't paginate the read yourself.graphConsumereplaces yourproduct-syncandproduct-deletesubscribers. Medusa routes each event ineventsto 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. Itstransformindexes published products only, which is the check thesyncProductsWorkflowperforms in itstransformfunction, and leaving any other product out of the documents it returns deletes that product's document, the waydeleteProductsFromAlgoliaStepdid.
Mapping Your Algolia Index to Medusa Search#
If you're creating a custom index for Medusa Search, you'll need to map each Algolia setting to its equivalent in Medusa Search:
Algolia | Medusa Search |
|---|---|
| A |
| The |
| A |
Replica indexes for sorting | The |
| The |
| The |
| The index's |
One index per environment | One index definition. Cloud scopes the physical indexes per environment, so the |
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.
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:
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:
The route returns a JSON response like the following:
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. It's close to the shape Algolia returned, with a hit's fields under document and its identifier in id rather than objectID. 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 Algolia, 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:
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:
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.
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 Algolia.
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 Algolia searchClient with the adapter's client. For example, create the file src/lib/search-client.ts with the following content:
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:
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 Algolia integration:
- Remove the Algolia
searchClientfromsrc/lib/config.ts, along with thealgoliasearchdependency and environment variables likeNEXT_PUBLIC_ALGOLIA_*. - Change every component that reads a hit's
objectIDto readidinstead, since the adapter builds a hit from the document your index holds. - Confirm each field your widgets filter, facet, or sort on is marked
filterable(),facetable(), orsortable()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 algolia.sync event. You no longer need that sync for everyday changes:
- Medusa fills the index on deployment.
- Medusa keeps the index current from the
eventsandconsumeproperties 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:
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.
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 Algolia 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 |
|---|---|
| The Search Module and its provider replace the client wrapper. |
The module's entry in the | Removing the directory without this leaves your application failing to boot. |
| The index definition's |
| Medusa subscribes to the events in the definition's |
The | Nothing in the backend calls Algolia anymore. |
The | 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#
- Index your other data models, including your custom ones, as explained in Searching Custom Data Models.
- Configure how Medusa Search treats your index and its fields with the Medusa Search Settings.
- Add semantic search, either over embeddings you store in the index or over ones Medusa Search creates for you, as explained in Vector and Hybrid Search.