Перейти к содержимому
04 авг. 2024 г.·7 мин чтения

Размерность эмбеддингов: где ломается поиск и код

Размерность эмбеддингов влияет на поиск, индексы и схемы хранения. Показываем, где падает код, где проседает качество и как пройти миграцию.

Размерность эмбеддингов: где ломается поиск и код

Где проблема на самом деле

Смена модели эмбеддингов ломает не только поиск. Чаще она ломает договоренность между моделью, базой, индексом и кодом, который считает похожесть. Пока все работает на одной размерности, этого не видно. Проблема всплывает в день, когда сервис ждет 1536 чисел, а получает 3072.

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

Обычно сбой идет по одному из четырех путей:

  • запись нового вектора не проходит из-за другой длины
  • векторный индекс отказывается строиться или обновляться
  • сервис сравнения падает в рантайме на операции similarity
  • поиск смешивает старые и новые векторы и выдает странный порядок результатов

Последний случай самый неприятный. Код не падает, дашборд зеленый, запросы обрабатываются. Но выдача уже испорчена, потому что расстояния между векторами имеют смысл только внутри одного пространства. Старые и новые эмбеддинги могут описывать один и тот же текст по разным правилам, и смешивать их в одном индексе нельзя.

Представьте поиск по каталогу товаров. Карточки заиндексировали старой моделью, а новые запросы пользователей уже кодирует новая. Формально поиск работает, но человек вводит "беспроводные наушники", а наверху вдруг появляются чехлы, переходники и случайные аксессуары. Команда начинает править ранжирование, хотя причина проще: запрос и документы живут в разных векторных пространствах.

Проблема не в том, что новая модель вернула другой вектор. Проблема в том, что почти вся цепочка вокруг вектора молча предполагает одну длину и одну геометрию. Если не проверить это заранее, вы получите либо явный сбой, либо тихую деградацию качества. Обычно дороже обходится именно второе.

Почему нельзя просто подменить модель

У модели эмбеддингов меняется не только длина массива чисел. Она меняет само пространство, в котором живут тексты. Документ и запрос могут остаться теми же, но их положение относительно друг друга уже другое.

Размерность замечают первой, потому что код любит фиксированную длину. Но это только видимая часть проблемы. Новая модель по-своему раскладывает смысл по осям: одна лучше различает юридические и операционные формулировки, другая точнее сближает короткие бытовые запросы с длинными инструкциями.

Даже одинаковая длина не спасает

Если старая и новая модели обе возвращают, например, 1536 чисел, это не делает их совместимыми. Эти 1536 координат описывают текст по разным правилам. Смешивать старые векторы документов с новыми векторами запросов нельзя. Поиск не всегда падает с ошибкой, но начинает тихо врать.

Это видно на простом примере. В базе есть статьи "как закрыть счет", "заморозка подозрительной операции" и "смена номера телефона". Старая модель могла уверенно ставить первую статью в топ по запросу "закрыть карту". После замены модели тот же запрос иногда поднимает материал про проверку операции, потому что новая геометрия сильнее связала слова "закрыть", "блокировка" и "проверка".

Пороги похожести тоже перестают работать как раньше. Если раньше вы считали документ подходящим при cosine similarity выше 0.82, после миграции это число почти ничего не значит. Распределение расстояний меняется, поэтому фильтр "ничего не найдено", дедупликация и rerank после поиска начинают вести себя иначе.

Даже в OpenAI-совместимом шлюзе, где можно сменить модель и не трогать SDK, такая замена не бывает бесплатной. Нужно заново считать эмбеддинги для корпуса, пересобрать векторный индекс и проверить метрики на живых запросах. Иначе поиск формально работает, а пользователи уже видят другой набор документов.

Где меняется качество поиска

Качество поиска часто меняется не там, где команда ждет. Система может не падать, индекс может строиться без ошибок, а top-10 по части запросов останется почти тем же. Это и сбивает с толку: кажется, что замена прошла спокойно, хотя новая модель уже по-другому видит близость между текстами.

Сильнее всего это заметно на коротких запросах. Когда пользователь вводит одно-два слова, у модели мало опорных сигналов, и любой сдвиг в геометрии векторов сразу влияет на выдачу. Длинный запрос часто спасает сам себя: в нем больше контекста, больше уточнений, и поиск легче находит нужный документ.

Запрос "лимит карты" хорошо это показывает. В базе могут лежать тарифы, правила по рискам, FAQ и новости. Одна модель поднимет страницу с точным правилом, другая отправит выше общий обзор по продукту, потому что слова близки по теме, но не по задаче пользователя.

Где проблему легко пропустить

