💡 ตัวอย่างการทำงานเต็มที่พร้อมใช้งานบน GitHub:
document-to-markdown-rag-chunks-java
ปัญหาโต๊ะรับเคลม: ทำไมการรับข้อมูล RAG ถึงล้มเหลวกับไฟล์จริง
สายงาน RAG ที่ทำงานบนเซิร์ฟเวอร์ภายในเป็นชั้นการแยกเอกสารที่เปลี่ยนไฟล์ที่ทีมมีอยู่แล้วให้เป็นข้อความที่ค้นหาได้และอ้างอิงได้โดยไม่ต้องส่งไปยังบุคคลที่สาม การดำเนินงานด้านประกันภัยแสดงให้เห็นว่าทำไมเรื่องนี้ถึงสำคัญ ผู้จัดการเคลมถามผู้ช่วยภายในว่า “ผู้ประเมินบอกอะไรเกี่ยวกับความเสียหายจากน้ำในกรมธรรม์นี้?” และคำตอบอยู่ในอีเมลจากเดือนมีนาคมที่ผ่านมา ภายในรายงาน PDF ที่แนบมาด้วย ภายใต้หัวข้อที่ชื่อว่า “Findings” ผู้ช่วยจะค้นหาได้ก็ต่อเมื่อการรับข้อมูลอ่านไฟล์แนบ เก็บหัวข้อไว้ และจำได้ว่าอีเมลใดที่รายงานมาพร้อม
งานรับข้อมูลส่วนใหญ่ทำผิดอย่างใดอย่างหนึ่งในสามอย่างนี้ ตัวสกัดข้อความแบบ plain‑text จะคืนเนื้อหาอีเมลเท่านั้นและละเลยไฟล์แนบ ตัวแบ่งโทเคนขนาดคงที่ 512 โทเคนจะตัดรายงานกลางหัวข้อ “Findings” และตัวแยกวิเคราะห์แบบโฮสต์จะแก้ปัญหาทั้งสองได้ แต่ต้องอัปโหลดไฟล์เคลมที่มีข้อมูลส่วนบุคคลและข้อมูลทางการแพทย์ไปยังเซิร์ฟเวอร์ของคนอื่นและคิดค่าบริการต่อหน้า
สิ่งที่ทีมต้องการคือสิ่งแคบและชัดเจน: อ่านไฟล์ Word, PDF, EML และ ZIP ภายในบริการ Java ที่เป็นเจ้าของข้อมูลอยู่แล้ว เก็บโครงสร้างเป็น Markdown แบ่งตามหัวข้อของผู้เขียน และแนบแหล่งที่มาและพาเรนต์ให้กับแต่ละชิ้นข้อมูล
ภาพรวมตลาด: คู่มือ 2026 เรื่อง “การแยกเอกสารสำหรับ RAG” ที่เรารีวิวเปรียบเทียบตัวแยก Python แบบโอเพ่นซอร์สกับ API คลาวด์ที่คิดค่าบริการต่อหน้า ไลบรารี Java ที่ทำงานบนฮาร์ดแวร์ของคุณเองยังค่อนข้างหายากในบทสนทนานั้น
ทำไม Pipeline ของ RAG ต้องการ Markdown แทน Plain Text?
Markdown รักษาส่วนของเอกสารที่บอกให้ตัวดึงข้อมูลรู้ว่าความคิดหนึ่งจบที่ไหนและความคิดต่อไปเริ่มที่ไหน หัวข้อจะกลายเป็นบรรทัดที่เริ่มด้วย # ตารางยังคงเป็นแถวและคอลัมน์ และรายการยังคงรายการของมัน ตัวแบ่งชิ้นข้อมูล (chunker) สามารถแบ่งตามหัวข้อเหล่านั้นได้ และ LLM ที่อ่านชิ้นข้อมูลจะเห็นโครงร่างเดียวกับที่คนอ่านเห็น Plain text จะทิ้งโครงร่างนั้นไป ทำให้ขนาดเป็นสัญญาณเดียวที่ใช้ในการแบ่ง
ทำไมโซลูชันทั่วไปถึงใช้ไม่ได้ที่นี่
ทีมมักลองทำอย่างใดอย่างหนึ่งในสามก่อนจะสร้างชั้นการรับข้อมูลที่เหมาะสม:
- การสกัดข้อความแบบ plain‑text พร้อมตัวแบ่งโทเคน: ตั้งค่าเร็ว แต่ชิ้นข้อมูลจะผสมส่วนและสูญเสียหัวข้อที่คุณต้องการอ้างอิง
- API การแยกข้อมูลบนคลาวด์: โครงสร้างดี แต่ไฟล์ต้องออกจากเครือข่ายและค่าใช้จ่ายจะเพิ่มขึ้นตามจำนวนหน้าที่ทำดัชนีใหม่ทุกครั้ง
- ตัวแยกที่พัฒนาขึ้นเองตามฟอร์แมต: ไลบรารีหนึ่งสำหรับ DOCX อีกอันสำหรับ PDF ตัวแยก MIME สำหรับอีเมลและ
java.util.zipสำหรับไฟล์บีบอัด แต่ละอันมีกรณีขอบที่แตกต่างกัน
GroupDocs.Parser for Java ครอบคลุมสี่ฟอร์แมตทั้งหมดภายใต้คลาส Parser เพียงคลาสเดียวและทำงานในกระบวนการเดียว
โซลูชัน: GroupDocs.Parser ใน Workflow การรับข้อมูลเคลม
GroupDocs.Parser for Java อ่านเอกสารและสามารถคืนข้อความเป็น Markdown ผ่าน getFormattedText อีเมลและไฟล์บีบอัดจะถูกเปิดเป็นคอนเทนเนอร์ที่รายการของมันเปิดเป็นอินสแตนซ์ Parser ใหม่ ดังนั้น PDF ที่แนบมากับอีเมลจะถูกประมวลผลเช่นเดียวกับ PDF ที่อยู่บนดิสก์ ตัวอย่างโปรเจกต์ห่อหุ้มฟังก์ชันสถิติเหล่านี้ไว้ในหกเมธอดแบบ static และเขียนผลลัพธ์เป็น JSON Lines ซึ่งงานฝังเวกเตอร์หรือ vector store ใดก็สามารถโหลดได้ ไม่ได้ใช้เฟรมเวิร์ก RAG ใด ๆ; รูปแบบผลลัพธ์คือจุดเชื่อมต่อ
เพื่อทำตามคุณต้องมี:
- JDK 11 หรือใหม่กว่า สำหรับ
pom.xmlที่ให้มา (โค้ดเองใช้คุณสมบัติของ 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 เราเก็บเมธอดเหล่านี้เป็น static ธรรมดาเพื่อให้คุณสามารถยกหนึ่งเมธอดไปใช้ใน worker ที่มีอยู่แล้วโดยไม่ต้องนำส่วนที่เหลือเข้ามา
ขั้นตอน 1 - เปิดไฟล์แต่ละไฟล์และกำหนดเส้นทาง
ไฟล์ที่อัปโหลดแต่ละไฟล์จะได้รับ Parser ของตนเอง ชื่อไฟล์จะกลายเป็น source 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, ยอมรับ Plain Text
ขั้นตอนสกัดข้อมูลจะตรวจสอบว่าฟอร์แมตรองรับอะไรบ้างแทนการกำหนดรายการส่วนขยายแบบคงที่
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” ของผู้ประเมินจะปรากฏต่อ chunker เป็น## Findingsทำให้เป็นจุดแบ่งและอ้างอิงได้isFormattedText(): ในการรันตัวอย่าง DOCX และอีเมลจะคืนเป็น Markdown ส่วน PDF จะถอยกลับเป็น plain text และฟิลด์formatจะบันทึกว่ามีอะไรเกิดขึ้น- ผลลัพธ์
none: รูปภาพเคลมใน ZIP จะไม่สร้างชิ้นข้อมูลหากไม่มี OCR ซึ่งเป็นการบ่งชี้ที่ตรงไปตรงมาและง่ายต่อการตรวจสอบ
ขั้นตอน 3 - แบ่งตามหัวข้อของผู้ประเมิน
chunker จะเก็บสแตกของหัวข้อปัจจุบันที่แต่ละระดับของ 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');
}
ส่วนที่ยาวเกิน 1 500 ตัวอักษรจะถูกตัดที่จุดพักย่อหน้า ดังนั้นส่วน “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 จะเขียนบรรทัด UTF‑8 หนึ่งบรรทัดต่อชิ้นข้อมูลพร้อม id, source, parent, heading_path, format และ text ด้วยอินพุตตัวอย่างสี่ไฟล์ การรันจะสร้าง 22 บรรทัด ชิ้นข้อมูลจากไฟล์แนบของอีเมลตัวอย่างคือ email-with-attachment.eml/attachment.txt#0 และ parent ของมันคือ email-with-attachment.eml คู่นี้ทำให้ผู้ช่วยสามารถพูดว่า “จากรายงานที่แนบมากับอีเมลวันที่ 14 มีนาคม” แทน “จากที่ไหนสักแห่งในไฟล์บีบอัด”
Workflow จริง: การทำดัชนีเคลมประจำสัปดาห์
ลองนึกภาพทีมปฏิบัติการเคลมที่ส่งออกกล่องเมลเคลมที่ปิดแล้วของสัปดาห์ทุกวันศุกร์เป็นไฟล์ EML และบันเดิล ZIP ของเอกสารสนับสนุน Java worker มีอยู่แล้วที่ย้ายไฟล์ส่งออกเหล่านั้นไปยัง document store การเพิ่มขั้นตอนรับข้อมูลหมายถึงการเรียก chunkDocument สำหรับแต่ละไฟล์และต่อผลลัพธ์ JSON Lines ไปยังคิวการฝังเวกเตอร์
ขั้นตอน 4 ทำให้รายงานสำรองที่แนบมากับอีเมลยังคงเชื่อมโยงกับอีเมลที่ส่งมา ขั้นตอน 3 ทำให้เมื่อผู้ช่วยอ้างอิง “Findings” การอ้างอิงจะระบุไฟล์และหัวข้อแทนหมายเลขชิ้นข้อมูลเท่านั้น หลักการของฉันสำหรับดัชนีประเภทนี้คือ หากชิ้นข้อมูลไม่สามารถอ้างอิงกลับไปยังไฟล์และหัวข้อได้ ควรลบออกจากดัชนีเลย
ผลกระทบต่อธุรกิจ: สิ่งที่เปลี่ยนแปลงไป
| ด้าน | การตั้งค่าปกติก่อนหน้า | หลังใช้ pipeline ของ GroupDocs.Parser |
|---|---|---|
| ที่ที่ไฟล์ถูกแยก | จุดสิ้นสุดของบุคคลที่สาม | ภายใน JVM ของคุณเอง |
| โมเดลค่าใช้จ่ายการแยก | ต่อหน้า, ทุกครั้งที่ทำดัชนีใหม่ | ไม่มีค่าใช้จ่ายต่อหน้าในคลาวด์ |
| ขอบเขตชิ้นข้อมูล | หน้าต่างโทเคนคงที่ | หัวข้อของผู้เขียน, จำกัด 1 500 ตัวอักษร |
| ไฟล์แนบของอีเมล | มักถูกข้ามหรือแบน | แยกเป็นเอกสารลูก |
| ข้อมูลการอ้างอิง | ชื่อไฟล์เท่านั้น | source, parent และ heading_path |
| การพึ่งพารันไทม์ | SDK + การเข้าถึงเครือข่าย | เพียง Maven artifact หนึ่ง |
ผลรวมคือ ดัชนีที่ทีมคอมพลายอาจเซ็นรับรองได้และผู้ช่วยที่ตอบกลับพร้อมชี้ไปยังเอกสารและส่วนที่เป็นจริง
สถานการณ์ประกันอื่น ๆ ที่ GroupDocs.Parser เหมาะสม
เมื่อชั้นรับข้อมูลพร้อมใช้งาน โค้ดเดียวกันสามารถให้บริการงานที่ต้องจัดการเอกสารจำนวนมากอื่น ๆ ได้:
- การค้นหาข้อความกรมธรรม์: เอกสาร Word ของกรมธรรม์แยกตามหัวข้อข้อสัญญา เพื่อให้คำตอบอ้างอิงส่วนที่ตรงกัน
- การส่งมอบของนายหน้า: การอัปโหลด ZIP ที่มี PDF และสเปรดชีตผสมกัน ดัชนีตามไฟล์ย่อยแต่ละไฟล์และเชื่อมโยงกับการส่งมอบ
- การจัดการข้อร้องเรียน: อีเมลของลูกค้าและไฟล์แนบของมันดัชนีร่วมกัน โดยอีเมลเป็นพาเรนต์ของทุกชิ้นข้อมูลไฟล์แนบ
เริ่มต้นในสภาพแวดล้อมเคลมของคุณ
คัดลอกตัวอย่าง, รันกับอีเมลที่คุณส่งออกเองหลายไฟล์, แล้วเปิด output/chunks.jsonl เพื่อตรวจสอบเส้นทางหัวข้อและ parent id ก่อนนำเข้าไปยังงานฝังเวกเตอร์ สถาปัตยกรรมแบบขั้นตอนต่อขั้นตอนมีรายละเอียดใน documentation use case for this pipeline และอ้างอิงการแสดงผล Markdown มีใน Markdown output reference