
Привет! Меня зовут Павел Яковлев, я инженер по разработке ПО искусственного интеллекта в YADRO. В команде GenAI мы занимаемся умными продуктами на основе корпоративных баз данных. В проектах мы часто используем современные генеративные модели и энкодеры. В статье расскажу, как мы в компании разрабатываем и оптимизируем семантический поиск по сложным документам: PDF, HTML и DOCX.
Проблема в том, что техническая документация представлена в разных, не самых удобных для программ форматах. Пользователь должен иметь возможность искать информацию во всех документах. Такая возможность уже реализована. Сейчас поиск работает по принципу полнотекстового поиска со стеммингом. Это значит, что при поиске, у слов не учитываются несколько последних символов по некоторому правилу, чтобы поиск не зависел форм слов.
Такой поиск хорошо работает, когда пользователи ищут серийные номера, кодовые названия изделий и другие редкие термины. Обычно запросы с терминами копируют со сторонних ресурсов. Однако при поиске информации по длинным фразам на естественном языке система работает хуже. Она ищет совпадения слов, а не совпадение смысла запроса со смыслом текста.
Например, в документе есть фраза: «Инструкция по установке жесткого диска “ЖД001”». Какой запрос может ввести пользователь, чтобы найти это предложение?
Возможный запрос |
Лексический (полнотекстовый) поиск |
Семантический поиск |
ЖД001 |
Мы найдем нужное предложение, так как запрос в точности совпадает с термом из нашего текста. |
Вероятность нахождения мала, так как запрос содержит мало семантики. Для обычного человека, не погруженного в тему, эта конструкция тоже мало чем может помочь. |
мануал по монтированию HDD |
Мы не найдем нужный текст, так как ни одно слово из запроса не совпадает с документом (с точностью до предлогов, союзов, частиц, которые, как правило, опускаются при поиске). |
Нужный текст найдется, потому что запрос представляет собой перефразированный искомый текст, а значит семантика у них очень близка. |
инструкция по установке HDD |
Слова запроса частично совпадают с текстом, поэтому нужный документ попадет в поисковую выдачу, но, возможно, будет не в ее начале. |
Нужный текст найдется, так как близок по смыслу. |
Мы видим, что каждый тип поиска полезен в разных ситуациях. Поэтому лучше иметь оба варианта или их сочетание. Однако сейчас не будем рассматривать гибридный поиск. Наша задача — добавить и оптимизировать новый режим поиска. Пользователь должен иметь возможность переключаться между «умным» поиском и стандартным полнотекстовым.
Чтобы пользователь мог формулировать запрос своими словами, а система его понимала, она должна извлекать смысл (семантику) написанного. Затем этот смысл сравнивается с содержимым документов, и пользователю показываются наиболее подходящие фрагменты.
Для этого используются языковые модели, такие как BERT. Они преобразуют естественный язык в плотный числовой вектор фиксированной длины — эмбеддинг (векторное вложение). Эмбеддинги отражают смысл текста, поэтому сравнивать значения становится проще. Достаточно вычислить расстояние между векторами, чтобы определить степень их схожести.
Для создания семантической поисковой системы нужны несколько вспомогательных компонентов:
-
DataReader — читает документы. Нам нужно искать в HTML, PDF и DOCX, а их обработка может быть сложной.
-
DataCleaner — очищает текст от ненужных символов (мусора), например, удаляет лишние пробелы.
-
DataSplitter — разбивает документ на части (чанки). Это важно, потому что языковые модели работают с ограниченным контекстом. Длинные тексты могут содержать разную по смыслу информацию, поэтому их делят на части, чтобы сохранить точность поиска.
-
Embedder (или encoder) — языковая модель, которая преобразует текст в числовые векторы.
-
Reranker — упорядочивает найденные результаты, делая их более релевантными.
-
ChunkMerger — объединяет соседние чанки после поиска, чтобы расширить найденную информацию. Обработанные и объединенные чанки называют пассажами (passage).
Компонент довольно много, и каждую из них нужно настроить. Чтобы не выбирать их реализацию наугад, важно понять, что мы ожидаем от семантического поиска. Нужно четко сформулировать задачу и выстроить процесс оптимизации поиска документов. Разберемся с формализацией задачи.
Пусть (D,Q) — это датасет, где D — множество документов, а Q — множество вопросов, а именно — пар (𝑞, pg). В каждой паре 𝑞 — вопрос, pg — априори релевантный документ из базы знаний (референсный документ, «золотой пассаж»). По запросу 𝑞 нам необходимо найти в базе знаний релевантный документ 𝑝. Для проверки правильности решения будем использовать референсный документ — pg.
Создание датасета — сложная тема, в которую мы углубляться не будем. Важно, что релевантность задается множеством данных, которые мы используем для тестирования. Это понятие не строгое: примеры могут противоречить друг другу, а оценка релевантности носит вероятностный характер.
Мы выделили три принципа оптимизации.
Используем публичные датасеты и стандартные метрики информационного поиска.
Датасеты семантического поиска разнообразны. Некоторые остаются полезными, а другие теряют актуальность. Например, датасет SQuAD (Stanford Question Answering Dataset) раньше был ключевым для вопросно-ответных систем, но сейчас почти не используется в задачах поиска. Это связано с его ограничениями, которые выявились после глубокого анализа: при создании вопросов аннотаторы видели текст, что упростило поиск. В результате совпадения токенов вопроса и ответа стали слишком очевидными.
Если у нас нет подходящего русскоязычного датасета, то переводим качественный англоязычный.
Русскоязычных датасетов для семантического поиска практически нет. Единственный, который можно упомянуть, — SberQuAD, но он унаследовал проблемы SQuAD. Чтобы восполнить нехватку, мы переводим англоязычные датасеты на русский с помощью высокоточных переводчиков. При этом часть семантической близости между вопросами и ответами теряется, но эксперименты показывают, что общая смысловая связь сохраняется. Такие датасеты полезны для оптимизации.
Создаем производные датасеты для сложных условий.
Обычные датасеты содержат тексты в простом формате (plain text), что упрощает их обработку. Однако в реальных условиях документы сложнее: они имеют структуру, таблицы и дополнительные элементы. Чтобы учесть это, мы создаем производные датасеты с более сложной структурой. Они помогают отладить поиск в условиях, приближенных к реальным.
Первый этап оптимизации
Рассмотрим метрики, которые широко используются в информационном поиске. Подробнее про них я предлагаю почитать в других статьях, а здесь приведу лишь формулы, по которым они считаются.
Top-k accuracy (Top-k hard accuracy) — подсчитывает процент случаев, когда правильный пассаж попал в первые k пассажей поисковой выдачи.
qi — i-й запрос
pig — идеальный пассаж для i-го запроса qi
Pi = [pi1,pi2, …, pik] — найденные пассажи для i-го запроса qi
N — общее количество запросов qi
Mean Reciprocal Rank (MRR) — учитывает не только наличие правильного пассажа в первых k пассажах поисковой выдачи, но еще и позицию, на которой стоит нужный пассаж.
qi — i-й запрос
pgi — идеальный пассаж для i-го запроса qi
Pi = [pi1,pi2, …,pik] — найденные пассажи для i-го запроса qi
N — общее количество запросов qi
ti — индекс в Pi первого правильного найденного пассажа для запроса qi
K — количество искомых пассажей
Normalized Discounted Cumulative Gain (nDCG) — более продвинутая метрика, которая позволяет учитывать не только наличие правильного пассажа в топ k, но и градацию правильности пассажа. То есть пассажи в выдаче могут быть оценены по некоторой шкале, а не бинарно: «верно» или «неверно».
qi — i-й запрос
Оценки найденных пассажей для qi:
Отсортированные оценки для qi:
K — количество искомых пассажей
N — общее количество запросов qi
Top-k partial accuracy — эта метрика не относится к числу общепринятых и распространенных. Однако она полезна, если мы хотим узнать про случаи, когда поиск «зацепил» идеальный результат.
qi — i-й запрос
pgi — идеальный пассаж для i-го запроса qi
Pi = [pi1, pi2, …, pik] — найденные пассажи для i-го запроса qi
N — общее количество запросов qi
di — максимальная доля pig в Pi для запроса qi
Чаще всего мы используем метрики top-k_hard_accuracy и top-k_partial_accuracy, так как их легче интерпретировать. Если для одного вопроса есть только один релевантный пассаж, то метрики MRR и nDCG дают похожие результаты.
Рассмотрим датасеты, на основе которых мы проверяли гипотезы по улучшению семантического поиска:
-
SOQA — вопросно-ответный датасет, основанный на переработке SberQuAD. Несмотря на недостатки оригинального датасета, SOQA до определенной степени остается рабочим инструментом.
-
ruNQOA — перевод датасета Natural Questions на русский язык с использованием одного из популярных переводчиков.
Чтобы усложнить задачу, в базу знаний этих датасетов мы добавили нерелевантные художественные пассажи из произведений Льва Толстого.
На первом этапе оптимизации мы использовали датасеты, в которых база знаний представлена в простом текстовом формате (plain text). Это позволило временно не учитывать задачу чтения сложных документов и сосредоточиться на качестве поиска.
Мы создали два сложных датасета, чтобы усложнить задачу разделения текста на чанки: SOQA_Complex и ruNQOA_Complex. Они содержат те же вопросы и релевантные пассажи, но в каждом документе базы знаний собрано 10–20 пассажей без повторений. Эти пассажи случайным образом выбраны из SOQA или ruNQOA и разделены заголовками вида «раздел №…». Таким образом у нас уменьшается количество файлов в базе знаний, однако увеличивается их размер.
В регулярных экспериментах мы использовали small-версии датасетов, которые содержат примерно 4 000 троек.
Выбор языковой модели
Как мы упоминали выше, для сопоставления смысла запроса пользователя и смысла чанка в базе знаний используется языковая модель, которая понимает семантику.
Для сопоставления смыслов есть две архитектуры моделей:

