Сотрудник вводит в строку поиска: «срок уведомления о расторжении». Система отвечает мгновенно и уверенно: тридцать дней. Ответ красивый, цитата аккуратная, и только случайный взгляд на номер страницы показывает, что этот пункт стоит в приложении № 3 к совсем другому соглашению, заключённому с тем же контрагентом двумя годами раньше. Основной договор говорит про шестьдесят дней. Поиск не сломался: он нашёл похожий текст. Сломалось другое — связь между фрагментом и документом, которому этот фрагмент принадлежит.

Эта статья о том, как построить поиск по договорам на собственном сервере, чтобы любой ответ можно было сопоставить с оригиналом и чтобы систему мог принять и понять другой человек. Речь не о том, как «научить модель разбираться в праве». Речь об инженерной дисциплине: что хранить, как нарезать, что показывать и где честно сказать «здесь нужен человек».

Что на самом деле лежит в папке «Договоры»

Первая ошибка — считать, что в папке лежат документы. Там лежат файлы, а документ — понятие более тонкое. Один и тот же договор может состоять из основного текста, двух дополнительных соглашений, трёх приложений со спецификациями, акта разногласий и протокола. Часть этого сложена в один PDF, часть — в разные. Что-то сканировали, что-то выгружали из редактора. Для человека связь очевидна по номерам и датам, для поиска — нет.

Поэтому до всякого индексирования стоит развести четыре сущности:

  • Файл — то, что физически лежит на диске: имя, хеш содержимого, дата загрузки.
  • Документ — юридически самостоятельный текст: договор, дополнительное соглашение, приложение. У него свой номер, своя дата и своя роль.
  • Комплект — договор вместе с теми, что к нему относятся. Именно комплект отвечает на вопрос «каков сейчас срок уведомления».
  • Редакция — состояние условия на определённую дату. Пункт мог быть изменён дополнительным соглашением, и тогда в комплекте два текста про один и тот же срок.

Простая схема хранения выглядит так: у каждого файла есть запись, у записи — ссылка на документ, у документа — ссылка на комплект и поле «чем является» (основной текст, приложение, допсоглашение). Поле «заменяет пункт такой-то» заполняется вручную или по подсказке системы, но с подтверждением человека. Автоматически выводить цепочку изменений из текста можно, но ошибка тут дорогая, поэтому подсказка должна оставаться подсказкой.

Реквизиты сторон — это фильтр, а не украшение

Вторая половина проблемы из вступления — контрагент. В организации легко оказывается три договора с компаниями, названия которых различаются на одно слово: «Север-Сервис», «Север Сервис Плюс», «СеверСервис». Поиск по смыслу сочтёт их одним и тем же, и срок из одного договора окажется в ответе про другой.

Надёжнее опираться не на название, а на реквизиты: ИНН, КПП, ОГРН, номер и дату договора. Их извлекают из шапки и блока подписей и сохраняют как отдельные поля у документа. Тогда запрос «срок уведомления» превращается в двухшаговый: сначала выбираем сторону и комплект (или просим сотрудника выбрать), затем ищем внутри. Если сотрудник не указал контрагента, система не угадывает, а возвращает список найденных договоров с реквизитами и просит уточнить. Это чуть менее эффектно, чем мгновенный ответ, зато исключает подмену, с которой мы начали.

Название стороны в договоре бывает записано по-разному: полное, сокращённое, с формой собственности в начале или в конце. ИНН в этом смысле стабильнее. Но и он может быть распознан с ошибкой, особенно на скане, поэтому реквизиты, как и всё остальное, нужно проверять на соответствие: у ИНН есть контрольные цифры, и несовпадение — повод пометить поле как сомнительное, а не молча принять.

Где OCR портит смысл тише всего

Если договоры приходят сканами, между бумагой и поиском стоит распознавание текста. Оно ошибается не так, как человек: не пропускает буквы случайным образом, а подменяет похожие символы уверенно и гладко. Для обычной статьи это терпимо. Для договора есть три места, где подмена меняет смысл, и их стоит знать заранее.

Даты. Цифры 1 и 7, 0 и 8, 3 и 8 на плохом скане легко путаются. Срок «до 14.03.2025» превращается в «до 74.03.2025» — такую дату хотя бы можно отловить проверкой на допустимость. Хуже, когда получается правдоподобная дата, просто неверная: «14.08» вместо «14.03».

Суммы. Запятая и точка, пробел-разделитель тысяч, знак процента, который сливается с цифрой. Разница между «150 000» и «15 000» — это ноль, который теряется при сжатии скана, а разница между «0,5%» и «5%» — это потерянная запятая.

