From 42df0f718a83aae5e9ec856e5751b162b6af0e27 Mon Sep 17 00:00:00 2001 From: 1Seob Date: Wed, 29 Jul 2026 21:10:55 +0900 Subject: [PATCH 1/3] =?UTF-8?q?feat(admin):=20Ask=20Chat=20RAG=20=ED=98=84?= =?UTF-8?q?=ED=99=A9=20=EC=A1=B0=ED=9A=8C=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 현재 embedding version 기준 Target·Indexed·Failed 집계 API를 추가하고, 어드민 페이지 진입 및 배치 완료 시 현황을 자동 갱신하도록 개선함 --- .../admin/api/AdminAskChatRagController.java | 9 ++++ ...AdminAskChatRagBackfillStatusResponse.java | 18 ++++++++ .../AskChatRagBackfillService.java | 5 ++ .../core/dto/AskChatRagBackfillStatusDto.java | 8 ++++ .../AskChatRagBackfillQueryRepository.java | 28 +++++++++++ .../templates/admin/ask-chat-rag.html | 46 +++++++++++++++---- .../api/AdminAskChatRagControllerTest.java | 39 ++++++++++++++++ .../AskChatRagBackfillServiceTest.java | 17 +++++++ .../AskChatRagRepositoryIntegrationTest.java | 28 +++++++++++ 9 files changed, 188 insertions(+), 10 deletions(-) create mode 100644 src/main/java/com/devkor/ifive/nadab/domain/admin/api/dto/response/AdminAskChatRagBackfillStatusResponse.java create mode 100644 src/main/java/com/devkor/ifive/nadab/domain/askchat/core/dto/AskChatRagBackfillStatusDto.java create mode 100644 src/test/java/com/devkor/ifive/nadab/domain/admin/api/AdminAskChatRagControllerTest.java diff --git a/src/main/java/com/devkor/ifive/nadab/domain/admin/api/AdminAskChatRagController.java b/src/main/java/com/devkor/ifive/nadab/domain/admin/api/AdminAskChatRagController.java index 9ec2c763..403309cf 100644 --- a/src/main/java/com/devkor/ifive/nadab/domain/admin/api/AdminAskChatRagController.java +++ b/src/main/java/com/devkor/ifive/nadab/domain/admin/api/AdminAskChatRagController.java @@ -1,13 +1,16 @@ package com.devkor.ifive.nadab.domain.admin.api; import com.devkor.ifive.nadab.domain.admin.api.dto.response.AdminAskChatRagBackfillResponse; +import com.devkor.ifive.nadab.domain.admin.api.dto.response.AdminAskChatRagBackfillStatusResponse; import com.devkor.ifive.nadab.domain.askchat.application.AskChatRagBackfillService; import com.devkor.ifive.nadab.domain.askchat.core.dto.AskChatRagBackfillResultDto; +import com.devkor.ifive.nadab.domain.askchat.core.dto.AskChatRagBackfillStatusDto; import com.devkor.ifive.nadab.global.core.response.ApiResponseDto; import com.devkor.ifive.nadab.global.core.response.ApiResponseEntity; import io.swagger.v3.oas.annotations.Hidden; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -20,6 +23,12 @@ public class AdminAskChatRagController { private final AskChatRagBackfillService askChatRagBackfillService; + @GetMapping("/backfill/daily-answers/status") + public ResponseEntity> getDailyAnswerBackfillStatus() { + AskChatRagBackfillStatusDto status = askChatRagBackfillService.getCompletedDailyAnswerStatus(); + return ApiResponseEntity.ok(AdminAskChatRagBackfillStatusResponse.from(status)); + } + @PostMapping("/backfill/daily-answers") public ResponseEntity> backfillDailyAnswers() { AskChatRagBackfillResultDto result = askChatRagBackfillService.backfillCompletedDailyAnswers(); diff --git a/src/main/java/com/devkor/ifive/nadab/domain/admin/api/dto/response/AdminAskChatRagBackfillStatusResponse.java b/src/main/java/com/devkor/ifive/nadab/domain/admin/api/dto/response/AdminAskChatRagBackfillStatusResponse.java new file mode 100644 index 00000000..7072d075 --- /dev/null +++ b/src/main/java/com/devkor/ifive/nadab/domain/admin/api/dto/response/AdminAskChatRagBackfillStatusResponse.java @@ -0,0 +1,18 @@ +package com.devkor.ifive.nadab.domain.admin.api.dto.response; + +import com.devkor.ifive.nadab.domain.askchat.core.dto.AskChatRagBackfillStatusDto; + +public record AdminAskChatRagBackfillStatusResponse( + long targetCount, + long indexedCount, + long failedCount +) { + + public static AdminAskChatRagBackfillStatusResponse from(AskChatRagBackfillStatusDto status) { + return new AdminAskChatRagBackfillStatusResponse( + status.targetCount(), + status.indexedCount(), + status.failedCount() + ); + } +} diff --git a/src/main/java/com/devkor/ifive/nadab/domain/askchat/application/AskChatRagBackfillService.java b/src/main/java/com/devkor/ifive/nadab/domain/askchat/application/AskChatRagBackfillService.java index 29000805..a2f258ce 100644 --- a/src/main/java/com/devkor/ifive/nadab/domain/askchat/application/AskChatRagBackfillService.java +++ b/src/main/java/com/devkor/ifive/nadab/domain/askchat/application/AskChatRagBackfillService.java @@ -1,6 +1,7 @@ package com.devkor.ifive.nadab.domain.askchat.application; import com.devkor.ifive.nadab.domain.askchat.core.dto.AskChatRagBackfillResultDto; +import com.devkor.ifive.nadab.domain.askchat.core.dto.AskChatRagBackfillStatusDto; import com.devkor.ifive.nadab.domain.askchat.core.dto.AskChatRagBackfillTargetDto; import com.devkor.ifive.nadab.domain.askchat.core.repository.AskChatRagBackfillQueryRepository; import com.devkor.ifive.nadab.domain.askchat.infra.AskChatEmbeddingClient; @@ -19,6 +20,10 @@ public class AskChatRagBackfillService { private final AskChatRagIndexingService indexingService; private final AskChatEmbeddingClient embeddingClient; + public AskChatRagBackfillStatusDto getCompletedDailyAnswerStatus() { + return backfillQueryRepository.findCompletedDailyAnswerStatus(embeddingClient.version()); + } + public AskChatRagBackfillResultDto backfillCompletedDailyAnswers() { List targets = backfillQueryRepository.findCompletedDailyAnswerTargets( embeddingClient.version(), diff --git a/src/main/java/com/devkor/ifive/nadab/domain/askchat/core/dto/AskChatRagBackfillStatusDto.java b/src/main/java/com/devkor/ifive/nadab/domain/askchat/core/dto/AskChatRagBackfillStatusDto.java new file mode 100644 index 00000000..6b6a9f50 --- /dev/null +++ b/src/main/java/com/devkor/ifive/nadab/domain/askchat/core/dto/AskChatRagBackfillStatusDto.java @@ -0,0 +1,8 @@ +package com.devkor.ifive.nadab.domain.askchat.core.dto; + +public record AskChatRagBackfillStatusDto( + long targetCount, + long indexedCount, + long failedCount +) { +} diff --git a/src/main/java/com/devkor/ifive/nadab/domain/askchat/core/repository/AskChatRagBackfillQueryRepository.java b/src/main/java/com/devkor/ifive/nadab/domain/askchat/core/repository/AskChatRagBackfillQueryRepository.java index 43b8115b..ae466c48 100644 --- a/src/main/java/com/devkor/ifive/nadab/domain/askchat/core/repository/AskChatRagBackfillQueryRepository.java +++ b/src/main/java/com/devkor/ifive/nadab/domain/askchat/core/repository/AskChatRagBackfillQueryRepository.java @@ -1,5 +1,6 @@ package com.devkor.ifive.nadab.domain.askchat.core.repository; +import com.devkor.ifive.nadab.domain.askchat.core.dto.AskChatRagBackfillStatusDto; import com.devkor.ifive.nadab.domain.askchat.core.dto.AskChatRagBackfillTargetDto; import com.devkor.ifive.nadab.domain.user.core.entity.InterestCode; import lombok.RequiredArgsConstructor; @@ -43,6 +44,33 @@ AND NOT EXISTS ( .addValue("limit", limit), this::mapTarget); } + public AskChatRagBackfillStatusDto findCompletedDailyAnswerStatus(int embeddingVersion) { + String sql = """ + SELECT COUNT(*) FILTER (WHERE d.id IS NULL) AS target_count, + COUNT(*) FILTER (WHERE d.embedding_status = 'COMPLETED') AS indexed_count, + COUNT(*) FILTER ( + WHERE d.embedding_status IN ('FAILED', 'DEAD_LETTER') + ) AS failed_count + FROM daily_reports dr + JOIN answer_entries ae ON ae.id = dr.answer_entry_id + LEFT JOIN ask_chat_rag_documents d + ON d.source_type = 'ANSWER_ENTRY' + AND d.source_id = ae.id + AND d.embedding_version = :embeddingVersion + WHERE dr.status = 'COMPLETED' + """; + + return jdbcTemplate.queryForObject( + sql, + new MapSqlParameterSource("embeddingVersion", embeddingVersion), + (rs, rowNum) -> new AskChatRagBackfillStatusDto( + rs.getLong("target_count"), + rs.getLong("indexed_count"), + rs.getLong("failed_count") + ) + ); + } + private AskChatRagBackfillTargetDto mapTarget(ResultSet rs, int rowNum) throws SQLException { String interestCode = rs.getString("interest_code"); return new AskChatRagBackfillTargetDto( diff --git a/src/main/resources/templates/admin/ask-chat-rag.html b/src/main/resources/templates/admin/ask-chat-rag.html index fdd678be..28720c2a 100644 --- a/src/main/resources/templates/admin/ask-chat-rag.html +++ b/src/main/resources/templates/admin/ask-chat-rag.html @@ -210,6 +210,7 @@

Daily Answer Backfill

Completed daily answers that do not have a RAG document for the current embedding version are indexed one batch at a time. Run repeatedly until Target becomes 0. Failed embeddings are saved and handled by the retry scheduler. + Status counts can be refreshed without running a batch.

@@ -229,9 +230,10 @@

Daily Answer Backfill

+
-
No run yet.
+
Loading status...
@@ -248,6 +250,7 @@ diff --git a/src/test/java/com/devkor/ifive/nadab/domain/admin/api/AdminAskChatRagControllerTest.java b/src/test/java/com/devkor/ifive/nadab/domain/admin/api/AdminAskChatRagControllerTest.java new file mode 100644 index 00000000..2e76cc15 --- /dev/null +++ b/src/test/java/com/devkor/ifive/nadab/domain/admin/api/AdminAskChatRagControllerTest.java @@ -0,0 +1,39 @@ +package com.devkor.ifive.nadab.domain.admin.api; + +import com.devkor.ifive.nadab.domain.admin.api.dto.response.AdminAskChatRagBackfillStatusResponse; +import com.devkor.ifive.nadab.domain.askchat.application.AskChatRagBackfillService; +import com.devkor.ifive.nadab.domain.askchat.core.dto.AskChatRagBackfillStatusDto; +import com.devkor.ifive.nadab.global.core.response.ApiResponseDto; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.ResponseEntity; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class AdminAskChatRagControllerTest { + + @Mock + private AskChatRagBackfillService askChatRagBackfillService; + + @InjectMocks + private AdminAskChatRagController controller; + + @Test + void getDailyAnswerBackfillStatus_returns_current_counts() { + when(askChatRagBackfillService.getCompletedDailyAnswerStatus()) + .thenReturn(new AskChatRagBackfillStatusDto(4, 12, 2)); + + ResponseEntity> response = + controller.getDailyAnswerBackfillStatus(); + + assertThat(response.getStatusCode().value()).isEqualTo(200); + assertThat(response.getBody()).isNotNull(); + assertThat(response.getBody().getData()) + .isEqualTo(new AdminAskChatRagBackfillStatusResponse(4, 12, 2)); + } +} diff --git a/src/test/java/com/devkor/ifive/nadab/domain/askchat/application/AskChatRagBackfillServiceTest.java b/src/test/java/com/devkor/ifive/nadab/domain/askchat/application/AskChatRagBackfillServiceTest.java index 6e87b0f7..9aaab9ac 100644 --- a/src/test/java/com/devkor/ifive/nadab/domain/askchat/application/AskChatRagBackfillServiceTest.java +++ b/src/test/java/com/devkor/ifive/nadab/domain/askchat/application/AskChatRagBackfillServiceTest.java @@ -1,6 +1,7 @@ package com.devkor.ifive.nadab.domain.askchat.application; import com.devkor.ifive.nadab.domain.askchat.core.dto.AskChatRagBackfillResultDto; +import com.devkor.ifive.nadab.domain.askchat.core.dto.AskChatRagBackfillStatusDto; import com.devkor.ifive.nadab.domain.askchat.core.dto.AskChatRagBackfillTargetDto; import com.devkor.ifive.nadab.domain.askchat.core.repository.AskChatRagBackfillQueryRepository; import com.devkor.ifive.nadab.domain.askchat.infra.AskChatEmbeddingClient; @@ -31,6 +32,22 @@ class AskChatRagBackfillServiceTest { @Mock private AskChatEmbeddingClient embeddingClient; + @Test + void getCompletedDailyAnswerStatus_queries_current_embedding_version() { + AskChatRagBackfillService service = new AskChatRagBackfillService( + backfillQueryRepository, + indexingService, + embeddingClient + ); + AskChatRagBackfillStatusDto expectedStatus = new AskChatRagBackfillStatusDto(3, 7, 1); + when(embeddingClient.version()).thenReturn(2); + when(backfillQueryRepository.findCompletedDailyAnswerStatus(2)).thenReturn(expectedStatus); + + AskChatRagBackfillStatusDto status = service.getCompletedDailyAnswerStatus(); + + assertThat(status).isEqualTo(expectedStatus); + } + @Test void backfillCompletedDailyAnswers_indexes_targets_for_current_embedding_version() { AskChatRagBackfillService service = new AskChatRagBackfillService( diff --git a/src/test/java/com/devkor/ifive/nadab/domain/askchat/core/repository/AskChatRagRepositoryIntegrationTest.java b/src/test/java/com/devkor/ifive/nadab/domain/askchat/core/repository/AskChatRagRepositoryIntegrationTest.java index 61c9cbfa..2c8ece45 100644 --- a/src/test/java/com/devkor/ifive/nadab/domain/askchat/core/repository/AskChatRagRepositoryIntegrationTest.java +++ b/src/test/java/com/devkor/ifive/nadab/domain/askchat/core/repository/AskChatRagRepositoryIntegrationTest.java @@ -1,5 +1,6 @@ package com.devkor.ifive.nadab.domain.askchat.core.repository; +import com.devkor.ifive.nadab.domain.askchat.core.dto.AskChatRagBackfillStatusDto; import com.devkor.ifive.nadab.domain.askchat.core.dto.AskChatRagBackfillTargetDto; import com.devkor.ifive.nadab.domain.askchat.core.dto.AskChatRagSearchResultDto; import com.devkor.ifive.nadab.domain.user.core.entity.InterestCode; @@ -68,6 +69,33 @@ void findCompletedDailyAnswerTargets_excludes_already_indexed_sources_and_limits .containsExactly(secondReportId, thirdReportId); } + @Test + void findCompletedDailyAnswerStatus_counts_targets_indexed_and_failed_for_current_version() { + Long userId = insertUser("backfill-status"); + Long questionId = insertDailyQuestion(InterestCode.RELATIONSHIP, "status question"); + Long targetAnswerId = insertAnswerEntry(userId, questionId, "target", LocalDate.of(2026, 7, 10)); + Long indexedAnswerId = insertAnswerEntry(userId, questionId, "indexed", LocalDate.of(2026, 7, 11)); + Long failedAnswerId = insertAnswerEntry(userId, questionId, "failed", LocalDate.of(2026, 7, 12)); + Long deadLetterAnswerId = insertAnswerEntry(userId, questionId, "dead letter", LocalDate.of(2026, 7, 13)); + Long oldVersionAnswerId = insertAnswerEntry(userId, questionId, "old version", LocalDate.of(2026, 7, 14)); + insertDailyReport(targetAnswerId, "target report", "COMPLETED", LocalDate.of(2026, 7, 10)); + insertDailyReport(indexedAnswerId, "indexed report", "COMPLETED", LocalDate.of(2026, 7, 11)); + insertDailyReport(failedAnswerId, "failed report", "COMPLETED", LocalDate.of(2026, 7, 12)); + insertDailyReport(deadLetterAnswerId, "dead letter report", "COMPLETED", LocalDate.of(2026, 7, 13)); + insertDailyReport(oldVersionAnswerId, "old version report", "COMPLETED", LocalDate.of(2026, 7, 14)); + insertRagDocument(userId, "ANSWER_ENTRY", indexedAnswerId, InterestCode.RELATIONSHIP, 2, "COMPLETED"); + insertRagDocument(userId, "ANSWER_ENTRY", failedAnswerId, InterestCode.RELATIONSHIP, 2, "FAILED"); + insertRagDocument(userId, "ANSWER_ENTRY", deadLetterAnswerId, InterestCode.RELATIONSHIP, 2, "DEAD_LETTER"); + insertRagDocument(userId, "ANSWER_ENTRY", oldVersionAnswerId, InterestCode.RELATIONSHIP, 1, "COMPLETED"); + + AskChatRagBackfillStatusDto status = + backfillQueryRepository.findCompletedDailyAnswerStatus(2); + + assertThat(status.targetCount()).isEqualTo(2); + assertThat(status.indexedCount()).isEqualTo(1); + assertThat(status.failedCount()).isEqualTo(2); + } + @Test void updateEmbedding_and_markEmbeddingFailed_update_pgvector_document_status() { Long userId = insertUser("vector-update"); From 98b49e5f793ef66e8cfde3243834bcc5065b921a Mon Sep 17 00:00:00 2001 From: 1Seob Date: Wed, 29 Jul 2026 21:23:52 +0900 Subject: [PATCH 2/3] =?UTF-8?q?feat(admin):=20Ask=20Chat=20RAG=20=EC=97=B0?= =?UTF-8?q?=EC=86=8D=20=EB=B0=B0=EC=B9=98=20=EC=8B=A4=ED=96=89=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 지정한 횟수만큼 백필 배치를 순차 실행하고, 배치 실패 또는 Target 소진 시 후속 실행을 중단하도록 개선함 --- .../templates/admin/ask-chat-rag.html | 117 ++++++++++++++++-- 1 file changed, 105 insertions(+), 12 deletions(-) diff --git a/src/main/resources/templates/admin/ask-chat-rag.html b/src/main/resources/templates/admin/ask-chat-rag.html index 28720c2a..3482db00 100644 --- a/src/main/resources/templates/admin/ask-chat-rag.html +++ b/src/main/resources/templates/admin/ask-chat-rag.html @@ -117,10 +117,31 @@ .btn-row { display: flex; + align-items: end; gap: 8px; flex-wrap: wrap; } + .batch-run-control { + display: grid; + gap: 5px; + } + + .batch-run-control label { + color: var(--muted); + font-size: 12px; + } + + .batch-run-control input { + width: 110px; + border: 1px solid var(--border); + border-radius: 6px; + background: var(--surface-2); + color: var(--text); + padding: 8px 10px; + font-size: 13px; + } + .btn { border: 0; border-radius: 6px; @@ -209,7 +230,8 @@

Admin Console

Daily Answer Backfill

Completed daily answers that do not have a RAG document for the current embedding version are indexed one batch at a time. - Run repeatedly until Target becomes 0. Failed embeddings are saved and handled by the retry scheduler. + Set how many batches to run. Processing stops immediately when a batch fails or Target becomes 0. + Failed embeddings are saved and handled by the retry scheduler. Status counts can be refreshed without running a batch.

@@ -229,7 +251,11 @@

Daily Answer Backfill

- +
+ + +
+
@@ -251,6 +277,7 @@