Architecture
@nx/searchis a library; it has no standalone runtime. All diagrams show it embedded in the host (commerce) process.
1. System Context (C4 L1)
2. Container View (C4 L2)
3. Component View (C4 L3) — Internal Layering
| Layer | Responsibility |
|---|---|
Mixin / SearchController | HTTP surface, auth + permission gate, scope merge |
SearchService | Ignis filter → Typesense params, include resolution, response shaping |
TypesenseConverter | where/order/fields → filter_by/sort_by/include_fields |
| CDC services | Consume, batch, route, cascade, enrich, index |
| Mappers | DB row (snake/camel) → Typesense doc; null for soft-deleted |
CollectionRegistry | Singleton registry of the 8 configs (+ embed.from wiring) |
| Helpers | Typesense client, schema-drift comparison, include parsing |
4. State Machines Index
The only stateful runtime element is the CDC circuit breaker (opt-in).
| Element | States | Diagram |
|---|---|---|
| CDC circuit breaker | CLOSED, OPEN | → jump |
CDC Circuit Breaker
| From | Event | To | Guards |
|---|---|---|---|
CLOSED | health probe fails | OPEN | APP_ENV_CDC_CIRCUIT_BREAKER_ENABLED=true |
OPEN | probe recovers | CLOSED | quiet window elapsed (Typesense 30s / Google 90s) |
OPEN | — | OPEN | capped at maxOpenMs (30 min) |
When
OPEN, the consumer pauses (auto-commit is off, so offsets are not advanced) — messages are re-read after recovery.
5. Runtime Scenarios
5.1 Query path (consumer → Typesense)
| Step | Detail |
|---|---|
| 2 | resolveSearchScope() injects tenant/merchant where; defaults to null (open) unless host overrides |
| 5 | If disableSemanticSearch is set, the embedding field is dropped from query_by (strict keyword) |
| 7 | include relations resolved via Typesense native join (reference) or hydration lookup |
5.2 CDC sync (Debezium → Typesense)
| Step | Detail |
|---|---|
| 3 | Batch tuned by CDCBatchingConfig (200 max, 2000ms flush) + maxBytes 5MB |
| 5 | Mapper returns null for soft-deleted → triggers Typesense delete |
| 6 | Cascade sources: ProductCategory, MetaLink, FareSet/Fare, ProductBundler, most inventory tables |
| 8 | Auto-commit off — offsets advance only after the batch succeeds (at-least-once) |
5.3 Schema-drift check on bootstrap
Divergence is logged, never auto-applied — auto-rebuild would drop documents. Use the backfill scripts for additive changes.
6. Crosscutting Concerns
| Concern | How this library handles it |
|---|---|
| AuthN | Inherited from host — SearchController + mixin accept JWT or Basic (AuthenticateStrategy.JWT, .BASIC) |
| AuthZ | Permissions search:search / search:search-count; mixin requires an authorize config from the consumer |
| Multi-tenancy | resolveSearchScope() injects a where scope, and-merged with caller filter (host overrides per entity) |
| i18n | { en, vi } flattened to name.en / name.vi Typesense fields; future locales caught by wildcard fields |
| Logging | Structured key-value (key: %s) via IGNIS logger |
| Idempotency | LSN/version guards (source_lsn, deleted_at) reject out-of-order CDC events |
| Soft-delete | Mapper returns null for deletedAt != null → document deleted from Typesense |
| Resilience | Circuit breaker (opt-in) + DLQ for poison messages + retry utility |
| IDs | Document id = source row id; library allocates no Snowflake IDs |