지식 그래프 검색: 문서 버전·순위 융합·안전한 탐색
TECH INSIGHT / EXPLAINED
문서 검색이 '비슷한 글'을 찾는 일이라면 관계 검색은 '이 결정이 어떤 사건을 참조했는지' 따라가는 일입니다. 두 경로를 함께 쓰되, 링크를 인과관계의 증거로 과장하지 않는 설계가 필요합니다.
실제 프로젝트의 로컬 실행 화면
기존 프로젝트의 화면 또는 함수를 로컬에서 실행해 촬영했습니다. 개인정보 없는 샘플 입력을 사용했으며, 실행한 범위는 각 이미지 아래에 설명했습니다.
레지스트리에는 위치와 버전을 함께
원문을 파일에 두고 DB를 색인으로 쓰는 방식은 사람이 편집하는 노트에 유용합니다. 원문을 DB에 저장하는 방식도 가능하므로 복구·동기화 요구로 선택하세요. 파일 방식의 키에는 tenant_id·agent_id·scope·정규화된 경로를 포함하고, 경로가 허용된 작업공간 안인지 확인합니다.
content_hash만 같다고 재색인을 생략하면 임베딩 모델이나 청크 규칙이 바뀐 경우를 놓칩니다. 원문 해시와 embedding_model, chunk_version을 함께 기록하세요. 변경·이동·삭제 이벤트를 놓칠 수 있으니 주기적으로 원문과 색인을 대조합니다.
위키링크는 출처가 붙은 관계
[[decisions/retry-policy|재시도 정책]]을 문서 ID로 해석하되, 없는 대상은 unresolved로 남길 수 있습니다. 다른 테넌트나 비공개 문서로 이어지는 링크는 목록·역링크·발췌를 표시할 때도 권한을 검사합니다. '참조함'과 '원인이 됨'은 다른 관계이므로 본문 근거 없이 인과로 승격하지 않습니다.
점수보다 순위를 합치는 작은 예제
코사인 유사도는 일반적으로 -1부터 1까지 가능하고, SQLite FTS5의 bm25는 더 작은 값이 더 좋은 일치를 뜻합니다. 서로 다른 점수를 최고값으로 나누는 것만으로 안전한 정규화가 되지는 않습니다. 다음은 이미 권한으로 걸러진 결과 목록을 순위로 합치는 RRF 예제입니다. k=60은 이 실습의 설정값입니다.
from collections import defaultdict
def fuse(rankings, k=60):
if k <= 0:
raise ValueError("k must be positive")
scores = defaultdict(float)
for ranking in rankings:
unique = dict.fromkeys(ranking)
for rank, document_id in enumerate(unique, 1):
scores[document_id] += 1 / (k + rank)
return sorted(scores, key=lambda x: (-scores[x], x))
assert fuse([["A", "B"], ["B", "C"]])[0] == "B"B는 두 검색 결과에 등장해 가장 위로 올라옵니다. 이 방법도 정답을 보장하지 않으므로 오류 코드·고유명사·의역 질의를 따로 평가하고, 실제 답의 근거 문단을 확인합니다.
UNION만으로 노드 중복이 없어지지 않는다
재귀 CTE의 행이 (doc_id, depth)라면 (A,0)과 (A,2)는 다른 행입니다. UNION도 이 둘을 합치지 않습니다. 깊이 제한과 방문 경로 또는 방문 집합을 별도로 사용하고, 결과는 문서별 최소 깊이로 묶으세요. 모든 홉에서 tenant·scope 조건을 적용하며 최대 방문 노드·시간·결과 수를 제한합니다.
두 홉이면 충분하다는 고정 답은 없습니다. 연결이 조밀하면 두 홉도 클 수 있습니다. 먼저 제목·출처·짧은 근거를 제공하고, 필요한 문단이나 전문만 추가로 읽는 편이 과도한 문맥 주입을 줄입니다.
참고 자료
내용·예제 검토: 2026-09-29
댓글
댓글 쓰기