Bi-Encoder — состоит из двух трансформеров encoder-only. С помощью passage-encoder получаются эмбеддинги для всех чанков в базе знаний. Запрос от пользователя кодируется с помощью query-encoder. На этапе поиска высчитывается косинусное расстояние между query-embedding и passage-embedding. Мы получаем поисковую выдачу после ранжирования всех пассажей по убыванию косинусного расстояния. В отличие от следующей архитектуры Cross-Encoder, можно заранее сохранить эмбеддинги для пассажей и использовать их для подсчета расстояния.
Cross-Encoder — трансформер с архитектурой encoder-only и ранжирующим слоем. Этот слой выдает оценку релевантности запроса к пассажу. На вход подается двойка: запрос и пассаж. Cross-Encoder лучше понимает семантическую связь между пассажем и запросом, но для каждого пользовательского запроса он работает медленнее, так как для оценки релевантности запроса и пассажей, cross-encoder нужно запустить N раз, где N — количество пассажей.
Мы будем использовать Bi-Encoder, так как у нас много пассажей в базе знаний.
Для выбора модели удобно использовать открытый бенчмарк MTEB с рейтингом по различным моделям в зависимости от задачи. Нам нужны русский язык и задача Retrieval.
На момент проведения экспериментов лучшая модель — multilingual-e5-large, Bi-Encoder c 560M параметров и размером эмбеддингов в 1024 элемента. Чтобы получить эмбеддинги для запроса, нужно на вход в модель передать текст с префиксом «query:», а для пассажа — «passage:». По MTEB модель multilingual-e5-large достигает среднего результата в 74,04% по метрике nDCG@10, а его «младший собрат» multilingual-e5-small — 65,85%.
Так как размер модели small в пять раз меньше, мы решили проверить, как две модели соотносятся по скорости работы и качеству.

