From 668d6aa9f1daa01be127cbe0a04e29129c04d48d Mon Sep 17 00:00:00 2001 From: pswaao88 Date: Sun, 31 May 2026 01:16:57 +0900 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20=EC=9E=84=EB=B2=A0=EB=94=A9=20?= =?UTF-8?q?=EA=B2=80=EC=83=89=20repository=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/DocumentEmbeddingRepository.java | 9 ++ .../JdbcDocumentEmbeddingRepository.java | 101 ++++++++++++++++++ 2 files changed, 110 insertions(+) diff --git a/backend/src/main/java/com/stackup/stackup/document/domain/DocumentEmbeddingRepository.java b/backend/src/main/java/com/stackup/stackup/document/domain/DocumentEmbeddingRepository.java index fbb91d98..4c9837ab 100644 --- a/backend/src/main/java/com/stackup/stackup/document/domain/DocumentEmbeddingRepository.java +++ b/backend/src/main/java/com/stackup/stackup/document/domain/DocumentEmbeddingRepository.java @@ -8,6 +8,15 @@ public interface DocumentEmbeddingRepository { int countByDocumentId(long documentId); + boolean existsActiveSession(long sessionId); + + int countSearchableSessionDocuments(long sessionId, List documentIds); + + List search(long sessionId, List documentIds, float[] queryEmbedding, int topK, Double minScore); + record EmbeddingChunk(int chunkIndex, String chunkText, float[] embedding) { } + + record EmbeddingSearchRow(long documentId, int chunkIndex, String chunkText, double score, String model) { + } } diff --git a/backend/src/main/java/com/stackup/stackup/document/infrastructure/JdbcDocumentEmbeddingRepository.java b/backend/src/main/java/com/stackup/stackup/document/infrastructure/JdbcDocumentEmbeddingRepository.java index e299ab06..f052c40b 100644 --- a/backend/src/main/java/com/stackup/stackup/document/infrastructure/JdbcDocumentEmbeddingRepository.java +++ b/backend/src/main/java/com/stackup/stackup/document/infrastructure/JdbcDocumentEmbeddingRepository.java @@ -1,7 +1,9 @@ package com.stackup.stackup.document.infrastructure; import com.stackup.stackup.document.domain.DocumentEmbeddingRepository; +import com.stackup.stackup.document.domain.DocumentEmbeddingRepository.EmbeddingSearchRow; import java.sql.PreparedStatement; +import java.util.Collections; import java.util.List; import javax.sql.DataSource; import org.springframework.jdbc.core.JdbcTemplate; @@ -25,6 +27,24 @@ ON CONFLICT (document_id, chunk_index) private static final String COUNT_SQL = "SELECT count(*) FROM document_embeddings WHERE document_id = ?"; + private static final String EXISTS_ACTIVE_SESSION_SQL = """ + SELECT EXISTS ( + SELECT 1 + FROM interview_sessions + WHERE id = ? AND is_deleted = FALSE + ) + """; + + private static final String COUNT_SEARCHABLE_DOCUMENTS_SQL = """ + SELECT COUNT(DISTINCT ad.id) + FROM session_contexts sc + JOIN analyzed_documents ad ON ad.id = sc.document_id + WHERE sc.session_id = ? + AND ad.id = ANY(?::bigint[]) + AND ad.is_deleted = FALSE + AND ad.analysis_status = 'ANALYZED' + """; + private final JdbcTemplate jdbc; public JdbcDocumentEmbeddingRepository(DataSource dataSource) { @@ -59,6 +79,87 @@ public int countByDocumentId(long documentId) { return n == null ? 0 : n; } + @Override + public boolean existsActiveSession(long sessionId) { + Boolean exists = jdbc.queryForObject(EXISTS_ACTIVE_SESSION_SQL, Boolean.class, sessionId); + return Boolean.TRUE.equals(exists); + } + + @Override + public int countSearchableSessionDocuments(long sessionId, List documentIds) { + if (documentIds == null || documentIds.isEmpty()) { + return 0; + } + Integer count = jdbc.query( + COUNT_SEARCHABLE_DOCUMENTS_SQL, + ps -> { + ps.setLong(1, sessionId); + ps.setArray(2, ps.getConnection().createArrayOf("bigint", documentIds.toArray(Long[]::new))); + }, + rs -> rs.next() ? rs.getInt(1) : 0 + ); + return count == null ? 0 : count; + } + + @Override + public List search( + long sessionId, + List documentIds, + float[] queryEmbedding, + int topK, + Double minScore + ) { + if (documentIds == null || documentIds.isEmpty()) { + return Collections.emptyList(); + } + + String queryVector = toVectorLiteral(queryEmbedding); + String sql = """ + SELECT document_id, chunk_index, chunk_text, score, model + FROM ( + SELECT + de.document_id, + de.chunk_index, + de.chunk_text, + 1 - (de.embedding <=> ?::vector) AS score, + de.model, + de.embedding <=> ?::vector AS distance + FROM document_embeddings de + JOIN session_contexts sc + ON sc.document_id = de.document_id + AND sc.session_id = ? + JOIN analyzed_documents ad + ON ad.id = de.document_id + WHERE ad.is_deleted = FALSE + AND ad.analysis_status = 'ANALYZED' + AND de.document_id = ANY(?::bigint[]) + ) ranked + WHERE (? IS NULL OR score >= ?) + ORDER BY distance + LIMIT ? + """; + + return jdbc.query( + sql, + ps -> { + ps.setString(1, queryVector); + ps.setString(2, queryVector); + ps.setLong(3, sessionId); + ps.setArray(4, ps.getConnection().createArrayOf("bigint", documentIds.toArray(Long[]::new))); + ps.setObject(5, minScore); + ps.setObject(6, minScore); + ps.setInt(7, topK); + }, + (rs, rowNum) -> new EmbeddingSearchRow( + rs.getLong("document_id"), + rs.getInt("chunk_index"), + rs.getString("chunk_text"), + rs.getDouble("score"), + rs.getString("model") + ) + ); + } + private static String toVectorLiteral(float[] embedding) { StringBuilder sb = new StringBuilder(embedding.length * 8 + 2); sb.append('['); From f67b73237305fbd95ba641dee330fd660ef9b749 Mon Sep 17 00:00:00 2001 From: pswaao88 Date: Sun, 31 May 2026 01:18:03 +0900 Subject: [PATCH 2/3] =?UTF-8?q?feat:=20=EC=9E=84=EB=B2=A0=EB=94=A9=20?= =?UTF-8?q?=EA=B2=80=EC=83=89=20=EC=84=9C=EB=B9=84=EC=8A=A4=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 --- .../DocumentEmbeddingSearchService.java | 108 +++++++++++ .../dto/EmbeddingSearchCommand.java | 12 ++ .../dto/EmbeddingSearchResult.java | 10 + .../DocumentEmbeddingSearchServiceTest.java | 180 ++++++++++++++++++ 4 files changed, 310 insertions(+) create mode 100644 backend/src/main/java/com/stackup/stackup/document/application/DocumentEmbeddingSearchService.java create mode 100644 backend/src/main/java/com/stackup/stackup/document/application/dto/EmbeddingSearchCommand.java create mode 100644 backend/src/main/java/com/stackup/stackup/document/application/dto/EmbeddingSearchResult.java create mode 100644 backend/src/test/java/com/stackup/stackup/document/application/DocumentEmbeddingSearchServiceTest.java diff --git a/backend/src/main/java/com/stackup/stackup/document/application/DocumentEmbeddingSearchService.java b/backend/src/main/java/com/stackup/stackup/document/application/DocumentEmbeddingSearchService.java new file mode 100644 index 00000000..866e398d --- /dev/null +++ b/backend/src/main/java/com/stackup/stackup/document/application/DocumentEmbeddingSearchService.java @@ -0,0 +1,108 @@ +package com.stackup.stackup.document.application; + +import com.stackup.stackup.common.exception.ApiErrorCode; +import com.stackup.stackup.common.exception.DomainException; +import com.stackup.stackup.document.application.dto.EmbeddingSearchCommand; +import com.stackup.stackup.document.application.dto.EmbeddingSearchResult; +import com.stackup.stackup.document.domain.DocumentEmbeddingRepository; +import java.util.LinkedHashSet; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class DocumentEmbeddingSearchService { + + private static final int DEFAULT_TOP_K = 8; + private static final int MAX_TOP_K = 20; + private static final int EMBEDDING_DIMENSION = 1536; + + private final DocumentEmbeddingRepository embeddingRepository; + + public List search(EmbeddingSearchCommand command) { + SearchCriteria criteria = validate(command); + + if (!embeddingRepository.existsActiveSession(criteria.sessionId())) { + throw new DomainException(ApiErrorCode.SESSION_NOT_FOUND); + } + + int searchableDocumentCount = embeddingRepository.countSearchableSessionDocuments( + criteria.sessionId(), + criteria.documentIds() + ); + if (searchableDocumentCount != criteria.documentIds().size()) { + throw new DomainException(ApiErrorCode.SESSION_FORBIDDEN); + } + + return embeddingRepository.search( + criteria.sessionId(), + criteria.documentIds(), + criteria.queryEmbedding(), + criteria.topK(), + criteria.minScore() + ) + .stream() + .map(row -> new EmbeddingSearchResult( + row.documentId(), + row.chunkIndex(), + row.chunkText(), + row.score(), + row.model() + )) + .toList(); + } + + private SearchCriteria validate(EmbeddingSearchCommand command) { + if (command == null || command.sessionId() == null) { + throw new DomainException(ApiErrorCode.EMBEDDING_BAD_REQUEST); + } + if (command.documentIds() == null || command.documentIds().isEmpty()) { + throw new DomainException(ApiErrorCode.EMBEDDING_BAD_REQUEST); + } + if (command.queryEmbedding() == null || command.queryEmbedding().length == 0) { + throw new DomainException(ApiErrorCode.EMBEDDING_BAD_REQUEST); + } + if (command.queryEmbedding().length != EMBEDDING_DIMENSION) { + throw new DomainException(ApiErrorCode.EMBEDDING_BAD_REQUEST); + } + if (command.minScore() != null && (command.minScore() < 0.0 || command.minScore() > 1.0)) { + throw new DomainException(ApiErrorCode.EMBEDDING_BAD_REQUEST); + } + + int topK = command.topK() == null ? DEFAULT_TOP_K : command.topK(); + if (topK <= 0 || topK > MAX_TOP_K) { + throw new DomainException(ApiErrorCode.EMBEDDING_BAD_REQUEST); + } + + List documentIds = command.documentIds() + .stream() + .filter(id -> id != null && id > 0) + .collect(java.util.stream.Collectors.collectingAndThen( + java.util.stream.Collectors.toCollection(LinkedHashSet::new), + List::copyOf + )); + if (documentIds.isEmpty()) { + throw new DomainException(ApiErrorCode.EMBEDDING_BAD_REQUEST); + } + + return new SearchCriteria( + command.sessionId(), + documentIds, + command.queryEmbedding(), + topK, + command.minScore() + ); + } + + private record SearchCriteria( + long sessionId, + List documentIds, + float[] queryEmbedding, + int topK, + Double minScore + ) { + } +} diff --git a/backend/src/main/java/com/stackup/stackup/document/application/dto/EmbeddingSearchCommand.java b/backend/src/main/java/com/stackup/stackup/document/application/dto/EmbeddingSearchCommand.java new file mode 100644 index 00000000..b8cfd36d --- /dev/null +++ b/backend/src/main/java/com/stackup/stackup/document/application/dto/EmbeddingSearchCommand.java @@ -0,0 +1,12 @@ +package com.stackup.stackup.document.application.dto; + +import java.util.List; + +public record EmbeddingSearchCommand( + Long sessionId, + List documentIds, + float[] queryEmbedding, + Integer topK, + Double minScore +) { +} diff --git a/backend/src/main/java/com/stackup/stackup/document/application/dto/EmbeddingSearchResult.java b/backend/src/main/java/com/stackup/stackup/document/application/dto/EmbeddingSearchResult.java new file mode 100644 index 00000000..a7a39d23 --- /dev/null +++ b/backend/src/main/java/com/stackup/stackup/document/application/dto/EmbeddingSearchResult.java @@ -0,0 +1,10 @@ +package com.stackup.stackup.document.application.dto; + +public record EmbeddingSearchResult( + long documentId, + int chunkIndex, + String chunkText, + double score, + String model +) { +} diff --git a/backend/src/test/java/com/stackup/stackup/document/application/DocumentEmbeddingSearchServiceTest.java b/backend/src/test/java/com/stackup/stackup/document/application/DocumentEmbeddingSearchServiceTest.java new file mode 100644 index 00000000..d0ffd1e5 --- /dev/null +++ b/backend/src/test/java/com/stackup/stackup/document/application/DocumentEmbeddingSearchServiceTest.java @@ -0,0 +1,180 @@ +package com.stackup.stackup.document.application; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +import com.stackup.stackup.common.exception.ApiErrorCode; +import com.stackup.stackup.common.exception.DomainException; +import com.stackup.stackup.document.application.dto.EmbeddingSearchCommand; +import com.stackup.stackup.document.application.dto.EmbeddingSearchResult; +import com.stackup.stackup.document.domain.DocumentEmbeddingRepository; +import com.stackup.stackup.document.domain.DocumentEmbeddingRepository.EmbeddingSearchRow; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; + +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +class DocumentEmbeddingSearchServiceTest { + + private static final long SESSION_ID = 99L; + private static final List DOCUMENT_IDS = List.of(42L, 17L); + private static final float[] QUERY_EMBEDDING = embedding(); + + @Mock + private DocumentEmbeddingRepository embeddingRepository; + + @InjectMocks + private DocumentEmbeddingSearchService service; + + @Test + void search_rejectsEmptyDocumentIds() { + EmbeddingSearchCommand command = new EmbeddingSearchCommand( + SESSION_ID, + List.of(), + QUERY_EMBEDDING, + 8, + null + ); + + assertThatThrownBy(() -> service.search(command)) + .isInstanceOfSatisfying(DomainException.class, exception -> + assertThat(exception.getErrorCode()).isEqualTo(ApiErrorCode.EMBEDDING_BAD_REQUEST) + ); + verifyNoInteractions(embeddingRepository); + } + + @Test + void search_rejectsEmptyQueryEmbedding() { + EmbeddingSearchCommand command = new EmbeddingSearchCommand( + SESSION_ID, + DOCUMENT_IDS, + new float[0], + 8, + null + ); + + assertThatThrownBy(() -> service.search(command)) + .isInstanceOfSatisfying(DomainException.class, exception -> + assertThat(exception.getErrorCode()).isEqualTo(ApiErrorCode.EMBEDDING_BAD_REQUEST) + ); + verifyNoInteractions(embeddingRepository); + } + + @Test + void search_usesDefaultTopKWhenTopKIsNull() { + allowSearch(DOCUMENT_IDS); + when(embeddingRepository.search(eq(SESSION_ID), eq(DOCUMENT_IDS), any(float[].class), anyInt(), isNull())) + .thenReturn(List.of()); + EmbeddingSearchCommand command = new EmbeddingSearchCommand( + SESSION_ID, + DOCUMENT_IDS, + QUERY_EMBEDDING, + null, + null + ); + + service.search(command); + + ArgumentCaptor topKCaptor = ArgumentCaptor.forClass(Integer.class); + verify(embeddingRepository).search( + eq(SESSION_ID), + eq(DOCUMENT_IDS), + eq(QUERY_EMBEDDING), + topKCaptor.capture(), + isNull() + ); + assertThat(topKCaptor.getValue()).isEqualTo(8); + } + + @Test + void search_clampsOrRejectsTopKAboveMax() { + allowSearch(DOCUMENT_IDS); + when(embeddingRepository.search(eq(SESSION_ID), eq(DOCUMENT_IDS), any(float[].class), anyInt(), isNull())) + .thenReturn(List.of()); + EmbeddingSearchCommand command = new EmbeddingSearchCommand( + SESSION_ID, + DOCUMENT_IDS, + QUERY_EMBEDDING, + 21, + null + ); + + try { + service.search(command); + + ArgumentCaptor topKCaptor = ArgumentCaptor.forClass(Integer.class); + verify(embeddingRepository).search( + eq(SESSION_ID), + eq(DOCUMENT_IDS), + eq(QUERY_EMBEDDING), + topKCaptor.capture(), + isNull() + ); + assertThat(topKCaptor.getValue()).isLessThanOrEqualTo(20); + } catch (DomainException exception) { + assertThat(exception.getErrorCode()).isEqualTo(ApiErrorCode.EMBEDDING_BAD_REQUEST); + verify(embeddingRepository, never()) + .search(anyLong(), anyList(), any(float[].class), anyInt(), any()); + } + } + + @Test + void search_returnsRepositoryRowsAsDto() { + allowSearch(DOCUMENT_IDS); + when(embeddingRepository.search(eq(SESSION_ID), eq(DOCUMENT_IDS), any(float[].class), eq(3), eq(0.2))) + .thenReturn(List.of( + new EmbeddingSearchRow(42L, 3, "Spring Security JWT filter", 0.86, "gemini-embedding-001"), + new EmbeddingSearchRow(17L, 1, "Repository testcontainers setup", 0.74, "gemini-embedding-001") + )); + EmbeddingSearchCommand command = new EmbeddingSearchCommand( + SESSION_ID, + DOCUMENT_IDS, + QUERY_EMBEDDING, + 3, + 0.2 + ); + + List results = service.search(command); + + assertThat(results).hasSize(2); + assertThat(results.get(0).documentId()).isEqualTo(42L); + assertThat(results.get(0).chunkIndex()).isEqualTo(3); + assertThat(results.get(0).chunkText()).isEqualTo("Spring Security JWT filter"); + assertThat(results.get(0).score()).isEqualTo(0.86); + assertThat(results.get(0).model()).isEqualTo("gemini-embedding-001"); + assertThat(results.get(1).documentId()).isEqualTo(17L); + assertThat(results.get(1).chunkIndex()).isEqualTo(1); + verify(embeddingRepository).search(SESSION_ID, DOCUMENT_IDS, QUERY_EMBEDDING, 3, 0.2); + } + + private void allowSearch(List documentIds) { + lenient().when(embeddingRepository.existsActiveSession(SESSION_ID)).thenReturn(true); + lenient().when(embeddingRepository.countSearchableSessionDocuments(SESSION_ID, documentIds)) + .thenReturn(documentIds.size()); + } + + private static float[] embedding() { + float[] embedding = new float[1536]; + embedding[0] = 0.12f; + embedding[1] = -0.03f; + return embedding; + } +} From 94ac11cf064bf7298a637e3ed7f5f2719ca1f934 Mon Sep 17 00:00:00 2001 From: pswaao88 Date: Sun, 31 May 2026 01:19:57 +0900 Subject: [PATCH 3/3] =?UTF-8?q?feat:=20=EB=82=B4=EB=B6=80=20=EC=9E=84?= =?UTF-8?q?=EB=B2=A0=EB=94=A9=20=EA=B2=80=EC=83=89=20API=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 --- .../InternalEmbeddingSearchController.java | 41 +++++++++++++++++++ .../dto/InternalEmbeddingSearchRequest.java | 19 +++++++++ .../dto/InternalEmbeddingSearchResponse.java | 34 +++++++++++++++ 3 files changed, 94 insertions(+) create mode 100644 backend/src/main/java/com/stackup/stackup/document/presentation/InternalEmbeddingSearchController.java create mode 100644 backend/src/main/java/com/stackup/stackup/document/presentation/dto/InternalEmbeddingSearchRequest.java create mode 100644 backend/src/main/java/com/stackup/stackup/document/presentation/dto/InternalEmbeddingSearchResponse.java diff --git a/backend/src/main/java/com/stackup/stackup/document/presentation/InternalEmbeddingSearchController.java b/backend/src/main/java/com/stackup/stackup/document/presentation/InternalEmbeddingSearchController.java new file mode 100644 index 00000000..7f4bba42 --- /dev/null +++ b/backend/src/main/java/com/stackup/stackup/document/presentation/InternalEmbeddingSearchController.java @@ -0,0 +1,41 @@ +package com.stackup.stackup.document.presentation; + +import com.stackup.stackup.document.application.DocumentEmbeddingSearchService; +import com.stackup.stackup.document.presentation.dto.InternalEmbeddingSearchRequest; +import com.stackup.stackup.document.presentation.dto.InternalEmbeddingSearchResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@Tag(name = "Internal: Embedding Search", description = "X-Internal-API-Key 필요. AI 서버가 세션 컨텍스트 문서 청크를 pgvector 로 검색.") +@RestController +@RequestMapping("/api/internal/embeddings") +@RequiredArgsConstructor +public class InternalEmbeddingSearchController { + + private final DocumentEmbeddingSearchService searchService; + + @Operation( + operationId = "internalSearchEmbeddings", + summary = "세션 컨텍스트 문서 임베딩 검색", + description = "AI 서버가 생성한 queryEmbedding 으로 세션에 연결된 분석 완료 문서의 상위 chunk 를 검색합니다." + ) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "검색 성공. 결과가 없으면 빈 results 배열 반환"), + @ApiResponse(responseCode = "400", description = "요청 payload 검증 실패"), + @ApiResponse(responseCode = "401", description = "X-Internal-API-Key 인증 실패"), + @ApiResponse(responseCode = "403", description = "요청 문서가 세션 컨텍스트에 없거나 분석 완료 상태가 아님"), + @ApiResponse(responseCode = "404", description = "sessionId 에 해당하는 세션 없음") + }) + @PostMapping("/search") + public InternalEmbeddingSearchResponse search(@Valid @RequestBody InternalEmbeddingSearchRequest request) { + return InternalEmbeddingSearchResponse.from(searchService.search(request.toCommand())); + } +} diff --git a/backend/src/main/java/com/stackup/stackup/document/presentation/dto/InternalEmbeddingSearchRequest.java b/backend/src/main/java/com/stackup/stackup/document/presentation/dto/InternalEmbeddingSearchRequest.java new file mode 100644 index 00000000..ae006d70 --- /dev/null +++ b/backend/src/main/java/com/stackup/stackup/document/presentation/dto/InternalEmbeddingSearchRequest.java @@ -0,0 +1,19 @@ +package com.stackup.stackup.document.presentation.dto; + +import com.stackup.stackup.document.application.dto.EmbeddingSearchCommand; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import java.util.List; + +public record InternalEmbeddingSearchRequest( + @NotNull Long sessionId, + @NotNull @NotEmpty List documentIds, + @NotNull float[] queryEmbedding, + Integer topK, + Double minScore +) { + + public EmbeddingSearchCommand toCommand() { + return new EmbeddingSearchCommand(sessionId, documentIds, queryEmbedding, topK, minScore); + } +} diff --git a/backend/src/main/java/com/stackup/stackup/document/presentation/dto/InternalEmbeddingSearchResponse.java b/backend/src/main/java/com/stackup/stackup/document/presentation/dto/InternalEmbeddingSearchResponse.java new file mode 100644 index 00000000..fe00a910 --- /dev/null +++ b/backend/src/main/java/com/stackup/stackup/document/presentation/dto/InternalEmbeddingSearchResponse.java @@ -0,0 +1,34 @@ +package com.stackup.stackup.document.presentation.dto; + +import com.stackup.stackup.document.application.dto.EmbeddingSearchResult; +import java.util.List; + +public record InternalEmbeddingSearchResponse(List results) { + + public static InternalEmbeddingSearchResponse from(List results) { + return new InternalEmbeddingSearchResponse( + results.stream() + .map(Result::from) + .toList() + ); + } + + public record Result( + long documentId, + int chunkIndex, + String chunkText, + double score, + String model + ) { + + private static Result from(EmbeddingSearchResult result) { + return new Result( + result.documentId(), + result.chunkIndex(), + result.chunkText(), + result.score(), + result.model() + ); + } + } +}