Отрицание. Самая коварная категория. «Не вправе», «вправе», «без согласия», «при наличии согласия». Частица «не» короткая, на скане может быть слабо пропечатана или попасть на сгиб листа. Если она исчезла, пункт говорит обратное тому, что подписано, и при этом остаётся грамматически безупречным.

Один вымышленный пункт и одна подменённая буква

Разберём короткий условный пример. Допустим, в договоре есть пункт:

7.3. Заказчик не вправе расторгнуть договор в одностороннем порядке без письменного уведомления Исполнителя за 30 календарных дней.

На скане с блёклой печатью OCR может прочитать «не» как «ни» или «ие» — тогда фраза становится бессмысленной, и поиск её, скорее всего, просто не найдёт. Это ещё благоприятный исход: пропажа заметна. Опаснее другая подмена. Допустим, в слове «вправе» буква «в» в начале слилась с пятном и распозналась как «б», а частица «не» потерялась в тени сгиба. Получится «бправе расторгнуть договор в одностороннем порядке без письменного уведомления». Слово «бправе» индекс не узнает, и ключевая часть пункта выпадет из поиска по смыслу, а оставшиеся слова «расторгнуть», «одностороннем», «уведомления» по-прежнему совпадут с запросом. Система найдёт пункт, но вернёт его в виде, где условие выглядит как свобода расторжения без предупреждения.

Вывод из примера не в том, что OCR плох. Вывод в том, что цитата сама по себе не заменяет чтения условия. Короткая выдержка «…расторгнуть договор в одностороннем порядке без письменного уведомления…» без начала и конца предложения утверждает почти противоположное тому, что написано в документе целиком. То же касается исключений: в пункте 7.4 может говорить «за исключением случаев, предусмотренных п. 7.3», и тогда обе нормы нужно читать вместе. Поэтому система обязана показывать не только найденное, но и то, что рядом.

Сохранить структуру, а не только текст

Чтобы показать соседний контекст и вернуться к скану, при разборе нужно сохранить больше, чем плоский текст. В документации Docling описана модель DoclingDocument: она хранит текст, таблицы, иерархию элементов и сведения о происхождении, а координаты элементов доступны, если они были получены при разборе (Docling: Document model). Для нашей задачи это важно по трём причинам.

  • Иерархия позволяет знать, что пункт 7.3 принадлежит разделу 7 и стоит перед пунктом 7.4. Без неё «соседний контекст» — это просто соседние строки, которые могут оказаться на другой странице или в другой колонке.
  • Таблицы остаются таблицами. Спецификация с ценами и сроками поставки, если превратить её в поток слов, теряет связь между столбцами; сумма оказывается рядом с чужим наименованием.
  • Происхождение и координаты дают возможность подсветить найденный фрагмент на странице скана. Оговорка здесь существенна: координаты есть, только если разбор их вернул, поэтому наличие подсветки нужно проверить на своих документах, а не предполагать.

Что касается самого распознавания, в примере Docling с полностраничным OCR показано включение распознавания всей страницы вместе со структурой таблиц; там же отмечено, что выбранному движку нужны собственные зависимости и что полный режим часто медленнее гибридного (Docling: Full-page OCR example). Практический смысл такой: полностраничный режим разумно держать для сканов, где текстового слоя нет или он плох, а для файлов с нормальным текстовым слоем скорость важнее, и гибридный путь может подойти лучше. Какой режим окажется лучше на вашем потоке, покажет только прогон на ваших же документах.

Что записать для каждого фрагмента

Нарезая документ на фрагменты для поиска, нельзя отбрасывать всё, что мы узнали при разборе. Минимальная запись фрагмента выглядит так:

  • идентификатор документа и комплекта;
  • номер страницы и координаты области, если они есть;
  • путь по иерархии: раздел, пункт, подпункт;
  • идентификаторы предыдущего и следующего фрагментов;
  • способ получения текста: текстовый слой или OCR;
  • версия разбора: какой конфигурацией и когда документ обрабатывался;
  • флаг сомнительного распознавания.

Версия разбора — скучное, но спасительное поле. Когда через полгода вы смените настройки OCR, часть фрагментов придётся переиндексировать, и без отметки невозможно понять, какие записи старые. А если сотрудник сошлётся на цитату, увиденную в прошлом месяце, вы сможете восстановить, как она тогда выглядела.

Размер фрагмента — отдельный выбор. Слишком мелкий (одно предложение) теряет условия и исключения; слишком крупный (целый раздел) размывает совпадение, и поиск возвращает «что-то про расторжение» на трёх страницах. Для договоров естественная единица — пункт с подпунктами, а границу задаёт нумерация. Если нумерация не распознана, остаётся резать по абзацам с перекрытием, но такие фрагменты стоит помечать как менее надёжно привязанные к структуре.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации Россия, США, Франция и Великобритания. Оплата картой РФ и по СБП.

