dimhold.by
← Статьи

На середине апгрейда половина моей коллекции набрала 0

2 бампа версии лежали у меня в заметках месяцами, по строчке на каждый: bge-base-en на bge-base-en-v1.5 и e5-base на e5-base-v2. Примечание bge обещает, что поиск стал лучше. e5 выкладывает v2 вообще без примечания, просто с числом повыше в таблице оценок. Оба выглядят как апгрейд, который берут не думая, как берут очередной патч библиотеки.

Единственный вопрос, который я себе задал, был про то, сколько часов уйдёт на повторное кодирование коллекции. Это та часть, за которую приходит счёт. Что бамп делает с ответами, я не мерил ни разу. Я исходил из того, что версия внутри одного семейства это небольшой шаг, а старые векторы и новые лежат примерно там же. В итоге я прогнал 4 пары, 2 семейства в 2 размерах. Это заняло 5 часов процессора против 1.5 на одну пару.

Стенд

SciFact из BEIR: 5183 документа и 300 тестовых запросов с 339 строками разметки. Точный косинус по нормированным векторам, никакого приблизительного индекса, так что ни одно число ниже не приходит от перестройки графа. Python 3.12.3, torch 2.14.0 на процессоре, sentence-transformers 6.0.1, numpy 2.5.3, 4 ядра, GPU нет. Одно кодирование коллекции занимает от 15 минут на маленьких моделях до 85 минут на базовой. На машине была и другая работа, так что эти минуты стоит читать как порядок величины. Вход обрезан на 512 токенах, e5 получает свои префиксы query: и passage: , bge получает инструкцию к запросу из своей карточки.

Прежде чем чему-то верить, я сверил стенд с опубликованными оценками MTEB на том же датасете. 4 модели из 8 попадают в опубликованное число до 4 знака: e5-base на 0.7308, e5-base-v2 на 0.7194, bge-small-en-v1.5 на 0.7127, e5-small на 0.6560. Ещё 3 расходятся в четвёртом знаке. Особняком стоит bge-base-en-v1.5: у меня выходит 0.7404, ровно то, что называет карточка модели BAAI, а репозиторий результатов MTEB называет 0.7435.

Выдача переставилась, оценка нет

300 запросов, что бамп версии делает с первой 10 из старой 10 осталось на месте мест nDCG@10 до после bge-base-en 1 -> 1.5 7.81 0.732 0.740 e5-base 1 -> 2 5.76 0.731 0.719 bge-small-en 1 -> 1.5 7.85 0.700 0.713 e5-small 1 -> 2 4.43 0.656 0.688
4 пары версий моделей на одних и тех же 5183 документах и 300 запросах. Столбик это сколько из старой первой 10 бамп оставил на месте, 2 числа рядом это nDCG@10 до и после. bge base 1 -> 1.5 сохраняет 7.81 места и прибавляет 0.0081, e5 small 1 -> 2 сохраняет 4.43 и прибавляет 0.0315, e5 base 1 -> 2 сохраняет 5.76 и набирает на 0.0114 меньше версии, которую заменяет.

bge-base-en-v1.5 сохраняет 7.81 из старой первой десятки и 85.3% первых мест. nDCG@10 идёт с 0.7323 до 0.7404, recall@10 с 0.8712 до 0.8742. У 240 запросов из 300 nDCG в точности тот же, что был, у 37 лучше, у 23 хуже. То есть на 2.19 места из 10 встал другой документ, а оценка качества шевельнулась в третьем знаке.

e5-small-v2 это громкий случай. Он сохраняет 4.43 из 10 и 59% первых мест. У одного запроса прежний победитель оказывается на месте 1423. nDCG при этом всё равно растёт, с 0.6560 до 0.6875. e5-base-v2 меняет 4.24 места из 10 и набирает меньше версии, которую заменяет: 0.7194 против 0.7308. Ровно то же говорит MTEB.

Перестановка садится туда, где прежний порядок держался на монетке. У bge-base-en у запросов, сохранивших своего победителя, отрыв от второго места был 0.0221 в среднем, а у тех, кто победителя потерял, 0.0033. Этот зазор идёт в одну сторону во всех 4 парах. Чего перевороты стоят, вопрос отдельный, а точек у меня всего 4: в 3 из них запросы, потерявшие победителя, всё равно вышли в плюс, те 44 запроса bge ушли с 0.3959 до 0.4135. Базовая пара e5 это исключение: 80 запросов потеряли победителя и упали с 0.5020 до 0.4451.

2 способа сломаться

два способа сломаться, по столбцу на каждый тот же документ в двух версиях 0 1 первая десятка выше 0.8 до после bge-base-en 1 -> 1.5 0.892 97.5% 1.8% e5-base 1 -> 2 0.606 82.1% 75.6% bge-small-en 1 -> 1.5 0.914 99.9% 8.3% e5-small 1 -> 2 -0.007 98.5% 98.6%
Те же 4 пары, 2 вещи, которые могут сломаться. Слева то, куда две версии кладут один и тот же документ: 0.892 у базовой пары bge против -0.007 у маленькой e5. Справа то, сколько из первой 10 проходит фиксированное отсечение 0.8. Пара bge геометрию сохраняет и уводит долю выше отсечения с 97.5% на 1.8%. У маленькой пары e5 эта доля держится на 98.6% против 98.5%, а геометрии у неё уже не осталось.

