> ## Documentation Index
> Fetch the complete documentation index at: https://docs.mercurjs.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Search module

> The provider-agnostic search module, its document shapes, and the provider contract.

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](https://oramasearch.com) 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

```ts theme={null}
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

```ts theme={null}
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:

```ts theme={null}
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:

| Method                    | Behavior                                  |
| ------------------------- | ----------------------------------------- |
| `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.

```ts theme={null}
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>
}
```

<Warning>
  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.
</Warning>

## 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.

## Related endpoints

`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

<CardGroup cols={2}>
  <Card title="Search integrations" href="/rc/resources/integrations/search" />

  <Card title="Store API" href="/rc/references/api/store" />
</CardGroup>
