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.
Select and Declare the Linked Fields#
To hold a linked module's data in the index:
- Select the linked fields in
graphSeedandgraphConsume, the way you'd select them inquery.graph. For example,brand.name. - Declare them in the index's
fieldswithsearch.object, mirroring the shapequery.graphreturns.
For example, 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 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:
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:
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
brandis not something you can filter on. A filter on it throwsUnknown 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.namerequiresfilterable()on that leaf, faceting requiresfacetable(), and sorting requiressortable().
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:
- Emit the brand update event in the workflow updating it.
- Pass the brand update event to the product index's
eventsarray so it can trigger theconsumefunction and re-ingest the affected products. - Implement the
consumefunction 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:
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.
Re-Ingest a Product After a Link Change#
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:
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.