feat(hier): 크로스-doc 절 라벨 조회 엔드포인트 (U-1, 'UG-79 보여줘' 진입점)
GET /api/documents/clause-lookup?label=UG-79 → 절 식별자로 크로스-doc 위치 해소.
절(node_type=clause/clause_split)은 in_corpus=false(검색 비활성)라 의미검색으론
못 찾으므로, 라벨 prefix 정확매칭으로 (doc, char_start)를 직접 반환해 읽기뷰 점프 가능케 함.
라벨 중복도 실측: 1335 라벨 중 다중-doc 10건(0.7%, 부록 A-/E-/F- 한정) → 에디션 UI
불요, 단순 조회 + 드문 다중반환. /{doc_id} 앞 선언(라우트 매칭 순서). document_chunks
직접 조회는 정확지목(retrieval 아님)이라 코퍼스 격리의 의도적 예외(/sections 와 동일).
A-1 후속: 절 타이핑(5180=550·5210=862·5209=43 라이브)으로 채워진 절을 사용자가 호출 가능케 함.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -672,6 +672,60 @@ async def list_duplicates(
|
||||
)
|
||||
|
||||
|
||||
class ClauseHit(BaseModel):
|
||||
doc_id: int
|
||||
doc_title: str
|
||||
section_title: str | None = None
|
||||
char_start: int | None = None
|
||||
chunk_id: int
|
||||
node_type: str | None = None
|
||||
|
||||
|
||||
class ClauseLookupResponse(BaseModel):
|
||||
label: str
|
||||
hits: list[ClauseHit]
|
||||
|
||||
|
||||
# NOTE: '/{doc_id}' (int path param) 라우트보다 먼저 선언해야 '/clause-lookup' 이 doc_id 로
|
||||
# 잘못 매칭되지 않는다 (FastAPI 선언 순서 매칭). 이동 금지.
|
||||
@router.get("/clause-lookup", response_model=ClauseLookupResponse)
|
||||
async def clause_lookup(
|
||||
label: str,
|
||||
user: Annotated[User, Depends(get_current_user)],
|
||||
session: Annotated[AsyncSession, Depends(get_session)],
|
||||
):
|
||||
"""절 식별자(예: UG-79)로 크로스-doc 절 위치 조회 — 'UG-79 보여줘' 진입점 (U-1).
|
||||
|
||||
절(node_type=clause/clause_split)은 in_corpus=false(검색 비활성)라 의미검색으론 못 찾으므로,
|
||||
라벨 prefix 정확매칭으로 (doc, char_start) 를 직접 해소해 읽기뷰 점프를 가능케 한다.
|
||||
대부분 1건; 부록(A-/E-/F-) 등 doc 간 공유 라벨만 다중 반환(에디션 선택). /sections 와 동일하게
|
||||
document_chunks 직접 조회 — corpus_chunks 우회는 retrieval 아닌 정확지목이므로 의도적 예외.
|
||||
"""
|
||||
from sqlalchemy import text as sql_text
|
||||
|
||||
lab = (label or "").strip()
|
||||
if not lab:
|
||||
return ClauseLookupResponse(label=label, hits=[])
|
||||
rows = (
|
||||
await session.execute(
|
||||
sql_text(
|
||||
"""
|
||||
SELECT c.doc_id, d.title AS doc_title, c.section_title,
|
||||
c.char_start, c.id AS chunk_id, c.node_type
|
||||
FROM document_chunks c
|
||||
JOIN documents d ON d.id = c.doc_id
|
||||
WHERE c.node_type IN ('clause', 'clause_split')
|
||||
AND (c.section_title ILIKE :lab_sp OR c.section_title ILIKE :lab_eq)
|
||||
AND d.deleted_at IS NULL
|
||||
ORDER BY c.doc_id, c.char_start NULLS LAST
|
||||
LIMIT 50
|
||||
"""
|
||||
).bindparams(lab_sp=lab + " %", lab_eq=lab)
|
||||
)
|
||||
).mappings().all()
|
||||
return ClauseLookupResponse(label=lab, hits=[ClauseHit(**dict(r)) for r in rows])
|
||||
|
||||
|
||||
@router.get("/{doc_id}", response_model=DocumentDetailResponse)
|
||||
async def get_document(
|
||||
doc_id: int,
|
||||
|
||||
Reference in New Issue
Block a user