АРЕНДОВАТЬ СЕРВЕР

Как выглядит правильная выдача

Теперь соберём то, что видит сотрудник. Хороший ответ на запрос о сроке уведомления — это не одно предложение, а небольшая карточка:

  1. Заголовок с опознанием: контрагент, ИНН, название и номер документа, дата, роль (основной договор или приложение).
  2. Найденный пункт целиком — с номером и названием раздела, без сокращений.
  3. Соседний контекст: предыдущий и следующий пункты, потому что исключения и оговорки живут рядом.
  4. Переход к скану: ссылка на страницу с подсвеченной областью, если координаты есть, а если их нет — просто на страницу.
  5. Пометки надёжности: распознано ли это место с оговорками, есть ли в комплекте другая редакция того же условия.

Эта карточка решает проблему из вступления. Если срок найден в приложении, заголовок прямо говорит «Приложение № 3 к соглашению от такой-то даты», и сотрудник не примет его за условие основного договора. Если у комплекта есть дополнительное соглашение, меняющее пункт, система показывает обе редакции рядом и не выбирает победителя за человека. Какая из них действует на конкретную дату, определяет юрист по документам, а не поисковый индекс.

Ещё одно правило, которое легко пропустить: выдача не должна пересказывать условие своими словами без ссылки на текст. Пересказ допустим как подсказка над цитатой, но цитата обязана быть видна, а подсказка — подписана как автоматическая. Иначе читатель начнёт доверять пересказу, а не документу, и вся работа по сохранению происхождения окажется напрасной.

Спорное распознавание: что делать с сомнительным местом

Вернёмся к примеру с «бправе». Как система должна поступить, когда видит, что слово не из словаря? Самый простой механизм — проверка на неизвестные слова и аномалии: слова, которых нет ни в общем словаре, ни в словаре договорной лексики, подозрительные сочетания цифр в датах, суммы, которые не сходятся с прописью. Каждое срабатывание не исправляет текст, а ставит флаг.

Автоматическое исправление опасно именно в таких местах. Если «бправе» молча превратить в «вправе», система может исправить и там, где в оригинале стояло что-то иное. Лучше хранить два слоя: текст как распознан и пометку «возможная замена», а при показе — отображать фрагмент как есть, рядом с изображением области. Сотрудник сразу видит: распознано одно, на скане, судя по картинке, другое.

Разумный порядок действий для спорных мест выглядит так:

  1. Флаг ставится автоматически по правилам: неизвестное слово, невозможная дата, расхождение суммы и прописи, отсутствие частицы там, где грамматика её ждёт.
  2. Фрагменты с флагом попадают в очередь на ручную сверку, но поиск по ним не отключается: лучше найти и предупредить, чем скрыть.
  3. Человек сверяет с изображением и либо подтверждает, либо правит. Правка сохраняется отдельным слоем с автором и датой, а исходный результат OCR остаётся.
  4. После правки фрагмент переиндексируется, а пометка надёжности меняется на «проверено вручную».

Для критичных мест — сроки, суммы, неустойки, условия расторжения — можно заранее решить, что такие пункты в выдаче всегда сопровождаются изображением области, независимо от флагов. Это дороже по вниманию, но экономит время там, где цена ошибки выше.

Несколько похожих договоров

Отдельная неприятность — типовые договоры. Организация подписывает с десятком поставщиков почти одинаковые тексты, различающиеся номерами, реквизитами и парой пунктов. Поиск по смыслу вернёт десять почти идентичных фрагментов, и найти среди них нужный по одному только тексту невозможно.

Здесь помогает порядок отбора. Сначала фильтр по комплекту, то есть по стороне и реквизитам. Если сотрудник не назвал контрагента, система показывает не десять одинаковых цитат, а свёртку: «срок уведомления — 30 дней в семи договорах, 60 дней в двух, 45 дней в одном», с раскрытием по каждому. Свёртка получается простым сравнением нормализованного текста пунктов, а различия подсвечиваются. Так сотрудник видит не только ответ, но и то, что ответ не единственный. Если расхождений нет, это само по себе полезное знание. Если есть — повод открыть конкретный договор.

Для сравнения редакций внутри одного комплекта работает тот же приём: показывать различия между пунктами основного договора и допсоглашения. Но выводов о том, какая редакция юридически действует, система не делает. Она показывает, что редакций две и откуда каждая взята.

