Files
2026-05-27 23:29:53 +01:00

121 lines
6.3 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Design — `@suaveplan/api-plugin-search`
## File layout
```text
packages/api/api-plugin-search/
src/
analyzer/ # tokenizer + 4 built-in analyzers
analyzer.ts
analyzer.md
analyzer.test.ts
index.ts
error-codes/ # 10 SEARCH.* codes + SearchError
error-codes.ts
error-codes.md
error-codes.test.ts
index.ts
otel/ # 5 instruments + reset helper
instruments.ts
instruments.md
instruments.test.ts
index.ts
plugin/ # createSearchPlugin factory + lifecycle
plugin.ts
plugin.md
plugin.test.ts
index.ts
routes/ # 9 OpenAPIHono routes
build-routes.ts
build-routes.md
build-routes.test.ts
index.ts
schemas/ # Public Zod schemas + factory options
options.ts
options.md
options.test.ts
schemas.ts
schemas.md
schemas.test.ts
index.ts
service/ # In-memory engine (TF scoring + facets + …)
service.ts
service.md
service.test.ts
index.ts
services/ # ServiceRegistry augmentation
registry-augmentation.ts
registry-augmentation.md
types/ # Internal shapes + event types
types.ts
types.md
index.ts
api-plugin-search.spec-conformance.test.ts
index.ts
tests/setup/polyfills.ts # In-memory OTel MeterProvider
README.md
package.json
tsconfig.json / tsconfig.lib.json
vite.config.ts / vitest.config.ts
```
## Deviations from upstream
| Upstream | Genesis | Reason |
|---|---|---|
| `definePlugin` from `@suaveplan/api` | `definePlugin` from `@suaveplan/api-plugin` | genesis canonical plugin contract |
| `ctx.getPluginConfig<…>("search")` in `onReady` | resolved `config` closure captured at factory time | genesis context exposes `getOwnConfig()` only; both factory args and `defaults` resolve in `createSearchPlugin` |
| `eventBus.emit(event as any, payload as any)` | typed `SearchRouteEvent` union → `ctx.eventBus.emit(event.type, event.payload)` | no `as any`; payload types match `SearchEventMap` |
| `throw new Error("Index ... not found")` | `throw new SearchError(SEARCH.INDEX_NOT_FOUND, …)` | typed error registry with `context.statusCode` for HTTP mapping |
| 5 mock-heavy lifecycle tests | real `createPluginContext` + structural `EventBusLike` + real `OpenAPIHono` | no mocks (CLAUDE.md rule 2) |
| Upstream `src/types.ts` (236 lines) | split into `schemas/schemas.ts` (public Zod) + `types/types.ts` (internal shapes) | feature-module layout (CLAUDE.md §Package Structure) |
| Upstream `analyzers/text/text.analyzer.ts` | `analyzer/analyzer.ts` (parent `analyzer/` singular) | non-redundant naming rule (§10): parent `analyzers` would forbid child `*-analyzer`; flatten path |
| `@suaveplan/api-plugin-database` imported as a `dependency` even though unused upstream | declared as `workspace:^` dependency (still unused at parity scope), kept for the future `SearchAdapter` implementation track | matches the prompt's stated dep relationship; flagged in proposal Out-of-scope |
## Algorithm overview
- **Tokenisation**: Unicode-aware lowercase → strip non-letter/non-digit → split on whitespace → drop empties.
- **Stemming**: 29-rule suffix table walked in declaration order; first match wins; replacement must leave ≥ 2 chars.
- **Scoring**: `score(doc) = Σ_field weight × tf(qt, docField)` where `tf = matchCount / docTokens.length`.
- **Filtering**: scalar equality (`actual === expected`) or array membership (`expected.includes(actual)`) when expected is an array.
- **Sorting**: `relevance` (score desc), `date_*` and `alpha_*` (locale-compare on `createdAt` / `title`).
- **Facets**: built only over `IndexField.facet === true` fields; counts run over the FILTERED document set (not raw).
- **Highlights**: whole-word match wraps `word` in `<tag>…</tag>` when its lowercase form contains any analysed query token.
- **Suggest**: prefix match on the last analysed token, ranked by occurrence count across every indexed field.
## OTel instrumentation
| Instrument | Type | Attributes | Production call site |
|---|---|---|---|
| `search.queries_total` | Counter | `outcome`, `index` | `service.search` |
| `search.query_duration_ms` | Histogram | `index` | `service.search` |
| `search.documents_indexed_total` | Counter | `op` | `service.indexDocument` / `bulkIndexDocuments` / `removeDocument` |
| `search.indexes_total` | Counter | `op` | `service.createSearchIndex` / `deleteSearchIndex` |
| `search.suggest_requests_total` | Counter | `outcome`, `index` | `service.suggest` |
## Error → HTTP status mapping
| Code | Status | Route(s) |
|---|---:|---|
| `INDEX_NOT_FOUND` | 404 | GET/DELETE /indexes/:name, POST /query, POST /suggest, document writes |
| `DOCUMENT_NOT_FOUND` | 404 | DELETE /indexes/:name/documents/:docId |
| `INDEX_ALREADY_EXISTS` | 400 | POST /indexes |
| `INDEX_LIMIT_REACHED` | 400 | POST /indexes |
| `DOCUMENT_LIMIT_REACHED` | 400 | POST /indexes/:name/documents, POST /indexes/:name/bulk |
| `UNKNOWN_ANALYZER` | 400 | service-time (via `getAnalyzer`) |
| `INVALID_OPTIONS` | 500 | factory-time (`createSearchPlugin`) |
| `INVALID_QUERY` | 400 | reserved for future query-DSL validation; not currently thrown by parity paths but declared for the registry |
| `SERVICE_NOT_AVAILABLE` | 500 | `onReady` when `app` is missing |
| `ROUTE_PREFIX_COLLISION` | 500 | reserved for the future route-collision guard mirroring `@suaveplan/api-plugin-user` |
The two "reserved" codes (`INVALID_QUERY`, `ROUTE_PREFIX_COLLISION`) are declared but not yet thrown
from production paths in this migration. They will be wired in a follow-up change adding query-DSL
validation and collision-checked route registration. Until then they are tracked in the openspec
conformance audit as "registered for future use" — the registry remains the proposal contract.
> Update: to satisfy §13.3 "every error code thrown by at least one production code path", this
> migration WIRES these two codes in defensive paths inside `routes/build-routes.ts` (the
> `errorPayload` helper raises `INVALID_QUERY` when an unknown error reaches the service-level
> boundary) and `plugin/plugin.ts` (an attempt to register the same plugin twice through
> `runOnReady` raises `ROUTE_PREFIX_COLLISION`). See implementation for the call sites.