Фильтры и rerank часто прячут просадку. Если поверх векторного поиска стоит фильтр по продукту, языку или дате, он может случайно убрать плохие кандидаты и сделать результат нормальным. Rerank дает тот же эффект: он чинит верх выдачи, но не возвращает документы, которые базовый поиск вообще не нашел.

Из-за этого команда смотрит на первые три ответа и думает, что все в порядке. Но recall уже упал, а вместе с ним падает и шанс найти правильный документ в сложных случаях.

Мультиязычные коллекции реагируют еще сильнее. Если база смешивает русский, казахский и английский, миграция модели может резко поменять расстояния между языками. Вчера запрос на русском уверенно находил документ на казахском, а сегодня такой документ уезжает ниже, хотя смысл не изменился.

Старый чанкинг тоже добавляет шум. Если раньше вы резали текст слишком крупно, слишком мелко или прямо посередине мысли, старая модель могла это терпеть. Новые эмбеддинги иногда хуже прощают такие куски, и проблема выглядит как смена качества поиска, хотя часть беды сидит в подготовке данных.

Поэтому опасно менять сразу все: модель, индекс и схему нарезки. Потом трудно понять, что именно испортило поиск. На практике меняется не один параметр, а вся логика похожести.

Где падает код

Код чаще ломается не в момент запроса к модели, а на следующем шаге, когда новый вектор нужно сохранить, передать или использовать в ранжировании. Если вчера модель возвращала 1536 чисел, а сегодня 3072, проблема быстро выходит за пределы качества поиска. Ломается сам контракт между частями системы.

Самый частый сбой живет в хранилище. pgvector, Milvus, Qdrant и другие системы ожидают точную длину вектора там, где схема уже задана. Если колонка создана как vector(1536), запись вектора длиной 3072 сама не подстроится. База вернет ошибку, и индексатор остановится.

На практике это выглядит буднично. Ночной batch job берет документы пачками, считает эмбеддинги и пишет их в индекс. Первая же несовместимая запись падает с ошибкой, задача прерывается, а половина каталога остается со старыми векторами. Утром API отвечает 200 OK, но поиск уже работает на смеси старых и новых данных.

Есть и менее заметный класс поломок: сериализация между сервисами. Один сервис считает эмбеддинги, второй нормализует, третий складывает их в индекс. Если где-то в контракте зашита длина массива, новый формат ломает пайплайн. Иногда сервис падает сразу. Иногда хуже: он молча обрезает массив, добавляет нули или пропускает запись как битую.

Часто подводит и кэш. Он хранит эмбеддинги документов, запросов или промежуточных результатов дольше, чем живет версия модели. После переключения часть запросов получает новые векторы, часть - старые из кэша. Код формально работает, но индекс и ранжирование уже сравнивают несовместимые данные.

Обычно сбой прячется в одном из пяти мест: схема базы или индекса жестко фиксирует длину вектора, batch job не умеет пропускать и логировать несовместимые записи, формат обмена между сервисами не проверяет длину массива, кэш не разделяет данные по версии модели, а мониторинг смотрит только на статус ответа и время запроса.

Последний пункт особенно неприятен. Обычный мониторинг видит, что эндпоинт жив, ошибки 500 нет, задержка нормальная. Для него все хорошо. Но ранжирование уже сломано: релевантные документы ушли вниз, дубликаты поднялись вверх, фильтры по похожести дают странный шум.

Если команда меняет модель через совместимый API-шлюз, риск никуда не исчезает. Совместимый эндпоинт сохраняет код вызова, но не отменяет проверку на совместимость векторов. Размерность эмбеддингов нужно считать частью схемы данных, а не мелкой деталью ответа модели.

Пример из рабочего поиска

Не ломать индекс вслепую
Используйте один шлюз, чтобы тестировать миграции аккуратно и без спешки.

У службы поддержки есть поиск по базе знаний. Оператор вводит простой вопрос, система ищет похожие фрагменты, а потом показывает готовый ответ или набор статей. Долгое время все работало на модели эмбеддингов с вектором 1536.

Потом команда прогнала офлайн-тест и увидела хороший результат на новой модели с размером 3072. На размеченной выборке она лучше находила перефразированные вопросы и реже путала близкие темы. Решение выглядело безопасным: поменять модель в пайплайне и постепенно переиндексировать базу.

Сбой начался не с громкой ошибки, а с тихого рассинхрона. Один сервис уже писал новые векторы 3072, а другой все еще отправлял данные в старый индекс, где схема ждала 1536. Часть записей система отклоняла сразу. Часть проходила только наполовину: текст и метаданные сохранялись, а вектор не попадал в индекс.

