Как мы выбирали модель для поиска по смыслу на русском
Человек помнит, о чём шла речь, и не помнит, какими словами. Обычный поиск здесь бессилен в принципе — он сравнивает слова.
«Где мы обсуждали оплату подрядчику?» В переписке это звучало как «перевёл аванс исполнителю по дизайну». Ни одного общего слова. Полнотекстовый поиск — даже с хорошей русской морфологией — такое не найдёт никогда: он сравнивает слова, а человек помнит смысл.
Как это устроено
Каждый кусок текста превращается в вектор из тысячи с лишним чисел так, что тексты об одном и том же оказываются рядом. Вопрос — тоже. Дальше ищутся ближайшие. Векторы лежат в той же базе 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: под точными совпадениями появится раздел «По смыслу». Подробнее — на странице поиска.