Read this first
It replaces the search service, not the search architecture
Open Mercato's search is three strategies fused with Reciprocal Rank Fusion: fulltext (Meilisearch), vector (pgvector/Chroma/Qdrant + an embedding provider), and tokens (a Postgres hash index that always works). This package supplies a second fulltext driver. Everything else is untouched, and tokens remains the fallback when Cloudflare is unreachable.
The practical trade: you stop operating a Meilisearch container, and you accept ~250ms search latency instead of single-digit milliseconds.
Backend list views are NOT affected
Despite what you might assume, list-view q filtering does not go through SearchService — it joins search_tokens directly inside QueryEngine. Installing this changes Cmd+K global search, /api/search, and the AI assistant's MCP search tool. Nothing else. That is what makes it low-risk and trivially reversible.
Your data goes to a managed service
Meilisearch was a container you controlled. Cloudflare AI Search is managed and must hold readable text in order to embed it. This driver honours fieldPolicy.excluded / fieldPolicy.hashOnly and, with SEARCH_EXCLUDE_ENCRYPTED_FIELDS=true, the tenant encryption map — so encrypted fields stay searchable only via the tokens strategy, which hashes rather than storing plaintext.
Decide this before indexing production data. It is a deployment decision the package cannot make for you.
Cloudflare AI Search is in open beta, and we found a data leak in it
See Cross-tenant safety. It is handled, but you should understand it before trusting the product with multi-tenant data.
Install
yarn add @northbound-run/search-cloudflare
Peer dependencies: @open-mercato/search and @open-mercato/shared (0.6.x).
1. Provision the instance
Custom metadata fields must exist before any upload — Cloudflare silently drops undeclared fields, which would produce hits that cannot be mapped back to an Open Mercato entity.
npx wrangler ai-search create my-instance \
--type builtin \
--hybrid-search \
--custom-metadata entity:text \
--custom-metadata org:text
--hybrid-search sets index_method to {vector: true, keyword: true}. Vector-only is the default and misses exact identifiers like PO-10432.
2. API token
Needs Account → AI Search:Edit and AI Search:Run. A wrangler login OAuth token already carries both.
3. Environment
CF_AI_SEARCH_ACCOUNT_ID=<account id> # npx wrangler whoami
CF_AI_SEARCH_API_TOKEN=<token>
CF_AI_SEARCH_INSTANCE=my-instance
# Optional
CF_AI_SEARCH_NAMESPACE= # default namespace when unset
CF_AI_SEARCH_RETRIEVAL_TYPE=hybrid # vector | keyword | hybrid
CF_AI_SEARCH_MATCH_THRESHOLD=0.3 # Cloudflare default is 0.4
4. Enable the module
// src/modules.ts
{ id: 'search_cloudflare', from: '@northbound-run/search-cloudflare' },
Then yarn generate. That is the whole integration — no app src/di.ts wiring. The module ships no entities, migrations, routes or UI: a di.ts that registers the driver, and the doctor CLI. It declares requires: ['search'].
Registration has to live in a module di.ts, not the app's. Open Mercato reaches app-level DI through await import('@/di') — a bundler-only path alias that throws ERR_MODULE_NOT_FOUND under raw Node into a bare catch {}. So app