Files
suaveplan/openspec/archive/2026-05-18-add-api-plugin-search/proposal.md
2026-05-27 23:29:53 +01:00

9.6 KiB

Add @suaveplan/api-plugin-search

Package: @suaveplan/api-plugin-search Status: PROPOSED Type: new-package Design: ./design.md Spec delta: ./specs/api-plugin-search/spec.md

Why

The Suaveplan API host needs an in-process full-text search plugin so consumer apps can index domain documents (users, posts, products, audit events, ...) and serve relevance-scored queries, facets, filters, highlights, and autocomplete suggestions without a separate search-engine deployment. Upstream @suaveplan/api-plugin-search provided this as an in-memory engine with TF-IDF scoring, four pluggable analyzers (standard, simple, whitespace, keyword), faceted aggregation, boolean filters, and optional highlighting. The genesis migration brings the full functional surface across, swaps the upstream "old-style" @suaveplan/api Plugin contract for the canonical @suaveplan/api-plugin PluginDescriptor shape, fixes the two as any casts in the event bus call site by routing through typed payloads, replaces raw throw new Error(...) sites with typed SearchError carrying stable SEARCH.* codes, wires OpenTelemetry instruments (5 counters/histograms) onto every hot path, and aligns the package with the rest of the genesis monorepo (5-files-per-feature, sibling .md docs, runner-neutral test imports, 100% istanbul coverage).

What changes

  • ADDED: createSearchPlugin(options?) — @suaveplan/api-plugin-shaped PluginDescriptor<SearchPluginConfig> factory that mounts the search routes at a configurable routePrefix (default /search) and registers a searchIndexes service in the host ServiceRegistry.
  • ADDED: SearchPluginOptionsSchema / SearchPluginOptions / SearchPluginConfig — Zod-validated factory options (routePrefix, maxIndexes, maxDocumentsPerIndex, defaultAnalyzer).
  • ADDED: In-memory SearchService functions (createSearchIndex / deleteSearchIndex / getSearchIndex / listSearchIndexes / indexDocument / bulkIndexDocuments / removeDocument / search / suggest) plus DEFAULT_SEARCH_CONFIG and the SearchServiceConfig type.
  • ADDED: Four text analyzers via factory functions (createStandardAnalyzer, createSimpleAnalyzer, createWhitespaceAnalyzer, createKeywordAnalyzer), plus exported primitives tokenize, removeStopWords, stem, getAnalyzer, and listAnalyzers. The TextAnalyzer and AnalyzerOptions interfaces are public so consumers can author and register their own analyzers.
  • ADDED: createSearchRoutes(deps) — OpenAPIHono router exposing 9 routes: POST /query, POST /suggest, GET /indexes, POST /indexes, GET /indexes/:name, DELETE /indexes/:name, POST /indexes/:name/documents, POST /indexes/:name/bulk, DELETE /indexes/:name/documents/:docId. Every route validates request bodies/params with Zod via @hono/zod-openapi, and every error response is the canonical { error: string } shape.
  • ADDED: 10 SEARCH.* error codes via defineCodes("SEARCH", [...]) — INDEX_NOT_FOUND, INDEX_ALREADY_EXISTS, INDEX_LIMIT_REACHED, DOCUMENT_LIMIT_REACHED, DOCUMENT_NOT_FOUND, UNKNOWN_ANALYZER, INVALID_OPTIONS, INVALID_QUERY, SERVICE_NOT_AVAILABLE, ROUTE_PREFIX_COLLISION — each thrown from at least one production code path.
  • ADDED: 5 OTel instruments under the @suaveplan/api-plugin-search meter — search.queries_total{outcome}, search.query_duration_ms (histogram), search.documents_indexed_total{op}, search.indexes_total{op} and search.suggest_requests_total{outcome}.
  • ADDED: Typed event-bus extension for search:initialized, search:ready, search:index:created, search:index:deleted, search:index:updated, search:document:indexed, search:bulk:completed, search:document:removed, and search:query:executed — payloads strongly typed; no as any casts on the emitter.
  • ADDED: searchIndexes slot on the canonical @suaveplan/api-plugin/service-registry via module augmentation, typed as Map<string, SearchIndex> so sibling plugins can read/write indexes with full type safety.

Out of scope

  • Persistent backend (SurrealDB FTS / Typesense / Meilisearch) — the upstream package is in-memory only and the migration matches parity. A separate follow-up change can introduce a SearchAdapter driver against @suaveplan/api-plugin-database once that package lands; the public SearchAdapter interface ships here so the contract is stable.
  • Tenant scoping, auth gates, rate limiting, audit logging — upstream did not carry these; the enterprise audit document flags them as future work. They are not in this migration to preserve parity.
  • Vector search, semantic ranking, learning-to-rank, saved searches, language-specific stemmers, query DSL — enterprise audit §4 enhancements, deferred to dedicated follow-up changes.
  • Benchmark suite under src/__benchmarks__/ — landing as a follow-up change so this migration ships at parity scope first.