По качеству ответов на вопросы результаты получились следующие:
|
метрика |
me5-small |
me5-large |
прирост метрики |
SOQA_small |
Top-1 hard accuracy, % |
60 |
64 |
4 |
Top-1 partial accuracy, % |
78 |
83 |
5 |
|
SOQA_Complex_small |
Top-1 hard accuracy, % |
45 |
48 |
3 |
Top-1 partial accuracy, % |
74 |
77 |
3 |
|
ruNQOA_small |
Top-1 hard accuracy, % |
58 |
65 |
7 |
Top-1 partial accuracy, % |
66 |
75 |
9 |
|
ruNQOA_Complex_small |
Top-1 hard accuracy, % |
42 |
47 |
5 |
Top-1 partial accuracy, % |
54 |
62 |
8 |
В таблице — сравнение качества поиска для моделей multilingual-e5-small и multilingual-e5-large
Из схемы выше и таблицы мы видим, что использование большой модели замедляет индексацию базы знаний примерно в три раза. Однако модель повышает качество поиска на 3–7% по метрике top-1_hard_accuracy. Индексация производится один раз, поэтому использование большой модели оправдано.
Как параметры компонент семантического поиска влияют на метрики
Пройдемся по компонентам семантического поиска, которые я описывал выше, и посмотрим, какие параметры мы меняли и к чему это привело.
DataReader
В этом эксперименте он зафиксирован, так как читать простые текстовые документы довольно просто. Мы использовали TextLoader из фреймворка LangChain.
DataCleaner
Основная задача DataCleaner — очистить прочитанный текст от ненужных символов (мусора). Нужно действовать аккуратно, чтобы не потерять структуру текста. Для простейших текстовых документов мы удаляем специфические и повторяющиеся знаки препинания, лишние пробелы и приводим текст к нижнему регистру.
Однако есть прочие символы, которые кто-то мог специально или случайно добавить в файл, а также артефакты чтения более сложных типов данных. Например, при чтении XLSX-файла может возникнуть много NaT-символов, а при чтении DOCX могут возникнуть неразрывные пробелы. Такие символы не несут смысла, но некоторые фреймворки могут выдавать на них ошибку, поэтому мы их удаляем.
Reranker
Задача Reranker (ранжировщика) — расположить документы в наиболее релевантном для пользователя порядке. Понятно, что чем выше нужный пассаж в поисковой выдаче, тем лучше для пользователя, так как ему не нужно тратить время на анализ ненужных пассажей и документов. Отмечу, что этот этап нужен не для перестановки всех пассажей из базы знаний для конкретного запроса, а чтобы «уточнить» сортировку первых элементов из выдачи. Получается двустадийная схема:
-
Берем первые M пассажей, используя Bi-Encoder.
-
Более ресурсоемким алгоритмом уточняем поисковую выдачу, чтобы оставить k наиболее подходящих.
В наших экспериментах мы использовали следующие ранжировщики:
-
MMR — штрафует пассажи за близость к уже выбранным пассажам. Это полезно, если в выборке есть одинаковые документы разных версий, например исправления в технической документации.
-
SVM, Logistic Regression — классификационные алгоритмы, которые обучаются в runtime. Суть в том, что мы составляем датасет размера (nsamples+1)×sizeembedding, где nsamples+1 — кол-во пассажей в выборке, плюс запрос, sizeembedding — размер эмбеддингов языковой модели. Для всех строк метка равна 0, кроме строки, которая соответствует эмбеддингу запроса, где метка равна 1. После обучения, мы берем значения решающей функции для каждого из эмбеддингов пассажа — это и есть схожесть пассажа и запроса.
-
Cross-Encoder — про него уже упоминалось выше. Так как использовать его для всей базы знаний ресурсоемко — будем запускать на ограниченном количестве предварительно отсеянных наиболее релевантных пассажей.
Алгоритмы мы сравнивали с одностадийной схемой, без участия ранжировщика. Результаты получились следующими:
На датасетах SOQA_Complex_small и ruNQOA_Complex_small хуже всего себя показали ранжировщики SVM и Logistic Regression. Отставание от версии без ранжировщика по метрике top-1_partial_accuracy:
-
на ruNQOA_Complex_small: 61% против 39%,
-
на SOQA_Complex_small: 77% против 53%.
На более простых SOQA_small и ruNQOA_small, SVM и Logistic Regression показывают результаты чуть хуже версии без ранжировщика.
Результаты MMR с точностью до погрешности повторили результаты без ранжировщика на всех датасетах. Это объясняется тем, что в датасете нет очень похожих пассажей: таких, которые могли бы возникнуть, если в базе знаний есть дубликаты документов.
Результаты Cross-Encoder с точностью до погрешности совпали с вариантом без ранжировщика на датасетах семейства ruNQOA, однако на SOQA Сross-Encoder незначительно вырывался вперед. Мы полагаем, что такое поведение связано с особенностями построения этого датасета.
Ранжирование с помощью модели multilingual-e5-large показало достойные результаты. Чтобы показать результаты лучше с двустадийным ранжировщиком, нужен очень хороший Cross-Encoder.
DataSplitter и ChunkMerger
Мы разделяем большие документы по двум причинам:
-
Языковые модели не могут работать с бесконечными текстами. У модели multilingual-e5-large входная последовательность должна быть не больше 512 токенов, остальное просто обрезается.
-
В большом тексте содержится больше разрозненных тезисов — он не может четко выразить одну мысль. Это будет мешать при поиске по эмбеддингам.
С другой стороны, мы отлаживаем поиск по датасетам. Поэтому в найденном пассаже должен содержаться «золотой пассаж». Так мы понимаем, что поиск отработал правильно. Для расширения контекста после поиска нам нужен ChunkMerger, который умеет «склеивать» несколько соседних чанков после поиска.
Как разделять прочитанный файл на чанки — отдельная задача, которой можно посвятить много времени. Мы считаем, что самые эффективные методы должны учитывать внутреннюю специфику документов, если она есть.
В общем случае вполне неплохо работают простые DataSplitters, такие как CharacterSplitter из LangChain, который разбивает текст на чанки длины chunk_size. Или RecursiveCharacterSplitter, который разделяет текст сначала по «nn». Если получившиеся фрагменты превышают chunk_size, то разделение идет по «n», затем — по пробелам и, наконец, по пустой строке. Отмечу, что у этих DataSplitters есть параметр chunk_overlap, который указывает размер перекрытия чанков. В наших экспериментах этот параметр равен 50% от chunk_size.
Так как эффективность мы проверяем на датасетах, то нет смысла брать слишком короткий пассаж — в него просто не поместится референсный пассаж. Поэтому если размер чанка маленький, длину итогового пассажа мы можем увеличить с помощью ChunkMerger.
Мы исследовали следующие конфигурации для CharacterSplitter и RecursiveCharacterSplitter:
Chunk_size, chars |
Chunk_merger, количество чанков слева и справа |
Passage_len, chars |
200 |
10 |
2200 |
200 |
5 |
1200 |
400 |
2 |
1200 |
400 |
5 |
2400 |
800 |
2 |
2400 |
В таблице — параметры DataSplitter и ChunkMerger.

