임베딩 검색이 삭제된 문서의 청크를 돌려주던 문제 - #216
Merged
Merged
Conversation
세션 생성 뒤 사용자가 워크스페이스에서 이력서를 지워도, session_contexts 에는 그 문서 id 가 남아 generate.followup·generate.feedback 페이로드로 계속 실려 나간다(SessionFollowupRequester/SessionFeedbackRequester 는 findBySession_Id 를 필터 없이 쓴다). 임베딩 검색에도 삭제 필터가 없어서, 지운 이력서 본문이 RAG 로 되살아나 꼬리질문 생성과 채점 근거로 그대로 쓰였다. 같은 문제를 SessionQuestionsRequester.buildDocumentContexts 는 이미 findActiveByIdAndOwner 로 막아두고 "삭제가 사용자 눈에는 지워졌지만 실제로는 계속 쓰이는 셈"이라고 적어놨다. RAG 경로만 그 원칙에서 빠져 있었다. 호출부마다 필터를 거는 대신 검색 쿼리에서 막는다 — 3개 호출부 중 어디도 삭제를 확인하지 않았고, 호출자가 늘 때마다 같은 실수를 반복할 수 있다. 벡터 단독·하이브리드(RRF) 두 경로 모두, 하이브리드는 벡터 CTE 와 full-text CTE 양쪽에 건다(한쪽만 막으면 본문 단어가 겹치는 순간 그대로 올라온다). 수정 과정에서 텍스트 블록을 쪼개 .formatted 가 마지막 조각에만 걸리는 함정을 밟았다 — placeholder 가 밀려 WHERE 가 두 번 나오는 SQL 이 만들어진다. 한 블록 유지 + %s 인자 주입으로 바로잡고 주석으로 남겼다.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
문제
세션 생성 뒤 사용자가 워크스페이스에서 이력서를 지워도, 지운 이력서 본문이 RAG 로 되살아나 꼬리질문 생성과 채점 근거로 쓰인다.
경로:
session_contexts에는 문서 id 가 그대로 남는다 (soft delete 는resumes·analyzed_documents만 건드린다)SessionFollowupRequester·SessionFeedbackRequester가findBySession_Id를 필터 없이 읽어contextDocumentIds로 싣는다POST /api/internal/embeddings/search호출document_embeddings에는 청크 원문(chunk_text)이 저장되므로, 이력서에서 뽑은 문장이 그대로 돌아온다.이건 이 저장소가 이미 세운 원칙에서 RAG 경로만 빠진 것이다.
SessionQuestionsRequester.buildDocumentContexts에는 같은 문제를 막아둔 주석이 이미 있다:수정
검색 쿼리에서 막는다.
호출부마다 거는 대신 쿼리에서 막은 이유: 3개 호출부(
questions/followup/feedback) 중 어디도 삭제를 확인하지 않았다. 호출자가 늘 때마다 같은 실수를 반복할 수 있어서, 마지막 관문 한 곳에 둔다.벡터 단독 경로와 하이브리드(RRF) 경로 모두 적용했다. 하이브리드는 벡터 CTE 와 full-text CTE 양쪽에 건다 — 한쪽만 막으면 본문 단어가 겹치는 순간 그대로 올라온다.
테스트 — #197 인프라
실제 Postgres(+pgvector)에서 세 경로를 각각 확인한다.
searchExcludesChunksOfDeletedDocuments— 벡터 단독, 문서 id 로 스코프unscopedSearchAlsoExcludesDeletedDocuments—documentIds미지정(전체 검색)hybridSearchExcludesDeletedDocuments—queryText동반 RRF 경로수정 전 코드에서 3개 모두 실패하는 것을 먼저 확인하고 고쳤다. 각 테스트는 지우기 전 상태(둘 다 조회됨)도 함께 단언한다 — 필터가 "아무것도 안 거르는" 상태와 구분되도록.
작업 중 밟은 함정 (주석으로 남김)
하이브리드 SQL 은 Java 텍스트 블록인데, 중간에 문자열을 이어붙여 블록을 쪼개면
.formatted가 마지막 조각에만 걸린다. placeholder 가 밀려서WHERE ... WHERE ...인 SQL 이 만들어지고 런타임에야 터진다(실제로 그렇게 깨졌다). 한 블록 유지 +%s인자 주입으로 바로잡고 그 자리에 주석을 남겼다.함께 확인했고 이번 범위 밖으로 둔 것
POST /api/internal/embeddings/search는 userId 파라미터가 아예 없다.documentIds가 비면 스펙상 "전체 검색" 이라 다른 사용자의 청크까지 대상이 된다.지금은 유출이 없다 — AI 3개 호출부가 모두 빈 목록을 사전에 걸러
(none)을 반환한다. 다만 방어가 전적으로 호출자에게 맡겨져 있고, 소유권 검증도 없다(남의 문서 id 를 넣으면 그대로 조회된다). 내부 API 라X-Internal-API-Key뒤에 있지만 계층 방어가 없는 셈이다.이번 PR 은 삭제 필터에 집중했다. 사용자 스코프 추가는 계약 변경(요청에 userId 추가 + AI 호출부 4곳 수정)이라 분리하는 게 리뷰하기 낫다고 판단했다. 필요하면 후속으로 진행하겠다.