From 5958186c1b6743fb7975846a57023c33594f57fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EA=B8=B0=EB=AF=BC?= Date: Tue, 18 Aug 2026 22:59:59 +0900 Subject: [PATCH] =?UTF-8?q?test:=20=EC=B5=9C=EC=86=8C=20=EC=9C=A0=EC=82=AC?= =?UTF-8?q?=EB=8F=84=20=EA=B0=80=EB=93=9C=EB=A0=88=EC=9D=BC=EC=9D=84=20?= =?UTF-8?q?=EA=B2=80=EC=A6=9D=ED=95=98=EB=8F=84=EB=A1=9D=20=EA=B2=80?= =?UTF-8?q?=EC=83=89=20E2E=EB=A5=BC=20=EC=9E=AC=EC=9E=91=EC=84=B1=ED=95=9C?= =?UTF-8?q?=EB=8B=A4=20(#233)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit LocalDocumentIndexingE2ETest가 pgvector 질의 결과에 제빵 DOCX도 포함되어야 한다고 단정해 실패하고 있었다. 검색 결함이 아니라 최소 유사도 정책 도입 이전에 작성된 기대값이 그대로 남은 것이다. DOCX는 검색 SQL의 WHERE 조건을 모두 통과하며, 탈락 지점은 VectorSearchQueryService의 최소 유사도 필터다. 실측 유사도는 PDF가 0.8272와 0.4262, DOCX가 0.2218로 임계값 0.30에 미달한다. 질의가 pgvector HNSW를 묻고 DOCX 본문은 제빵 절차라 관련이 없으므로 제외가 정상 동작이다. 무관한 문서가 제외되는지와, 같은 DOCX가 관련 질의에는 검색되는지를 함께 단정하도록 바꿨다. 한 질의만 보면 문서가 빠진 이유가 관련성 판정인지 인덱싱 누락인지 구분할 수 없어서다. SEARCH_VECTOR_MIN_SIMILARITY을 0.0으로 낮춰 가드레일을 무력화하면 이 테스트만 실패하는 것을 확인했다. --- .../e2e/LocalDocumentIndexingE2ETest.java | 58 +++++++++++-------- 1 file changed, 35 insertions(+), 23 deletions(-) diff --git a/backend/src/test/java/com/opensource/docgrid/e2e/LocalDocumentIndexingE2ETest.java b/backend/src/test/java/com/opensource/docgrid/e2e/LocalDocumentIndexingE2ETest.java index 11513c5d..41bbf934 100644 --- a/backend/src/test/java/com/opensource/docgrid/e2e/LocalDocumentIndexingE2ETest.java +++ b/backend/src/test/java/com/opensource/docgrid/e2e/LocalDocumentIndexingE2ETest.java @@ -141,8 +141,8 @@ void upload_indexesDocumentsAndSearchesRelatedContent() throws Exception { assertIndexedInvariants(pdfUpload, true); assertIndexedInvariants(docxUpload, false); - // 5. 실제 단건 Query Embedding과 pgvector 코사인 검색에서 관련 PDF가 먼저 반환된다. - assertSemanticSearchRanksPdfFirst(pdfUpload, docxUpload); + // 5. 실제 단건 Query Embedding과 pgvector 코사인 검색이 질의별로 관련 문서만 반환한다. + assertSemanticSearchAppliesRelevanceGuardrail(pdfUpload, docxUpload); // 6. 같은 ADMIN JWT로 실제 관리자 조회와 OpenAPI 노출·민감정보 제거 계약을 검증한다. assertAdminHttpContracts(accessToken, pdfUpload); @@ -240,37 +240,49 @@ private void assertEventOrder(Long jobId) { ); } - private void assertSemanticSearchRanksPdfFirst( + /** + * 질의와 관련된 문서만 검색되고, 관련 없는 문서는 최소 유사도에서 제외되는지 확인한다. + * + *

두 질의를 함께 검증한다. 한 질의만 보면 문서가 빠진 이유가 관련성 판정 때문인지 + * 인덱싱 누락 때문인지 구분할 수 없기 때문이다. + */ + private void assertSemanticSearchAppliesRelevanceGuardrail( UploadedDocument pdfUpload, UploadedDocument docxUpload ) { - float[] queryVector = embeddingClient.embed( - "How does pgvector HNSW improve cosine similarity search for embeddings?" - ); - assertThat(queryVector).hasSize(1024); - for (float value : queryVector) { - assertThat(Float.isFinite(value)).isTrue(); - } Long modelId = queryLong( "SELECT id FROM embedding_models WHERE is_active = TRUE AND is_searchable = TRUE" ); + List permittedIds = List.of(pdfUpload.documentId(), docxUpload.documentId()); - List candidates = vectorSearchQueryService.search( - queryVector, + // 1. pgvector 질의에는 관련 PDF만 남고 무관한 제빵 DOCX는 최소 유사도에서 제외된다. + List vectorQueryHits = searchByQuery( + "How does pgvector HNSW improve cosine similarity search for embeddings?", modelId, - List.of(pdfUpload.documentId(), docxUpload.documentId()), - 10 + permittedIds ); - assertThat(candidates).isNotEmpty(); - assertThat(candidates.get(0).documentId()).isEqualTo(pdfUpload.documentId()); - assertThat(candidates).anyMatch(candidate -> candidate.documentId().equals(docxUpload.documentId())); - assertThat(candidates.get(0).similarityScore()).isGreaterThan( - candidates.stream() - .filter(candidate -> candidate.documentId().equals(docxUpload.documentId())) - .findFirst() - .orElseThrow() - .similarityScore() + assertThat(vectorQueryHits).isNotEmpty(); + assertThat(vectorQueryHits.get(0).documentId()).isEqualTo(pdfUpload.documentId()); + assertThat(vectorQueryHits) + .allMatch(candidate -> candidate.documentId().equals(pdfUpload.documentId())); + + // 2. 같은 DOCX도 관련 질의에는 검색된다. 1의 제외가 인덱싱 누락이 아니라 관련성 판정임을 확인한다. + List bakingQueryHits = searchByQuery( + "How is bread dough fermented with yeast before baking?", + modelId, + permittedIds ); + assertThat(bakingQueryHits).isNotEmpty(); + assertThat(bakingQueryHits.get(0).documentId()).isEqualTo(docxUpload.documentId()); + } + + private List searchByQuery(String query, Long modelId, List permittedIds) { + float[] queryVector = embeddingClient.embed(query); + assertThat(queryVector).hasSize(1024); + for (float value : queryVector) { + assertThat(Float.isFinite(value)).isTrue(); + } + return vectorSearchQueryService.search(queryVector, modelId, permittedIds, 10); } private void assertAdminHttpContracts(String accessToken, UploadedDocument upload) {