По результатам экспериментов СharacterSplitter и RecursiveCharacterSplitter показывают практически одинаковые результаты, а вот лидера в конфигурациях выше выявить сложнее. На разных датасетах выигрывают сплиттеры с разными параметрами.
Комбинации с длиной пассажа 1200 проигрывают остальным. На датасетах семейства SOQA лучшей оказалась конфигурация с (chunk_size, chunk_merger, passage_len) = (200, 10, 2200), тогда как на датасетах ruNQOA лучшей была (chunk_size, chunk_merger, passage_len) = (800, 2, 2400). В итоге мы оставили вторую комбинацию, так как результаты на датасетах ruNQOA более приоритетные.
Промежуточный итог
В результате первого этапа оптимизации семантического поиска на датасетах в формате TXT метрики распределились следующим образом:

Второй этап оптимизации
Мы научились искать пассажи по семантике и улучшили нахождение по имеющимся датасетам в формате TXT. Вспомним первоначальную задачу — семантический поиск по документам компании. Поэтому нужно понять, что делать с более сложными форматами данных. Корпоративные документы чаще всего представлены в форматах PDF, HTML и DOCX. Чтение таких файлов — нетривиальная задача, так как аккуратно вытащить нужный текст мешает разметка. Давайте научим наш семантический поиск работать со сложными документами.
Чтобы эффективно находить пассажи в сложных документах, нам нужно собрать датасет, на котором мы будем оценивать качество поиска. Подходящих готовых датасетов для работы со сложноструктурированными документами в открытом доступе практически нет. Мы нашли только один, однако он англоязычный и очень маленький: всего пять PDF-файлов. Поэтому будем моделировать сложноструктурированные документы из тех датасетов, которые у нас есть, — а именно из ruNQOA_small.