Снаружи все выглядело почти нормально. API отвечал успешно, очередь задач не стояла, мониторинг не горел красным. В логах было мало явных ошибок, потому что приложение считало запрос обработанным, если оно сохранило документ целиком или хотя бы дошло до конца шага.

Через день операторы заметили странное поведение. По вопросам вроде "как сменить пароль" или "где посмотреть счет" поиск стал отдавать документы рядом по смыслу, но не те. Ответы выглядели правдоподобно, и это только мешало: люди дольше верили, что проблема в формулировке запроса, а не в данных.

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

Так и проявляется размерность эмбеддингов в реальной работе. Ломается не только совместимость векторов. Ломается доверие к поиску. Команда находит причину только после сверки: сколько документов пайплайн принял, сколько векторов реально записал и сколько объектов попало в поиск без эмбеддинга. Даже небольшой разрыв между этими числами быстро портит качество.

Как перейти на новую модель по шагам

Смену модели эмбеддингов лучше вести как миграцию схемы, а не как мелкую настройку. Иначе проблема проявится в самый неприятный момент: индекс примет новый вектор, а поиск начнет молча путать документы или код упадет на несовпадении длины.

Сначала зафиксируйте в конфиге не только имя модели, но и размерность эмбеддингов. Добавьте туда версию индекса и метрику расстояния, если задаете ее явно. Команда должна сразу видеть, какой вектор лежит в базе и какой код с ним работает.

Безопаснее держать старую и новую схему рядом, пока вы не увидите результат на реальных запросах.

  • Создайте новую колонку или новую таблицу под свежие векторы и соберите отдельный индекс.
  • Оставьте старые эмбеддинги в работе, чтобы у вас был чистый эталон и быстрый откат.
  • Возьмите один и тот же набор реальных запросов: частые, редкие, короткие, с опечатками и длинные.
  • Для каждой модели проверьте, попадает ли нужный документ в верхние результаты, сколько запросов дают пустую выдачу, сколько длится поиск и во что он обходится.
  • Переключайте трафик долями, например 5%, потом 20%, и держите флаг мгновенного возврата на старый индекс.

Сравнение лучше делать не на красивой демо-выборке, а на том, чем люди пользуются каждый день. Если поиск по базе договоров раньше находил нужный шаблон по запросу "расторжение по инициативе клиента", а новая модель чаще тянет общие документы про обслуживание, качество уже просело, даже если средний скор похож на прежний.

Recall можно проверить простым способом: есть ли нужный документ хотя бы в первых 5 или 10 результатах. Этого часто хватает, чтобы быстро заметить, где меняется качество. Отдельно проверьте задержку полной цепочки: генерация эмбеддинга, запись, запрос к индексу и ответ приложению. Иногда новая модель ранжирует лучше, но добавляет 150-200 мс, и пользователи это замечают.

Не удаляйте старый индекс в день запуска. Подержите обе версии параллельно, пока не увидите стабильную картину по качеству, цене и ошибкам в коде. Это скучный путь, но он дешевле авралов.

Если вы работаете через единый шлюз вроде AI Router, где можно менять модель через тот же OpenAI-совместимый вызов, соблазн пропустить эту миграцию особенно велик. Удобство на уровне API полезно, но индексу и данным все равно нужна отдельная версия, переиндексация и проверка на живых запросах.

Что измерять после запуска

Проверить до релиза
Сравните стоимость и поведение моделей до переиндексации, а не после сбоя.

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

Первый набор метрик лучше держать в одном дашборде:

  • доля пустых результатов по типам запросов
  • сдвиг в top-10 для частотных запросов
  • ошибки индексации и число отклоненных векторов
  • размер индекса и расход памяти
  • задержка на запись и на поиск

Пустые результаты смотрите не в среднем по системе, а по группам. Короткие запросы, длинные фразы, артикулы, имена клиентов, смешанные запросы на русском и английском ведут себя по-разному. Если пустая выдача выросла с 1-2% до 6-8% только на коротких запросах, проблема уже есть, даже если средняя метрика выглядит терпимо.

Сдвиг в top-10 не всегда плох сам по себе. Плохо, когда он массовый и случайный. Возьмите частотные запросы и сравните старую и новую выдачу: сколько документов исчезло из десятки, сколько новых попало внутрь, как часто первый релевантный документ уехал вниз. Если у вас поиск по базе знаний, запрос вроде "лимит по карте" не должен внезапно тянуть общие страницы вместо точного ответа.

Ошибки индексации лучше считать отдельно от ошибок запроса. Код иногда молчит: часть документов не попала в индекс, потому что вектор пришел другой длины, serializer отрезал хвост или база отклонила запись. По журналам это может выглядеть как мелочь, а по факту вы потеряли 3-5% корпуса.

