# @medusajs/search-postgres

> PostgreSQL search provider for Medusa, backed by tsvector and pg_trgm

Latest version **2.21.2** (published 2026-09-28) · MIT license · 0 weekly downloads

## Install

```sh
npm install @medusajs/search-postgres
pnpm add @medusajs/search-postgres
yarn add @medusajs/search-postgres
bun add @medusajs/search-postgres
```

## Health

**Score 75/100 (B)** — status: active.

Positive: has types; no vulnerabilities; has provenance; recently updated; high maintenance score; high quality score; popular repo.

Warnings: low downloads; no esm support.

## Facts

| | |
|---|---|
| Version | 2.21.2 |
| Published | 2026-09-28 |
| First published | 2026-08-11 |
| Weekly downloads | 0 |
| License | MIT |
| TypeScript types | bundled |
| Module format | CommonJS |
| Node | >=20 |
| Dependencies | 0 |
| Unpacked size | 180.9 KB |
| Known vulnerabilities | 0 |
| Install scripts | no |
| Provenance | attested (GitHub Actions) |
| GitHub stars | 36323 |
| Author | Medusa |
| Maintainers | sebrindom, oliverjuhl, nicolas-gorga, sradevski, olijuhl, shahednasser |
| Keywords | medusa-plugin, medusa-plugin-search |

## Links

- npm: https://www.npmjs.com/package/@medusajs/search-postgres
- Repository: https://github.com/medusajs/medusa
- Homepage: https://github.com/medusajs/medusa#readme
- Issues: https://github.com/medusajs/medusa/issues
- npm.io page: https://npm.io/package/@medusajs/search-postgres

## Alternatives