Выше я уже кратко описал, как мы получили и обработали датасеты NQ, ruNQOA и ruNQOA_small.
Чтобы создать датасет в формате TeX, мы взяли множество троек (вопрос, ответ и пассаж) без изменений из ruNQOA_small. Корпус TeX-документов получили с помощью нашего алгоритма генерации, который генерирует случайную структуру разделов и заполняет пассажи разделами из ruNQOA_small.
Разделы могут быть следующих видов:
-
только текст (содержит один пассаж),
-
только таблица (несколько пассажей),
-
таблица (несколько пассажей) и текст (один пассаж),
-
текст (один пассаж) и изображение с фиксированной подписью,
-
ненумерованный список (несколько пассажей),
-
нумерованный список (несколько пассажей).
Также документы могут содержать дополнительные элементы:
-
номера страниц и сноски с фиксированным текстом,
-
заголовок, авторы и дата создания документа на первой странице.
Как работает TEX-алгоритм:
-
чтобы заполнить раздел, алгоритм выбирает случайным образом один пассаж из их исходного множества ruNQOA_small,
-
каждый пассаж содержится только в одном TeX-документе,
-
множество пассажей в точности совпадает со множеством пассажей ruNQOA_small,
-
процедура генерации TeX-документов завершается, когда исчерпано множество пассажей.
Отмечу, что наш TEX-алгоритм написан на Python и использует пакет pylatex.
Из сгенерированного датасета удобно сформировать нужные форматы документов:
-
TeX -> преобразование в PDF -> ruNQOA_Complex_PDF
-
TeX -> преобразование в HTML -> ruNQOA_Complex_HTML
-
TeX -> преобразование в DOCX -> ruNQOA_Complex_DOCX



