Переход с нарезки по длине на структурный чанкинг вместе с перелинковкой графа знаний дал +6.5 п.п. доли найденных ключевых документов в топ-10 и +2.7 п.п. nDCG@10, оба прироста статистически значимы. Это самый большой вклад среди всех изменений конвейера за цикл. В контексте документа работают три вещи: границы чанков по смыслу, заголовок с путём раздела в каждом чанке и контекстные эмбеддинги. Их вклады по отдельности мы честно не разделили, и ниже сказано почему.
Если коротко — для бизнеса
- Самое заметное улучшение за цикл: доля ключевых документов в топ-10 выросла на 6.5 п.п. (с 0.647 до 0.712), качество порядка — на 2.7 п.п.
- Длинные документы и таблицы стали находимыми: раньше документ обрезался на 32 тысячах токенов, а строка таблицы теряла шапку. Теперь каждая строка знает, из какой она таблицы и раздела.
- Корпус переиндексирован за час вместо шести без остановки поиска; узким местом был внешний API эмбеддингов, а не наш код.
- Урок про измерения: первое сравнение показало просадку, которой не было. Новая система находила документы, которые ещё никто не оценил.
О цикле «RAG в проде»
Четыре статьи о том, как за две-три недели октября 2026 года мы перестроили поиск корпоративного RAG-ассистента. «Мы» — это автор и AI-агенты, которым он делегировал часть исследований и правок. Компания и системы обезличены, цифры настоящие.
- Гибридный поиск: как «ноги» усиливают друг друга
- Индексы под гибридный поиск в PostgreSQL
- Структурный чанкинг и контекстные эмбеддинги ← вы здесь
- Граф знаний в RAG
Термины и метрики за минуту
- Чанк — фрагмент документа, единица поиска и контекста модели.
- Чанкинг по длине — нарезка текста окнами фиксированного размера, без учёта разделов и таблиц.
- Структурный чанкинг — нарезка по дереву документа: разделы, списки, блоки кода, таблицы.
- Эмбеддинг — вектор, кодирующий смысл текста; близкие по смыслу тексты дают близкие векторы.
- Контекстный эмбеддинг — вектор чанка, посчитанный с учётом всего документа, а не одного куска.
- Late chunking — родственный приём: сначала кодируется весь документ, потом выход модели нарезается на чанки.
- tsvector и веса A/C — полнотекстовое представление в PostgreSQL; совпадение в поле с весом A весит больше, чем в поле с весом C.
- Пулинг оценок — LLM-судья [6] размечает пары «запрос — документ», собранные из выдач всех сравниваемых прогонов.
- judged@10 — доля документов топ-10, у которых есть судейская оценка. Неоценённое метрики считают нерелевантным.
- nDCG@10 — качество порядка в первой десятке с учётом степени релевантности; key-doc R@10 (g3) — доля ключевых документов (оценка 3 из 0–3) в топ-10; nR@10 — полнота в топ-10, нормированная на min(10, число релевантных).
- п.п. — процентные пункты. 95%-й бутстреп-интервал [a; b] — если он не включает ноль, эффект статистически значим.
Что терял чанкинг по длине
Кому особенно полезен раздел:Бизнес и продуктАрхитекторы
Исходная схема резала markdown-текст окнами фиксированной длины. На вики-корпусе это давало три системные потери.
- Хвосты длинных документов. Документ обрезался на 32 тысячах токенов, остаток не попадал в индекс. В нашем корпусе p99 документа — 78 тысяч токенов, максимум — 309 тысяч.
- Таблицы. Окно разрезало строки посередине, а шапка оставалась в первом куске. Строка «3.2 | 01.03.2025 | Уточнены требования…» без шапки не сообщает, что это версия и дата документа.
- Контекст раздела. Кусок из середины стандарта не знал, что он из «Каталога типовых решений», раздел «Технические требования». Эмбеддинг и реранкер видели одинаковые куски соседних стандартов и не могли их различить.
Исходные условия
Кому особенно полезен раздел:Бизнес и продуктИнженеры
К началу работы над чанкингом конвейер уже пережил перф-исправления и первую работу над релевантностью, а данные в индексе оставались прежними.
- Все документы нарезаны окнами по длине с обрезкой на 32 тысячах токенов. У чанка не было ни пути раздела, ни терминов, ни второго лексического вектора; реранкер видел только название документа.
- Контекстная модель эмбеддингов уже стояла, но клиент резал контекст документа на куски по 64 чанка. Запросы векторизовались обычной моделью и так и остались.
- Граф знаний связывал сущности с 70.8% живых чанков; сущности извлекались из первых 16 тысяч символов документа.
- Корпус был неполным. У одной из вики API не отдавал тело примерно трети статей через основной эндпоинт, и новые такие статьи в индекс не попадали. После перехода на запасной эндпоинт добавилось около 2 тысяч документов.
| Метрика | До всех работ | Перед перечанкингом | После перечанкинга и графа |
|---|---|---|---|
| nDCG@10 | 0.542 | 0.756 | 0.783 |
| key-doc R@10 (g3) | 0.439 | 0.647 | 0.712 |
| nR@10 | 0.497 | 0.747 | 0.790 |
| Чанков с сущностями графа | — | 70.8% | 73.3% |
| Связей графа на удалённые чанки | — | 59 461 | 0 |
«До всех работ» — самый ранний сохранившийся прогон эталонного набора. Между ним и точкой «перед перечанкингом» лежит работа над реранкером и глубиной пула из первой статьи цикла.
Анатомия структурного чанка
Кому особенно полезен раздел:АрхитекторыИнженеры
Структурный чанкер режет документ по его дереву блоков: вводная часть, разделы, списки, блоки кода, таблицы [3]. Потолок — 600 токенов на чанк: от размера единицы поиска заметно зависит качество выдачи [4]; короткие соседние абзацы склеиваются. Таблица режется по строкам, и каждая строка разворачивается в пары «колонка: значение», так что шапка едет с каждой строкой. В вики-корпусе строки таблиц — почти половина всех чанков.
Главное в чанке — заголовок: название документа, путь в вики, путь раздела и диапазон строк таблицы. Иерархия документа сама по себе источник контекста [5]. Он хранится отдельно от тела и подаётся каждому потребителю в своей форме.
В вектор идёт короткий заголовок без пути в вики: длинный путь одинаков у всех чанков документа и стягивает их векторы друг к другу. Реранкер получает полный заголовок как префикс, и строка таблицы перестаёт быть безымянной. В лексическом индексе заголовок и извлечённые термины весят больше тела (подробнее о втором лексическом векторе — во второй статье цикла). Из тех же полей строится привязка сущностей графа знаний — о ней ниже.
Архитектору
Заголовок чанка — это не часть текста, а отдельное поле с разными «проекциями» для разных потребителей. Одно и то же знание о структуре документа по-разному полезно вектору, лексике и реранкеру, и смешивать их в одну строку значит проиграть хотя бы одному из них.
Контекстные эмбеддинги: контекст легко потерять в клиенте
Кому особенно полезен раздел:ML и исследователиИнженеры
Документы мы векторизуем контекстной моделью эмбеддингов внешнего провайдера (4B параметров, 2000 измерений, int8). Ей передаётся не отдельный чанк, а документ как массив чанков. Каждый вектор считается с учётом всего документа, поэтому кусок «см. таблицу ниже» всё равно оказывается рядом со своим документом. Запросы векторизуются обычной моделью того же семейства: у запроса нет документа, из которого брать контекст.
Идея близка к late chunking [1] и Contextual Document Embeddings [2]: оба подхода переносят контекст документа в вектор чанка без дополнительной LLM-генерации. Этим они отличаются от схемы, где к каждому чанку дописывается сгенерированная справка.
Лимиты API
Кому особенно полезен раздел:Инженеры
| Ограничение | Значение |
|---|---|
| Документов в запросе | до 512 |
| Чанков в запросе | до 16 000 |
| Токенов на документ | до 32 000 |
| Токенов на запрос | до 120 000 |
| Пропускная способность, чанков/с | 415 / 835 / 1 670 по уровням аккаунта |
Документ длиннее 32 тысяч токенов приходится резать на окна, и каждое окно кодируется как отдельный документ. Мы считаем токены своим токенизатором, а не токенизатором провайдера, поэтому держим запас 5% и при отказе «слишком длинно» делим окно пополам.
Артефакт: контекст резал клиент
Кому особенно полезен раздел:ИнженерыML и исследователи
При разборе пропускной способности нашлась неочевидная потеря. Клиент делил чанки одного документа на пачки по 64 и отправлял каждую отдельным запросом. Для провайдера это были разные документы. У документа из 215 чанков (наш p99) четвёртый кусок кодировался, не видя первых трёх. Это касалось примерно 5% документов, и именно самых длинных, где контекст нужен больше всего.
Мы подняли пачку до 256 чанков. Теперь документ режется только по лимиту в 32 тысячи токенов, который не обойти. На 512 запросы у нас работали нестабильно, хотя документация это допускает.
Проверьте у себя
- У контекстной модели размер пачки — не параметр производительности, а параметр качества: он задаёт границу контекста. Убедитесь, что клиент не дробит документ на независимые запросы.
- Если токены считаются не токенизатором провайдера, держите запас и обрабатывайте отказ «слишком длинно» делением окна, а не падением пачки.
Качество: что измерено, а что нет
Кому особенно полезен раздел:ML и исследователиБизнес и продукт
Измерен суммарный эффект структурного чанкинга с контекстными эмбеддингами, вклад каждого по отдельности — нет. Контекстная модель стояла и до, и после; поменялось то, что в неё подаётся.
| Замер, 111 запросов | Δ nDCG@10, п.п. | Δ key-doc R@10, п.п. |
|---|---|---|
| Пилот: перечанковано подмножество, ~5 тыс. документов | +1.5 [−0.4; +3.5] | +2.6 [−0.8; +6.0] |
| Весь корпус + перелинковка графа | +2.7 [+0.4; +5.1] | +6.5 [+2.6; +10.6] |
Пилот дал направление, но не значимость: перечанкована была лишь часть документов, релевантных эталонным запросам. Полный прогон подтвердил эффект. MRR@10 при этом не изменился (~0.97): первый правильный документ система находила и раньше. Растёт полнота: в топ чаще попадают нужные части длинных документов и строки таблиц.
Ловушка неоценённых документов
Кому особенно полезен раздел:ML и исследователи
Первое сравнение после пилота показало просадку nDCG@10 на 4.8 п.п. Причина — в оценке, а не в поиске. Новая нарезка привела в топ-10 документы, которых не было в пуле оценок, и judged@10 упал с 0.99 до 0.87. Метрики считают неоценённое нерелевантным, поэтому любое изменение, которое находит «другие» документы, систематически выглядит хуже. После дооценки 2 126 новых пар тем же LLM-судьёй [6] [7] просадка превратилась в +1.5 п.п.
ML-исследователю
Правило, которое мы из этого вынесли: сравнение недействительно, пока judged@10 ниже 0.9 у любого из прогонов. Инструмент сравнения сам предупреждает об этом, а дооценка идёт с кэшем судейских оценок, так что старые пары не переоцениваются. Чем сильнее изменение меняет состав выдачи, тем выше риск принять неполноту разметки за регресс.
Чего мы не знаем
Кому особенно полезен раздел:ML и исследователиАрхитекторы
- Вклад контекстной модели против обычной на тех же чанках. Чистый эксперимент — вторая колонка векторов от обычной модели и прогон на одних и тех же оценках.
- Эффект исправления пачки 64 → 256. Затронуты только ~5% длинных документов, и на 111 запросах такой эффект не различим.
- Разложение по классам запросов. По 22–23 запроса на класс дают интервалы шире самих эффектов.
Граф знаний поверх новых чанков
Кому особенно полезен раздел:АрхитекторыИнженеры
Смена чанкинга ломает граф знаний тихо: сущности привязаны к чанкам, и после перенарезки часть связей указывает на чанки, которых больше нет. У нас таких связей набралось 59 тысяч. Обычная чистка их не видела: она удаляла связи чужих версий документа, а перенарезка переписывала ту же версию.
Исправление — инвариант «проекция документа в графе = ровно то, что записал последний проход». Проекция сначала пишет новые связи, потом удаляет всё, что записано не ею, и документ ни на миг не исчезает из графа. Линкер сущностей теперь видит путь раздела и извлечённые термины чанка: сущность из заголовка раздела связывается со строками таблицы этого раздела, даже если в тексте строки её нет.
Итог перелинковки тех же ~30 тысяч документов: связей на удалённые чанки — 0, доля живых чанков, связанных хотя бы с одной сущностью, выросла с 70.8% до 73.3%. В графе нет узлов-чанков и текста: связь — это свойство упоминания сущности, а текст остаётся в реляционной БД. Почему так — в четвёртой статье цикла.
Архитектору
«Сначала записать новое, потом удалить всё чужое» — общий приём для любых производных проекций (граф, поисковый индекс, кэш). Читатель никогда не видит пустого состояния, а повторный прогон идемпотентен.
Потолок LLM-извлечения
Кому особенно полезен раздел:ML и исследователи
Почему не больше 73%? Сущности извлекает LLM из первых 16 тысяч символов документа, и чанки дальних разделов остаются без своих сущностей. Мы проверили очевидное решение — дать модели 49 тысяч символов — и оно не сработало. Модель возвращает 10–35 сущностей независимо от длины текста: втрое больше текста даёт те же сущности, размазанные тоньше. Покрытие на 10 пробных длинных документах выросло на 1 п.п., а у части документов сущностей стало меньше.
Правильный путь — извлечение окнами: несколько вызовов на длинный документ и слияние результатов через дедупликацию сущностей. Структурные чанки дают для этого естественные границы окон — разделы. Это наш следующий шаг.
Переиндексация корпуса на живом проде
Кому особенно полезен раздел:Инженеры
Перечанковка ~30 тысяч документов заняла около часа вместо ожидавшихся шести. Узкое место было не в нашем коде и не в базе, а в ожидании внешнего API эмбеддингов.
- Переиндексация идёт внутри текущей версии документа, одна транзакция на документ. Старый набор чанков выключается, новый включается атомарно. Инвариант «у версии ровно один живой набор» проверяется после каждого прогона и еженочным сторожевым заданием.
- Один документ за запрос — это задержка, а не лимит. При 8 одновременных запросах по ~4.5 с выходило около 36 чанков в секунду, а лимит аккаунта — от 415. Шесть параллельных процессов по непересекающимся спискам документов дали ~690 документов в минуту без единой ошибки лимита.
- Одна проблемная страница не должна ронять пачку. Страница с ~500 вложенными inline-тегами вызывала переполнение рекурсии в рендере и теряла все 100 документов пачки. Обход стал итеративным, а ошибка рендера изолируется до одной страницы.
- Ревизии источника меняют формат. Перечанкинг пропускает документ, если его ревизия в источнике изменилась, — пусть его заберёт обычная синхронизация. Одна из вики сменила формат ревизии с ISO-даты на epoch, и ~940 статей навсегда считались «изменившимися», а синхронизация их не трогала. Понадобился явный операторский флаг, при этом проверка ревизии на момент записи осталась.
- Старые чанки держатся до проверки качества и перелинковки графа, но не дольше. Пока они лежат в таблице, лексический скан медленнее: p50 вырос со 175 до 252 мс (+44%) (подробности — во второй статье цикла).
Известные недоработки чанкера
Жирный абзац вместо заголовка не всегда распознаётся как раздел, оглавление страницы иногда становится отдельным чанком, 0.16% чанков — одиночные строки таблиц длиннее лимита в 600 токенов. В худшем случае такой чанк ведёт себя как старый чанк по длине, только с заголовком.
Литература
Кому особенно полезен раздел:ML и исследователи
- Günther M. et al. Late Chunking: Contextual Chunk Embeddings Using Long-Context Embedding Models. arXiv:2409.04701, 2024.
- Morris J. X., Rush A. M. Contextual Document Embeddings. arXiv:2410.02525, 2024.
- Jimeno Yepes A. et al. Financial Report Chunking for Effective Retrieval Augmented Generation. arXiv:2402.05131, 2024 — чанкинг по элементам структуры документа вместо окон.
- Chen T. et al. Dense X Retrieval: What Retrieval Granularity Should We Use?. arXiv:2312.06648, 2023 — влияние гранулярности единицы поиска.
- Sarthi P. et al. RAPTOR: Recursive Abstractive Processing for Tree-Organized Retrieval. arXiv:2401.18059, 2024 — иерархия документа как источник контекста.
- Upadhyay S. et al. UMBRELA: UMbrela is the (Open-Source Reproduction of the) Bing RELevance Assessor. arXiv:2406.06519, 2024 — шкала 0–3 и промпт LLM-судьи.
- Thomas P. et al. Large language models can accurately predict searcher preferences. arXiv:2309.10621, 2023.
Идентификаторы arXiv сверены с API arXiv 25 октября 2026.