Medusa Search vs PostgreSQL

This guide compares Medusa Search and the PostgreSQL Search Module Provider to help you decide which one to use for your Medusa project.

Medusa Search is a managed search service for Cloud projects. It stores your search indexes outside of your database and serves search queries for them, from full-text queries with relevance ranking to vector search over embeddings.


What is the PostgreSQL Provider?#

The PostgreSQL Search Module Provider ships with Medusa and stores your search indexes as tables in the database your Medusa application already uses. It needs no external service, and Medusa registers it by default in local development.


Comparison Summary#

Concern

Medusa Search

PostgreSQL

Setup

Medusa registers and configures it for every Cloud environment.

Registered by default locally. Can also be used in production, but requires manual registration and configuration.

Local development

Either use the PostgreSQL provider locally, or connect to the environment's Medusa Search endpoint.

The default provider locally, so search works with no extra service.

Per-environment isolation

Medusa manages one set of indexes per environment for you.

Indexes are tables, so they travel with the environment's database.

What you pay for

No separate subscription. Your plan includes a monthly allowance of search requests, and anything above it is Flex Usage.

Your database's own resources, since search competes with your transactional workload.

Full-text search

Relevance scales with the weight you set on each field.

Weights collapse onto four relevance tiers.

Typo tolerance

Supported, through fuzzy matching that ranks below exact matches.

Supported, through trigram word similarity.

Filters

Every operator, including $or, $not, and several operators on one field.

Every operator, including $or, $not, and several operators on one field.

Facets

Value, range, and stats. The hits, the count, and every facet of a query run as one search request.

Value, range, and stats.

Sorting

Any number of fields, but not alongside a vector query.

Any number of fields, alongside a text query.

Vector and hybrid search

Supported on the Scale and Enterprise plans, including a hybrid of text and vector search. Medusa Search can also create the embeddings, on the Enterprise plan.

Not supported. The provider ignores a vector field in the index definition and rejects a vector query.

Deep pagination

Up to the 10,000th result, which is well past what a storefront paginates through.

No limit, though a deep offset gets slower as the query scans and discards more rows.

Storefront querying

Supported by Medusa's InstantSearch adapter.

Supported by Medusa's InstantSearch adapter, except for highlighting.

Admin dashboard search

Uses your indexes automatically.

Uses your indexes automatically, and works in local development.


Setup and Infrastructure#

Medusa provisions Medusa Search for every Cloud environment. It passes the credentials to your Medusa application, which registers the provider for you. So, you don't need to manually configure or maintain the search infrastructure. You can immediately start searching.

PostgreSQL#

The PostgreSQL provider needs no external service, since it indexes into the database your Medusa application already uses. It may need additional setup based on your use case, such as configuring languages other than English in your database.

You must also make sure that your database can support the additional load imposed by the search indexes and queries. The index lives beside your orders and your products, so a heavy search workload competes with checkout for the same CPU, memory, and disk. Medusa Search moves that load off your database entirely.

Local Development#

Medusa Search runs on Cloud only. In local development, Medusa registers the PostgreSQL Search Module Provider instead, which indexes into your local database. You declare and test an index without running a search engine on your machine. You can also connect to your environment's search endpoint.


Integration with Medusa#

Both providers ship with Medusa and implement the same Search Module Provider interface, so there's no integration code to write for either one.


Environments and Previews#

Medusa Search#

Medusa gives each Cloud environment its own set of Medusa Search indexes, scoped by the environment's handle, so a preview environment never writes to production indexes. When you configure a base environment in the shared previews settings, Cloud copies the base environment's indexes into the new preview environment along with its database.

PostgreSQL#

PostgreSQL indexes need no separate arrangement, since they're tables in the database. An environment's indexes travel with its database, so a database copy brings a filled index with it and a fresh database starts with an empty one.


Cost and Packaging#

Neither provider bills separately, so compare where the load lands instead:

Provider

What you pay for

What drives the cost up

Medusa Search

No separate subscription, since Medusa Search is available on all Cloud plans. Each plan includes a monthly allowance of search requests. Refer to Plans & Pricing for the allowance your plan includes.

Search requests beyond your plan's allowance, which count as Flex Usage.

PostgreSQL

Nothing on top of the database you already run, since the indexes are tables in it.

