
Introduction :: Spring AI Reference
Support for all major Vector Database providers such as Apache Cassandra, Azure Cosmos DB, Azure Vector Search, Chroma, Elasticsearch, GemFire, MariaDB, Milvus, MongoDB Atlas, Neo4j, OpenSearch, Oracle, PostgreSQL/PGVector, Pinecone, Qdrant, Redis, SAP Han
docs.spring.io
https://docs.spring.io/spring-ai/reference/api/vectordbs.html
Vector Databases :: Spring AI Reference
Some vector stores require their backend schema to be initialized before usage. It will not be initialized for you by default. You must opt-in, by passing a boolean for the appropriate constructor argument or, if using Spring Boot, setting the appropriate
docs.spring.io

Set-ExecutionPolicy RemoteSigned















https://docs.spring.io/spring-ai/reference/
Introduction :: Spring AI Reference
Support for all major Vector Database providers such as Apache Cassandra, Azure Cosmos DB, Azure Vector Search, Chroma, Elasticsearch, GemFire, MariaDB, Milvus, MongoDB Atlas, Neo4j, OpenSearch, Oracle, PostgreSQL/PGVector, Pinecone, Qdrant, Redis, SAP Han
docs.spring.io




코사인 유사도 기반 거리
데이터를 벡터화 시킴, API 이용

VectorStore 인터페이스

row 하나가 document 하나
Double score 유사도점수 ( != 거리가 아님, 0에 가까울수록 유사도 낮고, 1에 가까울수록 유사도 높음 )


public List<Document> searchDocument2(String question) {
List<Document> documents = vectorStore.similaritySearch(
SearchRequest.builder()
.query(question)
.topK(1)
.similarityThreshold(0.4)
.filterExpression("source == '헌법' && year >= 1987")
.build());
return documents;
}
public List<Document> searchDocument2(String question) {
FilterExpressionBuilder feb = new FilterExpressionBuilder();
List<Document> documents = vectorStore.similaritySearch(
SearchRequest.builder()
.query(question)
.topK(1)
.similarityThreshold(0.4)
.filterExpression(feb
.and(
feb.eq("source", "헌법"),
feb.gte("year", 1987))
.build())
.build());
return documents;
}







http://localhost:50001/get-face-image

http://localhost:50001/get-face-vector





// 유사한 얼굴 찾기: <=>는 pgvector 코사인 거리(0~2)를 구하는 연산자임(0에 가까울수록 유사) / socre 와 반대
String sql = """
SELECT content, (embedding <=> ?::vector) AS similarity
FROM face_vector_store
ORDER BY embedding <=> ?::vector
LIMIT 3
""";




https://www.egovframe.go.kr/home/main.do
표준프레임워크 포털 eGovFrame
본문 내용 바로가기 대메뉴 바로가기 소개 구성 구성상세 버전별 구성 오픈소스 SW 현황 아키텍쳐 실행환경 개발환경 운영환경 관리환경 공통컴포넌트 모바일 라이선스 적용사례 추진성과 기술
www.egovframe.go.kr

검색 증강 생성 RAG, Retrieval Augmented Generation
사전 학습 된 LLM 에 기업체 내부 정보를 사용하는 방법
1. 파인 튜닝 ( fine-tuning )
2. 도구호출방식 ( MCP, Model Context Protocol )
3. 검색 증강 생성 ( RAG, Retrieval Augmented Generation )
Advisor는 Spring AOP의 “실행 단위”이며,
Spring의 모든 AOP 기능은 Advisor를 중심으로 동작한다.
ETL 파이프라인 (Extract · Transform · Load)
ETL 파이프라인은 다양한 원천 데이터(Source)를 추출·정제·변환하여
분석·서비스에 적합한 저장소로 안정적으로 적재하는 데이터 처리 흐름이다.
DocumentTransformer는 RAG(Retrieval-Augmented Generation) 파이프라인에서 문서를 임베딩·검색에 적합한 형태로 변환하는 전처리 컴포넌트를 의미합니다. 주로 원본 문서의 구조·내용을 정규화, 분할, 메타데이터 보강 등의 작업을 수행합니다.
TokenTextSplitter는 RAG 파이프라인에서 텍스트를 “토큰 수 기준”으로 분할하는 TextSplitter 구현체입니다.
문자 수나 줄 단위가 아니라, LLM 토크나이저 기준 토큰 개수를 직접 고려한다는 점이 핵심입니다.



