5 мин чтения
Гибридный поиск по инженерной документации: почему ключевые слова + FAISS лучше чисто векторного поиска
Заметки о поиске по PDF, Word, Excel и сканам: OCR до индексации, чанкинг с метаданными, лексический и векторный поиск, слияние рангов и ответы с опорой на найденные фрагменты.
С января 2026 года я работаю в «Электросервисе» (Москва) над поиском и RAG по инженерной документации: PDF, Word, Excel и отсканированные документы. Стек — OCR для сканов, гибридный поиск по ключевым словам и по смыслу (эмбеддинги sentence-transformers в FAISS) и конвейеры загрузки и обработки запросов на FastAPI. В этой статье разберу, почему для такого корпуса гибридный поиск подходит лучше чисто векторного. Система и данные под NDA, поэтому всё описание остаётся на уровне общих техник, а примеры кода упрощённые и условные.
Контекст
Инженерные документы не похожи на статьи в блоге. В одном файле могут соседствовать текст, таблицы, чертежи с подписями и длинные списки обозначений. Запросы к такому корпусу обычно бывают двух очень разных типов:
- Описательные вопросы — например, как защищён от перегрузки определённый тип оборудования. Ответ в документе сформулирован иначе, чем вопрос.
- Точный поиск — конкретный артикул, код изделия или пункт стандарта. Полезна только точная строка, а «похожая» строка — ошибка.
Поиску, который справляется только с одним из этих типов, быстро перестают доверять.
Варианты
Только ключевые слова (например, BM25)
Лексическое ранжирование быстрое, объяснимое и отлично работает с точными токенами. Но на перефразировании оно проваливается: если в документе написано «тепловая защита», а пользователь спрашивает про «перегрев», общих слов может не хватить.
Только векторный поиск
Модели эмбеддингов из sentence-transformers хорошо справляются с перефразированием, а FAISS делает поиск ближайших соседей по большому числу чанков дешёвым. Но плотные векторы кодируют смысл, а не точную последовательность символов. Два артикула, отличающиеся одной цифрой, могут оказаться очень близко в векторном пространстве, и у модели нет причин предпочесть точный. Для инженера это худший вид ошибки: уверенный, правдоподобный и неверный результат.
Гибридный поиск
Запускать оба поиска по одним и тем же чанкам и объединять результаты. Лексический поиск отвечает за точные обозначения, векторный — за описательные вопросы. Цена — больше компонентов и отдельный шаг слияния, который нужно продумать.
Подход
Гибридный поиск, а загрузка документов и обработка запросов — отдельные конвейеры на FastAPI. Ниже — этапы, из которых состоит такой конвейер, и решения, которые я считаю на каждом из них важными.
OCR до индексации
Отсканированная страница — это картинка. Без OCR она ничего не даёт ни одному индексу, и поиск молча ведёт себя так, будто этих документов нет. Поэтому извлечение текста — начало загрузки, а не доработка на потом:
- из PDF, Word и Excel текст извлекается напрямую;
- страницы без текстового слоя проходят через OCR;
- результат до разбиения на чанки нормализуется: пробелы, переносы на концах строк, типичные ошибки распознавания.
Нормализация важнее всего для обозначений. Если OCR прочитал ноль как букву O, точный код превратился в другой, и никакой трюк на этапе ранжирования это уже не исправит.
Чанкинг с метаданными
Документы делятся на чанки — достаточно маленькие, чтобы эмбеддинг был качественным, и достаточно большие, чтобы мысль не рвалась. Строки таблиц по возможности не разрезаются: иначе артикул отрывается от своего описания. У каждого чанка должны быть метаданные:
# Упрощённая условная структура проиндексированного чанка
chunk = {
"id": "doc-123#p4#c2",
"text": "...",
"source": "doc-123",
"file_type": "pdf",
"page": 4,
"section": "3.2 Уставки защиты",
"ocr": True,
}
Метаданные решают две задачи: позволяют фильтровать (по документу, по типу) и делают каждый ответ прослеживаемым до файла и страницы.
Лексический и векторный поиск рядом
Одни и те же чанки попадают в два индекса: лексический и FAISS с эмбеддингами sentence-transformers. При запросе работают оба, и каждый возвращает свой топ.
Токенизатору лексической части нужно уделить внимание. Стандартный токенизатор может разбить код вроде «AB-1200/3» на фрагменты, которые совпадут с половиной корпуса. Надёжным точный поиск делает именно то, что токены, похожие на обозначения, сохраняются целиком — в дополнение к обычным.
Слияние двух списков
Лексические оценки и косинусная близость живут в разных шкалах, поэтому складывать их напрямую без аккуратной калибровки бессмысленно. Распространённый способ обойти проблему — reciprocal rank fusion (RRF), который использует только ранги:
# Упрощённая условная реализация RRF по ранжированным спискам id чанков
def rrf(result_lists, k=60):
scores = {}
for results in result_lists:
for rank, chunk_id in enumerate(results, start=1):
scores[chunk_id] = scores.get(chunk_id, 0.0) + 1.0 / (k + rank)
return sorted(scores, key=scores.get, reverse=True)
merged = rrf([keyword_ids, dense_ids])
Чанк, который высоко в обоих списках, поднимается наверх; чанк, найденный только одним из поисков, всё равно получает шанс. Метод простой, не требует обучающих данных, и с ним легко разобраться, почему результат выглядит странно.
Ответы с опорой на найденные фрагменты
Когда система формирует ответ, а не список, ответ должен строиться только из найденных фрагментов, а каждое утверждение — ссылаться на источник: файл, страницу, раздел. Если во фрагментах ответа нет, правильный вывод — «в документах не найдено», а не догадка. Для инженерных данных «не знаю» со ссылками полезнее гладкого абзаца без источников.
Компромиссы
- Два индекса нужно синхронизировать. Каждое обновление документа должно дойти и до лексического индекса, и до FAISS, а частичное обновление даёт запутанные результаты. Индексация документа при загрузке должна работать по принципу «всё или ничего».
- Больше работы на запрос. Два поиска и слияние дороже одного. Впрочем, поиск обычно дёшев по сравнению с OCR и построением эмбеддингов, которые выполняются один раз — при загрузке.
- OCR задаёт потолок. Поиск не может быть лучше извлечённого текста. Плохие сканы ограничивают все последующие этапы, поэтому нормализация заслуживает не меньше внимания, чем ранжирование.
- RRF теряет силу оценок. Метод не учитывает, насколько уверен был каждый поиск. Для старта это разумно, а взвешенный вариант можно рассмотреть, если один из поисков стабильно надёжнее для какого-то типа запросов.
Выводы
Точные строки — требование первого класса
В инженерной документации обозначение часто и есть весь вопрос. Архитектура, которая считает его просто ещё одной семантикой, провалится именно на самых важных запросах.
Качество загрузки определяет качество поиска
OCR, нормализация и границы чанков задают верхнюю планку для всего, что может сделать ранжирование. Если результат неверный, первым делом стоит смотреть на проиндексированный чанк.
Каждый ответ должен быть прослеживаемым
Метаданные у каждого чанка делают возможной отладку и позволяют проверять ответы. Инженер доверяет результату, который можно открыть на нужной странице.
Начинать слияние с простого
Слияние по рангам вроде RRF — разумная отправная точка: оно даёт приличный результат без настройки. Всё, что сложнее, должно доказать своё преимущество в сравнении с ним.