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