Index size and search traffic, since both raise the database tier you need. Search also competes with checkout for the same resources.

Other Cloud Usage PostgreSQL Affects#

Using the PostgreSQL provider for Search on Cloud can drive up the costs of the following Cloud usage metrics:

  • Database Storage grows with every index, since each one is a table in your database. A large catalog with several indexes adds gigabytes to the metric you're billed on, on top of your commerce data.
  • Compute covers the indexing work, since the seed and the event handlers that fill an index run in your Medusa application.

Refer to Usage on how to monitor these metrics.


Index Definition Support#

The Search Module provides the interface to define different types of fields in search indexes, but it's the responsibility of each provider to actually support them. The following table summarizes how each provider handles the different types of fields and settings.

A field a provider can't store doesn't make the whole index fail. The PostgreSQL provider logs a warning and creates the index without the vector fields in your definition, so one definition works on both providers.

Index Definition

Medusa Search

PostgreSQL

A search.vector() field

Holds the embedding as a fixed-width float vector. Fails at startup if the field is an array or declares no positive dimensions.

Ignores the field and logs a warning, since the provider has nowhere to store an embedding. It creates the rest of the index, so the same definition stays usable on both providers.

A search.geo() field

Fails at startup.

Fails at startup.

settings.synonyms or settings.stop_words

Fails at startup.

Ignored.

settings.distinct_attribute

Applied to every query that doesn't pass its own distinct.

Ignored. Pass distinct per query instead.

settings.provider_options and providerOptions()

Reads its own entry on the index, on a field, and on a query.

Ignored. The provider reads no entry of its own.

A document whose primary key is longer than 64 bytes

Throws when the document is written, which fails the seed or the write.

Written as-is.


Indexing and Synchronization#

The Search Module orchestrates the flow for indexing and data synchronization. It uses the methods implemented in the configured provider to interact with the underlying search engine, whether it's Medusa Search or PostgreSQL.

What differs is how each provider implements the logic to support indexing and synchronization. Since Medusa implements both providers, it ensures consistent behavior from the perspective of the Search Module. The main difference is where the index resides and how it is managed.

Behavior

Medusa Search

PostgreSQL

Where the index lives

The Medusa Search service, scoped to the environment handle you configure.

A table per index in your PostgreSQL database, tracked in a catalog table.

What a new index version is built as

A separate namespace in the Medusa Search service, which the module writes to until it makes that version active.

A separate table in your database, which the module writes to until it makes that version active.

How a batch of documents is written

Split into requests of at most 480 MiB. A single document above that limit throws.

200 documents per statement.


Search Capabilities#

A provider that can't serve part of a request rejects it instead of answering differently. The tables below say when that rejection happens:

  • Fails at startup: the provider throws while the Search Module creates or migrates the index, so your application doesn't boot or db:migrate fails.
  • Fails per query: the provider throws for the requests that use the feature. Other requests are unaffected.
  • Ignored: the provider accepts the request and doesn't act on the feature, so results look as if you never passed it.

Refer to searchable() for how each provider reads a field's weight.

Search Option Support#

What each provider does with the search options of a query.search call.

Option

Medusa Search

PostgreSQL

vector

Runs a similarity search on the embedding you pass as value, or on the text you pass as query when the field declares embed. Blends it with a q filter by semantic_ratio. Fails per query if the search also orders by a field.

Fails per query.

typo_tolerance

Widens the match with fuzzy matching on every searchable field that carries a fuzzy index, ranking those hits below the exact ones. Ignored when the query passes no q filter, or when no searched field has a fuzzy index.

Widens the match with trigram word similarity.

min_score

Fails per query.

Discards hits below the threshold before paginating, so the count stays truthful.

match_strategy

Serves all, any, and last.

Serves all and any. Fails per query for last.

count: "none"

Skips the count query and returns null as the count.

Skips the count query and returns null as the count.

distinct

Keeps one hit per value.

Keeps one hit per value, picked by the query's order. Fails per query on an unknown, array, or vector field.

highlight

Returns a fragment per highlighted field. Ignored when the query passes no q filter, and for a field that isn't searchable.

Fails per query. The provider doesn't implement highlighting.

locales

Fails per query. Configure the language on the index's fields instead.

