Skip to main content
The Search module is a provider abstraction: it owns no data models and wraps exactly one registered search provider behind three verbs — index, remove, search. Mercur ships a default in-process provider built on Orama that requires zero external infrastructure; operators can swap in Algolia, Meilisearch, or anything else by implementing the same contract. Both products (master catalog entries) and offers (per-seller listings with offer-scoped prices) are indexed.

Document shape

interface SearchDoc {
  id: string
  type: "product" | "offer"
  title: string
  description?: string
  handle: string
  thumbnail?: string
  seller_handle?: string          // offers only
  collection_id?: string
  collection?: string
  category_ids?: string[]
  categories?: string[]
  product_id?: string             // offers
  variant_id?: string             // offers
  sku?: string                    // offers
  attribute_tokens: string[]      // facetable: "attr:<handle>:<value_id>"
  attributes: Record<string, string> // stored label map
  prices: Record<string, SearchDocPrice> // keyed by region_id, stored not indexed
}
Prices are stored, not searched: each document carries a per-region price map (calculated_amount, original_amount, currency_code), and the provider projects calculated_price for the requested region at query time. Suspended sellers’ offers and unpublished products are excluded at index time. Attributes flagged is_filterable are tokenized into attribute_tokens for faceting.

Query and results

interface SearchQueryBase {
  q?: string
  limit?: number
  offset?: number
  context?: Record<string, unknown>  // pricing/display context, not a filter
  filters?: Record<string, unknown>  // provider-owned shape
}

interface SearchResults {
  hits: SearchDoc[]
  count: number
  facets: { collections; categories; attributes } // pre-labelled by the provider
}
filters is an open passthrough — each provider defines its own filter shape. The built-in Orama provider accepts:
filters?: {
  type?: "product" | "offer"
  collection_ids?: string[]
  category_ids?: string[]
  seller_handle?: string
  attributes?: Record<string, string[]> // attribute handle -> selected value ids
}

Service

SearchModuleService exposes:
MethodBehavior
index(docs)Upserts documents into the provider index
remove(ids)Removes documents by ID
search(query)Runs a query against the provider
getProviderIdentifier()Returns the active provider’s identifier

Provider contract

Implement AbstractSearchProvider (exported from the module) and register it via the module’s provider option. Exactly one provider must be registered — the module throws at boot otherwise. If no provider is configured, the Orama provider registers automatically.
abstract class AbstractSearchProvider<TQuery extends SearchQueryBase> {
  static identifier: string
  abstract index(docs: SearchDoc[]): Promise<void>
  abstract remove(ids: string[]): Promise<void>
  abstract search(query: TQuery): Promise<SearchResults>
}
The built-in Orama provider holds its index in process memory. It starts empty, is rebuilt on boot via the reindex event, and does not share state across processes. For multi-instance deployments, use an external provider.

Index synchronization

Sync is event-driven — there are no search workflows:
  • Subscribers react to product, offer, and seller changes (search-product-changed, search-product-deleted, search-offer-changed, search-seller-changed) and reindex or remove the affected documents.
  • A full rebuild runs when the search.reindex event (SEARCH_REINDEX_EVENT) is emitted; in worker mode the module emits it automatically on application start.
POST /store/search — the storefront query route. Body: q, limit (default 12, max 100), offset, region_id / country_code / province (pricing context), and the provider-owned filters. Responds with { hits, count, limit, offset, facets }. Customer authentication is optional.

Next steps

Search integrations

Store API