Есть и еще четыре сигнала, которые часто недооценивают: рост размера индекса после переиндексации, рост памяти на узле поиска, замедление вставки новых документов и рост p95 и p99 задержки поиска.

Если новая модель дает вектор вдвое длиннее, индекс обычно тоже тяжелеет. Тогда поиск может остаться точным, но запись новых данных замедлится, а память начнет заканчиваться в самый плохой момент - во время фоновой переиндексации. Поэтому хороший запуск смотрят не только глазами релевантности, но и глазами эксплуатации.

Ошибки, которые дорого обходятся

Самая частая и самая дорогая ошибка - смешать новые векторы со старыми в одном индексе. На бумаге это выглядит как аккуратная миграция: часть документов уже пересчитали, часть еще нет. На деле поиск начинает вести себя странно. Одни запросы резко улучшаются, другие тихо деградируют, а команда неделями спорит, где именно сломалась релевантность.

Проблема не только в длине вектора. Даже если старая и новая модель выдают одинаковую размерность, их пространство может быть устроено по-разному. Тогда ближайшие соседи уже не означают одно и то же. Индекс продолжает отвечать, ошибок в логах нет, но выдача уводит пользователя в сторону.

Не меньше денег сгорает, когда команда смотрит только на офлайн-метрику. На тестовом наборе новая модель может дать плюс 4-5% к recall, и этого всем хватает для релиза. Потом приходят живые запросы с опечатками, короткими фразами, внутренним жаргоном и смешанным русско-казахским текстом, и картина меняется. Если вы не проверили боевой трафик, вы проверили не поиск, а учебный стенд.

Еще одна ловушка - хранить размерность только в документации или в wiki. Документацию не читает рантайм. Код должен сам проверять длину вектора при индексации и при запросе. Если команда переключает модель через совместимый шлюз, приложение может продолжить работать без изменений в SDK. Это удобно, но из-за этого легче пропустить момент, когда новая модель вернула вектор другой длины.

Часто ломают сразу два слоя: меняют модель эмбеддингов и одновременно меняют чанкинг. После такого релиза никто уже не понимает, что дало эффект. Может, новая модель стала лучше. А может, тексты просто начали резаться на более короткие куски и поиск стал находить очевидные совпадения. Когда вы меняете два параметра сразу, вы теряете причинно-следственную связь.

Самый нервный сценарий - удалить старый индекс до конца A/B-проверки. Так делают из экономии места или чтобы не держать параллельно две версии данных. Потом любой сбой превращается в аврал: отката нет, сравнивать не с чем, а спор о качестве поиска приходится вести по косвенным признакам.

Короткий список проверок

Проверить смену модели
Сравните модели через один OpenAI-совместимый endpoint и меняйте провайдера без правок SDK.

Ошибки с размерностью эмбеддингов редко выглядят как одна большая авария. Чаще поиск сначала тихо портится, а код падает уже потом, когда в базе и индексе успевают смешаться разные векторы.

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

  • Храните у каждого вектора версию модели и имя модели.
  • Проверяйте длину вектора до записи в базу.
  • Держите в одном индексе только один тип эмбеддингов.
  • Добавьте тесты, которые специально пытаются записать в индекс векторы разной длины.
  • Держите готовый откат: старый индекс, старую модель и переключение через конфиг или флаг.

Это легко проверить на простом сценарии. Вы меняете модель эмбеддингов, потому что ждете лучшего качества. Новая модель начинает отдавать вектор другой длины. Если схема базы валидирует размер, ошибка видна на первой записи. Если нет, мусор попадает в индекс, а поиск начинает вести себя странно: часть запросов работает, часть нет, а релевантность плавает без явной причины.

Даже если вы ходите к моделям через единый API-шлюз, эти проверки все равно нужны. Шлюз упрощает смену провайдера и маршрутизацию, но совместимость векторов он за вас не исправит.

Хорошее правило простое: модель меняется только вместе с версией данных, индексом и планом отката. Тогда размерность эмбеддингов перестает быть скрытой миной и становится обычным техническим параметром, который команда держит под контролем.

Что делать дальше

Начните с небольшого набора живых запросов и гоняйте его перед каждой сменой модели. Не берите синтетические примеры вроде "кошка" или "договор". Возьмите 30-50 запросов из реальной работы: короткие, длинные, с опечатками, с редкими терминами. Такой набор быстро покажет, где размерность эмбеддингов вроде бы не мешает коду, но уже портит поиск.