Fails per query. Set language in the provider options instead.

Pagination and Sorting Support#

Request

Medusa Search

PostgreSQL

Ordering by more than one field

Sends every key to the service in the order you list them.

Applies every key in the order you list them.

Ordering by a field alongside a text query

Orders by the field, which replaces relevance ranking rather than refining it. Alongside a vector query it fails per query instead.

Orders by the field. A hybrid text and vector search fails per query, since its results are rank-fused and only _score can order them.

Ordering by an array field

Sends the field to the service as a rank key without checking it first.

Fails per query.

A page beyond the 10,000th result

Fails per query when skip plus take is above 10,000.

Served.

Paginating with pagination.cursor

Fails per query. Paginate with skip and take.

Fails per query. Paginate with skip and take.

Filter Support#

What each provider does with a filter tree. Refer to Filter Operators for what each operator means.

Filter

Medusa Search

PostgreSQL

$or or $not

Compiled into the query.

Compiled into the query.

Two operators on one field, such as $gt with $ne

Both applied, combined with AND.

Both applied, combined with AND.

$exists

Compiled into a comparison against no value.

Compiled into a presence check.

$prefix or $like

Compiled into a glob match, with % and _ translated for $like.

Compiled into a pattern match. Fails per query on an array field.

$ne or $nin on a numeric, boolean, or array field

Compiled into a negated match.

Compiled into a negated match.

Several values for one numeric or boolean field, with $in or a plain array

Matches any of the values.

Matches any of the values.

Facet Support#

What each provider does with the facets a query requests.

Request

Medusa Search

PostgreSQL

A stats facet

Returns the minimum, maximum, sum, and count, but no average. Costs one query per aggregate, so four for a numeric field and three for a date. Fails per query on a field that isn't numeric, or on an array field.

Returns the minimum, maximum, average, sum, and count. Fails per query on a field that isn't numeric.

A range facet

Counts each bucket with its own query, so a facet with five buckets costs five queries.

Counts each bucket in one query. Fails per query on a field that isn't numeric.

Many facets in one query

Batches the hits, the count, and every facet query into one request, and fails per query above 16 of them.

Runs one extra query per facet.

A facet value sort or limit

Applied by the provider after it reads every group back.

Applied by the query that computes the facet.

Disjunctive facets

Runs the fan-out as one request, since the provider accepts more than one query per request.

Runs the fan-out as one request, since the provider accepts more than one query per request.


Storefront Integration#

InstantSearch is a family of open source UI libraries, which render a search interface from widgets such as a search box, a hit list, facet filters, and pagination. It's available for plain JavaScript, React, Vue, and Angular.

Medusa's InstantSearch adapter connects those widgets to Medusa's Search Module. It works with any Search Module Provider you integrate in your Medusa application, including both Medusa Search and the PostgreSQL provider.

Code
1import {2  createInstantSearchAdapter,3} from "@medusajs/instantsearch-adapter"4import { sdk } from "./sdk"5import {6  Configure,7  InstantSearch,8} from "react-instantsearch"9
10const PRODUCT_INDEX_NAME = "product"11
12export const { searchClient } = createInstantSearchAdapter({13  sdk,14  path: "/store/search",15})16
17const Search = () => (18  <InstantSearch19    indexName={PRODUCT_INDEX_NAME}20    searchClient={searchClient}21  >22    <Configure hitsPerPage={12} />23    {/* Render search widgets */}24  </InstantSearch>25)

So, the same storefront code runs against the PostgreSQL provider locally and Medusa Search on Cloud. The difference is which options each provider rejects, which is why a storefront that works locally can fail on Cloud. Build against the narrower of the two, or gate the options you pass on the provider in use.

Refer to the InstantSearch Adapter guide for more details and examples of setting up search and filters.


The Medusa Admin dashboard's global search calls the Admin Search API route, which uses the Search Module for every entity that has an index and falls back to database queries for the rest. Declaring an index for an entity therefore improves admin search with no extra work.

This is agnostic across different Search Module Providers, so it works the same for both providers. The PostgreSQL provider has one advantage here: it works locally, so you see the indexed admin search behavior in development rather than after a deployment.


Comparisons to Other Search Engines#

The following guides compare Medusa Search to other search engines:

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