Source mapping

This change derives from ../suaveplan/enterprise/api-plugin-search.md and the upstream source at ../suaveplan/packages/server/api-plugin-search/src/.

Requirement Source lines Notes
In-memory search engine with TF-IDF scoring enterprise L8-L13; src/services/search/search.service.ts#scoreDocuments L302-L336 verbatim parity
Four built-in analyzers (standard/simple/whitespace/keyword) enterprise L8-L13; src/analyzers/text/text.analyzer.ts L156-L223 verbatim parity
Tokenize + stop-word + Porter-style stemmer src/analyzers/text/text.analyzer.ts L52-L123 verbatim parity
createSearchPlugin factory + four lifecycle hooks src/index.ts L39-L100 re-typed for @suaveplan/api-plugin
SearchPluginConfigSchema (basePath/maxIndexes/maxDocumentsPerIndex/defaultAnalyzer) src/index.ts L21-L33 renamed to SearchPluginOptionsSchema for genesis convention
9 OpenAPIHono routes with Zod request/response schemas src/routes/search/search.ts L41-L479 verbatim route surface
Bulk index limit (1000 docs/request) src/types.ts#BulkIndexSchema L113-L116 verbatim
Faceted aggregation honouring IndexField.facet src/services/search/search.service.ts#buildFacets L431-L459 verbatim
Boolean / array filter pass src/services/search/search.service.ts#applyFilters L338-L355 verbatim
Sort orders (relevance / date_asc / date_desc / alpha_asc / alpha_desc) src/services/search/search.service.ts#sortResults L357-L398 verbatim
Highlighting with configurable HTML tag src/services/search/search.service.ts#buildHighlights L400-L429 verbatim
Prefix-based suggest src/services/search/search.service.ts#suggest L250-L291 verbatim
Per-index document limit enforcement src/services/search/search.service.ts#indexDocument L117-L154 enforced via SEARCH.DOCUMENT_LIMIT_REACHED
Event-bus emission on lifecycle, query, index updates src/index.ts L64-L93; src/routes/search/search.ts L92-L99/L350-L356/L414-L420/L465-L471 drop both as any casts (audit §3)
Typed SearchQueryExecutedEvent / SearchIndexUpdatedEvent payloads src/types.ts L188-L218 preserved verbatim
SearchAdapter interface (future SurrealFTS / Typesense backends) src/types.ts#SearchAdapter L172-L184 preserved verbatim

Requirements with no source line

Requirement Justification
Typed SEARCH.* error registry via defineCodes Genesis-wide rule (every package extends SuaveplanError); enterprise audit §3 explicitly calls for it.
OTel instruments on every hot path Genesis-wide observability rule + enterprise audit §3.
Runner-neutral test imports (@suaveplan/testing/runner) Genesis rule 11.
100 / 100 / 100 / 100 istanbul coverage Genesis-wide policy.
Sibling .md documentation per implementation file Genesis-wide policy.
Subpath exports per feature module Genesis-wide tree-shaking rule (§6).
ServiceRegistry module augmentation for searchIndexes slot Genesis convention introduced by @suaveplan/api, @suaveplan/api-plugin-auth, @suaveplan/api-plugin-user.

Verification approach

Every requirement in the spec delta carries a **Verified by:** line at draft time pointing at the planned it(...) titles below.

  • src/error-codes/error-codes.test.ts — SEARCH.* registry shape + SearchError cause chain.
  • src/otel/instruments.test.ts — meter binding, cache reset, instrument names.
  • src/analyzer/analyzer.test.ts — tokenize / stop-words / stem / four analyzer factories / registry lookup / unknown-name failure.
  • src/schemas/schemas.test.ts — every Zod schema parse/reject path.
  • src/schemas/options.test.ts — SearchPluginOptionsSchema defaults and bounds.
  • src/service/service.test.ts — index lifecycle, document indexing, search, filters, sort, facets, highlighting, suggest, error codes, OTel emission.
  • src/routes/build-routes.test.ts — every route (9 endpoints, success + error branches).
  • src/plugin/plugin.test.ts — descriptor metadata, onInit / onReady / onClose, custom routePrefix, missing app service.
  • src/api-plugin-search.spec-conformance.test.ts — type-level public-surface pins + barrel exports + side-effect imports.

Migration impact

None — additive only. No existing genesis package depends on @suaveplan/api-plugin-search.

Impact

Affected dependents

No dependents yet. After publication, sibling plugins (api-plugin-user, api-plugin-cms, etc.) can read the augmented searchIndexes service slot.

  • N/A — [x] no action needed (new package, zero pre-existing consumers in genesis)