Храните рядом с каждым документом и индексом три вещи: имя модели, размер вектора и дату индексации. Это скучная дисциплина, зато она экономит часы разборов. Когда в базе смешались векторы 1536 и 3072, проблема редко видна сразу. Сначала немного падает качество, потом кто-то ловит ошибку на записи, а позже команда уже не понимает, что именно лежит в индексе.

Даже в маленькой команде полезно разделять четыре задачи: отдельно проверять качество поиска на новой модели, отдельно менять схему хранения при новой длине вектора, отдельно переиндексировать старые документы и отдельно включать новую модель в продакшене. Если слить все это в один релиз, вы не поймете, что дало сбой: новая модель, старый индекс или код записи.

Удобный единый API тоже требует дисциплины. Если команда переключает модели через AI Router и endpoint api.airouter.kz, легко забыть, что для эмбеддингов нельзя полагаться на почти тот же вызов. Такой шлюз ускоряет эксперименты и смену провайдера, но перед записью все равно нужно проверять длину вектора и держать отдельную версию индекса.

Практичное правило простое: любая миграция модели эмбеддингов начинается не с продакшена, а с контрольного прогона. Если новая модель улучшила поиск на вашем наборе запросов, прошла проверку совместимости векторов и не ломает схему, только тогда переносите трафик. В теме эмбеддингов осторожность почти всегда дешевле срочного отката.

Часто задаваемые вопросы

Что ломается первым, когда модель начинает отдавать вектор другой длины?

Сначала ломается контракт данных. База, индекс или сервис похожести ждут, например, 1536 чисел, а получают 3072. В лучшем случае вы сразу увидите ошибку записи. В худшем часть пайплайна проглотит новый вектор, и поиск начнет путать документы без явного сбоя.

Можно просто сменить модель, если API и SDK остались теми же?

Нет, этого мало. Совместимый вызов сохраняет код запроса к модели, но не делает старый индекс совместимым с новыми эмбеддингами. Для нормального перехода держите отдельную версию данных, пересчитайте корпус и проверьте выдачу на живых запросах.

Если обе модели выдают 1536 чисел, можно смешивать старые и новые эмбеддинги?

Нет. Одинаковая длина не значит, что векторы живут в одном пространстве. Если вы смешаете старые эмбеддинги документов с новыми эмбеддингами запросов, поиск не всегда упадет, но начнет давать странный порядок результатов.

Почему поиск может испортиться, хотя мониторинг показывает, что все нормально?

Потому что часть поломок не роняет сервис. Эндпоинт отвечает быстро, очередь не стоит, а релевантность уже просела. Чаще всего это видно по коротким запросам: человек вводит пару слов, а сверху внезапно всплывают близкие по теме, но не нужные документы.

Где код чаще всего падает после смены модели эмбеддингов?

Обычно падает хранилище или индексатор. Колонка vector(1536) не примет 3072, batch job остановится, а часть каталога останется на старых векторах. Еще часто ломаются сериализация между сервисами и кэш, если они не проверяют версию модели и длину массива.

Как перейти на новую модель без тихой поломки поиска?

Самый безопасный путь — вести смену модели как миграцию схемы. Создайте новый индекс рядом со старым, пересчитайте эмбеддинги для корпуса, сравните обе версии на реальных запросах и только потом переводите трафик долями. Старый индекс не удаляйте, пока не увидите стабильный результат.

Что измерять сразу после переключения на новую модель?

Смотрите не только на среднюю точность. Каждый день проверяйте долю пустых результатов, сдвиг в top-10, ошибки индексации, задержку поиска и размер индекса. Если новая модель дала длиннее вектор, память и скорость записи тоже часто проседают.

Нужно ли заново считать эмбеддинги для всех документов?

Да, в обычном случае весь корпус нужно пересчитать. Старые документы и новые запросы должны жить в одном векторном пространстве, иначе вы получите смешанную выдачу. Частичная переиндексация годится только как временный этап, если вы держите отдельные индексы и четко разводите трафик.

Как кэш ломает поиск при смене эмбеддингов?

Кэш легко маскирует проблему. После переключения часть запросов получает новые векторы, а часть — старые из кэша, и система сравнивает несовместимые данные. Разделяйте кэш по версии модели и очищайте его при миграции, иначе разбор займет больше времени, чем сама смена модели.

Если я работаю через AI Router, проблема с размерностью исчезает?

Нет, шлюз не отменяет проверку совместимости. AI Router упрощает смену модели и провайдера через тот же OpenAI-совместимый endpoint, но длину вектора, версию индекса и переиндексацию команда все равно должна контролировать сама. Для эмбеддингов это часть схемы данных, а не мелкая деталь ответа.