Чем читать сложноструктурированные документы
Мы рассмотрели три наиболее подходящих инструмента. Разберемся, какие у них есть недостатки и преимущества.
PyMuPDF
В этом инструменте отсутствует классификация на заголовки, параграфы и элементы списка. Элементы документа делятся на блоки. Каждый блок характеризуется координатами на странице и типом: текст или изображение.
Для любой страницы в документе можно вызвать метод find_tables. Он вернет список таблиц, которые находятся на этой странице. Однако в PyMuPDF нет привязки таблицы к структуре документа.
С помощью флага TEXT_DEHYPHENATE есть возможность автоматически склеивать слова, которые разделяются дефисом при переносе на следующую строку.
PyMuPDF распространяется по лицензии AGPL 3.0.

Посмотрите на скриншот: PyMuPDF читает файлы постранично, а таблицу разбивает на отдельные блоки.
Unstructured
В нем есть функции partition_{pdf, docx, html, …}, которые разбивают документ на элементы:
-
Title,
-
NarrativeText,
-
ListItem,
-
Footer,
-
UncategorizedText.
Для таблиц отдельного элемента нет: они читаются как комбинации нескольких Title и UncategorizedText. Unstructured с помощью OCR (Optical Character Recognition) выделяет таблицу в отдельный элемент Table. Но ее структура не сохраняется, а трансформируется в простой текст (plain text). При этом снижается качество определения других элементов документа.
Также в Unstructured нет механизма для автоматического склеивания слов, которые разделяются дефисом при переносе на следующую строку.
Unstructured распространяется по лицензии Apache 2.0.

Из скриншота понятно, что Unstructured не смог распознать заголовок «Pushing the Chatbot State-of-the-art with QLoRA». Он также прочитал таблицу как смесь заголовков и UncategorizedText.
nlm-ingestor
Документ читается в древовидную структуру, которая записывается в виде блоков в JSON-формат. В каждом блоке определены теги header, paragraph, list_item и table, а также уровень этого блока в общей структуре документа. Такая структура позволяет реализовать разные алгоритмы создания чанков.
nlm-ingestor интегрирует таблицу в общую древовидную структуру. Внутри блока table определен список table_rows с отдельными ячейками внутри строки.
Инструмент автоматически склеивает слова, которые разделяются дефисом при переносе на следующую строку. Но этот механизм не работает в ячейках таблиц.
Абзац, который разделен таблицей или разрывом страницы, nlm-ingestor распознает как единый блок.
nlm-ingestor распространяется по лицензии Apache 2.0.