- [jsforce](https://npm.io/package/jsforce.md) — 851.2K weekly downloads
- [react-native-qrcode-svg](https://npm.io/package/react-native-qrcode-svg.md) — 693.5K weekly downloads
- [@salesforce/plugin-data](https://npm.io/package/@salesforce/plugin-data.md) — 394.9K weekly downloads
- [@backstage/plugin-search-common](https://npm.io/package/@backstage/plugin-search-common.md) — 308.5K weekly downloads
- [@chain-registry/types](https://npm.io/package/@chain-registry/types.md) — 38.4K weekly downloads

## Recent versions

- 2.21.2 (latest) — 2026-09-28
- 2.22.0-snapshot-20261008225942 (snapshot) — 2026-10-08
- 2.22.0-preview-20261002101246 (preview) — 2026-10-02
- 2.22.0-snapshot-20261008220405 — 2026-10-08
- 2.22.0-snapshot-20261008144129 — 2026-10-08
- 2.22.0-snapshot-20261006025321 — 2026-10-06
- 2.22.0-preview-20261002093930 — 2026-10-02
- 2.21.2-snapshot-20260928075521 — 2026-09-28
- 2.21.2-snapshot-20260925150426 — 2026-09-25
- 2.21.2-preview-20260925133948 — 2026-09-25
- 2.21.1 — 2026-09-22
- 2.21.1-preview-20260922104932 — 2026-09-22
- 2.21.1-preview-20260922093132 — 2026-09-22
- 2.21.1-preview-20260922080739 — 2026-09-22
- 2.21.1-preview-20260921152858 — 2026-09-21
- … 49 more at https://npm.io/package/@medusajs/search-postgres/versions

## README

# PostgreSQL search provider

Search provider for Medusa backed by PostgreSQL, with two engines:

| Engine             | When to use                        | Keyword                     | Vector               |
| ------------------ | ---------------------------------- | --------------------------- | -------------------- |
| `native` (default) | Local / self-hosted / any Postgres | GIN + `ts_rank` + `pg_trgm` | Not supported        |
| `lakebase`         | Medusa Cloud / Lakebase Search     | `lakebase_bm25` (BM25)      | `lakebase_ann` (ANN) |

## Enable it

### Native (default, works locally and on Medusa Cloud)

```ts
modules: [
  {
    resolve: "@medusajs/medusa/search",
    options: {
      providers: [
        {
          resolve: "@medusajs/medusa/search-postgres",
          id: "postgres",
          options: {
            // engine: "native", // default
            // language: "english",
          },
        },
      ],
    },
  },
]
```

### Lakebase (works only on Medusa Cloud)

```ts
{
  resolve: "@medusajs/medusa/search-postgres",
  id: "postgres",
  options: {
    engine: "lakebase",
    // Optional: embed text for search_options.vector.query
    // embedder: async (text) => { ... return number[] },
    // vector_distance: "cosine", // or "l2" | "inner_product"
  },
}
```

Then:

```bash
npx medusa db:migrate
npx medusa db:migrate-search
```

The migration enables `pg_trgm` + `unaccent` and creates the catalog. On Medusa Cloud it also enables `lakebase_vector` and `lakebase_text` (`CREATE EXTENSION ... CASCADE`). Those statements soft-fail on engines that do not ship Lakebase Search.

## Provider options

| Option            | Default     | Description                                                                            |
| ----------------- | ----------- | -------------------------------------------------------------------------------------- |
| `engine`          | `"native"`  | `"native"` or `"lakebase"`                                                             |
| `language`        | `"english"` | Text search config. With `unaccent`, creates `medusa_search_<language>`.               |
| `embedder`        | —           | `(text) => Promise<number[]>`. Required for `search_options.vector.query` on lakebase. |
| `vector_distance` | `"cosine"`  | ANN metric: `cosine`, `l2`, or `inner_product`.                                        |

## Vector fields (lakebase only)

On the native engine vector fields are not rejected: the provider logs a warning and drops them from the index, so one definition stays portable across providers. A query that asks for `search_options.vector` there is still rejected, since answering it with keyword results would be wrong.

Supply embeddings yourself:

```ts
defineSearchIndex({
  name: "product",
  entity: "product",
  fields: search.define({
    id: search.keyword().filterable(),
    title: search.text().searchable({ weight: 3 }),
    embedding: search.vector(1536),
  }),
  // ...
})
```

Documents must include the embedding array on upsert. Query with `search_options.vector.value`.

Or let the provider embed a string on the same field (`.embed()` requires `embedder`):

```ts
embedding: search.vector(1536).embed()

// documents: { embedding: "title and description to encode" }

await query.search({
  entity: "product",
  search_options: {
    vector: {
      field: "embedding",
      query: "red shoes",
      semantic_ratio: 0.5, // 0 = keyword, 1 = vector, in between = RRF hybrid
    },
  },
})
```

Query with a client-supplied embedding against either kind of field:

```ts
await query.search({
  entity: "product",
  filters: { q: "red shoes" },
  search_options: {
    vector: {
      field: "embedding",
      value: embeddingArray,
      semantic_ratio: 0.5,
    },
  },
})
```

## What it supports

|                  | native                                                  | lakebase                 |
| ---------------- | ------------------------------------------------------- | ------------------------ |
| Free text        | `ts_rank` + weights                                     | BM25 via `lakebase_bm25` |
| Typo tolerance   | `pg_trgm` (`word_similarity`)                           | `pg_trgm` (same)         |
| `match_strategy` | `"all"` (default), `"any"`, `"last"`                    | same                     |
| Filters          | `$eq` `$ne` `$in` `$nin` ranges, `$and`/`$or`/`$not`, … | same                     |
| Facets           | value, range, stats — scoped to the query matches       | same                     |
| `distinct`       | one hit per value, count follows                        | same                     |
| `min_score`      | yes, keeps the requested sort                           | same                     |
| Vector / hybrid  | fields ignored, queries rejected                        | ANN + RRF                |

Unsupported on both (rejected explicitly): highlighting, geo, cursor pagination, query-time locales.

`"last"` is typeahead: completed terms must match in full and the last term is a prefix, so `"dtc sta"` matches `"Dtc starter"`.

### Filter semantics

- Equality and `$in` compile to jsonb containment (`indexed @> …`), which the
  `jsonb_path_ops` GIN index accelerates and which compares numbers and
  booleans natively.
- On array fields, a bare value or `$eq` means membership — `{ tags: "sale" }`
  matches documents whose `tags` array contains `"sale"`.
- Range operators (`$gt`, …), `$prefix` and `$like` are rejected on array
  fields.

### Hybrid queries (lakebase)

- Results are rank-fused (RRF), so they can only be ordered by `_score`.
- `min_score` applies to the fused RRF score (per-arm contributions are at most
  `weight / 61`).
- `metadata.count` is the exact size of the union of both arms' match sets;
  the fused hit list itself is a bounded candidate window.

---
_Source: https://npm.io/package/@medusajs/search-postgres · Machine-readable twin of the npm.io package page. Health data is recomputed on every publish._
