# Architect — F-012 Search: interface plus PostgreSQL FTS ## Decision Split product search from product persistence behind a dedicated catalog port. The HTTP contract stays `GET /products/search`, while the implementation is swappable from PostgreSQL FTS to a future external engine without route changes. ## Domain/application Add a `ProductSearchRepository` (or equivalent) port in catalog domain: - Input: `q`, `limit`, `offset`, optional `brandSlug`, `activeOnly`. - Output: `Product[]` for this slice to preserve current API payload shape. `SearchProducts` must depend on the search port, not on `ProductRepository`. Product CRUD remains in `ProductRepository`. ## PostgreSQL implementation Add `PgProductSearchRepository` under catalog infrastructure. Search scope: - Product: `catalog_products.name`, `description`, `seo_title`, `seo_description`. - Brand: `brands_brands.name`, `slug`, `seo_title`, `seo_description`. - Category: `categories_categories.name`, `slug`, `seo_title`, `seo_description` through `catalog_product_categories`. Ranking/order: - When `q` is present, use PostgreSQL full text search with `websearch_to_tsquery('spanish', q)` and `ts_rank_cd`. - Stable ordering: rank descending, then `p.created_at DESC`, then `p.name ASC`, then `p.id ASC`. - When `q` is absent, keep stable listing order by `p.created_at DESC`, `p.name ASC`, `p.id ASC`. - Preserve pagination with bounded `limit` and `offset`. - Preserve active-only public search and `brandSlug` filtering. Migration: - Add GIN expression indexes for product, brand, and category searchable text. Do not add extensions or new runtime dependencies. - Keep migration reversible. ## Telemetry Measure search duration in the route or use case using `performance.now()` and log a structured event through the existing logger: - `event: 'catalog_search'` - `queryPresent`, `brandSlug`, `limit`, `offset`, `durationMs`, `resultCount` - If `q` is present, include a bounded/sanitized `query` value for popular-search analysis (trimmed max 200; never secrets). ## Tests/evidence - Unit: `SearchProducts` calls the search port with defaults and active-only true. - Unit or repository-level SQL construction: stable ordering and FTS query path should be represented in code and typechecked. - Existing integration tests may remain skipped without `TEST_DATABASE_URL`. - Required commands: `npm run lint`, `npm run typecheck`, `npm test`, `./scripts/verify.sh`. ## Out of scope No Elasticsearch, Meilisearch, Algolia, typo tolerance beyond PostgreSQL FTS, search suggestions UI, cache, or Redis persistence in this ticket.