💡 Повний робочий приклад доступний на GitHub:
document-to-markdown-rag-chunks-java

Проблема столу претензій: чому інжестія RAG не працює з реальними файлами

Он‑преміс конвеєр інжестії RAG — це шар парсингу документів, який перетворює файли, вже наявні у команди, у пошуковий, цитований текст без передачі їх третій стороні. Операції в страховій сфері показують, чому це важливо. Обробник претензій запитує внутрішнього асистента: «Що регулювальник сказав про пошкодження водою за цією полісом?», а відповідь знаходиться в листі від березня, у PDF‑звіті, прикріпленому до нього, під заголовком «Findings». Асистент зможе знайти її лише тоді, коли інжестія прочитає вкладення, збереже заголовок і запам’ятає, до якого листа належить звіт.

Більшість інжестійних завдань помиляються в одному з трьох пунктів. Екстрактор простого тексту повертає лише тіло листа і ігнорує вкладення. Фіксований розділювач у 512 токенів розрізає звіт посередині «Findings». А хостований парсер вирішує обидва питання, але лише після того, як файл претензії з особистими та медичними даними завантажено на сервери сторонньої компанії і за кожну сторінку стягується плата.

Команді потрібне чітке і вузьке рішення: читати Word, PDF, EML і ZIP‑файли всередині Java‑служби, яка вже володіє даними, зберігати їхню структуру у Markdown, розділяти за заголовками автора і прикріплювати джерело та батьківський елемент до кожного чанка.

Market snapshot: у посібниках 2026 року «парсинг документів для RAG», які ми переглянули, порівнюються open‑source 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 читає документ і може повернути його текст у форматі Markdown через getFormattedText. Листи та архіви виступають як контейнери, елементи яких відкриваються як нові екземпляри Parser, тому PDF‑файл, прикріплений до листа, обробляється точно так само, як PDF на диску. Прикладний проєкт обгортає це в шість статичних методів і записує результат у форматі JSON Lines, який може завантажити будь‑яка задача ембедінгу або векторне сховище. Жоден RAG‑фреймворк не залучений; формат виводу — це точка інтеграції.

Щоб слідувати інструкціям, вам потрібно:

  • JDK 11 або новіший для постаченого pom.xml (сам код дотримується можливостей Java 8)
  • GroupDocs.Parser for Java 26.10 (get a temporary licence)
  • Невелика кількість файлів DOCX, PDF, EML або ZIP

pom.xml проєкту оголошує репозиторій https://releases.groupdocs.com/java/repo/ і одну залежність, тому збірка виконується однією командою:

mvn compile exec:java

Реалізація шару інжестії крок за кроком

Нижче наведені кроки, які проходять один файл претензії від вхідної скриньки до індексу. Кожен крок відповідає одному‑двом методам у RagChunksDemo.java. Ми залишили їх у вигляді простих статичних методів навмисно, щоб ви могли підняти будь‑який з них у існуючий воркер без необхідності приймати інші.

Крок 1 – Відкрити кожен файл і спрямувати його

Кожен завантажений файл отримує власний Parser. Ім’я файлу стає ідентифікатором джерела, а файл верхнього рівня не має батька.

// 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 – Розбити за заголовками регулятора

Розділювач підтримує стек поточного заголовка для кожного з шести рівнів 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; кожен елемент відкривається як дочірній документ і успадковує ідентифікатор батька.

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 березня», а не «з якоїсь частини архіву».

Реальний робочий процес: щотижневе переіндексування претензій

Уявіть команду операцій з претензіями, яка кожну п’ятницю експортує закриті скриньки у вигляді EML‑файлів і ZIP‑пакетів супровідних документів. Java‑воркер вже переміщує ці експорти у сховище документів. Додавання інжестії означає виклик chunkDocument для кожного файлу та додавання виводу JSON Lines у чергу ембедінгу.

Крок 4 саме забезпечує збереження резервного звіту, прикріпленого до листа, який його доставив. Крок 3 гарантує, що коли асистент цитує «Findings», у цитаті вказується файл і заголовок, а не просто номер чанка. Моє правило для такого індексу: якщо чанк не можна прив’язати до файлу і заголовка, його не слід включати в індекс.

Бізнес‑вплив: що змінює це рішення

Аспект Типове налаштування раніше З використанням конвеєра GroupDocs.Parser
Де файли обробляються Точка доступу сторонньої служби Внутрішньо у вашій JVM
Модель вартості парсингу За сторінку, при кожному переіндексуванні Без плати за сторінку в хмарі
Межі чанків Фіксовані вікна токенів Заголовки автора, обмеження 1500 символів
Вкладення електронної пошти Часто пропускаються або сплющуються Обробляються як дочірні документи
Дані для цитування Назва файлу в кращому випадку source, parent і heading_path
Залежності під час виконання SDK плюс мережевий доступ Один Maven‑артефакт

Сумарний ефект — індекс, який команда з комплаєнсу може затвердити, і асистент, чиї відповіді вказують на реальний документ і розділ.

Інші страхові сценарії, де підходить GroupDocs.Parser

Після створення шару інжестії той самий код служить іншим завданням, пов’язаним з великим обсягом документів:

  • Пошук за формулюванням полісу: Word‑документи поліса розбиваються за заголовками пунктів, тому відповіді цитують точний розділ.
  • Подання брокерів: ZIP‑завантаження з міксом PDF‑файлів і електронних таблиць індексуються по кожному елементу, кожен прив’язаний до подання.
  • Обробка скарг: листи клієнтів і їхні вкладення індексуються разом, причому лист є батьком кожного чанка вкладення.

Перші кроки у вашому середовищі претензій

Клонуйте приклад, запустіть його проти кількох ваших власних експортованих листів і відкрийте output/chunks.jsonl, щоб перевірити шляхи заголовків і ідентифікатори батьків перед підключенням до вашої задачі ембедінгу. Примітки щодо архітектури крок за кроком розміщені у випадку використання документації для цього конвеєра, а довідка щодо виводу Markdown перераховує, яке форматування зберігає режим.

Ресурси