Store Search API Route
In this guide, you'll learn how to expose your search indexes to a storefront through the Store Search API route, and how to control what a storefront can find in them.
What is the Store Search API Route?#
Medusa exposes a single POST /store/search route that searches any index of your application, so you don't create an API route per index. It answers with the search engine's own results, which is the contract the InstantSearch adapter is built on.
The route exposes nothing by default. An index is only searchable through it once the configureStoreSearch middleware allows it, and a request for any other index is answered as if the index didn't exist. That way, the route never tells a storefront what your application holds.
Allow Indexes on the Route#
To allow an index to be searched through the route, apply the configureStoreSearch middleware to /store/search in 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 product_category: true,15 },16 }),17 ],18 },19 ],20})
A storefront can now post a query with "entity": "product" or "entity": "product_category". Every other index still answers with a 404 error, and Medusa logs why for whoever configured the route.
You can apply the middleware more than once. Each one merges its allowed_indexes into the ones before it, so a plugin exposes its own index without discarding what your application allowed.
How Medusa Narrows a Product Index#
A storefront must never find a draft product, or a product that isn't in the request's sales channel. So, for an index whose entity is product, Medusa adds the following filters to every query:
Filter | Applied when |
|---|---|
| The index declares a |
| The index declares a |
So, to have Medusa scope your product index for you, declare both fields in the index definition. sales_channel_ids isn't a field of the product itself, so select the product's sales_channels.id and flatten them onto the document with a transform:
1const productFields = search.define({2 id: search.keyword().filterable().retrievable(),3 title: search4 .text()5 .searchable({ weight: 3 })6 .retrievable(),7 // Read by `/store/search` to scope the index.8 status: search.keyword().filterable().retrievable(false),9 sales_channel_ids: search10 .keyword()11 .array()12 .filterable()13 .retrievable(false),14 // ...15})16 17const source = {18 fields: ["id", "title", "status", "sales_channels.id"],19 transform: (products) => {20 return products21 .filter((p) => p.status === "published")22 .map((product) => ({23 id: product.id,24 title: product.title,25 status: product.status,26 sales_channel_ids: product.sales_channels.map(27 (salesChannel) => salesChannel.id28 ),29 }))30 },31}32 33export default defineSearchIndex({34 name: "product",35 entity: "product",36 fields: productFields,37 consume: graphConsume(source),38 seed: graphSeed(source),39 // ...40})
The status and sales_channel_ids fields use retrievable(false), so the route can filter on them without ever returning them in a hit.
If the index is missing either field, Medusa logs a warning once for that index and skips that filter. Then, either add the missing field to the definition, or scope the index yourself as explained in the next section.
Filter an Index for Your Storefront#
To constrain an index beyond what Medusa applies, pass a filters function for it instead of true. The function receives the request, so a constraint can depend on who is asking, and it can be asynchronous.
For example, to only find active product categories:
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 product_category: {15 filters: (req) => ({ is_active: true }),16 },17 },18 }),19 ],20 },21 // ...22 ],23})
Medusa combines your filters with the ones it applies itself and the ones the storefront posts using an $and operator. So, a storefront can narrow its own results but never widen them past what you allowed.
Search Documents in the Storefront#
Once an index is allowed, send a POST request to /store/search with the index's name in entity:
Learn more about the accepted request body and the response in the Store Search API reference.