Что для этого нужно от сервера

Теперь о железе, потому что от него зависит, какой режим распознавания вы сможете себе позволить. Нагрузка здесь двух видов. Разбор и OCR — тяжёлые, но разовые: документ обработали и забыли. Поиск — лёгкий, но постоянный: индекс должен быть под рукой и отвечать быстро. Лучше разделить эти режимы: обработку запускать очередью, не мешая поиску, а индекс держать на быстром диске.

На VPS MAATRIX доступны NVMe-диски, процессоры AMD EPYC и root-доступ, значит, можно поставить нужные зависимости OCR-движка и сервис разбора самостоятельно; список локаций в публичном предложении включает UK, US, RU и FR (VPS MAATRIX). Линейка тарифов начинается с Ba (1 vCPU, 2 ГБ RAM, 40 ГБ NVMe) и доходит до Djed (16 vCPU, 32 ГБ, 320 ГБ). Это ресурсы, а не обещание ёмкости: сколько страниц в час потянет конкретный тариф, зависит от выбранного движка, качества сканов и режима распознавания, и узнать это можно только замером на своих документах.

Ориентиры для рассуждения без цифр производительности такие. Небольшому архиву, в котором в основном файлы с текстовым слоем, хватит скромной конфигурации: нагрузка на разбор невелика. Если же приходят тысячи многостраничных сканов и нужен полностраничный режим, который, как отмечено в примере Docling, часто медленнее гибридного, имеет смысл брать больше ядер и памяти и ставить обработку в очередь, чтобы пиковая загрузка не отражалась на скорости ответов. Место на диске нужно считать с запасом: помимо оригиналов, хранятся изображения страниц для переходов, результаты разбора и индекс. Оригиналы при этом остаются источником истины и не должны перезаписываться.

Про сам процесс распознавания подробнее, включая выбор движков и подготовку сканов, есть отдельный материал: OCR на VPS: распознавание текста с изображений. Он полезен, если вы только прикидываете, что потянет ваш сервер.

Отдельно о размещении. Договоры содержат персональные и коммерческие данные, и выбор локации сервера — вопрос не только скорости, но и того, что допустимо для ваших документов. Из факта, что сервер находится в определённой стране, автоматически не следует ни соответствие требованиям закона, ни его нарушение: это вопрос для ответственного за данные в вашей организации. Нужные уточнения по наличию образов и операционных систем в конкретной локации проверяются в форме заказа, а не принимаются как данность.

Как передать систему коллеге

В заголовке статьи стоит условие: систему должен суметь принять другой человек. Здесь работает несколько простых договорённостей, и все они вытекают из того, о чём шла речь выше.

  • Видимое происхождение. Любой ответ содержит документ, страницу и ссылку на скан. Коллега, пришедший позже, не обязан верить системе на слово.
  • Явные пометки сомнений. Флаги распознавания не прячутся: их видно и в выдаче, и в очереди на сверку.
  • Журнал правок. Исправленный вручную текст хранится отдельно от результата OCR, с автором и датой. Исходное распознавание не стирается.
  • Версия разбора у каждого фрагмента. Понятно, чем и когда обработан документ, и что будет, если сменить настройки.
  • Описанные правила комплектов. Как документ попадает в комплект, кто подтверждает связь «заменяет пункт», как обрабатывается появление новой редакции.

Эти пункты нужно записать коротко, на одной странице, в том месте, куда заглянет следующий человек. Не как регламент для галочки, а как ответ на вопросы, которые он задаст в первую очередь: откуда взялся этот ответ, почему он выглядит так и что делать, если он кажется неверным.

Где поиск заканчивается

Вернёмся к сотруднику из начала. Теперь он вводит тот же запрос и получает иную картину. Сверху — контрагент с ИНН и перечень документов комплекта. Ниже — пункт о сроке уведомления из основного договора: шестьдесят дней, с номером пункта, соседними пунктами и ссылкой на страницу скана. Рядом пометка: в комплекте есть Приложение № 3, содержащее пункт с тем же названием и другим сроком, открыть для сравнения. Одно слово в пункте подсвечено как распознанное с сомнением, с миниатюрой области скана рядом.

Сотрудник нашёл нужное место за минуту и увидел, что вопрос не так прост, как казалось. Это и есть результат: не готовый вывод, а точный адрес, по которому нужно идти и читать. Какой срок применим в его ситуации, действует ли редакция из приложения, как совместить пункты — решает человек с правом это делать, и решает он по проверенному документу, а не по строчке в окне поиска. Система сделала то, что ей по силам: быстро довела до нужной страницы и не позволила ошибиться в том, чья это страница.