package com.jscode.chat.rag;
@Configuration
public class RagConfig {
@Bean
public List<DocumentReader> documentReaders(@Value("${app.rag.documents-location-pattern}") String documentsLocationPattern) throws IOException {
// 1. 설정한 경로 패턴에 맞는 파일 찾기
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
Resource[] resources = resolver.getResources(documentsLocationPattern);
// 2. 찾아온 파일들을 담을 빈 리스트 생성
List<DocumentReader> readers = new ArrayList<>();
// 3. 파일의 개수만큼 for문을 돌면서 tika로 예쁘게 포장해서 리스트에 넣기
for(Resource resource : resources){
readers.add(new TikaDocumentReader(resource));
}
// 4. 리스트 반환
return readers;
}
}
✅ 2. Transform 구현
영어 ‘Apple’은 1토큰이지만, 한글 ‘사과’는 모델에 따라 2~3토큰으로 쪼개 지기도함
이것을 방지하기 위해 글자수 그대로 자르는 로직을 구현해보자
LengthTextSplitter.java
package com.jscode.chat.rag;
import org.springframework.ai.transformer.splitter.TextSplitter;
import org.springframework.util.StringUtils;
import java.util.ArrayList;
import java.util.List;
/*
글자수 기반 슬라이딩 윈도우 텍스트 분할기
긴 문서를 지정된 글자수(chunkSize)만큼 자르되,
문맥이 끊기지 않도록 이전 조각과 일정 부분을 겹치게(chunkOverlap) 자르는 클래스
*/
public class LengthTextSplitter extends TextSplitter {
private final int chunkSize; // 한 조각(Chunk)의 최대 글자 수 (예: 1000자)
private final int chunkOverlap; // 다음 조각과 겹치게 할 글자 수 (예: 200자)
/*
객체를 생성할 때 자를 크기와, 겹칠 크기를 필수적으로 받기!
*/
public LengthTextSplitter(int chunkSize, int chunkOverlap) {
this.chunkSize = chunkSize;
this.chunkOverlap = chunkOverlap;
}
@Override
protected List<String> splitText(String text) { // "안녕하세요반갑습니다"(10글자), chunk:5, over:2
// 1. 잘라낼 조각들을 담을 빈 리스트 생성
List<String> chunks = new ArrayList<>();
// 2. 만약 넘어온 텍스트가 비어있거나, 공백뿐이라면 자를게 없으므로 그대로 리턴
if(!StringUtils.hasText(text)){
return chunks;
}
int textLength = text.length(); // 10글자
// 3. 자르기 시작할 '시작점(인덱스)'를 0으로 세팅
int chunkStart = 0;
// 4. 끝까지 반복해서 자르기
while (chunkStart < textLength) {
// [끝점 계산] 자를 조각의 끝점 계산
// (시작점 + 자를 크기)를 하되, 만약 남은 글자가 부족해서 전체 길이를 넘어가 버리면 전체 길이에서 딱 멈추기
int chunkEnd = Math.min(chunkStart + chunkSize, textLength); // 5,10중에 작은값 5 / 8,10 중에 작은값 8
// [자르기]
String slicedText = text.substring(chunkStart, chunkEnd); // "안녕하세요", "세요반갑습"
chunks.add(slicedText);
// [다음 시작점 계산]
// 방금 자른 조각의 '끝점'에서 '겹칠 크기(Overlap)'만큼 뒤로 되돌아간 곳이 다음 번 시작점이 됨
// 이렇게 해야 다음 조각을 자를 때 이전 조각의 뒷부분이 자연스럽게 포함됨.
int nextStart = chunkEnd - chunkOverlap; // 3
// 남은 텍스트가 너무 짧거나 설정 오류로 인해 다음 시작점이 제자리에 머물거나 뒤로 밀리면 무한루프에 빠짐.
// 이를 방지하기 위해 강제로 반복문을 탈출
if (nextStart <= chunkStart) {
break;
}
// [위치 이동] 시작점을 방금 계산한 다음 시작점으로 이동시키고 다시 루프를 돌기
chunkStart = nextStart; // 3
}
// 5. 리스트 반환
return chunks;
}
}
RagConfig.java
@Bean
public DocumentTransformer textSplitter(){
return new LengthTextSplitter(200,100);
}
@Bean
public DocumentTransformer keywordMetadataEnricher(ChatModel chatModel){
return new KeywordMetadataEnricher(chatModel, 4);
}
✅ 3-1. Load 구현
원본 문서가 텍스트 분할기에 의해 어떻게 쪼개졌는지 확인해보기
VectorStore에 담기는 데이터를 저장하기 전 확인!
RagConfig.java
@Bean
public DocumentWriter jsonConsoleDocumentWriter(ObjectMapper objectMapper){
// 앞 단계에서 가공되어 넘어온 문서 조각 리스트(documents)를 받아서 로직실행
return documents -> {
// 1. 현재 들어온 총 문서 조각(Chunk)의 개수가 몇 개인지 콘솔에 명확하게 표기
System.out.println("======= 저장할 문서 조각(Chunk) 개수: " + documents.size() + " ========");
try {
// 들여쓰기와 줄바꿈 적용된 예쁜 JSON 문자열로 출력
String jsonString = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(documents);
System.out.println(jsonString);
} catch (Exception e) {
System.out.println("JSON 변환 중 에러가 발생했습니다: " + e.getMessage());
}
System.out.println("======================================================");
};
}