# 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.
