ETL 파이프라인
ETL 파이프라인 (Document, Reader, Splitter, VectorStore)
RAG를 만들 때 핵심은 원시 데이터를 벡터 스토어에 넣기 좋은 형태로 가공하는 일이에요. Spring AI의 ETL 파이프라인이 바로 그 흐름을 정리된 구조로 잡아 줘요. 이 글은 Document, DocumentReader, TokenTextSplitter, VectorStore로 구성되는 ETL 파이프라인을 다뤄요.
ETL(Extract, Transform, Load) 프레임워크는 RAG(Retrieval Augmented Generation) 유스케이스 안에서 데이터 처리의 중심을 담당해요. ETL 파이프라인은 원시 데이터 소스에서 구조화된 벡터 스토어로의 흐름을 조율해서, AI 모델이 검색하기에 최적의 형식으로 데이터가 유지되게 해요.
RAG 유스케이스는 생성 모델의 능력을 강화하기 위해 데이터 본문에서 관련 정보를 검색해서 생성 출력의 품질과 관련성을 높이는 방식이에요.
API 개요 (API Overview)
ETL 파이프라인은 Document 인스턴스를 생성·변환·저장해요. Document 클래스는 텍스트, 메타데이터, 그리고 선택적으로 이미지·오디오·비디오 같은 추가 미디어 타입을 포함해요.
ETL 파이프라인에는 세 가지 주요 컴포넌트가 있어요.
Supplier<List<Document>>를 구현하는DocumentReaderFunction<List<Document>, List<Document>>를 구현하는DocumentTransformerConsumer<List<Document>>를 구현하는DocumentWriter
Document 클래스의 콘텐츠는 DocumentReader의 도움으로 PDF, 텍스트 파일 등 다양한 문서 타입에서 생성돼요.
간단한 ETL 파이프라인을 구성하려면 각 타입의 인스턴스 하나씩을 연결하면 돼요.
예를 들어 세 가지 ETL 타입의 인스턴스가 있다고 해 볼게요.
PagePdfDocumentReader—DocumentReader의 구현TokenTextSplitter—DocumentTransformer의 구현VectorStore—DocumentWriter의 구현
RAG 패턴에 사용하기 위해 벡터 데이터베이스에 데이터를 기본적으로 로드하려면, Java 함수형 문법으로 다음 코드를 사용해요.
vectorStore.accept(tokenTextSplitter.apply(pdfReader.get()));
도메인에 더 자연스럽게 표현되는 메서드 이름을 쓸 수도 있어요.
vectorStore.write(tokenTextSplitter.split(pdfReader.read()));
ETL 인터페이스
ETL 파이프라인은 다음 인터페이스와 구현으로 구성돼요.
DocumentReader
다양한 출처에서 문서의 원본을 제공해요.
public interface DocumentReader extends Supplier<List<Document>> {
default List<Document> read() {
return get();
}
}
DocumentTransformer
처리 워크플로의 일부로 문서 배치를 변환해요.
public interface DocumentTransformer extends Function<List<Document>, List<Document>> {
default List<Document> transform(List<Document> transform) {
return apply(transform);
}
}
DocumentWriter
ETL 과정의 마지막 단계를 관리하고, 저장을 위해 문서를 준비해요.
public interface DocumentWriter extends Consumer<List<Document>> {
default void write(List<Document> documents) {
accept(documents);
}
}
DocumentReaders
경고: 아래에 설명된 대부분의
DocumentReader와DocumentWriter구현은Resource또는 리소스 패턴으로 구성되며, 내부적으로DefaultResourceLoader또는 그 변형을 사용해 저장소에 접근해요. 사용자가 직접 제공한 URL로 이런 인스턴스를 구성하지 않도록 주의하세요 — 보안 문제가 있어요.
JSON — JsonReader
JsonReader는 JSON 문서를 처리해서 Document 객체 목록으로 변환해요.
예시
@Component
class MyJsonReader {
private final Resource resource;
MyJsonReader(@Value("classpath:bikes.json") Resource resource) {
this.resource = resource;
}
List<Document> loadJsonAsDocuments() {
JsonReader jsonReader = new JsonReader(this.resource, "description", "content");
return jsonReader.get();
}
}
생성자 옵션
JsonReader는 여러 생성자 옵션을 제공해요.
JsonReader(Resource resource)JsonReader(Resource resource, String... jsonKeysToUse)JsonReader(Resource resource, JsonMetadataGenerator jsonMetadataGenerator, String... jsonKeysToUse)
파라미터
resource: JSON 파일을 가리키는 SpringResource객체.jsonKeysToUse: 결과Document객체의 텍스트 콘텐츠로 사용할 JSON 키 배열.jsonMetadataGenerator: 각Document의 메타데이터를 만들기 위한 선택적JsonMetadataGenerator.
동작
JsonReader는 JSON 콘텐츠를 다음과 같이 처리해요.
- JSON 배열과 단일 JSON 객체를 모두 처리할 수 있어요.
- 각 JSON 객체(배열 안이든 단일 객체든)에 대해:
- 지정된
jsonKeysToUse를 기준으로 콘텐츠를 추출해요. - 키를 지정하지 않으면 전체 JSON 객체를 콘텐츠로 사용해요.
- 제공된
JsonMetadataGenerator로 메타데이터를 생성해요(없으면 빈 것으로). - 추출된 콘텐츠와 메타데이터로
Document객체를 만들어요.
- 지정된
JSON Pointer 사용
JsonReader는 이제 JSON Pointer를 사용해 JSON 문서의 특정 부분을 가져오는 것을 지원해요. 복잡한 JSON 구조에서 중첩 데이터를 쉽게 추출할 수 있게 해 주는 기능이에요.
get(String pointer) 메서드
public List<Document> get(String pointer)
이 메서드는 JSON Pointer를 사용해 JSON 문서의 특정 부분을 가져와요.
파라미터
pointer: JSON 구조에서 원하는 요소를 찾기 위한 JSON Pointer 문자열(RFC 6901에 정의).
반환값
- pointer가 가리키는 JSON 요소에서 파싱된 문서를 담은
List<Document>반환.
동작
- 이 메서드는 제공된 JSON Pointer로 JSON 구조의 특정 위치로 이동해요.
- pointer가 유효하고 기존 요소를 가리키면:
- JSON 객체: 단일 Document를 담은 리스트 반환.
- JSON 배열: 배열의 각 요소마다 Document 하나씩 담은 리스트 반환.
- pointer가 유효하지 않거나 없는 요소를 가리키면
IllegalArgumentException을 던져요.
예시
JsonReader jsonReader = new JsonReader(resource, "description");
List<Document> documents = this.jsonReader.get("/store/books/0");
예시 JSON 구조
[
{
"id": 1,
"brand": "Trek",
"description": "A high-performance mountain bike for trail riding."
},
{
"id": 2,
"brand": "Cannondale",
"description": "An aerodynamic road bike for racing enthusiasts."
}
]
이 예시에서 JsonReader가 jsonKeysToUse로 "description"으로 구성되면, 배열의 각 자전거에 대해 "description" 필드의 값이 콘텐츠인 Document 객체를 만들어요.
참고사항
JsonReader는 JSON 파싱에 Jackson을 사용해요.- 배열에는 스트리밍을 사용해서 대용량 JSON 파일도 효율적으로 처리해요.
jsonKeysToUse에 여러 키를 지정하면 콘텐츠는 그 키들의 값의 연결(concatenation)이 돼요.- 리더는
jsonKeysToUse와JsonMetadataGenerator를 커스터마이즈해서 다양한 JSON 구조에 적응할 수 있어요.
Text — TextReader
TextReader는 일반 텍스트 문서를 처리해서 Document 객체 목록으로 변환해요.
예시
@Component
class MyTextReader {
private final Resource resource;
MyTextReader(@Value("classpath:text-source.txt") Resource resource) {
this.resource = resource;
}
List<Document> loadText() {
TextReader textReader = new TextReader(this.resource);
textReader.getCustomMetadata().put("filename", "text-source.txt");
return textReader.read();
}
}
생성자 옵션
TextReader는 두 가지 생성자 옵션을 제공해요.
TextReader(String resourceUrl)TextReader(Resource resource)
파라미터
resourceUrl: 읽을 리소스의 URL을 나타내는 문자열.resource: 텍스트 파일을 가리키는 SpringResource객체.
설정
setCharset(Charset charset): 텍스트 파일을 읽을 때 사용하는 문자 집합을 설정해요. 기본은 UTF-8.getCustomMetadata(): 문서에 추가할 커스텀 메타데이터를 넣을 수 있는 변경 가능한 맵을 반환해요.
동작
TextReader는 텍스트 콘텐츠를 다음과 같이 처리해요.
- 텍스트 파일의 전체 콘텐츠를 단일
Document객체로 읽어요. - 파일의 콘텐츠가
Document의 콘텐츠가 돼요. - 메타데이터가
Document에 자동으로 추가돼요.charset: 파일을 읽는 데 사용된 문자 집합(기본: "UTF-8").source: 원본 텍스트 파일의 파일명.
getCustomMetadata()로 추가된 모든 커스텀 메타데이터가Document에 포함돼요.
참고사항
TextReader는 파일 전체 콘텐츠를 메모리로 읽으므로 매우 큰 파일에는 적합하지 않을 수 있어요.- 텍스트를 더 작은 청크로 나눠야 하면 문서를 읽은 후
TokenTextSplitter같은 텍스트 스플리터를 사용할 수 있어요.
List<Document> documents = textReader.get();
List<Document> splitDocuments = TokenTextSplitter.builder().build().apply(this.documents);
- 리더는 Spring의
Resource추상화를 사용해서(classpath, 파일 시스템, URL 등) 다양한 소스에서 읽을 수 있어요. getCustomMetadata()메서드로 리더가 만든 모든 문서에 커스텀 메타데이터를 추가할 수 있어요.
HTML (JSoup) — JsoupDocumentReader
JsoupDocumentReader는 JSoup 라이브러리를 사용해 HTML 문서를 처리해서 Document 객체 목록으로 변환해요.
의존성
Maven이나 Gradle로 프로젝트에 의존성을 추가해요.
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-jsoup-document-reader</artifactId>
</dependency>
dependencies {
implementation 'org.springframework.ai:spring-ai-jsoup-document-reader'
}
예시
@Component
class MyHtmlReader {
private final Resource resource;
MyHtmlReader(@Value("classpath:/my-page.html") Resource resource) {
this.resource = resource;
}
List<Document> loadHtml() {
JsoupDocumentReaderConfig config = JsoupDocumentReaderConfig.builder()
.selector("article p") // Extract paragraphs within <article> tags
.charset("ISO-8859-1") // Use ISO-8859-1 encoding
.includeLinkUrls(true) // Include link URLs in metadata
.metadataTags(List.of("author", "date")) // Extract author and date meta tags
.additionalMetadata("source", "my-page.html") // Add custom metadata
.build();
JsoupDocumentReader reader = new JsoupDocumentReader(this.resource, config);
return reader.get();
}
}
JsoupDocumentReaderConfig는 JsoupDocumentReader의 동작을 커스터마이즈할 수 있게 해 줘요.
charset: HTML 문서의 문자 인코딩을 지정해요(기본 "UTF-8").selector: 텍스트를 추출할 요소를 지정하는 JSoup CSS 선택자(기본 "body").separator: 여러 선택 요소의 텍스트를 연결하는 데 쓰는 문자열(기본 "\n").allElements:true면selector를 무시하고<body>요소의 모든 텍스트를 추출해요(기본false).groupByElement:true면selector가 매칭한 각 요소마다 별도의Document를 만들어요(기본false).includeLinkUrls:true면 절대 링크 URL을 추출해서 메타데이터에 추가해요(기본false).metadataTags: 콘텐츠를 추출할<meta>태그 이름 목록(기본["description", "keywords"]).additionalMetadata: 생성된 모든Document객체에 커스텀 메타데이터를 추가할 수 있게 해 줘요.
샘플 문서: my-page.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>My Web Page</title>
<meta name="description" content="A sample web page for Spring AI">
<meta name="keywords" content="spring, ai, html, example">
<meta name="author" content="John Doe">
<meta name="date" content="2024-01-15">
<link rel="stylesheet" href="style.css">
</head>
<body>
<header>
<h1>Welcome to My Page</h1>
</header>
<nav>
<ul>
<li><a href="/">Home</a></li>
<li><a href="/about">About</a></li>
</ul>
</nav>
<article>
<h2>Main Content</h2>
<p>This is the main content of my web page.</p>
<p>It contains multiple paragraphs.</p>
<a href="https://www.example.com">External Link</a>
</article>
<footer>
<p>© 2024 John Doe</p>
</footer>
</body>
</html>
동작: JsoupDocumentReader는 HTML 콘텐츠를 처리하고 설정에 따라 Document 객체를 만들어요.
selector는 텍스트 추출에 사용할 요소를 결정해요.allElements가true면<body>안의 모든 텍스트를 단일Document로 추출해요.groupByElement가true면selector와 일치하는 각 요소가 별도의Document를 만들어요.allElements도groupByElement도 아니면selector와 일치하는 모든 요소의 텍스트를separator로 연결해요.- 문서 제목, 지정된
<meta>태그의 콘텐츠, (선택적으로) 링크 URL이Document메타데이터에 추가돼요. - 상대 링크를 해석하기 위한 base URI는 URL 리소스에서 추출돼요.
리더는 선택된 요소의 텍스트 콘텐츠는 보존하지만, 그 안의 HTML 태그는 제거해요.
Markdown — MarkdownDocumentReader
MarkdownDocumentReader는 Markdown 문서를 처리해서 Document 객체 목록으로 변환해요.
의존성
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-markdown-document-reader</artifactId>
</dependency>
dependencies {
implementation 'org.springframework.ai:spring-ai-markdown-document-reader'
}
예시
@Component
class MyMarkdownReader {
private final Resource resource;
MyMarkdownReader(@Value("classpath:code.md") Resource resource) {
this.resource = resource;
}
List<Document> loadMarkdown() {
MarkdownDocumentReaderConfig config = MarkdownDocumentReaderConfig.builder()
.withHorizontalRuleCreateDocument(true)
.withIncludeCodeBlock(false)
.withIncludeBlockquote(false)
.withAdditionalMetadata("filename", "code.md")
.build();
MarkdownDocumentReader reader = new MarkdownDocumentReader(this.resource, config);
return reader.get();
}
}
MarkdownDocumentReaderConfig는 MarkdownDocumentReader의 동작을 커스터마이즈할 수 있게 해 줘요.
horizontalRuleCreateDocument:true일 때 Markdown의 수평선이 새Document객체를 만들어요.includeCodeBlock:true일 때 코드 블록이 주변 텍스트와 같은Document에 포함돼요.false일 때 코드 블록이 별도의Document객체를 만들어요.includeBlockquote:true일 때 인용구가 주변 텍스트와 같은Document에 포함돼요.false일 때 인용구가 별도의Document객체를 만들어요.additionalMetadata: 생성된 모든Document객체에 커스텀 메타데이터를 추가할 수 있게 해 줘요.
샘플 문서: code.md
This is a Java sample application:
```java
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
```
Markdown also provides the possibility to `use inline code formatting throughout` the entire sentence.
---
Another possibility is to set block code without specific highlighting:
```
./mvnw spring-javaformat:apply
```
동작: MarkdownDocumentReader는 Markdown 콘텐츠를 처리하고 설정에 따라 Document 객체를 만들어요.
- 헤더는 Document 객체의 메타데이터가 돼요.
- 문단은 Document 객체의 콘텐츠가 돼요.
- 코드 블록은 자체 Document 객체로 분리하거나 주변 텍스트와 함께 포함할 수 있어요.
- 인용구는 자체 Document 객체로 분리하거나 주변 텍스트와 함께 포함할 수 있어요.
- 수평선으로 콘텐츠를 별도의 Document 객체로 나눌 수 있어요.
리더는 인라인 코드, 리스트, 텍스트 스타일링 같은 서식을 Document 객체의 콘텐츠 안에 보존해요.
PDF Page — PagePdfDocumentReader
PagePdfDocumentReader는 Apache PdfBox 라이브러리를 사용해 PDF 문서를 파싱해요.
의존성
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-pdf-document-reader</artifactId>
</dependency>
dependencies {
implementation 'org.springframework.ai:spring-ai-pdf-document-reader'
}
예시
@Component
public class MyPagePdfDocumentReader {
List<Document> getDocsFromPdf() {
PagePdfDocumentReader pdfReader = new PagePdfDocumentReader("classpath:/sample1.pdf",
PdfDocumentReaderConfig.builder()
.withPageTopMargin(0)
.withPageExtractedTextFormatter(ExtractedTextFormatter.builder()
.withNumberOfTopTextLinesToDelete(0)
.build())
.withPagesPerDocument(1)
.build());
return pdfReader.read();
}
}
PDF Paragraph — ParagraphPdfDocumentReader
ParagraphPdfDocumentReader는 PDF 카탈로그(예: TOC) 정보를 사용해 입력 PDF를 텍스트 단락으로 나누고 단락마다 단일 Document를 출력해요.
참고: 모든 PDF 문서가 PDF 카탈로그를 포함하는 것은 아니에요.
의존성
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-pdf-document-reader</artifactId>
</dependency>
dependencies {
implementation 'org.springframework.ai:spring-ai-pdf-document-reader'
}
예시
@Component
public class MyPagePdfDocumentReader {
List<Document> getDocsFromPdfWithCatalog() {
ParagraphPdfDocumentReader pdfReader = new ParagraphPdfDocumentReader("classpath:/sample1.pdf",
PdfDocumentReaderConfig.builder()
.withPageTopMargin(0)
.withPageExtractedTextFormatter(ExtractedTextFormatter.builder()
.withNumberOfTopTextLinesToDelete(0)
.build())
.withPagesPerDocument(1)
.build());
return pdfReader.read();
}
}
Tika (DOCX, PPTX, HTML...) — TikaDocumentReader
TikaDocumentReader는 Apache Tika를 사용해 PDF, DOC/DOCX, PPT/PPTX, HTML 등 다양한 문서 형식에서 텍스트를 추출해요. 지원 형식의 전체 목록은 Tika 문서를 참고해요.
의존성
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-tika-document-reader</artifactId>
</dependency>
dependencies {
implementation 'org.springframework.ai:spring-ai-tika-document-reader'
}
예시
@Component
class MyTikaDocumentReader {
private final Resource resource;
MyTikaDocumentReader(@Value("classpath:/word-sample.docx")
Resource resource) {
this.resource = resource;
}
List<Document> loadText() {
TikaDocumentReader tikaDocumentReader = new TikaDocumentReader(this.resource);
return tikaDocumentReader.read();
}
}
Transformers
TextSplitter
TextSplitter는 문서를 나눠 AI 모델의 컨텍스트 윈도우에 맞도록 돕는 추상 베이스 클래스예요.
TokenTextSplitter
TokenTextSplitter는 텍스트를 토큰 수에 따라 청크로 나누는 TextSplitter의 구현이에요. 설정 가능한 인코딩 타입(예: CL100K_BASE, P50K_BASE, O200K_BASE)을 지원하고 기본값은 CL100K_BASE예요.
사용법
기본 사용
@Component
class MyTokenTextSplitter {
public List<Document> splitDocuments(List<Document> documents) {
TokenTextSplitter splitter = TokenTextSplitter.builder().build();
return splitter.apply(documents);
}
public List<Document> splitCustomized(List<Document> documents) {
TokenTextSplitter splitter = TokenTextSplitter.builder()
.withChunkSize(1000)
.withMinChunkSizeChars(400)
.withMinChunkLengthToEmbed(10)
.withMaxNumChunks(5000)
.withKeepSeparator(true)
.build();
return splitter.apply(documents);
}
}
커스텀 인코딩 타입
토큰화에 사용할 인코딩 타입을 구성할 수 있어요. 다른 토크나이저를 사용하는 모델에서 유용해요.
TokenTextSplitter splitter = TokenTextSplitter.builder()
.withEncodingType(EncodingType.O200K_BASE)
.withChunkSize(1000)
.build();
커스텀 문장 부호
텍스트를 의미 있는 청크로 나누는 데 사용할 문장 부호를 커스터마이즈할 수 있어요. 국제화에서 특히 유용해요.
@Component
class MyInternationalTextSplitter {
public List<Document> splitChineseText(List<Document> documents) {
// Use Chinese punctuation marks
TokenTextSplitter splitter = TokenTextSplitter.builder()
.withChunkSize(800)
.withMinChunkSizeChars(350)
.withPunctuationMarks(List.of('。', '?', '!', ';')) // Chinese punctuation
.build();
return splitter.apply(documents);
}
public List<Document> splitWithCustomMarks(List<Document> documents) {
// Mix of English and other punctuation marks
TokenTextSplitter splitter = TokenTextSplitter.builder()
.withChunkSize(800)
.withPunctuationMarks(List.of('.', '?', '!', '\n', ';', ':', '。'))
.build();
return splitter.apply(documents);
}
}
설정
TokenTextSplitter.builder()를 사용해 인스턴스를 만들어요. 모든 생성자는 빌더를 위해 deprecated 상태예요.
파라미터
encodingType: 사용할 토크나이저 인코딩 타입(기본:CL100K_BASE). 지원 값은CL100K_BASE,P50K_BASE,O200K_BASE.chunkSize: 각 텍스트 청크의 목표 크기(토큰)(기본: 800).minChunkSizeChars: 각 텍스트 청크의 최소 크기(문자)(기본: 350).minChunkLengthToEmbed: 포함할 청크의 최소 길이(기본: 5).maxNumChunks: 텍스트에서 생성할 청크의 최대 개수(기본: 10000).keepSeparator: 청크에 구분자(새줄 등)를 유지할지 여부(기본: true).punctuationMarks: 분할할 문장 경계로 사용할 문자 목록(기본:.,?,!,\n).
동작
TokenTextSplitter는 텍스트 콘텐츠를 다음과 같이 처리해요.
- 입력 텍스트를 CL100K_BASE 인코딩으로 토큰으로 인코딩해요.
- 인코딩된 텍스트를
chunkSize에 따라 청크로 나눠요. - 각 청크에 대해:
a. 청크를 다시 텍스트로 디코딩해요.
b. 토큰 총량이 청크 크기를 초과할 때만,
minChunkSizeChars이후에 설정된punctuationMarks를 사용해 적절한 분할 지점을 찾아요. c. 분할 지점이 있으면 그 지점에서 청크를 잘라요. d. 청크를 다듬고keepSeparator설정에 따라 선택적으로 새줄 문자를 제거해요. e. 결과 청크가minChunkLengthToEmbed보다 길면 출력에 추가해요. - 이 과정은 모든 토큰을 처리하거나
maxNumChunks에 도달할 때까지 계속돼요. minChunkLengthToEmbed보다 길면 남은 텍스트를 마지막 청크로 추가해요.
중요: 문장 부호 기반 분할은 토큰 수가 청크 크기를 초과할 때만 적용돼요. 청크 크기와 정확히 같거나 더 작은 텍스트는 문장 부호 기반 잘림 없이 단일 청크로 반환돼요. 이는 작은 텍스트의 불필요한 분할을 방지해요.
예시
Document doc1 = new Document("This is a long piece of text that needs to be split into smaller chunks for processing.",
Map.of("source", "example.txt"));
Document doc2 = new Document("Another document with content that will be split based on token count.",
Map.of("source", "example2.txt"));
TokenTextSplitter splitter = TokenTextSplitter.builder().build();
List<Document> splitDocuments = splitter.apply(List.of(doc1, doc2));
for (Document doc : splitDocuments) {
System.out.println("Chunk: " + doc.getContent());
System.out.println("Metadata: " + doc.getMetadata());
}
참고사항
TokenTextSplitter는jtokkit라이브러리의 CL100K_BASE 인코딩을 사용하는데, 이는 최신 OpenAI 모델과 호환돼요.- 스플리터는 가능한 문장 경계에서 나눠 의미 있는 청크를 만들려고 해요.
- 원본 문서의 메타데이터는 보존되어 그 문서에서 파생된 모든 청크에 복사돼요.
copyContentFormatter가true로 설정되면(기본 동작) 원본 문서의 콘텐츠 포맷터(설정된 경우)도 파생 청크에 복사돼요.- 이 스플리터는 토큰 제한이 있는 대규모 언어 모델을 위한 텍스트 준비에 특히 유용해서, 각 청크가 모델의 처리 용량 안에 있게 해 줘요.
- 커스텀 문장 부호: 기본 문장 부호(
.,?,!,\n)는 영어 텍스트에 잘 맞아요. 다른 언어나 특수 콘텐츠에는 빌더의withPunctuationMarks()메서드로 문장 부호를 커스터마이즈해요. - 성능 고려 사항: 스플리터는 어떤 개수의 문장 부호도 처리할 수 있지만, 각 청크마다 모든 부호를 검사하므로 최적 성능을 위해 목록을 합리적으로 작게(20자 미만) 유지하는 걸 권장해요.
- 확장성:
getLastPunctuationIndex(String)메서드는protected라서 서브클래스가 전문 유스케이스의 문장 부호 감지 로직을 오버라이드할 수 있어요. - 작은 텍스트 처리: 버전 2.0부터 크기 제한에 이미 맞는 작은 텍스트(토큰 수가 청크 크기 이하)는 문장 부호에서 더 이상 분할되지 않아, 불필요한 조각화를 방지해요.
KeywordMetadataEnricher
KeywordMetadataEnricher는 생성 AI 모델을 사용해 문서 콘텐츠에서 키워드를 추출하고 메타데이터로 추가하는 DocumentTransformer예요.
사용법
@Component
class MyKeywordEnricher {
private final ChatModel chatModel;
MyKeywordEnricher(ChatModel chatModel) {
this.chatModel = chatModel;
}
List<Document> enrichDocuments(List<Document> documents) {
KeywordMetadataEnricher enricher = KeywordMetadataEnricher.builder(chatModel)
.keywordCount(5)
.build();
// Or use custom templates
KeywordMetadataEnricher enricher = KeywordMetadataEnricher.builder(chatModel)
.keywordsTemplate(YOUR_CUSTOM_TEMPLATE)
.build();
return enricher.apply(documents);
}
}
생성자 옵션
KeywordMetadataEnricher는 두 가지 생성자 옵션을 제공해요.
KeywordMetadataEnricher(ChatModel chatModel, int keywordCount): 기본 템플릿을 사용하고 지정된 수의 키워드를 추출.KeywordMetadataEnricher(ChatModel chatModel, PromptTemplate keywordsTemplate): 키워드 추출에 커스텀 템플릿을 사용.
동작
KeywordMetadataEnricher는 문서를 다음과 같이 처리해요.
- 각 입력 문서에 대해 문서 콘텐츠를 사용해 프롬프트를 만들어요.
- 이 프롬프트를 제공된
ChatModel에 보내 키워드를 생성해요. - 생성된 키워드를 "excerpt_keywords" 키로 문서 메타데이터에 추가해요.
- 강화된 문서를 반환해요.
커스터마이즈
기본 템플릿을 사용하거나 keywordsTemplate 파라미터로 템플릿을 커스터마이즈할 수 있어요. 기본 템플릿은 다음과 같아요.
\{context_str}. Give %s unique keywords for this document. Format as comma separated. Keywords:
여기서 +{context_str}+는 문서 콘텐츠로, %s는 지정된 키워드 개수로 대체돼요.
예시
ChatModel chatModel = // initialize your chat model
KeywordMetadataEnricher enricher = KeywordMetadataEnricher.builder(chatModel)
.keywordCount(5)
.build();
// Or use custom templates
KeywordMetadataEnricher enricher = KeywordMetadataEnricher.builder(chatModel)
.keywordsTemplate(new PromptTemplate("Extract 5 important keywords from the following text and separate them with commas:\n{context_str}"))
.build();
Document doc = new Document("This is a document about artificial intelligence and its applications in modern technology.");
List<Document> enrichedDocs = enricher.apply(List.of(this.doc));
Document enrichedDoc = this.enrichedDocs.get(0);
String keywords = (String) this.enrichedDoc.getMetadata().get("excerpt_keywords");
System.out.println("Extracted keywords: " + keywords);
참고사항
KeywordMetadataEnricher는 키워드를 생성하려면 동작하는ChatModel이 필요해요.- 키워드 개수는 1 이상이어야 해요.
- 인리처는 처리된 각 문서에 "excerpt_keywords" 메타데이터 필드를 추가해요.
- 생성된 키워드는 쉼표로 구분된 문자열로 반환돼요.
- 이 인리처는 문서 검색성 향상과 문서 태그·카테고리 생성에 특히 유용해요.
- Builder 패턴에서
keywordsTemplate파라미터를 설정하면keywordCount파라미터는 무시돼요.
SummaryMetadataEnricher
SummaryMetadataEnricher는 생성 AI 모델을 사용해 문서 요약을 만들고 메타데이터로 추가하는 DocumentTransformer예요. 현재 문서뿐 아니라 인접 문서(이전·다음)의 요약도 생성할 수 있어요.
사용법
@Configuration
class EnricherConfig {
@Bean
public SummaryMetadataEnricher summaryMetadata(OpenAiChatModel aiClient) {
return new SummaryMetadataEnricher(aiClient,
List.of(SummaryType.PREVIOUS, SummaryType.CURRENT, SummaryType.NEXT));
}
}
@Component
class MySummaryEnricher {
private final SummaryMetadataEnricher enricher;
MySummaryEnricher(SummaryMetadataEnricher enricher) {
this.enricher = enricher;
}
List<Document> enrichDocuments(List<Document> documents) {
return this.enricher.apply(documents);
}
}
생성자
SummaryMetadataEnricher는 두 가지 생성자를 제공해요.
SummaryMetadataEnricher(ChatModel chatModel, List<SummaryType> summaryTypes)SummaryMetadataEnricher(ChatModel chatModel, List<SummaryType> summaryTypes, String summaryTemplate, MetadataMode metadataMode)
파라미터
chatModel: 요약 생성에 사용되는 AI 모델.summaryTypes: 생성할 요약을 나타내는SummaryTypeenum 값 목록(PREVIOUS, CURRENT, NEXT).summaryTemplate: 요약 생성을 위한 커스텀 템플릿(선택).metadataMode: 요약 생성 시 문서 메타데이터를 처리하는 방법(선택).
동작
SummaryMetadataEnricher는 문서를 다음과 같이 처리해요.
- 각 입력 문서에 대해 문서 콘텐츠와 지정된 요약 템플릿을 사용해 프롬프트를 만들어요.
- 이 프롬프트를 제공된
ChatModel에 보내 요약을 생성해요. - 지정된
summaryTypes에 따라 각 문서에 다음 메타데이터를 추가해요.section_summary: 현재 문서의 요약.prev_section_summary: 이전 문서의 요약(있고 요청된 경우).next_section_summary: 다음 문서의 요약(있고 요청된 경우).
- 강화된 문서를 반환해요.
커스터마이즈
커스텀 summaryTemplate을 제공해서 요약 생성 프롬프트를 커스터마이즈할 수 있어요. 기본 템플릿은 다음과 같아요.
"""
Here is the content of the section:
{context_str}
Summarize the key topics and entities of the section.
Summary:
"""
예시
ChatModel chatModel = // initialize your chat model
SummaryMetadataEnricher enricher = new SummaryMetadataEnricher(chatModel,
List.of(SummaryType.PREVIOUS, SummaryType.CURRENT, SummaryType.NEXT));
Document doc1 = new Document("Content of document 1");
Document doc2 = new Document("Content of document 2");
List<Document> enrichedDocs = enricher.apply(List.of(this.doc1, this.doc2));
// Check the metadata of the enriched documents
for (Document doc : enrichedDocs) {
System.out.println("Current summary: " + doc.getMetadata().get("section_summary"));
System.out.println("Previous summary: " + doc.getMetadata().get("prev_section_summary"));
System.out.println("Next summary: " + doc.getMetadata().get("next_section_summary"));
}
제공된 예시는 기대 동작을 보여 줘요.
- 두 문서 목록에서 두 문서 모두
section_summary를 받아요. - 첫 번째 문서는
next_section_summary를 받지만prev_section_summary는 없어요. - 두 번째 문서는
prev_section_summary를 받지만next_section_summary는 없어요. - 첫 번째 문서의
section_summary는 두 번째 문서의prev_section_summary와 일치해요. - 첫 번째 문서의
next_section_summary는 두 번째 문서의section_summary와 일치해요.
참고사항
SummaryMetadataEnricher는 요약을 생성하려면 동작하는ChatModel이 필요해요.- 인리처는 어떤 크기의 문서 목록도 처리할 수 있고, 첫 번째·마지막 문서의 경계 사례를 적절히 처리해요.
- 이 인리처는 문맥 인지 요약을 만드는 데 특히 유용해서 시퀀스 내 문서 관계를 더 잘 이해할 수 있게 해 줘요.
MetadataMode파라미터는 요약 생성 과정에 기존 메타데이터를 어떻게 통합할지를 제어해요.
Writers
File — FileDocumentWriter
FileDocumentWriter는 Document 객체 목록의 콘텐츠를 파일에 쓰는 DocumentWriter 구현이에요.
사용법
@Component
class MyDocumentWriter {
public void writeDocuments(List<Document> documents) {
FileDocumentWriter writer = new FileDocumentWriter("output.txt", true, MetadataMode.ALL, false);
writer.accept(documents);
}
}
생성자
FileDocumentWriter는 세 가지 생성자를 제공해요.
FileDocumentWriter(String fileName)FileDocumentWriter(String fileName, boolean withDocumentMarkers)FileDocumentWriter(String fileName, boolean withDocumentMarkers, MetadataMode metadataMode, boolean append)
파라미터
fileName: 문서를 쓸 파일의 이름.withDocumentMarkers: 출력에 문서 마커를 포함할지 여부(기본: false).metadataMode: 파일에 쓸 문서 콘텐츠를 지정(기본: MetadataMode.NONE).append: true면 파일 시작이 아니라 끝에 데이터를 씀(기본: false).
동작
FileDocumentWriter는 문서를 다음과 같이 처리해요.
- 지정된 파일 이름으로 FileWriter를 연다.
- 입력 목록의 각 문서에 대해:
a.
withDocumentMarkers가 true면 문서 인덱스와 페이지 번호를 포함한 문서 마커를 쓴다. b. 지정된metadataMode에 따라 문서의 포맷된 콘텐츠를 쓴다. - 모든 문서를 쓴 후 파일을 닫는다.
문서 마커
withDocumentMarkers가 true로 설정되면 라이터는 각 문서에 다음 형식의 마커를 포함해요.
### Doc: [index], pages:[start_page_number,end_page_number]
메타데이터 처리
라이터는 두 개의 특정 메타데이터 키를 사용해요.
page_number: 문서의 시작 페이지 번호를 나타냄.end_page_number: 문서의 끝 페이지 번호를 나타냄.
이들은 문서 마커를 쓸 때 사용돼요.
예시
List<Document> documents = // initialize your documents
FileDocumentWriter writer = new FileDocumentWriter("output.txt", true, MetadataMode.ALL, true);
writer.accept(documents);
이렇게 하면 모든 문서를 문서 마커를 포함해 모든 메타데이터를 사용해 "output.txt"에 쓰고, 파일이 이미 있으면 끝에 추가해요.
참고사항
- 라이터는
FileWriter를 사용하므로 운영체제의 기본 문자 인코딩으로 텍스트 파일을 써요. - 쓰는 동안 오류가 발생하면 원래 예외를 원인으로 한
RuntimeException이 던져져요. metadataMode파라미터는 기존 메타데이터가 작성된 콘텐츠에 어떻게 통합되는지를 제어해요.- 이 라이터는 문서 컬렉션 디버깅이나 사람이 읽을 수 있는 출력을 만드는 데 특히 유용해요.
VectorStore
다양한 벡터 스토어와의 통합을 제공해요. 전체 목록은 Vector DB 문서를 참고해요.