Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
356 changes: 356 additions & 0 deletions docs/design/kangcheolung-#75-rag-facade-integration.md

Large diffs are not rendered by default.

25 changes: 25 additions & 0 deletions src/main/java/com/opensource/docgrid/domain/rag/dto/RagAnswer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.opensource.docgrid.domain.rag.dto;

import java.util.ArrayList;
import java.util.List;

import com.opensource.docgrid.domain.search.dto.VectorSearchCandidate;
import com.opensource.docgrid.domain.search.dto.response.CitationResponse;

/**
* RagFacade.generate()의 반환 타입. SearchController가 이 값을 SearchResponse에 병합한다.
*/
public record RagAnswer(String answerText, List<CitationResponse> citations) {

public static RagAnswer of(String answerText, List<VectorSearchCandidate> candidates) {
List<CitationResponse> items = new ArrayList<>();
for (int i = 0; i < candidates.size(); i++) {
items.add(CitationResponse.of(i + 1, candidates.get(i)));
}
return new RagAnswer(answerText, List.copyOf(items));
}

public static RagAnswer noContext(String answerText) {
return new RagAnswer(answerText, List.of());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,11 @@ public class RagResponse extends BaseEntity {
@Column(name = "answer_text", nullable = false, columnDefinition = "TEXT")
private String answerText;

// LLM 제공자 이름
@Column(name = "llm_provider", length = 50)
private String llmProvider;

// LLM 모델 이름
@Column(name = "llm_model_name", length = 100)
private String llmModelName;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package com.opensource.docgrid.domain.rag.service;

import java.util.List;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.opensource.docgrid.domain.rag.dto.OllamaGenerateResult;
import com.opensource.docgrid.domain.rag.dto.RagAnswer;
import com.opensource.docgrid.domain.rag.entity.RagResponse;
import com.opensource.docgrid.domain.rag.service.command.RagResponseCommandService;
import com.opensource.docgrid.domain.rag.service.command.ResponseCitationCommandService;
import com.opensource.docgrid.domain.search.dto.VectorSearchCandidate;
import com.opensource.docgrid.domain.search.entity.SearchQuery;
import com.opensource.docgrid.domain.search.entity.SearchResult;
import com.opensource.docgrid.global.exception.DocGridException;

import jakarta.persistence.EntityManager;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

/**
* RAG 답변 생성 전체 흐름을 조율하는 Facade (F-RAG-05).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

내부 번호 라벨을 설명형 문구로 바꾸세요.

F-RAG-05 형식의 내부 순번 라벨이 코드 Javadoc에 노출됩니다. 실제 이슈 번호 또는 기능 설명만 남기세요.

수정 예시
- * RAG 답변 생성 전체 흐름을 조율하는 Facade (F-RAG-05).
+ * RAG 답변 생성·저장 전체 흐름을 조율하는 Facade.

As per coding guidelines, “Never expose private numbered PR sequence labels in … code, or comments.”

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
* RAG 답변 생성 전체 흐름을 조율하는 Facade (F-RAG-05).
* RAG 답변 생성·저장 전체 흐름을 조율하는 Facade.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/com/opensource/docgrid/domain/rag/service/RagFacade.java` at
line 23, RagFacade의 클래스 Javadoc에서 내부 순번 라벨 “F-RAG-05”를 제거하고, 해당 위치에는 실제 이슈 번호나
RAG 답변 생성 흐름을 조율한다는 설명형 문구만 남기세요.

Source: Coding guidelines

*
* <pre>
* 1. candidates가 비어있으면(NO_CONTEXT) LLM 호출 없이 고정 응답 저장
* 2. PromptBuilder로 프롬프트 조립
* 3. OllamaClient 호출
* 4. rag_responses 저장 (성공/실패)
* 5. 성공 시 response_citations 저장
* </pre>
*
* <p>SearchFacade와 별도 트랜잭션으로 분리되어 있다(SearchController가 순차 호출) — 검색 DB 작업과
* LLM HTTP 호출을 포함한 RAG DB 작업이 하나의 커넥션을 오래 물고 있지 않도록 하기 위함이다.
*/
@Transactional
@Service
@RequiredArgsConstructor
@Slf4j
public class RagFacade {

private final PromptBuilder promptBuilder;
private final OllamaClient ollamaClient;
private final RagResponseCommandService ragResponseCommandService;
private final ResponseCitationCommandService responseCitationCommandService;
private final EntityManager entityManager;

public RagAnswer generate(
Long queryId, String queryText, List<VectorSearchCandidate> candidates, List<SearchResult> searchResults
) {
SearchQuery queryRef = entityManager.getReference(SearchQuery.class, queryId);

// 검색 후보가 없으면(NO_CONTEXT) LLM 호출 없이 고정 응답 저장
if (candidates.isEmpty()) {
RagResponse ragResponse = ragResponseCommandService.createNoContext(queryRef);
log.info("[RAG] no context queryId={} responseId={}", queryId, ragResponse.getId());
return RagAnswer.noContext(ragResponse.getAnswerText());
}

// 검색 후보가 있으면 프롬프트 조립 후 LLM 호출
String prompt = promptBuilder.build(queryText, candidates);
try {
OllamaGenerateResult result = ollamaClient.generate(prompt);
RagResponse ragResponse = ragResponseCommandService.createSuccess(queryRef, prompt, result);
responseCitationCommandService.saveAll(ragResponse, candidates, searchResults);
log.info("[RAG] done queryId={} responseId={} latencyMs={}", queryId, ragResponse.getId(), result.latencyMs());
return RagAnswer.of(result.answerText(), candidates);
} catch (DocGridException e) {
ragResponseCommandService.createFailed(queryRef, prompt, e.getMessage());
throw e;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ public class RagResponseCommandService {
private static final String LLM_PROVIDER = "Ollama";
// answer_text는 NOT NULL 제약이라 실패 시에도 고정 문구를 저장한다. 실제 사유는 errorMessage에 담긴다.
private static final String FAILED_ANSWER_TEXT = "답변 생성에 실패했습니다.";
// 검색 결과가 0건이라 LLM을 호출하지 않은 경우의 고정 응답 문구
private static final String NO_CONTEXT_ANSWER_TEXT = "관련 문서를 찾지 못했습니다.";

private final RagResponseRepository ragResponseRepository;

Expand All @@ -44,6 +46,16 @@ public RagResponse createSuccess(SearchQuery query, String promptText, OllamaGen
return ragResponseRepository.save(ragResponse);
}

// 검색 결과가 0건이라 LLM 호출 자체를 생략한 경우. citation 없이 고정 문구로 SUCCESS 기록한다.
public RagResponse createNoContext(SearchQuery query) {
RagResponse ragResponse = RagResponse.builder()
.query(query)
.answerText(NO_CONTEXT_ANSWER_TEXT)
.status(ResultStatus.SUCCESS)
.build();
return ragResponseRepository.save(ragResponse);
}

// REQUIRES_NEW: 상위 트랜잭션이 롤백돼도 FAILED 기록은 독립 트랜잭션으로 저장된다.
@Transactional(propagation = Propagation.REQUIRES_NEW)
public RagResponse createFailed(SearchQuery query, String promptText, String errorMessage) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import com.opensource.docgrid.domain.rag.entity.ResponseCitation;
import com.opensource.docgrid.domain.rag.repository.ResponseCitationRepository;
import com.opensource.docgrid.domain.search.dto.VectorSearchCandidate;
import com.opensource.docgrid.domain.search.entity.SearchResult;

import jakarta.persistence.EntityManager;
import lombok.RequiredArgsConstructor;
Expand All @@ -19,8 +20,11 @@
* 응답 출처(citation) 저장 서비스 (F-RAG-04).
*
* <p>PromptBuilder가 프롬프트에 포함시킨 것과 동일한 candidates 순서를 citation_order/citation_label
* 근거로 그대로 재사용한다. search_result_id는 이번 이슈에서 채우지 않는다(null) — SearchResultCommandService.saveAll()이
* 저장된 SearchResult를 반환하지 않아 이 시점에는 알 수 없고, Issue 5에서 채운다.
* 근거로 그대로 재사용한다. search_result_id는 searchResults 인자로 함께 받아 연결한다.
*
* <p>searchResults는 SearchFacade의 트랜잭션이 이미 끝난(detached) 엔티티라, 그 객체를 그대로 FK에
* 대입하지 않고 getId()만 꺼내 entityManager.getReference()로 이 트랜잭션의 프록시를 새로 만든다 —
* chunk 필드를 연결할 때와 동일한 방식이다.
*/
@Transactional
@Service
Expand All @@ -30,13 +34,15 @@ public class ResponseCitationCommandService {
private final ResponseCitationRepository responseCitationRepository;
private final EntityManager entityManager;

public void saveAll(RagResponse response, List<VectorSearchCandidate> candidates) {
// candidates와 searchResults는 SearchResultCommandService.saveAll()이 동일한 순서로 만든 것이므로 인덱스로 1:1 대응한다.
public void saveAll(RagResponse response, List<VectorSearchCandidate> candidates, List<SearchResult> searchResults) {
List<ResponseCitation> citations = new ArrayList<>();
for (int i = 0; i < candidates.size(); i++) {
VectorSearchCandidate c = candidates.get(i);
citations.add(ResponseCitation.builder()
.response(response)
.chunk(entityManager.getReference(DocumentChunk.class, c.chunkId()))
.searchResult(entityManager.getReference(SearchResult.class, searchResults.get(i).getId()))
.citationOrder(i + 1)
.citationLabel("[" + (i + 1) + "]")
.quotedText(c.chunkText())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
import org.springframework.web.bind.annotation.RestController;

import com.opensource.docgrid.domain.auth.annotation.CurrentUser;
import com.opensource.docgrid.domain.rag.dto.RagAnswer;
import com.opensource.docgrid.domain.rag.service.RagFacade;
import com.opensource.docgrid.domain.search.dto.SearchOutcome;
import com.opensource.docgrid.domain.search.dto.request.SearchRequest;
import com.opensource.docgrid.domain.search.dto.response.SearchResponse;
import com.opensource.docgrid.domain.search.service.SearchFacade;
Expand All @@ -26,19 +29,25 @@
public class SearchController {

private final SearchFacade searchFacade;
private final RagFacade ragFacade;

@Operation(
summary = "벡터 검색",
description = "질문 텍스트를 임베딩 후 pgvector 코사인 유사도 기준 Top-K 문서 청크를 반환합니다. "
summary = "벡터 검색 + RAG 답변 생성",
description = "질문 텍스트를 임베딩 후 pgvector 코사인 유사도 기준 Top-K 문서 청크를 찾고, "
+ "그 청크를 근거로 LLM이 생성한 답변(answer)과 출처(citations)를 함께 반환합니다. "
+ "topK 기본값은 5이며 1~20 범위에서 지정할 수 있습니다. "
+ "collectionId를 지정하면 해당 컬렉션 내 문서로 검색 범위를 좁힙니다. "
+ "권한이 없는 문서는 결과에 포함되지 않으며, 접근 가능한 문서가 없으면 빈 배열을 반환합니다."
+ "권한이 없는 문서는 결과에 포함되지 않으며, 접근 가능한 문서가 없으면 answer에 고정 안내 문구가 반환됩니다."
)
@PostMapping
public ResponseEntity<ApiResponse<SearchResponse>> search(
@Parameter(hidden = true) @CurrentUser Long userId,
@RequestBody @Valid SearchRequest request
) {
return ResponseUtils.ok(searchFacade.search(userId, request));
SearchOutcome outcome = searchFacade.search(userId, request);
RagAnswer ragAnswer = ragFacade.generate(
outcome.response().queryId(), request.queryText(), outcome.candidates(), outcome.savedResults()
);
return ResponseUtils.ok(outcome.response().withAnswer(ragAnswer.answerText(), ragAnswer.citations()));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.opensource.docgrid.domain.search.dto;

import java.util.List;

import com.opensource.docgrid.domain.search.dto.response.SearchResponse;
import com.opensource.docgrid.domain.search.entity.SearchResult;

/**
* SearchFacade가 SearchController에게 검색 결과와 함께 넘겨주는 내부 전달용 객체 (API 응답 아님).
*
* <p>candidates/savedResults는 RagFacade.generate() 호출에 필요하다 — SearchResponse(API 응답)는
* 표시용 SearchResultItem만 담고 있어 chunk/document id, 저장된 SearchResult의 id가 없다.
*/
public record SearchOutcome(
SearchResponse response,
List<VectorSearchCandidate> candidates,
List<SearchResult> savedResults
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.opensource.docgrid.domain.search.dto.response;

import com.opensource.docgrid.domain.search.dto.VectorSearchCandidate;

import io.swagger.v3.oas.annotations.media.Schema;

public record CitationResponse(
@Schema(description = "인용 라벨") String label,
@Schema(description = "출처 문서 ID") Long documentId,
@Schema(description = "출처 문서 제목") String documentTitle,
@Schema(description = "근거 chunk ID") Long chunkId,
@Schema(description = "원본 문서 페이지 번호, 페이지 개념이 없는 형식은 null") Integer pageNo,
@Schema(description = "인용된 텍스트") String quotedText
) {
Comment on lines +7 to +14

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

신규 DTO에 클래스 수준 주석을 추가해 주세요.

CitationResponse는 새로 생성된 record지만 역할, 책임, API 경계를 설명하는 class-level comment가 없습니다.

제안
+/**
+ * RAG 답변에 포함되는 검색 출처 1건을 표현하는 읽기 전용 응답 DTO.
+ *
+ * <p>검색 후보의 문서·청크 메타데이터를 API 응답 형태로 변환한다.
+ */
 public record CitationResponse(

코딩 가이드라인의 “새로 생성된 class, interface, record에는 역할·책임·경계를 설명하는 class-level comment가 있어야 한다” 규칙에 따른 수정입니다.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public record CitationResponse(
@Schema(description = "인용 라벨") String label,
@Schema(description = "출처 문서 ID") Long documentId,
@Schema(description = "출처 문서 제목") String documentTitle,
@Schema(description = "근거 chunk ID") Long chunkId,
@Schema(description = "원본 문서 페이지 번호, 페이지 개념이 없는 형식은 null") Integer pageNo,
@Schema(description = "인용된 텍스트") String quotedText
) {
/**
* RAG 답변에 포함되는 검색 출처 1건을 표현하는 읽기 전용 응답 DTO.
*
* <p>검색 후보의 문서·청크 메타데이터를 API 응답 형태로 변환한다.
*/
public record CitationResponse(
`@Schema`(description = "인용 라벨") String label,
`@Schema`(description = "출처 문서 ID") Long documentId,
`@Schema`(description = "출처 문서 제목") String documentTitle,
`@Schema`(description = "근거 chunk ID") Long chunkId,
`@Schema`(description = "원본 문서 페이지 번호, 페이지 개념이 없는 형식은 null") Integer pageNo,
`@Schema`(description = "인용된 텍스트") String quotedText
) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/com/opensource/docgrid/domain/search/dto/response/CitationResponse.java`
around lines 7 - 14, CitationResponse record에 클래스 수준 주석을 추가해 역할과 책임, API 응답 경계를
설명하세요. 기존 필드와 Schema 설명은 변경하지 말고, record 선언 바로 앞에 프로젝트의 Javadoc 스타일로 작성하세요.

Source: Coding guidelines

public static CitationResponse of(int order, VectorSearchCandidate candidate) {
return new CitationResponse(
"[" + order + "]",
candidate.documentId(),
candidate.documentTitle(),
candidate.chunkId(),
candidate.pageNo(),
candidate.chunkText()
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,23 @@

public record SearchResponse(
@Schema(description = "검색 요청 ID (search_queries.id)") Long queryId,
@Schema(description = "검색 결과 목록 (유사도 내림차순)") List<SearchResultItem> results
@Schema(description = "검색 결과 목록 (유사도 내림차순)") List<SearchResultItem> results,
@Schema(description = "RAG로 생성된 답변, 아직 생성 전이면 null") String answer,
@Schema(description = "답변의 근거 출처 목록") List<CitationResponse> citations
) {
public static SearchResponse of(Long queryId, List<VectorSearchCandidate> candidates) {
List<SearchResultItem> items = new java.util.ArrayList<>();
for (int i = 0; i < candidates.size(); i++) {
items.add(SearchResultItem.of(i + 1, candidates.get(i)));
}
return new SearchResponse(queryId, List.copyOf(items));
return new SearchResponse(queryId, List.copyOf(items), null, List.of());
}

public static SearchResponse empty(Long queryId) {
return new SearchResponse(queryId, List.of());
return new SearchResponse(queryId, List.of(), null, List.of());
}

public SearchResponse withAnswer(String answer, List<CitationResponse> citations) {
return new SearchResponse(queryId, results, answer, citations);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@
import com.opensource.docgrid.domain.embedding.dto.EmbedResult;
import com.opensource.docgrid.domain.embedding.service.query.QueryEmbeddingService;
import com.opensource.docgrid.domain.permission.service.query.PermissionQueryService;
import com.opensource.docgrid.domain.search.dto.SearchOutcome;
import com.opensource.docgrid.domain.search.dto.VectorSearchCandidate;
import com.opensource.docgrid.domain.search.dto.request.SearchRequest;
import com.opensource.docgrid.domain.search.dto.response.SearchResponse;
import com.opensource.docgrid.domain.search.entity.SearchQuery;
import com.opensource.docgrid.domain.search.entity.SearchResult;
import com.opensource.docgrid.domain.search.service.command.SearchQueryCommandService;
import com.opensource.docgrid.domain.search.service.command.SearchResultCommandService;
import com.opensource.docgrid.domain.search.service.query.AccessibleDocumentQueryService;
Expand Down Expand Up @@ -57,7 +59,7 @@ public class SearchFacade {
private final UserRepository userRepository;
private final CollectionRepository collectionRepository;

public SearchResponse search(Long userId, SearchRequest request) {
public SearchOutcome search(Long userId, SearchRequest request) {
long start = System.currentTimeMillis();

// 1. 질문 임베딩
Expand All @@ -84,7 +86,7 @@ public SearchResponse search(Long userId, SearchRequest request) {
log.info("[SEARCH] no accessible documents userId={}", userId);
int latency = latencyMs(start);
searchQueryCommandService.markSuccess(searchQuery, latency);
return SearchResponse.empty(searchQuery.getId());
return new SearchOutcome(SearchResponse.empty(searchQuery.getId()), List.of(), List.of());
}

// 5. pgvector Top-K 후보 추출 (F-SEARCH-05)
Expand All @@ -102,13 +104,13 @@ public SearchResponse search(Long userId, SearchRequest request) {
userId, candidates.size(), verified.size(), latencyMs(liveStart));

// 7. search_results 저장 (F-SEARCH-07)
searchResultCommandService.saveAll(searchQuery, verified);
List<SearchResult> savedResults = searchResultCommandService.saveAll(searchQuery, verified);

int latency = latencyMs(start);
searchQueryCommandService.markSuccess(searchQuery, latency);
log.info("[SEARCH] done queryId={} results={} latencyMs={}", searchQuery.getId(), verified.size(), latency);

return SearchResponse.of(searchQuery.getId(), verified);
return new SearchOutcome(SearchResponse.of(searchQuery.getId(), verified), verified, savedResults);
Comment on lines +107 to +113

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

rg -n -C 6 'class ResponseCitation|searchResult|`@ManyToOne`|cascade' src/main/java

Repository: DocGrid/backend

Length of output: 50371


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== candidate files =="
git ls-files | rg 'SearchOutcome|SearchQuery\.java|SearchResultCommandService\.java|ResponseCitationCommandService\.java|RagFacade\.java|ResponseCitation\.java|SearchResult\.java|SearchQueryRepository\.java|SearchResultRepository\.java|ResponseCitationRepository\.java' || true

echo
echo "== SearchOutcome =="
rg -n -C 8 'record SearchOutcome|class SearchOutcome|SearchOutcome' src/main/java || true

echo
echo "== SearchQueryCommandService =="
fd -a 'SearchQueryCommandService\.java$' . | while read -r f; do
  echo "--- ${f#$(pwd)/} ---"
  wc -l "$f"
  sed -n '1,180p' "$f"
done

echo
echo "== ResponseCitationCommandService =="
fd -a 'ResponseCitationCommandService\.java$' . | while read -r f; do
  echo "--- ${f#$(pwd)/} ---"
  wc -l "$f"
  sed -n '1,80p' "$f"
done

echo
echo "== ResponseCitation entity/repository =="
fd -a 'ResponseCitation.*\.java$' . | while read -r f; do
  if echo "$f" | rg '(entity|repository|Repository)' >/dev/null; then
    echo "--- ${f#$(pwd)/} ---"
    wc -l "$f"
    sed -n '1,180p' "$f"
  fi
done

echo
echo "== search result repositories =="
fd -a 'SearchQueryRepository\.java$|SearchResultRepository\.java$' . | while read -r f; do
  echo "--- ${f#$(pwd)/} ---"
  wc -l "$f"
  sed -n '1,220p' "$f"
done

echo
echo "== SearchQuery entity relevant mapping =="
fd -a 'SearchQuery\.java$' . | while read -r f; do
  echo "--- ${f#$(pwd)/} ---"
  sed -n '1,180p' "$f"
done

Repository: DocGrid/backend

Length of output: 31127


🏁 Script executed:

rg -n -C 6 'class ResponseCitation|searchResult|`@ManyToOne`|cascade' src/main/java

Repository: DocGrid/backend

Length of output: 50371


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== ResponseCitation searchResult builder/setter excerpts =="
rg -n -C 4 'searchResult|search_result_id' src/main/java/com/opensource/docgrid/domain/rag/entity src/main/java/com/opensource/docgrid/domain/rag/service

echo
echo "== ResponseCitation full builder annotation block =="
sed -n '90,180p' src/main/java/com/opensource/docgrid/domain/rag/entity/ResponseCitation.java

echo
echo "== Search facade transaction outline =="
sed -n '1,28p' src/main/java/com/opensource/docgrid/domain/search/service/SearchFacade.java
sed -n '35,120p' src/main/java/com/opensource/docgrid/domain/search/service/SearchFacade.java

echo
echo "== RAG facade relevant transaction outline =="
sed -n '1,75p' src/main/java/com/opensource/docgrid/domain/rag/service/RagFacade.java

echo
echo "== all `@Transactional` declarations around Search/RAG controllers/services =="
rg -n -C 3 '`@Transactional`' src/main/java/com/opensource/docgrid/domain/search src/main/java/com/opensource/docgrid/domain/rag

Repository: DocGrid/backend

Length of output: 29961


RAG 트랜잭션에서 SearchResult를 참조할 수 있게 해 주세요.

savedResults는 검색 트랜잭션 종료가후 detached 상태라서 ResponseCitation.searchResult(build)로 직접 연결하면 search_result_id가 저장에서 누락됩니다. 기존 chunkId처럼 searchResultId만 넘겨 entityManager.getReference(SearchResult.class, ...)로 연결하거나, searchResult 필드를 nullable로 두고 ID 기반 매핑을 적용해 주세요. 함께 save_result_id 저장을 단가/통합 테스트로 검증해 주세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/com/opensource/docgrid/domain/search/service/SearchFacade.java`
around lines 107 - 113, Update the SearchFacade flow and ResponseCitation
mapping so RAG citations retain each saved SearchResult reference after the
transaction. Pass searchResultId values from savedResults, resolve them with
EntityManager.getReference(SearchResult.class, ...) like the existing chunkId
mapping, and support nullable searchResult fields if required. Add unit and
integration coverage verifying search_result_id is persisted.


} catch (Exception e) {
searchQueryCommandService.markFailed(searchQuery, e.getMessage());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public class SearchResultCommandService {
private final SearchResultRepository searchResultRepository;
private final EntityManager entityManager;

public void saveAll(SearchQuery searchQuery, List<VectorSearchCandidate> candidates) {
public List<SearchResult> saveAll(SearchQuery searchQuery, List<VectorSearchCandidate> candidates) {
List<SearchResult> results = new ArrayList<>();
for (int i = 0; i < candidates.size(); i++) {
VectorSearchCandidate c = candidates.get(i);
Expand All @@ -44,6 +44,6 @@ public void saveAll(SearchQuery searchQuery, List<VectorSearchCandidate> candida
.matchedText(c.chunkText())
.build());
}
searchResultRepository.saveAll(results);
return searchResultRepository.saveAll(results);
}
}
Loading