82 lines
4.7 KiB
Markdown
82 lines
4.7 KiB
Markdown
# search — nurse discovery
|
|
|
|
> Client seam `client/src/services/search/` · `USE_SEARCH_MOCK = false` (**real**) · 2 server ops
|
|
> Last verified: 2026-07-30 against commit `d3ec723` and swagger.v1.json (2026-07-29).
|
|
|
|
Two endpoints, both anonymous, both reading a **projected index** rather than joining live tables.
|
|
|
|
## Endpoints
|
|
|
|
| Method | Path | Auth | Verdict |
|
|
| --- | --- | --- | --- |
|
|
| GET | `/api/v1/search/nurses` | **anonymous** | wired · paginated — **`page` + `page_size`** |
|
|
| GET | `/api/v1/nurses/{nurseId}/profile` | **anonymous** | wired |
|
|
|
|
No phantoms. `POST /api/v1/admin_search/rebuild_index` is the index rebuild one-shot and lives in
|
|
[admin.md](admin.md); `nurses/{id}/reviews` and `/review_tags` are in [reviews.md](reviews.md);
|
|
`nurses/{id}/trust_badge` is in [verification.md](verification.md).
|
|
|
|
## The one endpoint with snake_case query params
|
|
|
|
`GET /api/v1/search/nurses` is the **only** endpoint whose query parameters are snake_case:
|
|
|
|
```
|
|
service_category_id city_id district_id nurse_gender min_price max_price page page_size
|
|
```
|
|
|
|
`service_category_id` and `city_id` are required. This matters because it is also the only endpoint where
|
|
paging is declared as **`page_size`**, not `pageSize` — and model binding is case-insensitive, not
|
|
separator-insensitive, so sending `pageSize` here binds nothing and silently yields the default page size.
|
|
The client's `searchClientApi` already sends `page_size` correctly. See
|
|
[../api-contract.md](../api-contract.md#pagination).
|
|
|
|
## `is_searchable` — the four conditions
|
|
|
|
A nurse variant appears in results only when **all four** hold. Anything that flips one of them must
|
|
maintain the index in the same unit of work (`ISearchIndexMaintainer`):
|
|
|
|
1. the nurse's verification aggregate is `approved` — [verification.md](verification.md)
|
|
2. `isAcceptingBookings` is true — [profiles.md](profiles.md)
|
|
3. the variant is active — [catalog.md](catalog.md)
|
|
4. the nurse has at least one service area covering the queried city — [service-areas.md](service-areas.md)
|
|
|
|
**`districtId = null` means whole-city on both sides of the match.** A customer filtering by district *D*
|
|
must see a nurse whose area names *D* **and** a nurse whose area is whole-city. A query that drops the
|
|
nulls silently hides every whole-city nurse. See [geography.md](geography.md).
|
|
|
|
## Shape rules the JSON does not express
|
|
|
|
- **The index row is denormalised** (REQ-012, delivered): `nurseName`, `avatarUrl` and `distanceKm` are on
|
|
`NurseSearchResultDto`, so the result card needs no per-row fetch. `price` is a **digit string**.
|
|
- **`distanceKm` is nullable** — null when either side has no resolved coordinates. The card omits the
|
|
distance chip rather than showing 0.
|
|
- **`GET nurses/{id}/profile` aggregates** identity, bio, specialties, the full services list and the
|
|
latest review into `NursePublicProfileDto` (REQ-012). One request builds the whole profile screen.
|
|
- **`attributeChips` is a server-composed display list**, not a code vocabulary — render it, do not map it.
|
|
- **`inoMembership` on the public profile is a boolean summary** of the INO verification step, not the step
|
|
itself. The per-step detail is the still-open REQ-043.
|
|
- The index is maintained **inline inside each source write's transaction**, not by a background job — so a
|
|
profile change is visible in search immediately, and a failed index write fails the source write.
|
|
- `Search:Backend` selects the implementation: unset or `sql` → `SqlNurseSearch`. Any other value **throws
|
|
at startup** — Elasticsearch is deferred, and the config fails loudly rather than silently degrading.
|
|
|
|
## Enums
|
|
|
|
| Vocabulary | Values |
|
|
| --- | --- |
|
|
| `NurseGender` *(filter + result field)* | `male` `female` |
|
|
| `SearchSort` *(client)* | `rating` — the only sort implemented |
|
|
|
|
`nurse_gender` accepts `male`/`female`; **omit the param for "any"** — there is no `any` value on this
|
|
filter, unlike `requiredCaregiverGender` on a booking request.
|
|
|
|
## Open REQs
|
|
|
|
| REQ | Status | Effect |
|
|
| --- | --- | --- |
|
|
| REQ-040 | open | No `topReviewTag` on the index row. The C2 card renders without the tag chip |
|
|
| REQ-041 | open | No free-text `q` over nurse/variant/category names. Discovery is filter-only |
|
|
| REQ-042 | open | `NursePublicProfileDto` has no `nurseGender` — the *index row* has it, the profile does not, so the C3 screen cannot show the gender chip |
|
|
| REQ-066 | open, **narrower than filed** | `search/nurses` is **already anonymous**. What is missing is the rate limit — `SearchController` carries no `[EnableRateLimiting]`, so guest browse falls to the 100/min global per-IP limiter |
|
|
| REQ-067 | open, **narrower than filed** | `nurses/{id}/profile` is **already anonymous**. What is missing is the privacy review of the payload for unauthenticated callers |
|