Search Index Field Modifiers

In this guide, you'll learn about the modifiers you can chain on a field in a search index definition.

Note: To learn about the types a field can hold, refer to the Search Index Fields guide.
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.

What is a Field Modifier?#

A modifier declares what the engine can do with a field. For example, searchable() includes the field in free-text matching, while filterable() allows filtering on it.

You can chain multiple modifiers to a field. A modifier that doesn't apply to a type isn't available on it, so an invalid combination fails to compile instead of failing when your application starts. Refer to Search Index Fields for the modifiers each type accepts.

Note: Adding, removing, or changing a modifier changes the definition's hash, which the Search Module treats as a schema change. Refer to the Reindexing and Migrations guide to learn how it rebuilds the index.

searchable()#

Includes the field in free-text matching, which is what the q filter passed to query.search searches against. Available on keyword and text fields.

Note: A field with no searchable modifier is never matched as free text, even when it holds a string.
src/search/product.ts
1import {2  defineSearchIndex,3  search,4} from "@medusajs/framework/utils"5
6export const productIndex = defineSearchIndex({7  fields: search.define({8    title: search.text().searchable(),9    // ...10  }),11  // ...12})

Pass a weight to boost how much the field contributes to a result's relevance. A field with a higher weight ranks its matches above equally good matches in a lower-weighted field. The weight defaults to 1, so searchable() and searchable({ weight: 1 }) behave the same.

src/search/product.ts
1import {2  defineSearchIndex,3  search,4} from "@medusajs/framework/utils"5
6export const productIndex = defineSearchIndex({7  fields: search.define({8    title: search.text().searchable({ weight: 3 }),9    description: search.text().searchable({ weight: 1 }),10    // ...11  }),12  // ...13})

In this example, a product matching a query in its title outranks one matching only in its description.

How much a weight changes ranking depends on the provider: 
  • The PostgreSQL provider maps the weight onto PostgreSQL's four relevance labels. A weight of 3 or more is the highest label, 2 the next, anything above 1 the next, and 1 or less the lowest. So there are four tiers, and raising a weight past 3 changes nothing.
  • The Medusa Search provider available for Cloud users multiplies the field's relevance by the weight, so a weight of 5 does outrank a weight of 3.

Refer to Medusa Search vs PostgreSQL for the other features providers treat differently.


filterable()#

Allows filters on the field. Available on every type except vector.

src/search/product.ts
1import {2  defineSearchIndex,3  search,4} from "@medusajs/framework/utils"5
6export const productIndex = defineSearchIndex({7  fields: search.define({8    status: search.keyword().filterable(),9    // ...10  }),11  // ...12})

Filtering on a field without this modifier throws an error, since the Search Module validates the query against the definition before it reaches the provider.


sortable()#

Allows ordering results by the field through passing pagination.order to query.search. Available on every type except vector.

src/search/product.ts
1import {2  defineSearchIndex,3  search,4} from "@medusajs/framework/utils"5
6export const productIndex = defineSearchIndex({7  fields: search.define({8    created_at: search.date().sortable(),9    // ...10  }),11  // ...12})

facetable()#

Allows facets on the field. A facet summarizes the matched documents by that field, which is what a storefront's filter sidebar shows. Available on every type except vector.

src/search/product.ts
1import {2  defineSearchIndex,3  search,4} from "@medusajs/framework/utils"5
6export const productIndex = defineSearchIndex({7  fields: search.define({8    brand: search.keyword().facetable(),9    // ...10  }),11  // ...12})

A query then requests the facet, and the result carries it in search_result.facets, keyed by field name:

Code
1const { search_result } = await query.search({2  entity: "product",3  filters: { q: "shirt" },4  search_options: { facets: ["brand"] },5})6
7console.log(search_result.facets)

The shape of each entry depends on the kind of facet, as shown in Facet Types.

Facet Types#

By default, the facet type depends on the field's type:

  • integer, float, and date fields default to ["range"].
  • stats is never a default. Add it explicitly, since it's the least widely supported kind and implying it would make numeric fields unusable on a provider without aggregations.
  • Every other type defaults to ["value"].

You can also pass types to facetable() to choose which kinds of facet the field allows. Only integer, float, and date accept the types option. For example:

src/search/product.ts
1import {2  defineSearchIndex,3  search,4} from "@medusajs/framework/utils"5
6export const productIndex = defineSearchIndex({7  fields: search.define({8    age: search.float().facetable({9      types: ["value"],10    }),11    // ...12  }),13  // ...14})

Each kind returns a different shape:

  • value: the distinct values of the field and how many documents hold each one. It can also return other_count for documents falling outside the returned values.
Example Value Facet
1{2  "brand": {3    "type": "value",4    "values": [5      { "value": "acme", "count": 12 },6      { "value": "borg", "count": 4 }7    ]8  }9}
  • range: how many documents fall into each bucket the query defines. The result echoes each bucket's key, from, and to with its count. Only available on the integer, float, and date field types.
Example Range Facet
1{2  "min_price": {3    "type": "range",4    "ranges": [5      { "key": "cheap", "to": 50, "count": 9 },6      { "key": "mid", "from": 50, "to": 200, "count": 21 }7    ]8  }9}
  • stats: one aggregate summary of the field across the matched documents, rather than a breakdown. Only available on the integer, float, and date field types.
Example Stats Facet
1{2  "min_price": {3    "type": "stats",4    "min": 10,5    "max": 450,6    "avg": 92.5,7    "sum": 3700,8    "count": 409  }10}

avg and sum are optional, since not every engine reports them.

Note: Requesting a facet kind the field doesn't allow throws an error. Passing a field name as a string, such as facets: ["min_price"], requests a value facet, so it fails on a numeric field that only allows range.

retrievable()#

Whether the field comes back on a hit. Most fields are retrievable by default, so you can omit this modifier unless you want to turn it off. Available on every type.

A vector field is the exception: it defaults to not retrievable, since a list of hundreds of numbers is rarely useful to the caller. Chain retrievable() on one to get the embedding back on every hit.

src/search/product.ts
1import {2  defineSearchIndex,3  search,4} from "@medusajs/framework/utils"5
6export const productIndex = defineSearchIndex({7  fields: search.define({8    search_blob: search.text().searchable().retrievable(false),9    // ...10  }),11  // ...12})
Tip: A field that isn't retrievable still counts toward matching. query.search reads this modifier to decide which requested fields the engine can serve, and which it must fetch with query.graph.
Note: An object field's container is never retrievable, since the index only stores the sub-fields you declared. Request those sub-fields by their dotted path, such as brand.name, rather than requesting brand.

array()#

Marks the field as holding a list of its type, rather than one value. Available on every type except vector.

src/search/product.ts
1import {2  defineSearchIndex,3  search,4} from "@medusajs/framework/utils"5
6export const productIndex = defineSearchIndex({7  fields: search.define({8    tags: search.keyword().array().filterable().facetable(),9    // ...10  }),11  // ...12})

A filter on an array field matches when any element matches. Faceting on one counts a document once per distinct element.


embed()#

Asks Medusa Search to create the field's embedding from text your documents pass on that same field, rather than having your documents supply the embedding. Available on a vector field only.

embed() takes no arguments:

src/search/product.ts
1import {2  defineSearchIndex,3  search,4} from "@medusajs/framework/utils"5
6export const productIndex = defineSearchIndex({7  fields: search.define({8    title: search.text().searchable(),9    title_embedding: search.vector(1536).embed(),10    // ...11  }),12  // ...13})

With embed, your documents pass a string on the vector field, and a query can search it by passing raw text as search_options.vector.query. Refer to Let Medusa Search Create the Embedding for the full details.

Note: embed is a Medusa Search feature, available for Cloud projects on the Enterprise plan.

providerOptions()#

Pass provider-specific options for one field, keyed by provider identifier. Use it to reach a feature the field definition doesn't model. Available on every type.

src/search/product.ts
1import {2  defineSearchIndex,3  search,4} from "@medusajs/framework/utils"5
6export const productIndex = defineSearchIndex({7  fields: search.define({8    title: search.text().searchable().providerOptions({9      "search-medusa": {10        glob: true,11      },12    }),13    // ...14  }),15  // ...16})

The options are passed to the targeted provider only, so you can pass different options to different providers for the same field.

An option only does something if the provider consumes it. A provider that doesn't read the providerOptions modifier ignores whatever you pass, so check the provider's documentation for the options it accepts. Of the providers Medusa ships, the Medusa Search provider available for Cloud users is the only one that reads them, and its guide lists the full set.

Tip: These options are separate from the options you set for a provider in medusa-config.ts. Those configure the provider itself, whereas these travel with the index definition and apply to one field.
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