Гибридный поиск для базы знаний: как совместить BM25 и векторы
Гибридный поиск для базы знаний помогает находить и точные фразы, и смысловые совпадения. Разберем BM25, векторы, смешивание рангов и проверки.

Почему одного метода поиска мало
В корпоративной базе знаний люди ищут по-разному. Утром сотрудник вводит точное название поля из API, днем коллега ищет тот же материал по смыслу, а вечером кто-то помнит только кусок формулировки из старого регламента. Один способ поиска редко справляется со всеми этими случаями.
BM25 хорошо работает, когда в запросе есть точные слова из документа. Если в инструкции написано payment_status, а человек ищет именно это поле, такой поиск обычно срабатывает быстро и точно. Но он часто теряет документ, когда пользователь пишет "статус оплаты" или "где посмотреть, прошел ли платеж". Смысл тот же, слова другие.
У векторного поиска обратная слабость. Он находит тексты, близкие по смыслу, даже если формулировка не совпадает. Это удобно для живых вопросов на обычном языке. Но когда человеку нужен артикул, номер формы, код ошибки, название колонки или точная версия политики, векторный поиск может поднять "похожий" документ выше нужного. Для базы знаний это особенно раздражает: документ есть, но его не видно в первых результатах.
Такая ситуация возникает в одной и той же команде в течение одного дня. Техподдержка ищет коды и названия полей. Юристы ищут точные пункты и номера приложений. HR пишет запросы обычными словами. Продуктовые команды часто хотят получить и смысловое совпадение, и точный матч сразу.
Из-за этого сотрудники тратят лишние минуты на каждый поиск, открывают несколько почти подходящих документов и все равно пишут коллегам в чат. Хуже то, что команда начинает считать базу знаний пустой или устаревшей, хотя нужный материал давно лежит в системе.
Поэтому гибридный поиск обычно работает лучше любого одного метода. Он не пытается заранее угадать тип запроса. Он одновременно ловит точные совпадения и смысловые связи, а значит реже прячет нужный документ под "почти правильными" результатами.
Когда BM25 сильнее
BM25 особенно полезен там, где одна буква или одно слово меняют ответ целиком. Он не угадывает смысл запроса, а ищет точные совпадения по словам и обычно поднимает выше документы, где редкий термин встречается в нужной форме.
Это хорошо видно в регламентах, договорах и внутренних инструкциях. Если сотрудник ищет фразу вроде "срок хранения согласия" или "условия досрочного расторжения", BM25 часто находит нужный фрагмент лучше векторного поиска. Векторный слой может принести текст на ту же тему, но пропустить точную формулировку, которая и нужна для ответа.
То же самое происходит с сущностями, которые нельзя надежно "понять по смыслу": номер заявки, артикул, код ошибки, ФИО, email, номер договора. Запрос ERR-4187 должен вернуть документы с ERR-4187, а не любые статьи про сбои авторизации. Для сервисной базы это обычная история: инженер ищет конкретный код, и BM25 быстро вытаскивает пару нужных заметок.
BM25 полезен и тогда, когда в запросе есть редкий термин. Например, в базе знаний встречается название внутреннего сервиса или короткое сокращение, которое есть только в нескольких документах. Для BM25 это сильный сигнал. Он быстро отсекает лишнее и показывает тексты, где термин действительно есть.
Еще один частый случай - короткие документы. В них одно слово может менять смысл полностью. "Разрешено" и "запрещено" иногда отличаются одной строкой, но для пользователя это два разных ответа. Если заметка состоит из трех предложений, точное совпадение слов обычно надежнее смысловой близости.
Поэтому в гибридной схеме BM25 почти всегда нужен как отдельный слой. Он особенно важен там, где цена ошибки высока: в регуляторных формулировках, идентификаторах, редких терминах и коротких служебных текстах.
Когда векторы выигрывают
Векторный поиск полезнее там, где человек и документ говорят об одном и том же разными словами. Сотрудник пишет: "как вернуть доступ после смены телефона", а в базе знаний раздел называется "перепривязка MFA-устройства". BM25 часто ищет буквальные совпадения и может такой раздел пропустить. Векторы обычно ловят сам смысл запроса, даже если слова не совпали.
Это особенно заметно, когда в текстах много синонимов, сокращений и разговорных формулировок. В одной команде пишут "учетка", в другой - "аккаунт", а в регламенте - "учетная запись". Для человека разница небольшая, а для поиска по словам она иногда решающая. Векторный слой лучше связывает такие варианты.
Еще один тип запросов - вопросы про действие, а не про термин. Пользователь спрашивает: "что делать, если касса не отправляет чеки", а полезный документ хранит шаги проверки: состояние ОФД, очередь отправки, время на устройстве, сетевое соединение. Точной фразы из запроса в тексте может не быть, но смысл совпадает. Векторный поиск чаще поднимает именно такие объясняющие блоки.
Он помогает и при коротких запросах без точных терминов. Люди часто пишут "не пускает в VPN", "не печатает", "ошибка входа". В таких фразах мало сигналов для BM25. Векторная модель достраивает контекст лучше и находит документы, где описан похожий сбой или нужный порядок действий.
В гибридной схеме этот слой закрывает слабые места поиска по словам: живую речь, смысловые совпадения и очень короткие запросы. Если сотрудники ищут так, как говорят в чатах и тикетах, без векторного поиска выдача обычно теряет много полезных материалов.
Как подготовить документы
Гибридный поиск начинает работать заметно лучше еще до индексации. Если документы собраны как попало, BM25 цепляется за шум, а векторный поиск тянет фрагменты с похожим смыслом, но не с тем ответом.
Длинные файлы лучше резать по смыслу. Берите границы разделов, шагов, вопросов и ответов, а не фиксированные 1000 или 2000 символов. Один фрагмент должен закрывать одну понятную задачу: как оформить отпуск, где взять код проекта, что означает ошибка в системе.
Не смешивайте в одном куске разные вещи. Если рядом лежат форма заявки, инструкция по заполнению и список исключений, поиск быстро начинает путаться. Пользователь спрашивает про номер шаблона, а получает общий регламент на несколько экранов.
Простой пример: в документе есть раздел про доступ в CRM, рядом памятка для новых сотрудников и форма согласования. Лучше сделать три отдельных фрагмента. Тогда запрос по названию системы найдет точное совпадение, а смысловой запрос вроде "как выдать доступ новичку" тоже попадет куда нужно.
Какие данные хранить рядом с текстом
Сам текст без контекста почти всегда проигрывает. Полезно сохранять источник, раздел, дату, тип документа, версию и владельца, если он известен. Эти поля помогают фильтровать выдачу и потом понимать, почему поиск поднял именно этот фрагмент.
Отдельно стоит вынести точные сущности: артикулы, коды ошибок, названия систем, внутренние сокращения, номера приказов. Не прячьте их только внутри длинного текста. BM25 особенно хорошо работает по таким данным, если они лежат в чистом виде.
Еще одна частая проблема - дубликаты и старые версии. Если в базе есть два почти одинаковых регламента, поиск начнет поднимать оба, а переранжирование не всегда поймет, какой актуален. Проще заранее убрать копии, пометить архивные материалы и оставить для индексации один основной документ.
Подготовка документов редко кажется самой интересной частью проекта, но именно она часто решает, будет ли поиск полезен в ежедневной работе.
Как собрать гибридную схему
Рабочая схема начинается с двух отдельных индексов для одного и того же корпуса. Первый хранит слова и их частоты, чтобы BM25 находил точные совпадения по терминам, кодам ошибок, названиям полей и редким сокращениям. Второй хранит эмбеддинги тех же фрагментов, чтобы векторный поиск ловил смысл даже тогда, когда пользователь сформулировал вопрос другими словами.
Важно, чтобы оба индекса ссылались на один и тот же chunk_id или document_id. Иначе быстро появятся дубли, путаница в метаданных и странные ответы в RAG.
Обычно хватает четырех частей: индекса BM25 по очищенному тексту, векторного индекса по тем же фрагментам, слоя слияния результатов и переранжирования для финального списка.
Запрос лучше отправлять в оба индекса параллельно. Так вы не теряете время и сразу получаете два набора кандидатов: один по словам, другой по смыслу. В финал редко попадают все найденные куски, поэтому из каждого поиска стоит брать запас. Если LLM потом увидит только top-5, на этапе retrieval разумно поднять хотя бы top-20 или top-30 из каждого источника.
Дальше возникает типичная ошибка: сырые оценки нельзя просто складывать. У BM25 одна шкала, у векторного поиска другая. Если не нормализовать scores, одна система почти всегда задавит другую. На практике используют min-max, z-score или rank-based fusion. Последний вариант часто проще и стабильнее.
После нормализации результаты объединяют по идентификатору фрагмента или документа. Если один и тот же кусок пришел из обоих индексов, это хороший сигнал. Такой кандидат обычно стоит поднять выше. Уже после этого имеет смысл отправлять объединенный список на переранжирование.
Если вы строите RAG-контур в компании с требованиями по 152-ФЗ, индексы и метаданные обычно держат рядом с базой знаний в российском контуре. А в LLM лучше отправлять уже найденные фрагменты, а не весь внутренний корпус.
Как смешивать результаты
Смешивание лучше делать в два шага: сначала собрать кандидатов, потом пересчитать общий балл. Такой подход не теряет точные вхождения и при этом вытаскивает близкие по смыслу фрагменты.
Сначала возьмите два отдельных списка кандидатов, например top-20 из BM25 и top-20 из векторного поиска по тем же фрагментам или документам. Затем сложите списки в один набор и удалите повторы по chunk_id или другому стабильному идентификатору. Если один и тот же фрагмент пришел из обоих поисков, сохраните оба балла.
Следующий шаг - привести оценки к одной шкале. Проще не сравнивать сырые scores напрямую, а нормализовать их в диапазон от 0 до 1 или считать балл по позиции в выдаче. После этого задайте веса. Если в вашей базе часто ищут коды ошибок, артикулы, номера форм и точные названия, разумно начать с 0.6 для BM25 и 0.4 для векторного поиска.
Отдельно стоит добавить бонус за точное совпадение. Если фрагмент содержит код E-451, артикул 782114 или полное название документа из запроса, поднимайте его выше даже при среднем векторном сходстве.
Формула может быть совсем простой:
общий_балл = 0.6 * bm25_norm + 0.4 * vector_norm + бонус_за_точное_совпадение
Этого достаточно для первого запуска. Не стоит сразу строить сложную систему переранжирования, если у вас еще нет проверочного набора запросов.
Небольшой пример: пользователь вводит "регламент по возврату товара 14 дней". Векторный поиск подтянет похожие тексты про обмен и гарантию, а BM25 найдет фрагмент, где фраза "14 дней" и полное название регламента встречаются буквально. После объединения именно такой фрагмент должен оказаться выше.
Дальше нужна ручная проверка. Возьмите первые 20-30 реальных запросов сотрудников, посмотрите top-5 результатов и отметьте, где поиск промахнулся: потерял точный термин, поднял слишком общий текст или вытянул не тот документ.
После этого меняйте веса понемногу. Если поиск пропускает точные совпадения, увеличьте долю BM25 или бонус за полный матч. Если выдача стала слишком буквальной и хуже понимает смысл запроса, верните часть веса векторному слою. Обычно двух-трех итераций хватает, чтобы получить заметно более ровную выдачу.
Как проверять качество на реальных вопросах
Оценивать поиск лучше не на случайных фразах, а на живых запросах сотрудников. Часто хватает 30-50 примеров из чатов поддержки, тикетов, почты или внутреннего поиска. Такой набор быстро показывает, где система находит ответ, а где уходит в сторону.
Для каждого запроса заранее отметьте документы, которые действительно помогают решить задачу. Не нужен идеальный датасет на сотни строк. Достаточно честно ответить на два вопроса: какой документ должен попасть в выдачу и в каком диапазоне позиций он еще считается полезным.
Запросы удобно разделить по типам: точные запросы с артикулами, кодами ошибок и названиями полей, короткие запросы с аббревиатурами и внутренним жаргоном, разговорные фразы, которыми люди описывают проблему своими словами, и длинные запросы с контекстом и несколькими условиями.
Дальше прогоните один и тот же набор через три режима: только BM25, только векторный поиск и гибридную схему. Смотрите не только на первый результат. Во внутренней базе знаний сотрудник часто открывает 3-5 документов, и если полезный текст стабильно попадает в этот диапазон, поиск уже работает заметно лучше.
Практичнее всего смотреть на две метрики: сколько раз нужный документ оказался на первом месте и сколько раз он вообще попал в топ-5. Первая показывает точность. Вторая помогает понять, можно ли потом спасти выдачу переранжированием.
Полезно вести простую таблицу. Например, запрос "Ошибка E-104 при выгрузке акта" часто выигрывает у BM25, а фраза "почему система не дает закрыть месяц" лучше находится векторным поиском. Если гибрид уверенно берет оба случая, схема настроена в правильную сторону.
Отдельно разберите промахи вручную. Обычно там быстро всплывают слабые места: плохая нарезка документов, шумные синонимы, потерянные аббревиатуры или слишком агрессивное смешивание скорингов.
Пример для корпоративной базы знаний
Представьте обычную смену в поддержке. Клиент пишет: "У меня ошибка E421 в личном кабинете, ничего не открывается". Сотруднику нужен не общий материал про вход или сбои, а точный ответ за пару секунд.
BM25 тут часто срабатывает первым. Он сразу находит заметку, где код E421 указан в заголовке или в тексте один в один. В такой заметке обычно есть причина: например, ошибка появляется после неудачной проверки сессии или сбоя при обновлении профиля.
Но на этом поиск лучше не останавливать. В базе знаний нередко есть другая инструкция, где кода E421 нет совсем, зато описан тот же случай обычными словами: "личный кабинет зависает после входа", "страница обновляется по кругу", "клиент не может открыть раздел с данными". Векторный поиск поднимает такой документ, потому что видит смысл запроса, а не только точные слова.
В гибридную выдачу обычно попадают оба результата. После переранжирования сверху остается заметка с точным кодом ошибки, а рядом держится инструкция с готовыми шагами.
Для сотрудника это выглядит просто: он получает точный документ по E421, инструкцию для похожего сбоя без кода, шаблон ответа клиенту и шаги проверки перед эскалацией. Не нужно отдельно искать расшифровку кода, потом открывать другую статью с действиями и собирать ответ вручную.
На практике такой сценарий особенно полезен в большой базе знаний, где документы писали разные команды. Одни статьи называют ошибку кодом, другие описывают ее по симптомам. Гибридная выдача соединяет оба подхода и помогает быстрее дать клиенту понятный ответ: что сломалось, что проверить прямо сейчас и когда передавать случай дальше.
Частые ошибки
Чаще всего гибридный поиск ломается не из-за модели, а из-за данных и простых настроек. Система вроде бы ищет и по словам, и по смыслу, но на практике отдает шум, дубли и старые ответы.
Первая частая ошибка - слишком мелкая нарезка документов. Если разбить регламент на куски по 100-150 токенов, BM25 еще найдет нужную фразу, а вот смысл соседних абзацев исчезнет. В итоге сотрудник видит пункт без условий, исключений и срока действия. Для базы знаний это неприятный сценарий: ответ выглядит точным, но вводит в заблуждение.
Не меньше проблем дает хранение старых и новых версий рядом без явного приоритета. Представьте инструкцию по согласованию отпуска: новая версия уже изменила порядок, а старая все еще лежит в индексе. Поиск честно находит обе, и ранжирование начинает метаться между ними. Пользователь получает два похожих ответа и не понимает, какой из них актуален.
Отдельная ловушка - смешивать оценки BM25 и векторного поиска без нормализации. Эти шкалы устроены по-разному. Если просто сложить сырые баллы, один сигнал почти всегда задавит другой. Из-за этого точное совпадение по редкому термину может проиграть документу, который просто похож по теме.
Еще один источник шума - дубли в индексе. Один и тот же документ попадает туда после каждой загрузки, иногда с разными ID, иногда в нескольких версиях чанков. Поиск начинает возвращать один и тот же материал несколько раз, а переранжирование тратит место в топе на повторы вместо полезных кандидатов.
Обычно такие проблемы видны по простым симптомам: в топе много похожих результатов, поиск находит отмененную инструкцию, редкий термин есть в документе, но сам документ сидит низко в выдаче, а верный ответ появляется только если вопрос совпал с тестовой формулировкой.
Последняя ошибка самая коварная: проверять качество только на аккуратном наборе тестовых вопросов. Живые запросы пишут с опечатками, жаргоном и обрывками фраз. Если не смотреть реальные логи поиска, система будет казаться хорошей только на демо.
Быстрые проверки перед запуском
Перед выкладкой в прод полезно пройтись по пяти коротким тестам. В гибридном поиске проблемы обычно видны сразу.
- Проверьте артикулы, номера заявок, коды ошибок, версии регламентов и точные названия форм. Такие запросы должны попадать в первые позиции. Если их обгоняют просто похожие тексты, BM25 или смешивание рангов настроены плохо.
- Попробуйте синонимы и живую речь. Сотрудник может искать "отгул за свой счет", хотя в документе написано "отпуск без сохранения заработной платы". Если нужный раздел не находится, векторный поиск работает слабо или документы нарезаны слишком грубо.
- Посмотрите первые 10 результатов вручную. Если там подряд идут старый регламент, его копия, PDF-версия и тот же текст из wiki, выдача шумит.
- Откройте каждый найденный фрагмент и проверьте метаданные. У куска должен быть понятный источник, дата, версия и короткий путь до полного документа. Иначе сотрудник не поймет, можно ли доверять ответу.
- Спросите у команды, почему документ оказался наверху. Хорошая система объясняет это без догадок: совпал код, сработало семантическое сходство, переранжирование подняло свежую версию. Если объяснения нет, отлаживать поиск будет тяжело.
Если один тест не проходит, не меняйте все сразу. Сначала исправьте один слой: разбиение документов, веса BM25 и векторов, дедупликацию или метаданные. Так проще понять, что именно ломает выдачу.
Что делать дальше
Не перестраивайте сразу всю корпоративную базу знаний. Возьмите 30-50 реальных запросов сотрудников и проверьте базовую точность на них. Нужны разные типы вопросов: с кодами ошибок, названиями документов, сокращениями и обычными фразами вроде "где описан порядок согласования".
Потом запустите гибридный поиск на одном разделе, где результат легко проверить. Для пилота часто подходят инструкции для техподдержки, регламенты или FAQ. Сравнивайте не общее впечатление, а простые вещи: попал ли нужный документ в первые три результата, сохранились ли точные совпадения по артикулу, номеру приказа или названию системы.
Удобный порядок такой: сначала измерить отдельно BM25 и отдельно векторный поиск, потом включить смешивание и проверить, где результат стал лучше, а где хуже. Переранжирование лучше добавлять после этого. Если у вас RAG, поиск и генерацию ответа тоже стоит держать как две разные части.
Такое разделение сильно экономит время. Если модель дала плохой ответ, вы сразу видите причину: поиск не нашел нужный документ или LLM неверно пересказала найденное. Без этого команды часто чинят не тот слой и неделями спорят о промптах, хотя проблема в индексе.
Если RAG должен работать в российском контуре, LLM-слой удобно подключать через OpenAI-совместимый шлюз вроде RU LLM. В таком случае команда может просто заменить base_url на api.rullm.com, оставить прежние SDK и код, а логи, биллинг и обработку в российском контуре держать внутри РФ. Для пилота это часто проще, чем одновременно менять и поиск, и всю обвязку вокруг модели.
Когда схема начнет стабильно работать, зафиксируйте ее в явных правилах. Иначе через месяц никто не вспомнит, почему запрос с номером договора должен поднимать BM25 выше векторного слоя.
Часто задаваемые вопросы
Зачем вообще совмещать BM25 и векторный поиск?
Если сотрудники ищут и по точным словам, и по смыслу, один метод начнет промахиваться. BM25 хорошо ловит ERR-4187, payment_status и названия форм, а векторный поиск находит ответ, когда человек пишет вопрос своими словами.
Гибридная схема закрывает оба сценария сразу и реже прячет нужный документ под похожими, но не теми результатами.
В каких запросах BM25 обычно сильнее?
Ставьте BM25 выше, когда запрос содержит код ошибки, артикул, номер договора, название поля, редкий термин или точную фразу из регламента. В таких случаях буквальное совпадение дает более надежный результат, чем поиск по смыслу.
Если цена ошибки высокая, добавьте бонус за полный матч по таким сущностям.
Когда векторный поиск дает лучший результат?
Векторный слой лучше работает с живой речью, синонимами и короткими жалобами вроде «не пускает в VPN». Он помогает, когда в документе и в запросе один смысл, но разные слова.
Это особенно полезно для тикетов, чатов и внутреннего жаргона, где люди редко формулируют вопрос так же, как автор инструкции.
Как правильно нарезать документы для гибридного поиска?
Режьте документы по смыслу, а не по фиксированному числу символов. Один фрагмент должен отвечать на одну задачу: оформить отпуск, расшифровать ошибку, выдать доступ.
Не складывайте в один кусок форму, инструкцию и исключения. Иначе поиск найдет общий текст вместо точного ответа.
Какие метаданные нужны кроме самого текста?
Храните рядом с текстом источник, раздел, дату, версию, тип документа и владельца. Эти поля помогают фильтровать выдачу и быстро понять, почему фрагмент оказался наверху.
Отдельно вынесите коды ошибок, артикулы, номера приказов, названия систем и сокращения. BM25 ищет по ним заметно точнее, если они лежат в чистом виде.
Как смешивать результаты двух поисков без странной выдачи?
Не складывайте сырые баллы BM25 и векторного поиска напрямую. Сначала приведите их к одной шкале, потом задайте простой вес, например 0.6 для BM25 и 0.4 для векторов, если у вас много точных запросов.
Дальше добавьте небольшой бонус за полный матч по коду, артикулу или названию документа. Для первого запуска этого обычно хватает.
Нужен ли переранжировщик с самого начала?
Нет, на старте можно обойтись без него. Сначала соберите нормальный hybrid retrieval, проверьте нарезку, уберите дубли и настройте веса.
Переранжирование имеет смысл добавлять после этого, когда вы уже видите, что кандидаты в top-20 или top-30 в целом хорошие, но порядок в top-5 еще плавает.
Как быстро проверить, что поиск реально работает?
Возьмите 30–50 живых запросов из чатов, тикетов или почты и для каждого отметьте документ, который реально помогает. Потом сравните три режима: только BM25, только векторы и гибрид.
Смотрите не только на первое место. Для базы знаний полезно считать, как часто нужный документ попадает хотя бы в top-5.
Почему поиск часто показывает старые версии и дубли?
Обычно причина в данных, а не в модели. Если вы держите в индексе архив и актуальную версию без явного приоритета, поиск честно покажет обе.
Уберите копии, пометьте архивные материалы и оставьте один основной документ для индексации. После этого выдача станет чище даже без сложной настройки.
Как аккуратно подключить такую схему к RAG в российском контуре?
Сначала держите поиск и генерацию как две разные части. Пусть retrieval находит только нужные фрагменты, а LLM получает уже узкий набор контекста, а не весь корпус.
Если вам нужен российский контур, подключайте LLM через OpenAI-совместимый шлюз вроде RU LLM и меняйте только base_url. Так команда не ломает существующие SDK и код во время пилота.