@endora-commerce/mod-search 0.100.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +53 -0
  3. package/dist/backend/cli/reindex.d.ts +4 -0
  4. package/dist/backend/cli/reindex.d.ts.map +1 -0
  5. package/dist/backend/cli/reindex.js +32 -0
  6. package/dist/backend/cli/reindex.js.map +1 -0
  7. package/dist/backend/entities/search-phrase-record.entity.d.ts +40 -0
  8. package/dist/backend/entities/search-phrase-record.entity.d.ts.map +1 -0
  9. package/dist/backend/entities/search-phrase-record.entity.js +85 -0
  10. package/dist/backend/entities/search-phrase-record.entity.js.map +1 -0
  11. package/dist/backend/index.d.ts +87 -0
  12. package/dist/backend/index.d.ts.map +1 -0
  13. package/dist/backend/index.js +153 -0
  14. package/dist/backend/index.js.map +1 -0
  15. package/dist/backend/plugin.d.ts +133 -0
  16. package/dist/backend/plugin.d.ts.map +1 -0
  17. package/dist/backend/plugin.js +250 -0
  18. package/dist/backend/plugin.js.map +1 -0
  19. package/dist/backend/routes.admin.d.ts +28 -0
  20. package/dist/backend/routes.admin.d.ts.map +1 -0
  21. package/dist/backend/routes.admin.js +33 -0
  22. package/dist/backend/routes.admin.js.map +1 -0
  23. package/dist/backend/routes.public.d.ts +22 -0
  24. package/dist/backend/routes.public.d.ts.map +1 -0
  25. package/dist/backend/routes.public.js +192 -0
  26. package/dist/backend/routes.public.js.map +1 -0
  27. package/dist/backend/services/embedder-config-resolver.d.ts +25 -0
  28. package/dist/backend/services/embedder-config-resolver.d.ts.map +1 -0
  29. package/dist/backend/services/embedder-config-resolver.js +47 -0
  30. package/dist/backend/services/embedder-config-resolver.js.map +1 -0
  31. package/dist/backend/services/llm-toggle.service.d.ts +50 -0
  32. package/dist/backend/services/llm-toggle.service.d.ts.map +1 -0
  33. package/dist/backend/services/llm-toggle.service.js +78 -0
  34. package/dist/backend/services/llm-toggle.service.js.map +1 -0
  35. package/dist/backend/services/search-event-subscriber.d.ts +125 -0
  36. package/dist/backend/services/search-event-subscriber.d.ts.map +1 -0
  37. package/dist/backend/services/search-event-subscriber.js +113 -0
  38. package/dist/backend/services/search-event-subscriber.js.map +1 -0
  39. package/dist/backend/services/search-indexer.d.ts +346 -0
  40. package/dist/backend/services/search-indexer.d.ts.map +1 -0
  41. package/dist/backend/services/search-indexer.js +670 -0
  42. package/dist/backend/services/search-indexer.js.map +1 -0
  43. package/dist/backend/services/search-phrase-recorder.service.d.ts +25 -0
  44. package/dist/backend/services/search-phrase-recorder.service.d.ts.map +1 -0
  45. package/dist/backend/services/search-phrase-recorder.service.js +89 -0
  46. package/dist/backend/services/search-phrase-recorder.service.js.map +1 -0
  47. package/dist/backend/services/search-query-port.d.ts +29 -0
  48. package/dist/backend/services/search-query-port.d.ts.map +1 -0
  49. package/dist/backend/services/search-query-port.js +20 -0
  50. package/dist/backend/services/search-query-port.js.map +1 -0
  51. package/dist/backend/services/search-query.service.d.ts +205 -0
  52. package/dist/backend/services/search-query.service.d.ts.map +1 -0
  53. package/dist/backend/services/search-query.service.js +346 -0
  54. package/dist/backend/services/search-query.service.js.map +1 -0
  55. package/dist/backend/services/search-reindex-worker.d.ts +27 -0
  56. package/dist/backend/services/search-reindex-worker.d.ts.map +1 -0
  57. package/dist/backend/services/search-reindex-worker.js +13 -0
  58. package/dist/backend/services/search-reindex-worker.js.map +1 -0
  59. package/dist/backend/services/search-suggest.service.d.ts +53 -0
  60. package/dist/backend/services/search-suggest.service.d.ts.map +1 -0
  61. package/dist/backend/services/search-suggest.service.js +64 -0
  62. package/dist/backend/services/search-suggest.service.js.map +1 -0
  63. package/dist/backend/services/suggestion-pricing-enricher.d.ts +62 -0
  64. package/dist/backend/services/suggestion-pricing-enricher.d.ts.map +1 -0
  65. package/dist/backend/services/suggestion-pricing-enricher.js +41 -0
  66. package/dist/backend/services/suggestion-pricing-enricher.js.map +1 -0
  67. package/dist/manifest.d.ts +263 -0
  68. package/dist/manifest.d.ts.map +1 -0
  69. package/dist/manifest.js +300 -0
  70. package/dist/manifest.js.map +1 -0
  71. package/dist/migrations/20260501T123145_search_phrase_records_init.d.ts +13 -0
  72. package/dist/migrations/20260501T123145_search_phrase_records_init.d.ts.map +1 -0
  73. package/dist/migrations/20260501T123145_search_phrase_records_init.js +40 -0
  74. package/dist/migrations/20260501T123145_search_phrase_records_init.js.map +1 -0
  75. package/dist/migrations/index.d.ts +27 -0
  76. package/dist/migrations/index.d.ts.map +1 -0
  77. package/dist/migrations/index.js +29 -0
  78. package/dist/migrations/index.js.map +1 -0
  79. package/docs/search.md +284 -0
  80. package/i18n/en.json +10 -0
  81. package/i18n/pl.json +10 -0
  82. package/package.json +72 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/migrations/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAE,+CAA+C,EAAE,MAAM,iDAAiD,CAAC;AAElH,eAAO,MAAM,UAAU,4DAEtB,CAAC;AAEF,OAAO,EACL,+CAA+C,GAChD,CAAC"}
