Как мы выбирали модель для поиска по смыслу на русском

Человек помнит, о чём шла речь, и не помнит, какими словами. Обычный поиск здесь бессилен в принципе — он сравнивает слова.

«Где мы обсуждали оплату подрядчику?» В переписке это звучало как «перевёл аванс исполнителю по дизайну». Ни одного общего слова. Полнотекстовый поиск — даже с хорошей русской морфологией — такое не найдёт никогда: он сравнивает слова, а человек помнит смысл.

Как это устроено

Каждый кусок текста превращается в вектор из тысячи с лишним чисел так, что тексты об одном и том же оказываются рядом. Вопрос — тоже. Дальше ищутся ближайшие. Векторы лежат в той же базе Postgres, что и сообщения, поэтому права доступа проверяются тем же запросом, что и у обычного поиска: чужое не может просочиться через «соседний» индекс.

Почему модель своя

Чтобы искать по смыслу, в модель должен пройти весь текст целиком: каждое сообщение, каждая задача, каждая расшифровка. Не отдельный вопрос, а вся переписка команды. Отдавать её сторонней службе ради поиска — неприемлемо. Значит, модель должна работать на нашем сервере, на процессоре, и быть достаточно быстрой.

Сравнение

Мы собрали 24 пары «вопрос — нужный текст» на русской рабочей переписке, где запрос и ответ не делят ни одного слова: «поломка на кухне» → «кофемашина сломалась». И посчитали, сколько раз модель ставит правильный ответ первым.

МодельВерный ответ первым
bge-m3 (int8)18 из 24
multilingual-e5-small12 из 24
multilingual-e5-base (int8)10 из 24

Выбрали bge-m3. Запрос считается за 60 миллисекунд на четырёх ядрах, модель занимает около 560 МБ памяти.

Одна неочевидная предосторожность: версия модели прибита, а имя модели с версией хранится у каждого вектора. Векторы разных версий между собой не сравниваются. Тихо обновившийся файл модели на стороне хранилища сделал бы выдачу случайной — без единой ошибки в журнале.

Грабли индекса

  • Правка текста должна сбрасывать вектор. Текст меняется из десятка мест — интерфейс, коннектор, импорт, массовые операции. Забытое одно место означало бы поиск по устаревшему тексту. Поэтому отметку «посчитано» сбрасывает триггер в самой базе.
  • Пока модель считала, текст могли поправить. Отметка ставится, только если текст не менялся за эти секунды — иначе правка навсегда осталась бы с вектором старой версии.
  • «Ок», «спасибо», «+» не индексируются. К любому вопросу они одинаково близки: на проверке «ок» был одинаково похож и на «оплату подрядчику», и на «поломку на кухне».
  • Копии не индексируются. Анонс задачи в канале, зеркало комментария, ответы бота — это пересказ написанного людьми, который однажды стал бы пересказом самого себя.
  • Строки о звонках не индексируются. На живых данных вопрос «проблемы со звуком в звонках» получал восемь одинаковых строк «Звонок · 0:30» подряд.
  • Длинное режется на куски по границам абзацев с перекрытием. Получасовой созвон «обо всём» одним вектором не похож ни на один конкретный вопрос.

Что это дало

Поиск по смыслу стал основой для следующего шага — ответов по переписке: чтобы модель ответила «как у нас восстанавливают бэкап», сначала нужно найти куски, где это обсуждали. А для этого снова нужен поиск, который понимает смысл.

Попробовать можно в строке поиска Kontext: под точными совпадениями появится раздел «По смыслу». Подробнее — на странице поиска.