Бамп bge сохраняет геометрию. Один и тот же документ в двух версиях стоит сам к себе на косинусе 0.892. Новый запрос по старому индексу даёт nDCG 0.7261 против 0.7323 нативных, то есть устаревший индекс всё ещё отвечает. Уехала шкала. Отсечение cos > 0.8 это как раз то число, которое оседает в конфиге. Через него проходит 97.5% оценок первой десятки до бампа против 1.8% после. 2 случайных документа коллекции стояли на 0.82 в старой версии и стоят на 0.58 в новой. bge-small-en делает то же самое жёстче, с 99.9% до 8.3%.

Про эту часть было объявлено, а я не прочитал. В примечании к выпуску v1.5 сказано, что модели смягчают проблему распределения похожести. Это ровно то самое число. Я прочитал в том же предложении слова про то, что поиск стал лучше, на этом и остановился.

e5 small ломается наоборот. Доля, проходящая через это отсечение, почти не двигается: 98.5% против 98.6%. А один и тот же документ в двух его версиях стоит на косинусе -0.007. Поиск по старому индексу с новым запросом даёт 0.0021 там, где старая модель по своему индексу даёт 0.6560. Я довольно долго искал ошибку знака в своём коде, прежде чем принял это число. Базовая пара e5 стоит посередине: тот же документ на 0.6061, доля выше отсечения с 82.1% до 75.6%.

Одно число тут заслуживает предупреждения, потому что именно на него обычно смотрит дашборд. На бампе e5 small средняя похожесть первого попадания ровная, 0.8682 до и 0.8724 после. По ней нельзя узнать, что пространство уехало под тобой. В по-настоящему сломанном состоянии она не молчит: среднее первое попадание 0.0573, выше 0.8 оказывается 0% первой десятки.

Состояние посередине

половина коллекции переэмбеддена, половина нет запросы, чей размеченный документ в переэмбедденной половине запросы, чей документ остался в старой nDCG@10 bge-base-en 1 -> 1.5 0.8011 0.4868 e5-base 1 -> 2 0.7670 0.0000 bge-small-en 1 -> 1.5 0.7261 0.5799 e5-small 1 -> 2 0.7153 0.0000
Индекс, в котором каждый второй документ закодирован заново, прочитанный по тому, в какой половине лежит размеченный документ. У 143 запросов вся разметка в переэмбедденной половине, у 148 в нетронутой, у 9 в обеих, поэтому они на диаграмму не попали. На готовом индексе те же 2 группы у базовой пары bge расходятся на 0.0136, то есть разрыв здесь делает смешанный индекс.

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

У bge-base-en-v1.5 половинный индекс набирает 0.6408, ниже обоих чистых концов. Переэмбедденная половина забирает 83.7% всех мест первой десятки против 48.9% на готовом индексе. Запросы, чей размеченный документ успели переэмбеддить, набирают 0.8011, то есть больше, чем на готовом индексе. Запросы, чей документ ещё ждал, набирают 0.4868. На полном индексе те же 2 группы набирают 0.7335 и 0.7471. Значит разделение делает смешанный индекс. У 9 запросов из 300 разметка лежит в обеих половинах, поэтому они не попали ни в одну группу.

Обе пары e5 доводят это до конца. 100% мест уходит переэмбедденной половине. Запросы, чей документ ещё ждёт, набирают 0.0000, то есть 0 релевантных документов на 1480 мест, которые достаются этим 148 запросам. Почему выигрывает переэмбедденная половина, я могу объяснить только для одного семейства. У e5-base две половины стоят на разных шкалах: средний косинус 0.7289 к новой стороне против 0.4391 к старой, а у e5-small разрыв ещё резче, 0.7774 против -0.0146. У bge-base-en средние идут в обратную сторону, 0.4623 против 0.4861, значит места там забирает не среднее. Что именно, я не знаю.

Поворот вместо перестройки

Про это есть статья, Drift-Adapter (arXiv:2509.23471). Она подгоняет небольшое отображение из нового пространства в старое и заявляет 95-99% от recall полной перестройки. На этих данных ортогональное отображение Прокруста, подогнанное на 4000 пар документов, даёт nDCG 0.7317 для базовой пары bge против 0.7323 нативных, 0.7177 против 0.7308 для базовой e5 и 0.5606 против 0.6560 для маленькой e5. Последнее это 85% пути назад между двумя пространствами, у которых средний косинус на одном и том же документе равен 0.

Я читал это как провал, пока не заметил, что отвечаю на другой вопрос. Статья считает Recall@10 против полной перестройки. На этой метрике данные дают 100.2% для базовой bge, 99.2% для базовой e5, 97.8% для маленькой bge и 86.1% для маленькой e5. 3 пары из моих 4 попадают в заявленную полосу. Хуже их выглядеть заставляла моя собственная рамка.

Мои 4000 пар это и не та небольшая выборка, о которой говорит статья. Это 4000 из 5183 документов того самого индекса, по которому потом идёт поиск. И 214 из 283 размеченных документов лежат среди них. Поэтому я подогнал то же отображение с bge-small-en на неё же саму, где верный ответ это единичная матрица. На 200 парах оно теряет 0.036, 0.6635 против 0.6995, а на 4000 не теряет ничего. Это убивает отговорку, которой мне хотелось: на том размере выборки, что я взял, зазор выше это дрейф, а не цена дешёвой подгонки.

Чего я не проверял

Приблизительного индекса не было, так что числа про то, что добавляет сверху граф HNSW, у меня нет. Одна коллекция, одна область, только английский. Прыжка между вендорами, про который написана большая часть разборов, нет нигде. Модели bge v1.5 я прогнал с инструкцией к запросу, хотя их карточка называет её необязательной.

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