// ##### 업로드된 파일로부터 텍스트를 추출하는 메소드 #####
private List<Document> extractFromFile(MultipartFile attach) throws IOException {
// 바이트 배열을 Resource로 생성
Resource resource = new ByteArrayResource(attach.getBytes());
List<Document> documents = null;
if (attach.getContentType().equals("text/plain")) {
// Text(.txt) 파일일 경우
DocumentReader reader = new TextReader(resource);
documents = reader.read();
} else if (attach.getContentType().equals("application/pdf")) {
// PDF(.pdf) 파일일 경우
DocumentReader reader = new PagePdfDocumentReader(resource);
documents = reader.read();
} else if (attach.getContentType().contains("wordprocessingml")) {
// Word(.doc, .docx) 파일일 경우
DocumentReader reader = new TikaDocumentReader(resource);
documents = reader.read();
}
return documents;
}


// ##### JSON의 ETL 과정을 처리하는 메소드 #####
public String etlFromJson(String url) throws Exception {
// URL로부터 Resource 얻기
Resource resource = new UrlResource(url);
// E: 추출하기
JsonReader reader = new JsonReader(
resource,
new JsonMetadataGenerator() {
@Override
public Map<String, Object> generate(Map<String, Object> jsonMap) {
return Map.of(
"title", jsonMap.get("title"),
"author", jsonMap.get("author"),
"url", "http://localhost:8080/document/constitution(19880225).json");
}
},
/*jsonMap -> Map.of(
"title", jsonMap.get("title"),
"author", jsonMap.get("author"),
"url", "http://localhost:8080/document/constitution(19880225).json"
),*/
"date", "content"
);
List<Document> documents = reader.read();
log.info("추출된 Document 수: {} 개", documents.size());
// T: 변환하기
DocumentTransformer transformer = new TokenTextSplitter();
List<Document> transformedDocuments = transformer.apply(documents);
log.info("변환된 Document 수: {} 개", transformedDocuments.size());
// L: 적재하기
vectorStore.add(transformedDocuments);
return "JSON에서 추출-변환-적재 완료 했습니다.";
}

QuestionAnswerAdvisor는 Spring AI에서 RAG(Retrieval-Augmented Generation) 질의응답을 표준화된 방식으로 구성해 주는 Advisor 컴포넌트입니다. 사용자가 질문을 던졌을 때, 벡터 검색 결과를 LLM 프롬프트에 자동으로 주입하고, “근거 기반 답변” 구조를 강제하는 것이 핵심 역할입니다.
ChatModel vs ChatClient
| 추상화 레벨 | Low-level | High-level |
| 실제 LLM 호출 | ✅ | 내부적으로 사용 |
| Advisor 지원 | ❌ | ✅ |
| RAG 지원 | ❌ | ✅ |
| Prompt Template | 제한적 | 풍부 |
| Memory | ❌ | ✅ |
| 실무 권장 | ❌ (직접 사용 X) | ✅ |



