Index Data from a Linked Module

In this guide, you'll learn how to include a linked module's data in a search index definition.

An index isn't limited to the fields of its own entity. Query resolves module links, so graphSeed and graphConsume can select a linked module's fields with the same dotted paths that query.graph accepts.

The examples in this section index products along with the brand each product is linked to, using the custom Brand Module and the product-brand link from the Medusa documentation.

Tip: The product indexes in this guide's snippets are 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.

Select and Declare the Linked Fields#

To hold a linked module's data in the index:

  1. Select the linked fields in graphSeed and graphConsume, the way you'd select them in query.graph. For example, brand.name.
  2. Declare them in the index's fields with search.object, mirroring the shape query.graph returns.

For example, 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 fields = [9  "id",10  "title",11  "status",12  "brand.id",13  "brand.name",14]15
16export const productIndex = defineSearchIndex({17  name: "product",18  entity: "product",19  fields: search.define({20    id: search.keyword().filterable(),21    title: search.text().searchable({ weight: 3 }),22    status: search.keyword().filterable(),23    brand: search.object({24      id: search.keyword().filterable(),25      name: search26        .keyword()27        .searchable()28        .filterable()29        .facetable(),30    }),31  }),32  events: [33    "product.created",34    "product.updated",35    "product.deleted",36  ],37  consume: graphConsume({ fields }),38  seed: graphSeed({ fields }),39})

query.graph returns a linked brand as a nested object, so an indexed document has the following shape:

Code
1{2  "id": "1",3  "title": "Product Title",4  "status": "active",5  "brand": {6    "id": "10",7    "name": "Acme",8  },9}

Refer to Search Index Fields for every field type and modifier.

The Store Search API route resolves the index by the entity a query names, so the fields you add to your product index become available to storefront searches. Refer to Search Products for how to query it.


Reference Nested Fields by Their Full Path#

The search engine flattens an object field into its leaves, each keyed by its dotted path, such as brand.name. Filters, facets, and sorting take that path as a string key.

For example, to filter and facet products by their linked brand's name:

Code
1const { data: products } = await query.search({2  entity: "product",3  fields: ["id", "title", "brand.name"],4  filters: {5    q: "shirt",6    "brand.name": "Acme",7  },8  search_options: {9    facets: ["brand.name"],10  },11})

Keep the following in mind when you filter, facet, or sort on nested fields:

  • The object itself isn't a field. Only the leaves are indexed, so brand is not something you can filter on. A filter on it throws Unknown filter field "brand".
  • Put the full path in the filter's key, not in its value. { "brand.name": "Acme" } works, whereas { brand: { name: "Acme" } } throws the same error, since the provider reads a nested object as an operator map.
  • A leaf must be declared, and it must carry the modifier for what you're doing with it. Filtering on brand.name requires filterable() on that leaf, faceting requires facetable(), and sorting requires sortable().

Keep the Index Current When Linked Data Changes#

Changing a brand doesn't emit a product event, so a product's document keeps the brand name it was indexed with until something re-ingests that product. The same is true when a link is created or removed, which emits no event at all.

You can ensure the index stays current when a brand is updated by:

  1. Emit the brand update event in the workflow updating it.
  2. Pass the brand update event to the product index's events array so it can trigger the consume function and re-ingest the affected products.
  3. Implement the consume function to handle the brand update event, resolve the affected products, and re-ingest them.

For example, to pass the brand update event to the product index and handle it in the consume function:

src/search/product.ts
1import {2  defineSearchIndex,3  graphConsume,4  graphSeed,5  search,6} from "@medusajs/framework/utils"7
8const consumeProducts = graphConsume({ fields })9
10export const productIndex = defineSearchIndex({11  // ...12  events: [13    "product.created",14    "product.updated",15    "product.deleted",16    "brand.updated",17  ],18  async consume(event, context) {19    if (event.name.startsWith("product.")) {20      return await consumeProducts(event, context)21    }22
23    const { data: brands } = await context.container24      .query.graph({25        entity: "brand",26        fields: ["products.id"],27        filters: { id: event.data.id },28      })29
30    const ids = brands[0]?.products?.map(31      (product) => product.id32    ) ?? []33
34    return await consumeProducts(35      { name: "product.updated", data: { id: ids } },36      context37    )38  },39  seed: graphSeed({ fields }),40})

You use graphConsume to handle product events. For brand updates, you manually resolve the affected products and re-ingest them as product.updated events.

Creating or removing a link doesn't emit an event either, so nothing tells the index that a product's brand changed. To keep the index current, emit your own event in the workflow that changes the link, then handle it in consume as you do the brand update event.

For example, , add the event to the index's events array and handle it in consume:

src/search/product.ts
1export const productIndex = defineSearchIndex({2  // ...3  events: [4    "product.created",5    "product.updated",6    "product.deleted",7    "brand.updated",8    "product-brand.linked",9  ],10  async consume(event, context) {11    // ...12
13    if (event.name === "product-brand.linked") {14      return await consumeProducts(15        {16          name: "product.updated",17          data: { id: event.data.product_id },18        },19        context20      )21    }22
23    // ... handle brand.updated24  },25})

This example assumes the event carries the product's ID. You re-ingest it as a product.updated event, which rebuilds the document with the brand it's now linked to.

Was this page 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