backend phase 7: search & matching (nurse_search_index)
Add the discovery layer: the denormalized nurse_search_index read model (one row per bookable variant x covered service area), maintained inline inside each source write's transaction, plus the single public search query behind the INurseSearch seam. - Entity + EF config + migration (search schema): covering search index, filtered-unique (variant_id, city_id, district_id) pair with NULL district participating, nurse_id index, soft-delete. - ISearchIndexMaintainer (write seam) + SearchIndexMaintainer: reindex variant / nurse / fan-out / remove-area / full rebuild, staged in the owning source write's unit of work; wired into the b3/b4/b5/b6 handlers. - INurseSearch (read seam) + SqlNurseSearch (real MVP backend): reads only is_searchable=1, category/city/district(NULL-aware)/gender/price filters, rating sort, pagination. Elasticsearch deferred (config Search:Backend). - SearchNursesQuery (+ validator) and RebuildSearchIndexCommand; public SearchController (GET search/nurses) + admin AdminSearchController (POST admin_search/rebuild_index). - Tests: 9 DB-backed maintainer/search + 4 WebApplicationFactory; updated affected b3/b4/b5/b6 handler tests. Build clean, 167 tests green. - Docs: server CLAUDE.md project map, contract search.md, swagger refresh, handoff, report, mocks-registry rows, STATUS. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1363,6 +1363,71 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v1/admin_search/rebuild_index": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"AdminSearch"
|
||||
],
|
||||
"operationId": "AdminSearch_RebuildIndex",
|
||||
"responses": {
|
||||
"400": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ApiResultOfDictionaryOfStringAndListOfString"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ApiResult"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ApiResult"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ApiResult"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ApiResultOfSearchIndexRebuildResult"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v1/admin_verifications": {
|
||||
"get": {
|
||||
"tags": [
|
||||
@@ -3721,7 +3786,7 @@
|
||||
"tags": [
|
||||
"Me"
|
||||
],
|
||||
"description": "Role claims live inside the access token — after selecting a role the client should\n refresh its tokens to pick the new role up.",
|
||||
"description": "Role claims live inside the access token \u2014 after selecting a role the client should\n refresh its tokens to pick the new role up.",
|
||||
"operationId": "Me_SelectRole",
|
||||
"requestBody": {
|
||||
"x-name": "command",
|
||||
@@ -6639,6 +6704,160 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v1/search/nurses": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Search"
|
||||
],
|
||||
"operationId": "Search_Nurses",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "service_category_id",
|
||||
"x-originalName": "serviceCategoryId",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"x-position": 1
|
||||
},
|
||||
{
|
||||
"name": "city_id",
|
||||
"x-originalName": "cityId",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"x-position": 2
|
||||
},
|
||||
{
|
||||
"name": "district_id",
|
||||
"x-originalName": "districtId",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"nullable": true
|
||||
},
|
||||
"x-position": 3
|
||||
},
|
||||
{
|
||||
"name": "nurse_gender",
|
||||
"x-originalName": "nurseGender",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"x-position": 4
|
||||
},
|
||||
{
|
||||
"name": "min_price",
|
||||
"x-originalName": "minPrice",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"nullable": true
|
||||
},
|
||||
"x-position": 5
|
||||
},
|
||||
{
|
||||
"name": "max_price",
|
||||
"x-originalName": "maxPrice",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"nullable": true
|
||||
},
|
||||
"x-position": 6
|
||||
},
|
||||
{
|
||||
"name": "price_unit",
|
||||
"x-originalName": "priceUnit",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"x-position": 7
|
||||
},
|
||||
{
|
||||
"name": "page",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"x-position": 8
|
||||
},
|
||||
{
|
||||
"name": "page_size",
|
||||
"x-originalName": "pageSize",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"x-position": 9
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"400": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ApiResultOfDictionaryOfStringAndListOfString"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ApiResult"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ApiResult"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ApiResult"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ApiResultOfPagedResultOfNurseSearchResultDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/support_alerts/get_support_alerts": {
|
||||
"get": {
|
||||
"tags": [
|
||||
@@ -7617,6 +7836,41 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"ApiResultOfSearchIndexRebuildResult": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/ApiResult"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"data": {
|
||||
"nullable": true,
|
||||
"oneOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/SearchIndexRebuildResult"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"SearchIndexRebuildResult": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"nursesProcessed": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"rowsWritten": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ApiResultOfPagedResultOfAdminPendingStepDto": {
|
||||
"allOf": [
|
||||
{
|
||||
@@ -10288,6 +10542,98 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"ApiResultOfPagedResultOfNurseSearchResultDto": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/ApiResult"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"data": {
|
||||
"$ref": "#/components/schemas/PagedResultOfNurseSearchResultDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"PagedResultOfNurseSearchResultDto": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"items": {
|
||||
"type": "array",
|
||||
"nullable": true,
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/NurseSearchResultDto"
|
||||
}
|
||||
},
|
||||
"total": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"page": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"pageSize": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
}
|
||||
}
|
||||
},
|
||||
"NurseSearchResultDto": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"variantId": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"nurseId": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"serviceCategoryId": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"price": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"priceUnit": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"nurseGender": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"averageRating": {
|
||||
"type": "number",
|
||||
"format": "decimal"
|
||||
},
|
||||
"totalReviews": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"totalCompletedBookings": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"cityId": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"districtId": {
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"nullable": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"ApiResultOfPagedResultOfSupportAlertDto": {
|
||||
"allOf": [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user