💡 전체 작동 예제가 GitHub에 있습니다: document-to-markdown-rag-chunks-java
청구 데스크 문제: 실제 파일에서 RAG 수집이 깨지는 이유
온프레미스 RAG 수집 파이프라인은 팀이 이미 보유한 파일을 제3자에게 전송하지 않고 검색 가능하고 인용 가능한 텍스트로 변환하는 문서 파싱 레이어입니다. 보험 업무가 왜 중요한지 보여줍니다. 청구 담당자가 내부 어시스턴트에게 “이 정책의 물 손해에 대해 조정자가 뭐라고 했나요?”라고 물으면, 답은 지난 3월에 받은 이메일 안에 첨부된 PDF 보고서의 “Findings”라는 제목 아래에 있습니다. 어시스턴트는 수집 단계가 첨부 파일을 읽고, 제목을 유지하며, 보고서가 어떤 이메일에 첨부되었는지 기억할 때만 이를 찾을 수 있습니다.
대부분의 수집 작업은 이 세 가지 중 하나를 잘못합니다. 일반 텍스트 추출기는 이메일 본문만 반환하고 첨부 파일을 무시합니다. 고정된 512‑토큰 분할기는 “Findings” 중간에서 보고서를 잘라냅니다. 그리고 호스팅 파서는 두 문제를 모두 해결하지만, 청구 파일(개인 및 의료 정보 포함)이 다른 사람의 서버에 업로드되고 페이지당 요금이 부과된 후에만 작동합니다.
팀이 필요로 하는 것은 구체적이고 제한된 요구사항입니다: 이미 데이터를 보유하고 있는 Java 서비스 내에서 Word, PDF, EML 및 ZIP 파일을 읽고, 구조를 Markdown으로 유지하며, 작성자의 제목에서 분할하고, 각 청크에 소스와 부모를 연결하는 것입니다.
시장 스냅샷: 우리가 검토한 2026년 “RAG용 문서 파싱” 가이드는 오픈소스 Python 파서와 페이지당 청구되는 클라우드 API를 비교합니다. 자체 하드웨어에서 실행되는 Java 라이브러리는 대부분 이 논의에서 빠져 있습니다.
왜 RAG 파이프라인은 일반 텍스트보다 Markdown이 필요한가?
Markdown은 아이디어가 끝나는 지점과 다음 아이디어가 시작되는 지점을 검색기에 알려주는 문서의 부분을 보존합니다. 제목은 # 라인으로 변하고, 표는 행과 열 형태를 유지하며, 리스트는 항목을 유지합니다. 청크 생성기는 이러한 제목을 기준으로 분할할 수 있고, 청크를 읽는 LLM은 사람이 보는 것과 동일한 개요를 확인합니다. 일반 텍스트는 그 개요를 없애버려 크기만이 분할 신호가 됩니다.
왜 일반적인 솔루션은 여기서 작동하지 않는가
팀은 보통 적절한 수집 레이어를 구축하기 전에 다음 세 가지 중 하나를 시도합니다:
- 일반 텍스트 추출 + 토큰 분할기: 설정이 빠르지만 청크가 섹션을 섞고 인용할 제목을 잃어버립니다.
- 클라우드 파싱 API: 구조는 좋지만 파일이 네트워크를 떠나고 페이지마다 청구가 증가합니다.
- 포맷별 자체 파서: DOCX용 라이브러리, PDF용 라이브러리, 이메일용 MIME 파서와
java.util.zip아카이브 파서 등 각각이 자체적인 엣지 케이스를 가집니다.
GroupDocs.Parser for Java는 하나의 Parser 클래스로 네 가지 포맷을 모두 지원하며 프로세스 내에서 실행됩니다.
솔루션: 청구 수집 워크플로우에서 GroupDocs.Parser
GroupDocs.Parser for Java는 문서를 읽고 getFormattedText를 통해 텍스트를 Markdown 형태로 반환할 수 있습니다. 이메일과 아카이브는 컨테이너로 노출되며, 각 항목은 새로운 Parser 인스턴스로 열리므로 이메일에 첨부된 PDF도 디스크에 있는 PDF와 동일하게 처리됩니다. 샘플 프로젝트는 이를 여섯 개의 정적 메서드로 감싸고 결과를 JSON Lines 형식으로 기록합니다. 임베딩 작업이나 벡터 스토어가 이를 로드할 수 있습니다. RAG 프레임워크는 전혀 사용되지 않으며, 출력 형식 자체가 통합 지점이 됩니다.
다음이 필요합니다:
pom.xml에 포함된 JDK 11 이상 (코드는 Java 8 기능만 사용)- GroupDocs.Parser for Java 26.10 (임시 라이선스 받기)
- 몇 개의 DOCX, PDF, EML 또는 ZIP 파일
프로젝트의 pom.xml은 https://releases.groupdocs.com/java/repo/ 저장소와 단일 의존성을 선언하므로 빌드는 한 명령으로 끝납니다:
mvn compile exec:java
수집 레이어 단계별 구현
아래 단계들은 받은 편지함에서 인덱스로 이동하는 하나의 청구 파일을 따라갑니다. 각 단계는 RagChunksDemo.java의 한두 메서드에 매핑됩니다. 우리는 의도적으로 이를 순수 정적 메서드로 유지했으므로, 나머지를 채택하지 않고도 원하는 하나만 기존 워커에 끌어올릴 수 있습니다.
단계 1 - 각 파일을 열고 라우팅하기
업로드된 각 파일은 자체 Parser를 가집니다. 파일 이름은 소스 ID가 되고, 최상위 파일은 부모가 없습니다.
// One call per uploaded claim document
String sourceId = new File(path).getName();
try (Parser parser = new Parser(path)) {
return chunkParser(parser, sourceId, null, 0); // null parent: top-level file
} catch (UnsupportedDocumentFormatException ex) {
// An unknown format is logged and skipped, the batch keeps going
System.out.println("skipped (unsupported format): " + sourceId);
return new ArrayList<>();
}
chunkParser는 본문을 추출하고 청크를 만든 뒤, 깊이가 2 이하인 경우 첨부 파일로 내려갑니다.
단계 2 - Markdown 요청, 일반 텍스트 수용
추출 단계는 확장자를 하드코딩하지 않고 포맷이 지원하는 기능을 탐색합니다.
if (parser.getFeatures().isFormattedText()) {
// Headings, lists and tables survive as Markdown
FormattedTextOptions options =
new FormattedTextOptions(FormattedTextMode.Markdown);
try (TextReader reader = parser.getFormattedText(options)) {
if (reader != null) {
return new ExtractedText("markdown", reader.readToEnd());
}
} catch (IOException ex) {
throw new RuntimeException(ex);
}
}
try (TextReader reader = parser.getText()) {
if (reader != null) { // fallback for formats without formatted text
return new ExtractedText("plain", reader.readToEnd());
}
} catch (IOException ex) {
throw new RuntimeException(ex);
}
return new ExtractedText("none", ""); // e.g. a photo with no OCR
청구 데이터에 이 설정이 중요한 이유:
FormattedTextMode.Markdown: 조정자의 “Findings” 제목이## Findings형태로 청크 생성기에 전달돼 분할 지점이자 인용이 됩니다.isFormattedText(): 샘플 실행에서는 DOCX와 이메일이 Markdown으로 반환되고 PDF는 일반 텍스트로 돌아오며,format필드에 그 결과가 기록됩니다.none결과: ZIP 안의 청구 사진은 OCR이 없으면 청크가 생성되지 않으며, 이는 솔직하고 모니터링하기 쉽습니다.
단계 3 - 조정자 헤딩에서 분할
청크 생성기는 6단계 Markdown 레벨 각각에 현재 제목을 스택으로 유지합니다. 새로운 제목이 나오면 이전 섹션을 닫고 해당 레벨 이하를 초기화합니다.
for (String line : markdown.split("\\r?\\n", -1)) {
Matcher heading = HEADING.matcher(line); // "#" to "######" + title
if (heading.matches()) {
// Close the previous section under the old path
addSection(chunks, section.toString(), sourceId, parentId, path, format);
section.setLength(0);
int level = heading.group(1).length();
stack[level - 1] = heading.group(2).trim();
for (int i = level; i < stack.length; i++) {
stack[i] = null; // forget deeper headings
}
path = joinPath(stack); // "Heading 1 > Heading 1.2"
}
section.append(line).append('\n');
}
1500자를 초과하는 섹션은 문단 구분에서 잘라내어, 긴 Findings 섹션이 하나의 제목 경로를 공유하는 여러 청크로 나뉩니다. 번들된 Word 파일에서는 Heading 1 > Heading 1.2 > Heading 1.2.1과 같은 경로가 생성됩니다.
단계 4 - 첨부 파일을 따라가고 JSON Lines 쓰기
청구 데스크가 가장 신경 쓰는 단계입니다. getContainer()는 이메일 첨부와 ZIP 항목을 열거하고, 각 항목은 자식 문서로 열리며 부모 ID를 상속합니다.
Iterable<ContainerItem> items = parser.getContainer();
if (items == null) {
return chunks; // not an email or archive
}
for (ContainerItem item : items) {
// "claim-0412.eml/report.pdf" style ids keep the lineage readable
String childId = sourceId + "/" + item.getFilePath();
try (Parser child = item.openParser()) {
chunks.addAll(chunkParser(child, childId, sourceId, depth + 1));
} catch (UnsupportedDocumentFormatException ex) {
System.out.println("skipped (unsupported format): " + childId);
}
}
writeJsonl은 각 청크를 id, source, parent, heading_path, format, text 필드가 포함된 UTF‑8 한 줄로 기록합니다. 네 개의 샘플 입력으로 실행하면 22줄이 생성됩니다. 샘플 이메일 첨부의 청크는 email-with-attachment.eml/attachment.txt#0이며, 그 parent는 email-with-attachment.eml입니다. 이 쌍 덕분에 어시스턴트는 “14 March 이메일에 첨부된 보고서에서”라고 말할 수 있습니다.
실제 워크플로우: 주간 청구 재인덱스
청구 운영 팀이 매주 금요일에 폐쇄된 청구 메일함을 EML 파일과 지원 문서 ZIP 번들로 내보낸다고 가정해 보세요. Java 워커가 이미 해당 내보내기를 문서 저장소로 이동합니다. 수집을 추가하면 각 파일에 대해 chunkDocument를 호출하고 JSON Lines 출력을 임베딩 큐에 추가하면 됩니다.
단계 4는 이메일에 첨부된 예비 보고서를 유지합니다. 단계 3은 어시스턴트가 “Findings”를 인용할 때 파일명과 제목을 표시하게 합니다. 이와 같은 인덱스에 대한 나의 경험 법칙: 청크가 파일과 제목으로 다시 인용될 수 없으면 인덱스에 포함되지 않아야 합니다.
비즈니스 영향: 이것이 바꾸는 것
| 측면 | 이전 일반 설정 | GroupDocs.Parser 파이프라인 적용 후 |
|---|---|---|
| 파일 파싱 위치 | 제3자 엔드포인트 | 자체 JVM 내부 |
| 파싱 비용 모델 | 페이지당, 매 재인덱스마다 | 페이지당 클라우드 요금 없음 |
| 청크 경계 | 고정 토큰 윈도우 | 작성자 제목, 1500자 제한 |
| 이메일 첨부 | 종종 건너뛰거나 평탄화 | 자식 문서로 파싱 |
| 인용 데이터 | 파일명 정도 | source, parent, heading_path |
| 런타임 의존성 | SDK + 네트워크 접근 | Maven 아티팩트 하나 |
이 복합 효과는 컴플라이언스 팀이 승인할 수 있는 인덱스를 만들고, 답변이 실제 문서와 섹션을 가리키는 어시스턴트를 제공합니다.
GroupDocs.Parser가 적용되는 기타 보험 시나리오
수집 레이어가 구축되면 동일한 코드가 다른 문서 중심 작업에도 활용됩니다:
- 정책 문구 검색: Word 정책 문서를 조항 제목별로 분할해 정확한 섹션을 인용합니다.
- 브로커 제출: 혼합된 PDF와 스프레드시트를 포함한 ZIP 업로드를 회원별로 인덱싱하고, 각 청크를 제출과 연결합니다.
- 불만 처리: 고객 이메일과 첨부 파일을 함께 인덱싱하고, 이메일을 모든 첨부 청크의 부모로 설정합니다.
청구 환경에서 시작하기
샘플을 복제하고 자체 내보낸 이메일 몇 개에 대해 실행한 뒤 output/chunks.jsonl을 열어 제목 경로와 부모 ID를 확인한 후 임베딩 작업에 연결하세요. 단계별 아키텍처 메모는 이 파이프라인에 대한 문서 사용 사례에 있으며, Markdown 출력 레퍼런스는 모드가 유지하는 포맷을 나열합니다.