Given the context and provided history information and not prior knowledge,
reply to the user comment. If the answer is not in the context, inform
the user that you can't answer the question.
', metadata={messageType=USER}, messageType=USER}], modelOptions=OpenAiChatOptions: {"streamUsage":false,"model":"gpt-4o-mini","temperature":0.7}}, context={qa_retrieved_documents=[Document{id='d3154ab3-5370-4e14-8c47-fad99c77c803', text='⑤대통령의 선거에 관한 사항은 법률로 정한다.
제68조 ①대통령의 임기가 만료되는 때에는 임기만료 70일 내지 40일 전에 후임자를 선거한다.
②대통령이 궐위된 때 또는 대통령 당선자가 사망하거나 판결 기타의 사유로 그 자격을 상실한 때에는 60일 이내에
후임자를 선거한다.', media='null', metadata={chunk_index=4, parent_document_id=16f659e3-e126-41e5-abe6-38ab2f6bbae6, source=헌법법, page_number=7, distance=0.5497711, total_chunks=11}, score=0.4502289295196533}, Document{id='d8d7ded9-c962-4e75-8a22-5e0ecb7ccd6c', text='제70조 대통령의 임기는 5년으로 하며, 중임할 수 없다.
제71조 대통령이 궐위되거나 사고로 인하여 직무를 수행할 수 없을 때에는 국무총리, 법률이 정한 국무위원의 순서로
그 권한을 대행한다.', media='null', metadata={chunk_index=6, parent_document_id=16f659e3-e126-41e5-abe6-38ab2f6bbae6, source=헌법법, page_number=7, distance=0.6054909, total_chunks=11}, score=0.39450907707214355}, Document{id='a4597e3f-16f0-4b46-a484-986b8e71c1de', text='정 또는 폐지되었던 법률은 그 명령이 승인을 얻지 못한 때부터 당연히 효력을 회복한다.
⑤대통령은 제3항과 제4항의 사유를 지체없이 공포하여야 한다.
제77조 ①대통령은 전시ㆍ사변 또는 이에 준하는 국가비상사태에 있어서 병력으로써 군사상의 필요에 응하거나 공공', media='null', metadata={chunk_index=2, parent_document_id=0e164d30-eb45-40d7-8132-dfd1177d1c5c, source=헌법법, page_number=8, distance=0.61336184, total_chunks=11}, score=0.38663816452026367}]}]

청크 크기를 너무 줄이면 문장의 의미를 잃게 됨
// 프롬프트를 LLM으로 전송하고 응답을 받는 코드
String answer = this.chatClient.prompt()
.system("프롬프트 컨텍스트에 없는 내용은 모른다고 답변을 하세요.")
.user(question)
.advisors(questionAnswerAdvisor)
.call()
.content();
return answer;
RetrievalAugmentationAdvisor는 Spring AI에서
RAG(Retrieval-Augmented Generation)를 가장 일반화된 형태로 구현하는 핵심 Advisor입니다.
앞서 QuestionAnswerAdvisor보다 범용적·저수준이며, “검색 결과를 프롬프트 컨텍스트로 주입하는 역할”에 집중합니다.
// 프롬프트를 LLM으로 전송하고 응답을 받는 코드
String answer = this.chatClient.prompt()
.user(question)
.advisors(
MessageChatMemoryAdvisor.builder(chatMemory).build(), // 프롬프트에 이전 대화 기록 추가, 이전 대화 이력(Context)을 프롬프트에 주입
retrievalAugmentationAdvisor // 이전 대화기록을 완전한 대화 완성, VectorStore 검색 결과(Context)를 프롬프트에 주입
)
.advisors(advisorSpec -> advisorSpec.param(
ChatMemory.CONVERSATION_ID, conversationId))
.call()
.content();
return answer;
}
ChatMemory.CONVERSATION_ID 는 Spring AI에서
대화 이력을 “하나의 세션”으로 묶기 위한 핵심 식별자입니다.
이 값이 없거나 잘못 관리되면, ChatMemory는 의도하지 않은 대화를 섞거나 아예 기억하지 못합니다.
ChatMemory.CONVERSATION_ID
ChatMemory가 “이 요청이 어떤 대화에 속하는지”를 판단하기 위한 세션 키
QueryTransformer
→ “사용자 질문을 검색에 적합한 형태로 변환”
DocumentRetriever
→ “변환된 질의를 기반으로 문서를 검색”
RAG 파이프라인에서의 위치
User Question
↓
QueryTransformer ← 질의 재작성
↓
DocumentRetriever ← 벡터/키워드 검색
↓
Documents
↓
Advisor (QA / RAG)
↓
LLM

1. text-embedding-3-small 사용 시 관찰된 결과