@@ -0,0 +1,29 @@
1
+ /**
2
+ * The `./migrations` subpath — every migration class this module owns, as one
3
+ * ordered `migrations` array.
4
+ *
5
+ * The array is what the platform reads when this module is **installed**:
6
+ * `src/packages/package-runtime.ts` takes `exported['migrations']` and refuses
7
+ * the package outright when it is absent (D-168).
8
+ *
9
+ * Listed in ascending timestamp, which is the order of this module's own
10
+ * migrations and of nothing else (feature 081): a manifest `dependencies` array
11
+ * is the only thing ordering this block against another module's.
12
+ *
13
+ * The **named** exports stay beside the array, and the asymmetry with
14
+ * `./backend` — which publishes an array and no named class (D-168) — is
15
+ * deliberate. `db/migrations-registry.generated.ts` imports each class by name
16
+ * from this specifier, and a migration class name is contract in a way an entity
17
+ * class name is not: `mikro_orm_migrations` persists it, so it is a string every
18
+ * already-migrated database holds.
19
+ *
20
+ * A class that is in neither the array nor the barrel is a migration that does
21
+ * not run: `migration:pending` reports nothing pending and the first symptom is
22
+ * a query against a table nobody created.
23
+ */
24
+ import { Migration20260501T123145SearchPhraseRecordsInit } from './20260501T123145_search_phrase_records_init.js';
25
+ export const migrations = [
26
+ Migration20260501T123145SearchPhraseRecordsInit,
27
+ ];
28
+ export { Migration20260501T123145SearchPhraseRecordsInit, };
29
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/migrations/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAE,+CAA+C,EAAE,MAAM,iDAAiD,CAAC;AAElH,MAAM,CAAC,MAAM,UAAU,GAAG;IACxB,+CAA+C;CAChD,CAAC;AAEF,OAAO,EACL,+CAA+C,GAChD,CAAC"}
package/docs/search.md ADDED
@@ -0,0 +1,284 @@
1
+ ---
2
+ title: search
3
+ description: Meilisearch indexer + query bridge
4
+ ---
5
+
6
+ # `search`
7
+
8
+ Meilisearch-backed catalog search. Owns the indexer that mirrors Catalog
9
+ events into per-Sales-Channel indexes, the typed query bridge, the
10
+ typeahead-popup feed, the analytics ingest for committed search phrases,
11
+ and the LLM-augmented-search opt-in.
12
+
13
+ The module composition root subscribes to Catalog events
14
+ (`product.created.v1`, `product.updated.v1`, `product.archived.v1`,
15
+ `attribute.updated.v1`) and to Settings events
16
+ (`settings.value_changed`) without imports into either module's
17
+ internals.
18
+
19
+ ## Public surface
20
+
21
+ | Verb + Path | Audience | Purpose |
22
+ | --- | --- | --- |
23
+ | `GET /api/v1/search/suggest?q=…&limit=N` | storefront (no auth) | Typeahead popup feed; returns up to `limit` `ProductSummary` rows ordered by Meilisearch relevance |
24
+ | `POST /api/v1/search/record` | storefront (no auth) | Fire-and-forget analytics ingest; persists one verbatim row in `search_phrase_records` per committed search |
25
+ | `POST /api/v1/admin/search/llm/toggle` | admin (`search:write`) | Cross-setting-validating wrapper around `search.llm.enabled`; refuses to enable when any embedder.* field is empty for any targeted channel |
26
+
27
+ The full search results page reuses `GET /api/v1/catalog/products?q=…` —
28
+ catalog owns that contract, and the storefront `/search` route just
29
+ re-exports `CatalogPage`. There is no parallel results-page contract.
30
+
31
+ ## Settings
32
+
33
+ Six knobs live under the `search` group, registered by
34
+ `packages/modules/search/src/manifest.ts`:
35
+
36
+ | Code | Type | Default | Purpose |
37
+ | --- | --- | --- | --- |
38
+ | `search.popup.suggestion_count` | `number` | `8` | How many products the storefront popup shows; `0` suppresses the popup but committed search still navigates |
39
+ | `search.popup.minimum_query_length` | `number` | `3` | Minimum characters before the storefront fires a suggest request and before the recorder persists a row |
40
+ | `search.llm.enabled` | `boolean` | `false` | Enables Meilisearch's hybrid lexical + semantic search on this channel |
41
+ | `search.llm.embedder_url` | `string` | `""` | OpenAI-compatible embedder endpoint URL (used by Meilisearch) |
42
+ | `search.llm.embedder_api_key` | `string` | `""` | Embedder API key (admin UI masks the input) |
43
+ | `search.llm.embedder_model` | `string` | `""` | Embedder model identifier (e.g. `text-embedding-3-small`) |
44
+
45
+ All six are sales-channel-scoped via the existing Settings model. The
46
+ storefront popup reads its count + threshold per request via the
47
+ universal getter (`SettingsService.get`); on any read error (cache
48
+ miss, `SettingNotRegistered`) the suggest service falls back to the
49
+ manifest defaults so a Settings hiccup never 500s the popup.
50
+
51
+ ## LLM-augmented search
52
+
53
+ Toggling `search.llm.enabled` on for a channel attaches a Meilisearch
54
+ embedder to that channel's index. We register the embedder under the
55
+ well-known name `default` with `source: 'openAi'`; the `url` parameter
56
+ makes it work against any OpenAI-compatible endpoint (OpenAI itself,
57
+ Azure OpenAI, Ollama's OpenAI shim, …).
58
+
59
+ The toggle wrapper (`POST /api/v1/admin/search/llm/toggle`) refuses to
60
+ flip `enabled=true` for any channel whose three embedder.* fields
61
+ aren't all populated. The error envelope's `details[]` lists
62
+ every missing `(channelCode, settingCode)` pair so the admin UI can
63
+ highlight the gaps.
64
+
65
+ The reactor (in `SearchEventSubscriber`) is belt-and-braces: a power
66
+ user PUTting `search.llm.enabled=true` directly through the generic
67
+ Settings admin route (bypassing the wrapper) does **not** poison
68
+ Meilisearch — the reactor warn-logs and leaves the index alone if the
69
+ embedder.* triplet is incomplete.
70
+
71
+ Disabling never refuses; the reactor calls `index.resetEmbedders()` so
72
+ the channel falls back to plain lexical ranking.
73
+
74
+ Meilisearch v1.11 gates `embedders` behind the `vectorStore`
75
+ experimental feature; v1.13+ treats embedders as stable. Both are
76
+ supported transparently — the admin endpoint does not gate on the
77
+ Meilisearch version.
78
+
79
+ ## Analytics ingest
80
+
81
+ Every committed storefront search lands one row in
82
+ `search_phrase_records` for the future Analytics module to aggregate:
83
+
84
+ | Column | Notes |
85
+ | --- | --- |
86
+ | `phrase` | Verbatim — no typo correction or LLM expansion |
87
+ | `phrase_normalized` | `lower(trim(phrase))`, maintained at insert time; supports case-insensitive aggregation without rewriting the verbatim phrase |
88
+ | `sales_channel_id` | FK → `sales_channels.id` (`ON DELETE RESTRICT` — dropping a channel must surface as a deliberate decision rather than silently delete history) |
89
+ | `result_count` | Number of products the search returned; `0` for dead-end phrases |
90
+ | `recorded_at` | `timestamptz default now()` |
91
+
92
+ Indexes:
93
+
94
+ - `idx_search_phrase_records_aggr` on `(sales_channel_id, phrase_normalized, recorded_at desc)` — supports the analytics-module's "phrases by channel and frequency over a window" query.
95
+ - `idx_search_phrase_records_recent` on `(recorded_at desc)` — for ops dashboards.
96
+
97
+ The recorder is fire-and-forget (`SearchPhraseRecorder.record(...)`):
98
+ it returns once the row has been queued, not once persisted, and
99
+ swallows every exception via warn-log. The route hands off the
100
+ promise without awaiting (`void recorder.record(...)`) and returns
101
+ `202 { ok: true }` immediately. The storefront response is therefore
102
+ never delayed or failed by analytics persistence.
103
+
104
+ Below-threshold phrases (shorter than the channel's
105
+ `minimum_query_length`) are silently no-op'd server-side as a
106
+ belt-and-braces against a UI bug flooding the table with stub
107
+ phrases.
108
+
109
+ ## Indexer + event subscriber
110
+
111
+ `SearchIndexer` writes one document per product into a per-channel
112
+ index named `products_<channel_code>`. Document shape carries the
113
+ catalog product surface (id, sku, name, description, type, status,
114
+ slug, primaryAssetUrl, categoryIds, categorySlugs, attributes,
115
+ searchableOptions, createdAt, updatedAt). The indexer also drives
116
+ Meilisearch's `searchableAttributes` + `filterableAttributes` from the
117
+ live `product_attributes.is_searchable` + `is_filterable` flags, and
118
+ its `sortableAttributes` from `SORTABLE_ATTRIBUTES` (see *Sorting*).
119
+
120
+ **A document carries no price.** It used to carry one, copied from the
121
+ legacy `attributes.defaultPrice` catalogue attribute — not any price
122
+ list's figure — and read by nothing on the query path, which resolves
123
+ the viewer's price from `price_lists` at hydrate time so the index
124
+ cannot decide what a buyer pays. A per-buyer price cannot be indexed
125
+ in any case: one index per sales channel and one document per product
126
+ means pricing the document would multiply the corpus by the customer
127
+ base. The legacy attribute is still indexed under
128
+ `attributes.defaultPrice`, where its name says what it is.
129
+
130
+ ### Sorting
131
+
132
+ Meilisearch refuses a sort on any field outside the index's
133
+ `sortableAttributes`, so the fields the query path may sort on are
134
+ declared once, in `SORTABLE_ATTRIBUTES` (`search-indexer.ts`):
135
+ `createdAt` and `name`. That constant is both what the indexer applies
136
+ to every channel index and the alphabet `buildSort` may emit — its
137
+ return type is built from it, so a sort naming a field the indexer
138
+ never declared does not compile.
139
+
140
+ The listing contract's four sorts map as follows:
141
+
142
+ | `?sort=` | Meilisearch |
143
+ | --- | --- |
144
+ | `relevance` (or absent) | none — the engine's ranking rules |
145
+ | `-createdAt` | `createdAt:desc` |
146
+ | `name` | `name:asc` |
147
+ | `-name` | `name:desc` |
148
+
149
+ The indexed `name` is resolved in the channel's own default language,
150
+ which is the language the listing renders, so the order a buyer sees
151
+ is the order they were sorted by.
152
+
153
+ **Upgrading an index built before the sort settings were applied** needs a reindex: the
154
+ settings were never applied (`sortableAttributes` was `[]` on every
155
+ index, and every sorted listing was silently answered by the Postgres
156
+ fallback), and `createdAt` was never written into a document. A full
157
+ reindex applies both. That happens automatically on the next periodic
158
+ sweep in the worker role (`search.reindex_interval_minutes`, default
159
+ 10); a deployment that runs no sweep needs the operator step — the
160
+ admin **Reindex products** action or `pnpm --filter backend run
161
+ search:reindex`. An `attribute.updated.v1` refresh reapplies the
162
+ settings but writes no documents, so it restores `name` sorting and
163
+ not `-createdAt`.
164
+
165
+ ### `searchableOptions` — option-list search
166
+
167
+ For attributes flagged `isSearchable` AND with a select-style
168
+ `valueType` (`select`, `enum`, `multiselect`), the indexer resolves
169
+ the per-locale option label and includes it in the document's
170
+ `searchableOptions: string[]` field. Meilisearch settings include
171
+ `searchableOptions` in `searchableAttributes` so a customer searching
172
+ for the rendered text they see (e.g. "Czerwony") hits products whose
173
+ raw value is the option key (e.g. "red"). Toggling `isSearchable` off
174
+ removes the option labels from `searchableOptions` on the next refresh
175
+ within the existing event-driven cadence.
176
+
177
+ `SearchEventSubscriber` keeps the indexes in sync:
178
+
179
+ | Event | Handler |
180
+ | --- | --- |
181
+ | `product.created.v1` | `indexer.upsertProduct(productId)` |
182
+ | `product.updated.v1` | `indexer.upsertProduct(productId)` |
183
+ | `product.archived.v1` | `indexer.deleteProduct(productId)` |
184
+ | `attribute.updated.v1` | `indexer.refreshAttributeSettings()` |
185
+ | `settings.value_changed` (when `settingCode === 'search.llm.enabled'`) | `indexer.attachEmbedderForChannel` / `detachEmbedderForChannel` |
186
+
187
+ All handlers swallow their own errors — a transient Meilisearch
188
+ outage does not break the catalog write path. The reserved fallback
189
+ in the read path (`catalog/routes.public.ts`) keeps storefront search
190
+ functional with a stale index until the next offline `search:reindex`.
191
+
192
+ ### When the index does not answer
193
+
194
+ The fallback stays a fallback — a public catalogue must not 503
195
+ because search is unhappy — but it distinguishes two facts that used
196
+ to be fused into one:
197
+
198
+ - **unreachable** — the engine is down, unroutable or timing out.
199
+ Transient. `catalog` logs it per request and re-runs the listing
200
+ through Postgres; nothing else is expected of anybody.
201
+ - **refused** — the engine answered, in milliseconds, that it will not
202
+ run *this* query: `invalid_search_sort`, `index_not_found`, a
203
+ rejected API key. Deterministic, will not pass on its own, and a
204
+ defect in how this module configured the index. The reason string
205
+ the caller logs names Meilisearch's own error code, and `search`
206
+ reports it itself at `error` level — once per code, because an index
207
+ setting is a deployment fact and a public listing would otherwise
208
+ report it on every request.
209
+
210
+ That distinction is the whole reason the defect could survive on five
211
+ live indexes: every sorted listing was falling back to Postgres, and
212
+ the only trace of it was a line reading `meilisearch unavailable`
213
+ about an engine that was up and healthy.
214
+
215
+ ## Storefront integration
216
+
217
+ The page header (`storefront/components/Header.tsx`) renders a
218
+ `<form action="/search" method="GET">` with a progressively-enhanced
219
+ `<SearchAutocomplete>` client component inside it:
220
+
221
+ - 200 ms debounce; AbortController cancels stale fetches.
222
+ - Below the channel's `minimum_query_length`, no request fires.
223
+ - Keyboard model: ArrowUp/Down to move selection, Enter to navigate
224
+ to the selected suggestion, Escape to close, click outside to
225
+ dismiss.
226
+ - 503 from `/search/suggest` surfaces a "search temporarily
227
+ unavailable" item in the popup without breaking the static form.
228
+ - With JS disabled, the form posts `?q=…` to `/search` natively
229
+ (no JS required for crawlability).
230
+
231
+ The `/search` page (`storefront/app/(catalog)/search/page.tsx`)
232
+ re-exports `CatalogPage` — the listing, filters, sort, pagination,
233
+ and empty state are identical to `/catalog`. The only search-specific
234
+ behaviour is the analytics fire-and-forget: when `?q=` is present,
235
+ the page awaits `listProducts` (so `resultCount` is meaningful), then
236
+ `void recordPhrase(...)` BEFORE delegating to `CatalogPage`.
237
+
238
+ ## Reindex CLI
239
+
240
+ `search reindex` walks every Sales Channel and pushes its public
241
+ product surface into Meilisearch. Idempotent — safe to run after a
242
+ fresh `endora demo seed` or whenever the index drifts from Postgres.
243
+
244
+ It is a command this module declares in its `manifest.ts` and the host
245
+ runs, so it reindexes through the one `SearchIndexer` the composition
246
+ holds rather than building a second one:
247
+
248
+ ```bash
249
+ pnpm --filter backend run search:reindex
250
+ # or, addressing the host binary directly:
251
+ pnpm --filter backend exec tsx src/cli.ts search reindex
252
+ ```
253
+
254
+ The body is `packages/modules/search/src/backend/cli/reindex.ts`.
255
+
256
+ ## Testing
257
+
258
+ - `backend/test/contract/search/public-suggest.contract.test.ts` (7 cases) — happy path, limit override, threshold, oversize, missing q, limit OOB.
259
+ - `backend/test/contract/search/public-record.contract.test.ts` (7 cases) — happy path, default `result_count`, channel pinning, empty/oversize/negative validation, below-threshold no-op.
260
+ - `backend/test/contract/search/admin-llm-toggle.contract.test.ts` (5 cases) — incomplete-config refusal (every embedder.* permutation), full-config success, disable always succeeds, unauthenticated → 401.
261
+ - `backend/test/integration/search/manifest-reconcile.test.ts` (2 cases) — group + 6 settings seeded with correct defaults; idempotent re-apply.
262
+ - `backend/test/integration/search/embedder-reactor.test.ts` (3 cases) — attach on enable=true, detach on flip-back-to-false, no event-fire when wrapper refuses the toggle.
263
+ - `backend/test/integration/search/event-subscriber.test.ts` (existing) — catalog-event-driven indexing.
264
+ - `backend/test/integration/search/catalog-via-meilisearch.test.ts` (existing) — env-flag-dispatched read path.
265
+ - `backend/test/integration/search/sort-order.test.ts` (6 cases) — each of the four sorts served by Meilisearch (`x-search-backend` is the load-bearing assertion: Postgres answers all four, so the order alone cannot tell a served page from a fallback), the declared `sortableAttributes`, and the absence of the indexed `price`.
266
+ - `backend/test/unit/search/search-sort-attributes.test.ts` (4 cases) — `buildSort` emits only fields `SORTABLE_ATTRIBUTES` declares.
267
+ - `backend/test/unit/search/search-degrade-observability.test.ts` (3 cases) — a refused query names the engine's error code and is reported once; an unreachable one is not reported twice.
268
+
269
+ The last two need no services; the rest run against real Postgres +
270
+ real Meilisearch.
271
+
272
+ ## Extension points
273
+
274
+ - **Custom rankers** — Meilisearch supports custom ranking rules per
275
+ index; the indexer can push the rule set when the admin UI grows a
276
+ per-channel weighting affordance.
277
+ - **Embedder-source enum** — `search.llm.embedder_source` with values
278
+ `openAi | huggingFace | rest | userProvided` would let a single
279
+ channel pick a non-OpenAI-compatible provider without renaming the
280
+ three credential settings.
281
+ - **Reserved fallback** — already implemented: when Meilisearch is
282
+ unavailable, the catalog read path degrades to Postgres ILIKE
283
+ search via `catalog-query.service.ts` so the storefront is never
284
+ fully broken.
package/i18n/en.json ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "errors.QUERY_TOO_SHORT": "Query Too Short.",
3
+ "errors.QUERY_TOO_LONG": "Query Too Long.",
4
+ "errors.LIMIT_OUT_OF_RANGE": "Limit Out Of Range.",
5
+ "errors.SEARCH_BACKEND_UNAVAILABLE": "Search Backend Unavailable.",
6
+ "errors.PHRASE_REQUIRED": "Phrase Required.",
7
+ "errors.PHRASE_TOO_LONG": "Phrase Too Long.",
8
+ "errors.RESULT_COUNT_INVALID": "Result Count Invalid.",
9
+ "errors.LLM_CONFIG_INCOMPLETE": "LLM search configuration is incomplete."
10
+ }
package/i18n/pl.json ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "errors.QUERY_TOO_SHORT": "Błąd: query too short.",
3
+ "errors.QUERY_TOO_LONG": "Błąd: query too long.",
4
+ "errors.LIMIT_OUT_OF_RANGE": "Błąd: limit out of range.",
5
+ "errors.SEARCH_BACKEND_UNAVAILABLE": "Błąd: search backend unavailable.",
6
+ "errors.PHRASE_REQUIRED": "Błąd: phrase required.",
7
+ "errors.PHRASE_TOO_LONG": "Błąd: phrase too long.",
8
+ "errors.RESULT_COUNT_INVALID": "Błąd: result count invalid.",
9
+ "errors.LLM_CONFIG_INCOMPLETE": "Konfiguracja wyszukiwania LLM jest niekompletna."
10
+ }
package/package.json ADDED
@@ -0,0 +1,72 @@
1
+ {
2
+ "name": "@endora-commerce/mod-search",
3
+ "version": "0.100.0",
4
+ "type": "module",
5
+ "sideEffects": false,
6
+ "description": "Per-channel Meilisearch indexes, suggest popup, and optional LLM-augmented search.",
7
+ "license": "MIT",
8
+ "endora": {
9
+ "type": "module",
10
+ "id": "search"
11
+ },
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/endora-commerce/endora-commerce.git",
15
+ "directory": "packages/modules/search"
16
+ },
17
+ "publishConfig": {
18
+ "access": "public"
19
+ },
20
+ "exports": {
21
+ ".": {
22
+ "types": "./dist/manifest.d.ts",
23
+ "default": "./dist/manifest.js"
24
+ },
25
+ "./backend": {
26
+ "types": "./dist/backend/index.d.ts",
27
+ "default": "./dist/backend/index.js"
28
+ },
29
+ "./migrations": {
30
+ "types": "./dist/migrations/index.d.ts",
31
+ "default": "./dist/migrations/index.js"
32
+ },
33
+ "./package.json": "./package.json"
34
+ },
35
+ "files": [
36
+ "dist",
37
+ "i18n",
38
+ "docs"
39
+ ],
40
+ "engines": {
41
+ "node": ">=22.18.0"
42
+ },
43
+ "peerDependencies": {
44
+ "@mikro-orm/core": "^6",
45
+ "@mikro-orm/migrations": "^6",
46
+ "@mikro-orm/postgresql": "^6",
47
+ "fastify": "^5",
48
+ "meilisearch": "^0",
49
+ "zod": "^4",
50
+ "@endora-commerce/platform": "0.100.0",
51
+ "@endora-commerce/contracts": "0.100.0"
52
+ },
53
+ "devDependencies": {
54
+ "@mikro-orm/core": "^6.6.13",
55
+ "@mikro-orm/migrations": "^6.6.13",
56
+ "@mikro-orm/postgresql": "^6.6.13",
57
+ "@types/node": "^22.9.0",
58
+ "fastify": "^5.12.5",
59
+ "meilisearch": "^0.57.0",
60
+ "typescript": "^5.9.3",
61
+ "vitest": "^4.1.11",
62
+ "zod": "^4.2.0",
63
+ "@endora-commerce/contracts": "0.100.0",
64
+ "@endora-commerce/platform": "0.100.0"
65
+ },
66
+ "scripts": {
67
+ "build": "tsc -p tsconfig.build.json",
68
+ "typecheck": "tsc -p tsconfig.json",
69
+ "lint": "eslint src",
70
+ "test": "vitest run"
71
+ }
72
+ }