From 8260ed9ff292a8ba382700aa25385e192b8c5b66 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 18:32:23 +0900 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20=EC=B5=9C=EC=B4=88=20=EC=9D=B8?= =?UTF-8?q?=EB=8D=B1=EC=8B=B1=20=EA=B5=AC=EA=B0=84=EC=97=90=20INDEXING=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=20=EB=85=B8=EC=B6=9C=20(#230)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit markIndexing() 호출부가 재시도·복구 서비스 두 곳뿐이라 정상 업로드 경로에서는 documentStatus가 UPLOADED에서 INDEXED로 직행했고, DocumentStatus.INDEXING은 사실상 쓰이지 않는 값이었다. 파싱 시작 Transaction에서 문서 상태도 함께 INDEXING으로 전환해 진행 상황이 드러나게 했다. 단 재인덱싱에는 적용하지 않는다. VectorSearchRepository.findTopK와 AccessibleDocumentQueryService가 d.status = 'INDEXED'로 검색 대상을 거르기 때문에, 이미 INDEXED인 문서의 상태를 처리 중에 바꾸면 기존에 검색되던 문서가 새 Version을 인덱싱하는 동안 검색 결과에서 이탈한다. Document.activateIndexedVersion() 주석이 명시한 "검색 조건이 중간 상태를 관찰하지 않게 한다"는 설계와도 어긋난다. 조건은 document.status == UPLOADED로 잡았다. currentVersion은 업로드 접수 시점에 이미 설정되므로(DocumentUploadService) null 검사로는 최초 인덱싱을 구분할 수 없다. UPLOADED는 최초 업로드와, FAILED 문서에 새 Version을 올려 재시도하는 경우(DocumentVersionUploadService의 markUploaded)를 함께 가리키며 두 경우 모두 검색 대상 Version이 없어 노출해도 검색 가용성에 영향이 없다. 실패 경로는 IndexingFailureTransitionService가 이미 INDEXING을 FAILED 전이 허용 상태로 처리하고 있어 추가 변경이 없었다. Co-Authored-By: Claude Opus 5 --- .../DocumentChunkTransactionService.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/backend/src/main/java/com/opensource/docgrid/domain/document/service/command/DocumentChunkTransactionService.java b/backend/src/main/java/com/opensource/docgrid/domain/document/service/command/DocumentChunkTransactionService.java index 7edd0c0d..f321ec5e 100644 --- a/backend/src/main/java/com/opensource/docgrid/domain/document/service/command/DocumentChunkTransactionService.java +++ b/backend/src/main/java/com/opensource/docgrid/domain/document/service/command/DocumentChunkTransactionService.java @@ -12,9 +12,11 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; +import com.opensource.docgrid.domain.document.entity.Document; import com.opensource.docgrid.domain.document.entity.DocumentChunk; import com.opensource.docgrid.domain.document.entity.DocumentVersion; import com.opensource.docgrid.domain.document.entity.FileObject; +import com.opensource.docgrid.domain.document.enums.DocumentStatus; import com.opensource.docgrid.domain.document.enums.DocumentType; import com.opensource.docgrid.domain.document.enums.DocumentVersionStatus; import com.opensource.docgrid.domain.document.repository.DocumentChunkRepository; @@ -101,6 +103,7 @@ public PreparationResult prepare( // 4. 최초 UPLOADED 요청만 PARSING 상태와 시작 이벤트를 같은 Transaction에 기록한다. if (documentVersion.getStatus() == DocumentVersionStatus.UPLOADED) { documentVersion.markParsing(); + markDocumentIndexingIfNotSearchable(documentVersion.getDocument()); indexingEventRepository.save(IndexingEvent.builder() .embeddingJob(embeddingJob) .eventType(IndexingEventType.PARSE_STARTED) @@ -119,6 +122,23 @@ public PreparationResult prepare( )); } + /** + * 검색 가능한 Version이 아직 없는 문서에 한해 진행 상태를 INDEXING으로 노출한다. + * + *

이미 INDEXED인 문서의 재인덱싱에는 적용하지 않는다. 검색 경로가 + * {@code d.status = 'INDEXED'}로 필터하므로(VectorSearchRepository, + * AccessibleDocumentQueryService), 재인덱싱 중 상태를 바꾸면 기존에 검색되던 문서가 + * 새 Version을 처리하는 동안 검색 결과에서 이탈한다. + * + *

UPLOADED는 최초 업로드와, FAILED 문서에 새 Version을 올려 재시도하는 경우를 함께 + * 가리킨다. 두 경우 모두 검색 대상 Version이 없어 노출해도 검색 가용성에 영향이 없다. + */ + private void markDocumentIndexingIfNotSearchable(Document document) { + if (document.getStatus() == DocumentStatus.UPLOADED) { + document.markIndexing(); + } + } + /** * 계산된 Draft를 현재 실행 소유권으로 검증한 뒤 Version Chunk Set으로 원자 저장한다. */ From 23ad84ea105052e5f1876c1332f466f2f90c7c1d 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 18:32:23 +0900 Subject: [PATCH 2/2] =?UTF-8?q?test:=20=EC=B5=9C=EC=B4=88=20=EC=9D=B8?= =?UTF-8?q?=EB=8D=B1=EC=8B=B1=20INDEXING=20=EC=A0=84=ED=99=98=EA=B3=BC=20?= =?UTF-8?q?=EC=9E=AC=EC=9D=B8=EB=8D=B1=EC=8B=B1=20INDEXED=20=EC=9C=A0?= =?UTF-8?q?=EC=A7=80=20=EA=B2=80=EC=A6=9D=20(#230)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit prepareEntities에 documentStatus 인자를 받는 오버로드를 추가해 기존 테스트 동작은 그대로 두고 문서 상태만 케이스별로 지정할 수 있게 했다. 두 번째 테스트가 이 변경의 회귀 방어선이다. 조건 없이 markIndexing()을 호출하도록 되돌리면 이 테스트만 실패하는 것을 확인했다. 재인덱싱 중 문서 상태가 바뀌면 검색 경로의 d.status = 'INDEXED' 조건에서 기존 Version이 이탈하므로, 그 사고를 테스트가 먼저 잡도록 남겼다. Co-Authored-By: Claude Opus 5 --- .../DocumentChunkTransactionServiceTest.java | 37 ++++++++++++++++++- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/backend/src/test/java/com/opensource/docgrid/domain/document/service/command/DocumentChunkTransactionServiceTest.java b/backend/src/test/java/com/opensource/docgrid/domain/document/service/command/DocumentChunkTransactionServiceTest.java index 27781ec7..afbec4e3 100644 --- a/backend/src/test/java/com/opensource/docgrid/domain/document/service/command/DocumentChunkTransactionServiceTest.java +++ b/backend/src/test/java/com/opensource/docgrid/domain/document/service/command/DocumentChunkTransactionServiceTest.java @@ -79,6 +79,7 @@ class DocumentChunkTransactionServiceTest { private DocumentChunkTransactionService service; private EmbeddingJob embeddingJob; + private Document document; private DocumentVersion documentVersion; private EmbeddingJobAttempt attempt; @@ -120,6 +121,30 @@ void prepare_marksParsingAndReturnsSnapshot() { assertThat(eventCaptor.getValue().getEventType()).isEqualTo(IndexingEventType.PARSE_STARTED); } + @Test + @DisplayName("검색 가능한 Version이 없는 문서는 파싱 시작 시 INDEXING으로 전환된다") + void prepare_marksDocumentIndexingWhenNotSearchable() { + prepareEntities(DocumentType.TXT, DocumentVersionStatus.UPLOADED, DocumentStatus.UPLOADED); + givenValidContext(); + + service.prepare(JOB_ID, ATTEMPT_ID, WORKER_ID, CLAIM_TOKEN); + + assertThat(document.getStatus()).isEqualTo(DocumentStatus.INDEXING); + } + + @Test + @DisplayName("이미 INDEXED인 문서의 재인덱싱은 검색 가용성을 위해 INDEXED를 유지한다") + void prepare_keepsIndexedDocumentSearchableWhileReindexing() { + prepareEntities(DocumentType.TXT, DocumentVersionStatus.UPLOADED, DocumentStatus.INDEXED); + givenValidContext(); + + service.prepare(JOB_ID, ATTEMPT_ID, WORKER_ID, CLAIM_TOKEN); + + // 검색 경로가 d.status = 'INDEXED'로 필터하므로 여기서 상태가 바뀌면 기존 Version이 검색에서 이탈한다. + assertThat(document.getStatus()).isEqualTo(DocumentStatus.INDEXED); + assertThat(documentVersion.getStatus()).isEqualTo(DocumentVersionStatus.PARSING); + } + @Test @DisplayName("PARSING 재개는 상태와 시작 이벤트를 다시 만들지 않는다") void prepare_resumesParsingWithoutDuplicateEvent() { @@ -266,6 +291,14 @@ private void givenValidContext() { } private void prepareEntities(DocumentType documentType, DocumentVersionStatus versionStatus) { + prepareEntities(documentType, versionStatus, DocumentStatus.INDEXING); + } + + private void prepareEntities( + DocumentType documentType, + DocumentVersionStatus versionStatus, + DocumentStatus documentStatus + ) { WorkerNode worker = WorkerNode.builder() .workerName("worker") .instanceId("instance") @@ -274,11 +307,11 @@ private void prepareEntities(DocumentType documentType, DocumentVersionStatus ve .build(); ReflectionTestUtils.setField(worker, "id", WORKER_ID); - Document document = Document.builder() + document = Document.builder() .title("문서") .documentType(documentType) .sourceType(DocumentSourceType.UPLOAD) - .status(DocumentStatus.INDEXING) + .status(documentStatus) .visibility(VisibilityType.PRIVATE) .build(); ReflectionTestUtils.setField(document, "id", 3L);