text-embedding-3-small을 사용하고, HNSW 인덱스를 비활성화한 상태에서 ETL은 정상적으로 완료되었으나
질의 결과에서는 의미적으로 중요한 조항을 검색하지 못하는 현상이 관찰되었다.
구체적으로는,
질문이 약관에 실제로 명시된 무면허운전·음주운전 면책 조항을 묻고 있음에도 불구하고
RAG 응답은
“해당 보상 제한 항목은 약관에 언급되어 있지 않다”
와 같은 부정확한 결론을 반환하였다.
이는 PDF 자체에 해당 조항이 존재함에도 불구하고,
임베딩 벡터 공간에서 *“무면허운전 / 음주운전 / 보상 제한”*이라는 조건적 의미 관계가 충분히 보존되지 못했음을 의미한다.
즉, text-embedding-3-small은
키워드 중심 검색에는 일정 수준 유효하지만
법률·약관 문서처럼 ‘원칙 + 예외 + 면책’ 구조가 강한 텍스트에서는
의미 구분력이 부족하여,
Retriever 단계에서 적절한 chunk를 상위 후보로 올리지 못하는 한계가 있었다.
2. text-embedding-3-large 사용 시 관찰된 결과


.
임베딩 모델을 text-embedding-3-large로 변경한 이후,
동일한 PDF와 동일한 질문에 대해 응답 품질이 명확히 개선되었다.
RAG 응답은 다음과 같은 특징을 보였다.
무면허운전, 음주운전 사고 시 보상 제한 항목을 조목조목 나열
“보상되지 않는다”는 결론이
운전자의 위법 행위
정상적인 운전 불가능 상태
범죄 목적 사용
과 같은 약관상의 면책 논리와 직접 연결되어 설명됨
실제 약관 문체와 유사한 서술 방식 유지
이는 text-embedding-3-large가
3,000차원 이상의 고차원 벡터를 통해
조건문, 예외 조항, 부정 표현(보상하지 않는다)을
의미적으로 더 정밀하게 분리·표현할 수 있기 때문으로 해석된다.
결과적으로, Retriever가 올바른 조항 단위 chunk를 상위에 랭크시키는 데 성공하면서
LLM이 근거 기반 답변을 생성할 수 있었다.
[결론]
자동차 보험 약관과 같이 조건·예외·면책 구조가 강한 문서를 대상으로 RAG를 수행한 결과,
text-embedding-3-small 모델은 기본적인 키워드 유사성 검색에는 활용 가능하나,
법률적 의미 관계를 정확히 반영하기에는 한계가 있음을 확인하였다.
반면 text-embedding-3-large 모델은 고차원 임베딩을 통해
약관 조항 간의 조건 논리와 부정 표현을 정밀하게 벡터화할 수 있었으며,
특히 무면허운전·음주운전과 같은 면책 질문에서 명확하고 근거 있는 답변을 생성하였다.
또한 RAG 성능은 단순히 임베딩 모델의 크기에 의존하지 않고,
조항 단위로 의미를 보존하는 트랜스포밍(잘게 분할) 전략과 결합될 때 극대화됨을 확인하였다.
따라서 자동차 보험 약관 RAG에서는
text-embedding-3-large + 의미 단위 분할이 가장 적합한 조합임을 결론으로 도출할 수 있다.
'푸닥거리' 카테고리의 다른 글
| Label Shift vs Covariate Shift — 실무 개발자를 위한 10분 가이드 (0) | 2025.08.23 |
|---|---|
| Sequence-to-Sequence (Seq2Seq) 모델 완벽 정리 — RNN vs Transformer 비교 (0) | 2025.08.16 |
| Inductive vs Transductive Learning – 무엇이 다를까? (0) | 2025.07.26 |
| 🔍 RAG(Retrieval-Augmented Generation)란? 검색과 생성의 만남 (0) | 2025.07.23 |
| YOLO와 Probability Calibration: 객체 탐지 모델의 신뢰도 높이기 (0) | 2025.07.19 |

댓글