Как мы выбирали модель для поиска по смыслу на русском
Человек помнит, о чём шла речь, и не помнит, какими словами. Обычный поиск здесь бессилен в принципе — он сравнивает слова.
«Где мы обсуждали оплату подрядчику?» В переписке это звучало как «перевёл аванс исполнителю по дизайну». Ни одного общего слова. Обычный поиск по словам — даже такой, что понимает русские окончания, — такое не найдёт никогда: он сравнивает слова, а человек помнит смысл.
Как это устроено
Каждый фрагмент текста превращается в набор из тысячи с лишним чисел — вектор. Тексты об одном и том же получают близкие векторы. Вопрос превращается в вектор так же, а дальше ищутся самые близкие к нему фрагменты. Векторы лежат в той же базе данных Postgres, что и сообщения, поэтому права доступа проверяются тем же запросом, что и в обычном поиске: чужое не может просочиться через отдельный «соседний» индекс.
Почему модель своя
Чтобы искать по смыслу, через модель должен пройти весь текст целиком: каждое сообщение, каждая задача, каждая расшифровка. Не отдельный вопрос, а вся переписка команды. Отдавать её чужому сервису ради поиска — недопустимо. Значит, модель должна работать на нашем сервере, на обычном процессоре без видеокарты, и при этом быть достаточно быстрой.
Сравнение
Мы собрали 24 пары «вопрос — нужный текст» по русской рабочей переписке — такие, где у вопроса и нужного текста нет ни одного общего слова: «поломка на кухне» → «кофемашина сломалась». И посчитали, сколько раз модель ставит правильный ответ первым.
| Модель | Верный ответ первым |
|---|---|
| bge-m3 (int8) | 18 из 24 |
| multilingual-e5-small | 12 из 24 |
| multilingual-e5-base (int8) | 10 из 24 |
Выбрали bge-m3. Один запрос обрабатывается за 60 миллисекунд на четырёх ядрах процессора, модель занимает около 560 МБ памяти.
Одна неочевидная предосторожность: версия модели жёстко закреплена, а у каждого вектора записано, какой моделью и какой её версией он получен. Векторы разных версий между собой не сравниваются. Если бы файл модели незаметно обновился там, откуда мы его скачиваем, результаты поиска стали бы случайными — без единой ошибки в журнале.
Что пришлось учесть в индексе
- Исправленный текст нужно обработать заново. Текст меняется в десятке мест — в интерфейсе, через коннектор, при импорте, при массовых операциях. Стоит забыть хотя бы одно — и поиск пойдёт по устаревшему тексту. Поэтому отметку «вектор готов» при любом изменении текста снимает сама база данных.
- Пока модель работала, текст могли поправить. Отметка ставится, только если текст не менялся за эти секунды, — иначе исправленный текст навсегда остался бы с вектором от старой версии.
- «Ок», «спасибо», «+» в индекс не попадают. К любому вопросу они одинаково близки: на проверке «ок» был одинаково похож и на «оплату подрядчику», и на «поломку на кухне».
- Копии в индекс не попадают. Сообщение о задаче в канале, копия комментария, ответы бота — это пересказ того, что написали люди. Если их индексировать, однажды ИИ начал бы пересказывать собственные пересказы.
- Служебные строки о звонках в индекс не попадают. На настоящей переписке вопрос «проблемы со звуком в звонках» находил восемь одинаковых строк «Звонок · 0:30» подряд.
- Длинный текст делится на части по границам абзацев, и соседние части частично повторяют друг друга. Получасовой созвон «обо всём», сжатый в один вектор, не похож ни на один конкретный вопрос.
Что это дало
Поиск по смыслу стал основой для следующего шага — ответов по переписке: чтобы модель ответила «как у нас восстанавливают резервную копию», сначала нужно найти фрагменты, где это обсуждали. А для этого снова нужен поиск, который понимает смысл.
Попробовать можно в строке поиска Kontext: под точными совпадениями появится раздел «По смыслу». Подробнее — на странице поиска.