На скриншоте видно, что nlm-ingestor распознал перенесенный абзац через таблицу и разрыв страницы. Также он выделил все элементы таблицы в JSON-структуру и правильно распознал заголовки.
Какой инструмент выбрать
Начнем с того, что рассмотренные инструменты поддерживают чтение форматов PDF, DOCX и HTML. Все три варианта имеют свои недостатки — идеального решения мы не нашли. Минус nlm-ingestor — ложные склейки блоков, которые между собой не связаны или наоборот разбиты слишком гранулярно. Поэтому для всех документов он не подходит. Однако его можно настроить с помощью параметров в коде, но для этого придется собирать docker-образ из исходников.
Мы выбрали nlm-ingestor, так как чтение документов в древовидную JSON-структуру обеспечивает большие возможности для реализации различных алгоритмов по восстановлению пассажей, которые повреждены разметкой. Пока мы реализовали простой алгоритм разбиения на чанки: пробегаем по всем блокам прочитанного документа и сохраняем каждый блок как отдельный чанк. Также мы скорректировали параметры nlm-ingestor, чтобы адаптировать его под наши документы.
Эксперименты с чтением сложных документов
Мы использовали в экспериментах три инструмента для чтения сложноструктурированных документов. PyMuPDF и Unstructured показали почти одинаковые результаты на наших датасетах, однако nlm-ingestor с модификацией параметров справился с задачей лучше.

Графики выше показывают, что модифицированный nlm-ingestor обогнал PyMuPDF и Unstructured по метрике top-1_hard_accuracy примерно на:
-
4% для PDF,
-
6% для DOCX,
-
5% для HTML.
На документах DOCX и HTML модифицированный нами nlm-ingestor показывает те же значения метрики top-1_hard_accuracy, что и на ruNQOA_small. Лучших результатов можно достичь, если модифицировать алгоритм поиска, так как на ruNQOA_small потери качества на этапе чтения минимальны.
Модифицированный nlm-ingestor на PDF-датасете показывает top-1_hard_accuracy на 5% ниже, чем на ruNQOA_small. Выходит, что задача правильного чтения PDF-документов еще не решена нами полностью.
Выводы
Мы разобрались, как работает семантический поиск по сложным документам. Резюмируем, что мы сделали:
-
рассмотрели основные компоненты семантического поиска,
-
составили русскоязычные датасеты на основе общепринятых, которые можно использовать для:
-
отладки семантического поиска,
-
проверки RAG-систем,
-
-
улучшили нахождение нужной информации,
-
научились составлять синтетические датасеты, в которых база знаний представлена сложноструктурированными документами в форматах PDF, HTML и DOCX,
-
исследовали инструменты для чтения сложных документов и выбрали наиболее подходящий.
Оптимизация на общедоступных данных не дает полной уверенности, что у пользователя будет все работать так же замечательно, но дает очень хороший старт для дальнейшего улучшения качества поиска.
Составление датасетов по документам компании — это наиболее полезная, но ресурсоемкая работа для улучшения качества семантического поиска, так как такие датасеты должны быть репрезентативны. Необходимо несколько тысяч качественных и желательно реальных примеров использования поисковика.
Мы продолжим улучшать семантический поиск по документам компании. Наш следующий шаг — составление датасета на реальных документах компании, а не на синтетических данных. Это позволит более точно оценивать самые новые техники семантического поиска, в том числе с использованием генеративных моделей.
Автор: Pahandrovich
- Запись добавлена: 31.03.2025 в 11:02
- Оставлено в
Советуем прочесть:
- Мнемоника
- В Adobe Premiere Pro появился визуальный поиск видеофайлов на базе нейросетей
- Разработчики анонсировали SynCity — нейросеть для генерации 3D-миров в стиле градостроительных симуляторов
- Reddit работает над нейросетевым поиском, который поможет пользователям находить ответы на сложные вопросы
- Учёные с помощью ИИ прочли зачеркнутые слова в рукописях Пушкина
- Нейросеть Llama3 получила улучшенное понимание речи
- AI vs. рекрутер: сможет ли нейросеть закрыть вашу вакансию?
- «Галлюцинации» ИИ в судебных документах создают проблемы для юристов
- Барьеры памяти
- Нейросеть «Сбера» GigaChat сдала экзамен в Волгоградском медуниверситете по специальности «Кардиология»