Procedure Hub 범용 Retrieval API 설계¶
상태: 로컬 v1 구현·검증 완료, 임상 fixture 승인 대기 · 2026-09-03
대상: 자연어 질의 하나로 Procedure Hub 전체 게시 지식에서 관련 정보를 찾는 API
선행 설계: 목표 임상 데이터 ERD
구현 경계: 도메인 메서드 계약
1. 목적¶
이 API는 안전성 전용 API, 시술 추천 API, 문헌 검색 API 또는 최종 상담 답변 생성 API가 아니다. 호출자가 분야·화면·테이블·분류를 알지 못해도 자연어 질의 하나만 보내면 Procedure Hub 전체 게시 지식 중 관련 있는 정보를 분야 경계 없이 찾아서 전달하는 범용 Retrieval API다.
자연어 query
→ 현재 published release 확정
→ PHub 전체 searchable domain에서 후보 검색
→ entity와 관계를 따라 관련 정보 확장
→ 안전 정보 강제 합류
→ 관련도 정렬·중복 제거·context budget 적용
→ 출처와 release가 붙은 Context Pack 반환
“모든 관련 정보”는 전체 DB dump를 의미하지 않는다.
- 모든 등록 domain을 검색한다. intent가 특정 domain 검색을 차단하지 않는다.
- 관련성 기준을 통과한 정보는 원래 메뉴나 데이터 종류와 무관하게 후보가 된다.
- 응답 크기는 명시적 budget으로 제한하고, 잘린 결과와 검색 coverage를 응답에 표시한다.
- safety·금기·응급 정보는 일반 관련도 순위 때문에 탈락하지 않는다.
2. 교체한 구조¶
기존 CanonicalRetrievalService는 여섯 intent를 여섯 고정 section으로 바꾸고, exact
entity가 확인된 뒤 일부 관계형 테이블만 조회했다. 현재는 그 계획기와 고정
section 응답을 제거하고 다음 구조로 교체했다.
기존: query → intent → 정해진 section → exact alias → section별 SQL
현재: query → 전체 corpus sparse 검색 → entity/관계 확장 → 안전·근거 합류 → 통합 ranking
교체 대상이었던 제한은 다음과 같다.
RetrievalPlanner._sections_by_intent가 검색할 domain을 미리 제외한다.- entity exact/normalized alias가 없으면 관련 문서와 문헌을 찾지 못한다.
- anatomy, consent, pricing, patient education, PIH, comparison, optimization, sequence, extracted knowledge 등은 현재 응답에 담을 공통 단위가 없다.
procedures/products/recommendations/safety/evidence/guidance고정 응답은 모든 PHub 지식을 손실 없이 표현할 수 없다.answer와generate_answer가 Retrieval 책임을 흐린다. 최종 문장 생성은 이 API의 필수 책임이 아니다.
따라서 현재 로컬 Retrieval 초안을 확장하지 않고 범용 retrieval model로
교체한다. 아직 STG·PRD에 배포되지 않았고 repository 내부 caller는 평가 script와 test뿐이므로
별도 v2 endpoint나 호환 adapter를 만들지 않는다. 기존 Insight라는 제품 개념과 API는
범용 지식 검색 책임과 중복되어 제거한다.
3. Public API¶
3.1 Endpoint¶
POST /procedure-hub/api/v1/knowledge/retrieve
Content-Type: application/json
3.2 Request¶
기본 사용자는 query만 보내면 된다.
{
"query": "눈가에 보톡스를 맞고 필러도 같이 받아도 될까?",
"lang": "ko",
"max_results": 40,
"cursor": null
}
| 필드 | 필수 | 계약 |
|---|---|---|
query |
예 | 1~500자 자연어 질의 |
lang |
아니오 | 표시 언어, 기본 ko |
max_results |
아니오 | 한 응답의 Knowledge Item 상한, 기본 40·최대 200 |
cursor |
아니오 | 같은 query·release의 다음 결과를 요청하는 opaque cursor |
fields |
아니오 | 반환할 응답 묶음 선택. 생략하면 전체 응답 반환 |
공개 request에는 검색 domain이나 entity type을 제한하는 filter를 두지 않는다.
fields는 검색 범위가 아니라 반환할 interpretation, items, sources, safety,
coverage, retrieval 묶음만 선택한다. Router가 검색 결과를 받은 뒤 응답을 구성하며,
서비스의 RetrievalCommand에는 fields를 전달하지 않는다. 자세한 계약은
연동 가이드의 요청 필드를 따른다.
환자 이름, 연락처, 주민등록번호 등 개인식별정보를 입력으로 받지 않는 것을 계약으로 한다. raw query는 기본적으로 영구 저장하지 않는다.
3.3 Response¶
서로 다른 domain의 정보를 같은 형식으로 반환하기 위해 결과의 최소 단위를
KnowledgeItem으로 통일한다.
{
"request_id": "uuid",
"status": "ok",
"query": "눈가에 보톡스를 맞고 필러도 같이 받아도 될까?",
"interpretation": {
"intents": ["combination_safety"],
"entities": [
{"type": "procedure", "id": "M1", "label": "보톡스", "match": "exact_alias"},
{"type": "procedure", "id": "M2", "label": "필러", "match": "exact_alias"}
],
"ambiguous": [],
"unmatched_terms": []
},
"items": [
{
"ref": "safety:CONSTRAINT-M1-M2",
"domain": "legacy.contraindications",
"kind": "safety_rule",
"title": "보톡스·필러 병용",
"content": "같은 날 가능하나 동일 부위는 보톡스 후 필러 순서를 권장합니다.",
"attributes": {
"level": "safe",
"interval": "same_day",
"from": "M1",
"to": "M2"
},
"entity_refs": ["M1", "M2"],
"source_refs": ["revision:uuid", "legacy.contraindications#rules[0]"],
"relevance": {
"score": 0.96,
"reasons": ["exact_entity", "relation_match", "safety_mandatory"]
}
}
],
"sources": [
{
"source_id": "revision:uuid",
"domain": "legacy.contraindications",
"title": "시술 병용 안전 규칙",
"locator": "contraindications:rules/0",
"revision_id": "uuid",
"verification_status": "published_revision"
}
],
"safety": {
"status": "needs_confirmation",
"item_refs": ["safety:CONSTRAINT-M1-M2"],
"required_checks": ["환자별 병력·복용 약물·알레르기 확인"],
"limitations": ["환자별 시술 가능 여부를 확정하는 결과가 아닙니다."]
},
"coverage": [
{"domain": "legacy.contraindications", "role": "searchable", "matched_count": 1},
{"domain": "legacy.anatomy-danger-zones", "role": "searchable", "matched_count": 2},
{"domain": "legacy.literature-evidence", "role": "resolution_only", "matched_count": 3}
],
"retrieval": {
"strategies": ["exact_catalog_identity", "sparse_trigram_index", "entity_relation_expansion", "mandatory_safety_injection", "verified_evidence_gate"],
"candidate_count": 34,
"returned_count": 17,
"truncated": false,
"next_cursor": null
},
"limitations": [],
"data_revision": {
"release_id": "uuid",
"version": "2026.09.1",
"published_at": "2026-09-03T12:00:00+09:00"
}
}
예시에서는 coverage를 세 domain만 표시했지만 실제 응답은 registry의 모든 domain을 0건까지
포함한다. 그래야 호출자가 전체 검색 수행 여부와 결과가 없었던 영역을 구분할 수 있다.
3.4 KnowledgeItem¶
| 필드 | 의미 |
|---|---|
ref |
응답 안에서 재사용 가능한 stable item reference |
domain |
원본 PHub 지식 영역 |
kind |
procedure_profile, safety_rule, clinical_claim 같은 세부 종류 |
title |
사람이 빠르게 식별할 제목 |
content |
원본 의미를 보존한 검색·표시용 본문 |
attributes |
간격, 가격, 중증도처럼 domain별 구조 값을 보존하는 JSON object |
entity_refs |
item이 연결된 canonical entity reference |
source_refs |
revision, artifact, claim, citation 역추적 reference |
relevance |
점수와 포함 이유. 의료적 신뢰도 점수가 아님 |
top-level sources는 item의 source_refs가 가리키는 출처 metadata를 중복 없이 제공한다.
문헌이면 제목·PMID·DOI와 검증 상태를, 내부 지식이면 source artifact·revision·locator를 담는다.
attributes는 원본 전체 snapshot을 담는 escape hatch가 아니다. Projector가 item kind별로
허용한 작은 구조만 넣는다. 동일 사실은 하나의 item으로 정규화하고 여러 source가 지지하면
source_refs를 합친다.
3.5 Status¶
| status | 의미 |
|---|---|
ok |
하나 이상의 관련 item을 정상 반환 |
no_match |
모든 domain을 검색했지만 기준을 통과한 item이 없음 |
needs_clarification |
entity 모호성 때문에 결과 의미가 크게 달라질 수 있음 |
partial |
등록 domain 또는 index 일부를 검색하지 못함 |
unavailable |
published release나 해당 release의 완성된 index가 없음 |
error |
요청은 유효하지만 retrieval을 완료하지 못함 |
결과가 budget 때문에 잘린 경우는 partial이 아니다. status=ok,
retrieval.truncated=true, next_cursor로 표현한다.
4. 검색 대상¶
4.1 Searchable domain registry¶
모든 published revision은 registry에서 searchable, resolution_only, excluded 중 하나의
명시적 상태를 가져야 한다. 등록되지 않은 domain이 조용히 검색에서 빠지는 것을 금지한다.
| 역할 | domain |
|---|---|
| identity/master | taxonomy, products, glossary/terminology alias |
| procedure knowledge | procedure_profiles, procedure_comparison, extracted knowledge |
| recommendation | combo_recipes, synergy, problem_combos, problem_packages, treatment_optimization |
| workflow/relation | treatment_sequence, contraindications, pre_procedure_checklist, consent_checklist |
| risk/guidance | anatomy_danger_zones, pih_risk, complications, post_procedure_timeline, patient_education |
| evidence | literature_evidence, verified clinical claims and citations |
| commercial context | market_pricing |
| resolution only | UI locale의 domain label, canonical alias |
| excluded | UI 문구 i18n, extraction guardrails, import log, test fixture |
분류는 source file 이름이 아니라 canonical domain_key로 관리한다. 원본 JSON 파일은
runtime 검색 입력이 아니다.
4.2 공통 index projection¶
기존 canonical/relational table과 published ContentRevision을 source of truth로 유지하고,
검색 전용 read model을 생성한다.
KnowledgeSearchUnit
id
revision_id
domain_key
stable_ref
kind
title
content
attributes JSONB
entity_refs JSONB
source_refs JSONB
search_text
이 index는 aggregate나 새로운 원본 데이터가 아니다. 언제든 published release에서 다시 만들
수 있는 projection이다. 화면이나 API가 KnowledgeUnit을 직접 수정하지 않는다.
Projector는 두 종류만 둔다.
- 구조화 projector: safety, 가격, 회복 기간처럼 typed attribute가 중요한 domain
- document projector: 설명·교육·문헌처럼 문장 단위 검색이 중요한 domain
모든 domain마다 별도 Service를 만들지 않고 classify_domain과
project_revision이 공통 lifecycle을 소유한다. 미등록 domain이 발행 대상에 들어오면
발행 transaction을 중단한다.
4.3 Publication과 index 전환¶
새 release를 먼저 공개하고 나중에 index를 만드는 순서를 사용하지 않는다.
approved revision 집합
→ candidate release 생성
→ revision별 KnowledgeSearchUnit 생성
→ registry coverage·source link 검사
→ release를 current로 전환
index build나 검증이 실패하면 publish transaction 전체가 rollback되어 기존 published release를 그대로 유지한다. unit은 immutable revision에 묶이므로 rollback release는 같은 unit을 재사용한다.
5. Retrieval pipeline¶
5.1 Release gate¶
- current published release 하나만 선택한다.
- 그 release의 revision에 묶인 search unit만 조회한다.
- 서로 다른 release의 unit을 한 응답에 섞지 않는다.
- item마다 source revision을 역추적할 수 있어야 한다.
5.2 Candidate retrieval¶
각 query는 하나의 통합 index를 통해 모든 searchable domain을 검색한다.
- canonical code/name/alias exact match
- normalized alias와 glossary match
- PostgreSQL trigram index를 사용한 sparse lexical retrieval
- 결과의 canonical entity를 기준으로 relation 확장
- 적용 가능한 safety·금기·응급 item 강제 합류
- 승인 claim·검증 source만 evidence item으로 합류
intent는 ranking weight와 관계 확장 방향에만 사용한다. intent가 검색할 domain을 제외하지 않는다. LLM은 entity나 사실을 새로 만들지 않으며 1차 retrieval 필수 의존성이 아니다.
현재 v1은 exact+sparse+relation+safety까지 구현한다. Dense retrieval은 임상 query
fixture에서 sparse 재현율 부족이 확인될 때만 EmbeddingProvider port로 추가한다.
로컬·서버 모두 별도 개인 API key에 의존하지 않는 versioned multilingual model을
선택하고, 모델이 다른 vector를 같은 index에서 섞지 않는다.
5.3 Fusion과 ranking¶
- v1은 exact entity·title·content·entity relation 일치를 합성해 점수화한다.
- dense retrieval을 추가할 때 sparse와 dense 순위를 Reciprocal Rank Fusion으로 합친다.
- exact canonical entity와 직접 relation match를 우선한다.
- 동일 사실·동일 source fragment는 하나로 합친다.
- 한 domain의 긴 문서가 결과를 독점하지 않도록 domain diversity를 적용한다.
- safety mandatory item은 score threshold와
max_results바깥에서도 우선 포함한다. - relevance score는 검색 관련도일 뿐 임상 정확도나 권고 강도를 뜻하지 않는다.
5.4 Context budget¶
전체 domain은 항상 검색하지만 반환량은 제한한다.
- 기본 40 items, 최대 200 items
- item content는 projector 단계에서 원자적 사실 또는 짧은 문단으로 만든다.
- 잘린 결과가 있으면
truncated=true와 release-bound cursor를 반환한다. - 다음 cursor 요청도 동일 release와 query hash에 고정한다.
- source와 safety item은 일반 설명보다 먼저 budget을 배정한다.
6. 안전·근거 정책¶
- safety 판단은 구조화 constraint와 protocol만 만든다.
- 의미상 가까운 문헌이나 생성 문장이 safety status를 바꾸지 못한다.
safe규칙도 환자별 clearance로 표현하지 않는다.- 시술·부위·증상 entity가 잡히면 관련 금기, 사전 확인, 응급 protocol을 강제 검색한다.
- clinical claim은 현재 statement version이
verified이고 published release에 포함된 경우만 반환한다. - citation identity가 검증되지 않은 source는 결과에 포함하지 않는다.
- 근거가 없는 사실을 보완하기 위해 LLM을 호출하지 않는다.
7. Application 경계¶
KnowledgeRetrievalRouter
Request → RetrievalCommand
↓
CanonicalRetrievalService
release gate → candidate retrieval → entity relation expansion
→ safety/evidence gate → rank/dedupe/budget → KnowledgeRetrievalResult
↓
RetrievalResponse
Internal result → Public response
| 구성요소 | 책임 |
|---|---|
CanonicalRetrievalService |
한 query의 전체 orchestration과 transaction 경계 |
CanonicalRetrievalRepository |
release-scoped unit·entity·safety·verified evidence 조회 |
KnowledgeIndexRepository |
publish 전 rebuildable search unit 생성 |
classify_domain / project_revision |
domain role을 강제하고 revision을 unit으로 변환 |
EntityResolver |
master/alias로 canonical entity 확정·모호성 보존 |
| Router/Presenter | Pydantic request/response 변환만 담당 |
Domain과 Application은 FastAPI/Pydantic을 import하지 않는다. Repository는 commit하지 않고, DI Container가 실제 구현을 조립한다.
8. 기존 코드 처리¶
범용 retrieval 구현과 동시에 아래 기존 코드를 제거했다.
- intent별 domain을 제외하는
RetrievalPlanner - 고정 six-section
RetrievalSectionsResult - Retrieval의
answer와generate_answer - section별로 직접 orchestration하는 현재
CanonicalRetrievalService - JSON report 기반 probe viewer와 offline probe script
기존 Repository의 release-scoped entity·safety·verified evidence SQL은 structured enrichment로 재사용했고, 나머지 section별 orchestration은 제거했다.
기존 Insight endpoint·compatibility service·probe는 범용 Knowledge Retrieval과 책임이 중복되고 실제 외부 caller가 없어 교체 commit에서 제거한다. 이중 호출이나 장기 fallback은 만들지 않는다.
9. 로컬 검토 화면¶
/knowledge-retrieval에 범용 Retrieval Inspector를 둔다.
[ 자연어 query 입력 ] [검색]
[예시 질의] 결과·후보·관련 domain·해석 entity
┌─ 관련 지식 목록 ───────────┬─ 선택 항목 본문·연결 엔티티·출처 ─────┬─ 안전·coverage·trace ─┐
│ domain / title / relevance │ content / source / raw attributes │ 강제 안전 확인과 검색 근거 │
└────────────────────────────┴─────────────────────────────────────────┴────────────────────────┘
화면은 /knowledge/retrieve 응답을 표시만 하고 자체 검색·판단·정렬을 하지 않는다. JSON raw
보기는 각 item의 보조 기능으로 두고 기본 화면은 domain, content, source, 포함 이유를 읽을 수
있게 표시한다. 게시 release가 없으면 draft·JSON fallback 대신 publication gate 상태를 그대로
설명한다. 실제 BFF 경로는 POST /api/v1/knowledge/retrieve다.
10. 최소 검증¶
테스트는 domain이나 query마다 늘리지 않는다.
SearchableDomainRegistrycoverage test 하나: published domain의 미분류 0건- index build integration test 하나: 한 release의 unit/source/entity link와 재실행 멱등성
- retrieval evaluation runner 하나: 의사가 승인한 query fixture 전체 실행
- safety invariant unit test 하나: 관련 safety item 누락 0,
safe의 clearance 승격 0 - release isolation integration test 하나: 다른 release item 혼합 0
- public contract test 하나: request, response, status와 nullability 고정
- Inspector smoke 하나: query 입력부터 source 확인까지 동작
평가 fixture는 query별 독립 pytest로 만들지 않고 다음 expected set을 데이터로 가진다.
query
expected_entities
expected_domains
required_item_refs
required_safety_refs
forbidden_source_refs
완료 gate:
- 모든 fixture에서 required safety recall 100%
- 다른 release의 item 반환 0건
- unverified claim/citation 반환 0건
- expected domain 누락 0건
- registry에 등록되지 않은 published domain 0건
no_match를 관련 정보가 있는 query에 반환한 사례 0건- 기존 API 응답 contract diff 0건
11. 구현 순서¶
| Task | 완료 단위 | 완료 기준 |
|---|---|---|
| U1 | Public contract와 domain registry | OpenAPI·검색 포함/제외 domain 확정, 기존 caller 영향 0 |
| U2 | Knowledge index projection | published release 전체 unit 생성, source/entity link·멱등성 확인 |
| U3 | Release retrieval | exact+sparse+relation expansion, 검색 가능한 전체 domain 후보화 |
| U4 | Safety·Context assembly | safety recall 100%, dedupe·budget·coverage 동작 |
| U5 | /knowledge/retrieve HTTP adapter |
새 contract와 실제 HTTP 결과 일치, 기존 API diff 0 |
| U6 | Retrieval Inspector | 화면에서 query→관련 item→source 추적 가능 |
| U7 | 임상 fixture·STG gate | 의사 승인 fixture 통과 후 동일 release/index를 STG에서 재현 |
U1~U6는 로컬에서 완료하고 검토한다. U7 전에는 STG에 배포하지 않는다.