#common
Сидите вы себе спокойно, разрабатываете поиск каких-нибудь объектов. Может это товары, может странички вики, может код. Что угодно. Ваш поиск — качественный keyword search.
Почесали вы репу и решили, что надо улучшать качество. Обучили новую модель, которая умеет понимать СмЫсЛ и выдавать эмбеддинги объектов. Эмбеддинги вы складываете в какую-нибудь векторную БД. Поиск там работает из коробки.
Вопрос: как интегрировать два поиска?
Проблема тут понятная: так как модели поиска работают различно, взять скор каждого объекта и сравнить со скорами объектов из другого поискового движка просто нельзя (это не имеет смысла, ведь в одном движке скор может быть от 0 до 1, а в другом [0; 1000]; считаются по-разному, означают разное).
Что делать?
Reciprocal Rank Fusion (RRF) — алгоритм объединения нескольких списков результатов поиска в один общий. Идея за ним простая: вместо сравнения сырых скоров из разных поисковых систем мы будем смотреть только на позицию айтема в изначальных списках.
Выражается формулой:
doc — документ
N — количество поисковых систем/движков
rank_i(doc) — позиция документа doc в i-м списке
k — некоторая константа, положим 60.
Фактически, чем выше документ находится в разных списках, тем больше его итоговый RRF score.
Давайте быстрый пример. Keyword search выдал (номер-документ):
а vector search:
Тогда
Итоговый результат выглядит так:
D и E можем опустить по некоторому трешхолду или просто потому что они не встретились в результатах обоих движков.
Почему мы взяли k=60?
Понятно, что большее значение k уменьшает влияние первых позиций, а меньшее наоборот: сильнее их награждает.
А 60 это просто значение, использовавшееся в оригинальной статье. Авторы его как-то примерно подобрали и потом отталкивались во всей статье от конкретного значения в формуле.
Плюсы RRF:
• не требует никакого обучения. Простой математический метод.
• очень легко реализовать. Джуна посадите и чекните PR через день.
• неплохо работает даже в самом базовом виде. Без подкручивания k.
• устойчив к разным шкалам score, потому что их не использует.
• очень легко добавлять новые движки.
Минус один, но жирный: не учитывает, на сколько разница между позициями сильная.
Оригинальные скоры для соседних документов в рамках одного движка могут различаться очень сильно, но из-за высокой позиции документ всё ещё может оказаться на высоком месте в итоговом результате.
Фактически RRF — первый бейзлайн для гибридного поиска. Но если хочется качество получше (то есть перестать просто «смешивать» документы, а реально понимать «что лучше показать юзеру»), нужно обучать отдельную модель, которая переранжирует результаты ещё раз. Может она даже будет тяжёлой, но в силу того, что работать ей надо не со всем набором документов, а только с топ-X найденных, это обычно не проблема.
@thisnotes. Patreon, newsletter.
Спасибо Artyom Garkavy и niki4smirn.
Сидите вы себе спокойно, разрабатываете поиск каких-нибудь объектов. Может это товары, может странички вики, может код. Что угодно. Ваш поиск — качественный keyword search.
Почесали вы репу и решили, что надо улучшать качество. Обучили новую модель, которая умеет понимать СмЫсЛ и выдавать эмбеддинги объектов. Эмбеддинги вы складываете в какую-нибудь векторную БД. Поиск там работает из коробки.
Вопрос: как интегрировать два поиска?
Проблема тут понятная: так как модели поиска работают различно, взять скор каждого объекта и сравнить со скорами объектов из другого поискового движка просто нельзя (это не имеет смысла, ведь в одном движке скор может быть от 0 до 1, а в другом [0; 1000]; считаются по-разному, означают разное).
Что делать?
Reciprocal Rank Fusion (RRF) — алгоритм объединения нескольких списков результатов поиска в один общий. Идея за ним простая: вместо сравнения сырых скоров из разных поисковых систем мы будем смотреть только на позицию айтема в изначальных списках.
Выражается формулой:
RRF(doc) = SUM( 1 / (k + rank_i(doc) ), i in [1; N]
doc — документ
N — количество поисковых систем/движков
rank_i(doc) — позиция документа doc в i-м списке
k — некоторая константа, положим 60.
Фактически, чем выше документ находится в разных списках, тем больше его итоговый RRF score.
Давайте быстрый пример. Keyword search выдал (номер-документ):
1 A
2 B
3 C
4 D
а vector search:
1 C
2 A
3 E
4 B
Тогда
RRF(A) = 0.0325
RRF(B) = 0.0317
RRF(C) = 0.0323
RRF(D) = 0.0156
RRF(E) = 0.0159
Итоговый результат выглядит так:
1 A
2 C
3 B
D и E можем опустить по некоторому трешхолду или просто потому что они не встретились в результатах обоих движков.
Почему мы взяли k=60?
Понятно, что большее значение k уменьшает влияние первых позиций, а меньшее наоборот: сильнее их награждает.
А 60 это просто значение, использовавшееся в оригинальной статье. Авторы его как-то примерно подобрали и потом отталкивались во всей статье от конкретного значения в формуле.
Плюсы RRF:
• не требует никакого обучения. Простой математический метод.
• очень легко реализовать. Джуна посадите и чекните PR через день.
• неплохо работает даже в самом базовом виде. Без подкручивания k.
• устойчив к разным шкалам score, потому что их не использует.
• очень легко добавлять новые движки.
Минус один, но жирный: не учитывает, на сколько разница между позициями сильная.
Оригинальные скоры для соседних документов в рамках одного движка могут различаться очень сильно, но из-за высокой позиции документ всё ещё может оказаться на высоком месте в итоговом результате.
Фактически RRF — первый бейзлайн для гибридного поиска. Но если хочется качество получше (то есть перестать просто «смешивать» документы, а реально понимать «что лучше показать юзеру»), нужно обучать отдельную модель, которая переранжирует результаты ещё раз. Может она даже будет тяжёлой, но в силу того, что работать ей надо не со всем набором документов, а только с топ-X найденных, это обычно не проблема.
@thisnotes. Patreon, newsletter.
Спасибо Artyom Garkavy и niki4smirn.
❤19🔥6👍4😁1
#cpp
Представьте вот такой код:
Где
Как вы будете писать код в (1)?
Вы наверное проверите какие-то свойства вашего
Или можете вообще сравнить его с чем-то (вдруг там всё-таки какое-то конкретное значение появилось):
Делаете ли вы что-то неправильное? Должен ли компилятор вам подсказать проблему в таком коде? Может UBsan? Может clang-tidy?
Да нет.
Более того, даже если вы сделаете что-то такое:
Вы возможно ничего плохого не сделали. Вы просто не знаете, что точно там лежит.
Конечно, проблемы могут возникнуть, если вы, не проверив состояние вашего объекта, будете предполагать, что он обладает какими-то свойствами. Например, думать, что он не пустой. Или что он содержит сколько-то каких-то элементов. Это всё может быть, но вообще-то мы точно утверждать не можем.
Использовать объект после
Вы опять же про этот
Чем эти примеры отличаются от работы с переменной после
Да ничем.
Мы так яро привыкли к правилу «использовать переменную после
Хотя никакого UB тут нет. UB может возникнуть от того, что вы пользуетесь объектом, предполагая, что у него есть какие-то свойства. Точно так же и в нашей функции
И к сожалению эта ошибка стала достаточно частой, чтобы мы выработали ощущение неправильности происходящего. Само действие стало табу. В clang-tidy вон проверка отдельная есть (которую приходится игнорировать с
Отсюда рождаются ментальные модели вида
• «после std::move ничего нельзя»
• «после std::move можно только переприсваивать или уничтожать объект, всё остальное запрещено».
Это упрощения, которые мы себе придумали, чтобы меньше думать. И они почти всегда работают. Но иногда всё же нет.
Давайте думать и не вестись на вот эти попытки наших развитых эволюционировавших мозгов упрощать. Это мы профессионально ленимся.
P.S. Важно подчеркнуть ещё один факт.
Состояние объекта после
А для стандартной библиотеки довольно просто понять, можно ли вызывать метод у мувнутого объекта: у метода не должно быть precondition. Например у
@thisnotes. Patreon, newsletter.
Спасибо Artyom Garkavy и niki4smirn.
Представьте вот такой код:
auto object = GetObject(params...);
auto another = object;
MessAround(object);
// (1) you want to write some logic here
Где
MessAround — функция, которая непредсказуемым образом меняет значение вашего object. То есть значение соответствует всем инвариантам, но вы не знаете, какое конкретно. Как вы будете писать код в (1)?
Вы наверное проверите какие-то свойства вашего
object. Может он там empty()/isNull()/valid(). Или может вы сразу решите его почистить: clear(). Или может присвоить что-нибудь туда захотите: object = Object{1, 2, 3}.Или можете вообще сравнить его с чем-то (вдруг там всё-таки какое-то конкретное значение появилось):
if (object == "str_val"_obj) {...}
Делаете ли вы что-то неправильное? Должен ли компилятор вам подсказать проблему в таком коде? Может UBsan? Может clang-tidy?
Да нет.
Более того, даже если вы сделаете что-то такое:
auto first = object["first"];
Вы возможно ничего плохого не сделали. Вы просто не знаете, что точно там лежит.
MessAround туда могло подложить что угодно. Конечно, проблемы могут возникнуть, если вы, не проверив состояние вашего объекта, будете предполагать, что он обладает какими-то свойствами. Например, думать, что он не пустой. Или что он содержит сколько-то каких-то элементов. Это всё может быть, но вообще-то мы точно утверждать не можем.
Использовать объект после
MessAround это примерно то же самое, что написать функцию, решающую некоторую задачу в вакууме. На примере функции, решающей любую задачу:
void solve(Object obj) {...}
Вы опять же про этот
obj ничего не знаете. Вам надо проверить разные корнер кейсы. А потом как-то там решать вашу проблему. Чем эти примеры отличаются от работы с переменной после
std::move? Да ничем.
Мы так яро привыкли к правилу «использовать переменную после
std::move опасно», что в нашей ментальной модели это часто равносильно undefined behaviour. Уже само это действие вызывает подозрения. Code smell так называемый. Хотя никакого UB тут нет. UB может возникнуть от того, что вы пользуетесь объектом, предполагая, что у него есть какие-то свойства. Точно так же и в нашей функции
solve, которая может не учесть какие-то потенциальные входные данные. Пытаться разыменовать пустой указатель, не проверив его, неправильно. И к сожалению эта ошибка стала достаточно частой, чтобы мы выработали ощущение неправильности происходящего. Само действие стало табу. В clang-tidy вон проверка отдельная есть (которую приходится игнорировать с
NOLINT). Отсюда рождаются ментальные модели вида
• «после std::move ничего нельзя»
• «после std::move можно только переприсваивать или уничтожать объект, всё остальное запрещено».
Это упрощения, которые мы себе придумали, чтобы меньше думать. И они почти всегда работают. Но иногда всё же нет.
Давайте думать и не вестись на вот эти попытки наших развитых эволюционировавших мозгов упрощать. Это мы профессионально ленимся.
P.S. Важно подчеркнуть ещё один факт.
Состояние объекта после
std::move — всем известное «valid but unspecified». По стандарту это работает для стандартной библиотеки. Но кажется, мы уже настолько к этому привыкли, что считаем это данностью в любом случае. Не то чтобы это ожидание обосновано. Авторы библиотек делают что хотят. Но наверное писать ломающий это ожидание код всё-таки не надо. А для стандартной библиотеки довольно просто понять, можно ли вызывать метод у мувнутого объекта: у метода не должно быть precondition. Например у
std::vector::front он есть: !empty(); у std::vector::clear такого нет. @thisnotes. Patreon, newsletter.
Спасибо Artyom Garkavy и niki4smirn.
👍14👎3
Фиксируем результаты.
Некоторые вещи мне не помогут писать посты. Но мне было интересно узнать, откуда у меня аудитория, например.
• География (>560).
РФ 65%, 11% Беларусь, несколько ожидаемых для меня стран одинакового размера (Сербия, Польша, UK по паре процентов). Чуть больше на остальную европу и столько же на остальной мир (по 8%).
• Род занятий (≈560).
86% разработчики. Ещё 5% — ML-enjoyers. В остальном либо очень мало, либо непонятно кто.
• Опыт (≈570).
Тут всё согласно нормальному распределению (что интересно, учитывая произвольность выбранных интервалов). Большинство (26%) 3-5 лет. По 20% на 1-3 и 5-10. По 14% на <1 года и 10-20 лет. И 6% имеют >20 лет.
Подтвердил ожидания.
Приятно, что вы разные: молодые неопытные и взрослые видавшие коллеги.
• Грейд (≈560).
8% не работают. Я надеюсь, это ваш осознанный выбор.
Столько же студентов/стажёров и джунов. 32% middle. 29% senior. Всё до этого момента выглядит ожидаемо.
6% подписчиков переросли senior (уважаемо, особенно если вы не просто себе лычку выбили). Порядка 6% — руководители (в основном IC, но есть и пару M2+, и даже один C-level, кто бы вы ни были).
Фактически получается, что аудитория у меня всё-таки больше опытная, чем неопытная. Это конечно хорошо, но возможно с толканием базы надо быть осторожнее.
• Размер компании (545).
Не то чтобы это радикально важно, но даёт примерную картинку, насколько разные инженерные практики вы видите. Потому что компания размером 50 явно отличается от компании в 20к человек. Хотя и одна компания в 20к может радикально отличаться от другой компании в те же 20к.
Значит вы разнообразные ещё и в самом опыте, и в проблемах, с которыми приходится сталкиваться.
23% в супергигантах каких-то. Почти 40% в небольших компаниях до 1000 человек. Остальные в серединке.
• ЯП (520).
Ожидаемо, большинство из вас трогает C++ (75%), C (21%), Python (47%) и Go (21%). Остальное поменьше. Rust всего 7%. Молодой ещё видимо.
Java/Kotlin и Js/Ts по 10%.
На Cobol никто не пишет. Слава богу.
• ЯП интересы (≈470).
Rust увлекает почти половину подписчиков (меня пока нет). Остальное +/- матчится с тем, что фактически используется.
• Сфера (≈550).
Половина — бэкендеры. Всё остальное в меньшинстве.
Получается, средний подписчик — C++ бэкендер. Это довольно необычное сочетание для мира разработки.
И я такой же. И такая комбинация мне очень нравится. Хорошо, что мы друг друга нашли.
• Интересы (≈460).
>40 % — backend. ≈30% — DB. 56% — low-level performance (значит можно и даже нужно больше уделять этому внимания). 38% конкретно backend performance (и сюда). 54% concurrency (и сюда!!).
Пятая часть подписчиков ещё интересуются engineering management (значит я буду время от времени что-то про это писать, но не увлекаться).
• Карьерная задача (≈510).
1/10 ищет первую работу. Успехов вам.
1/4 растёт до senior. Если будете делать правильные вещи, то получится достаточно быстро. Тут важно быть надёжным работягой и не подводить важных людей. Вы этому научитесь.
13% хотят вырасти дальше senior. Я тут концептуально понимаю, как это, но на практике пока не наработал. Давайте разбираться вместе.
8% хотят двигаться в сторону EM и дальше (см. канал Андрея и его community).
7% и 14% процентов хотят в бигтех или более технический домен. Значит вам нужно понять, чего ждут от хорошего кандидата и пойти усиленно над этим работать. Если не понимаете, что надо, приходите ко мне поговорить.
1/5 кайфует от жизни. Рад за вас!
• Уровень понимания (≈490).
1/4 понимает всё.
45% тратит некоторые усилия на вникнуть. То есть не прям очевидно, но достаточно понятно.
1/5 чуть сложнее. Улавливают концепции, но есть сложности с деталями.
Меньшинству очень сложно. Приходите с вопросами, чего вы!
5% шутников не вникают, потому что им не интересно. Ну штош!
Получается, что большинство понимает всё довольно успешно. Варианта 2: либо я пишу очень понятно (круто); либо топики базовичковые (надо грузить вас чем-то более сложным).
Повторим когда-нибудь, когда вас станет гораздо больше. Будем думать, как это применять теперь мощно.
Некоторые вещи мне не помогут писать посты. Но мне было интересно узнать, откуда у меня аудитория, например.
• География (>560).
РФ 65%, 11% Беларусь, несколько ожидаемых для меня стран одинакового размера (Сербия, Польша, UK по паре процентов). Чуть больше на остальную европу и столько же на остальной мир (по 8%).
• Род занятий (≈560).
86% разработчики. Ещё 5% — ML-enjoyers. В остальном либо очень мало, либо непонятно кто.
• Опыт (≈570).
Тут всё согласно нормальному распределению (что интересно, учитывая произвольность выбранных интервалов). Большинство (26%) 3-5 лет. По 20% на 1-3 и 5-10. По 14% на <1 года и 10-20 лет. И 6% имеют >20 лет.
Подтвердил ожидания.
Приятно, что вы разные: молодые неопытные и взрослые видавшие коллеги.
• Грейд (≈560).
8% не работают. Я надеюсь, это ваш осознанный выбор.
Столько же студентов/стажёров и джунов. 32% middle. 29% senior. Всё до этого момента выглядит ожидаемо.
6% подписчиков переросли senior (уважаемо, особенно если вы не просто себе лычку выбили). Порядка 6% — руководители (в основном IC, но есть и пару M2+, и даже один C-level, кто бы вы ни были).
Фактически получается, что аудитория у меня всё-таки больше опытная, чем неопытная. Это конечно хорошо, но возможно с толканием базы надо быть осторожнее.
• Размер компании (545).
Не то чтобы это радикально важно, но даёт примерную картинку, насколько разные инженерные практики вы видите. Потому что компания размером 50 явно отличается от компании в 20к человек. Хотя и одна компания в 20к может радикально отличаться от другой компании в те же 20к.
Значит вы разнообразные ещё и в самом опыте, и в проблемах, с которыми приходится сталкиваться.
23% в супергигантах каких-то. Почти 40% в небольших компаниях до 1000 человек. Остальные в серединке.
• ЯП (520).
Ожидаемо, большинство из вас трогает C++ (75%), C (21%), Python (47%) и Go (21%). Остальное поменьше. Rust всего 7%. Молодой ещё видимо.
Java/Kotlin и Js/Ts по 10%.
На Cobol никто не пишет. Слава богу.
• ЯП интересы (≈470).
Rust увлекает почти половину подписчиков (меня пока нет). Остальное +/- матчится с тем, что фактически используется.
• Сфера (≈550).
Половина — бэкендеры. Всё остальное в меньшинстве.
Получается, средний подписчик — C++ бэкендер. Это довольно необычное сочетание для мира разработки.
И я такой же. И такая комбинация мне очень нравится. Хорошо, что мы друг друга нашли.
• Интересы (≈460).
>40 % — backend. ≈30% — DB. 56% — low-level performance (значит можно и даже нужно больше уделять этому внимания). 38% конкретно backend performance (и сюда). 54% concurrency (и сюда!!).
Пятая часть подписчиков ещё интересуются engineering management (значит я буду время от времени что-то про это писать, но не увлекаться).
• Карьерная задача (≈510).
1/10 ищет первую работу. Успехов вам.
1/4 растёт до senior. Если будете делать правильные вещи, то получится достаточно быстро. Тут важно быть надёжным работягой и не подводить важных людей. Вы этому научитесь.
13% хотят вырасти дальше senior. Я тут концептуально понимаю, как это, но на практике пока не наработал. Давайте разбираться вместе.
8% хотят двигаться в сторону EM и дальше (см. канал Андрея и его community).
7% и 14% процентов хотят в бигтех или более технический домен. Значит вам нужно понять, чего ждут от хорошего кандидата и пойти усиленно над этим работать. Если не понимаете, что надо, приходите ко мне поговорить.
1/5 кайфует от жизни. Рад за вас!
• Уровень понимания (≈490).
1/4 понимает всё.
45% тратит некоторые усилия на вникнуть. То есть не прям очевидно, но достаточно понятно.
1/5 чуть сложнее. Улавливают концепции, но есть сложности с деталями.
Меньшинству очень сложно. Приходите с вопросами, чего вы!
5% шутников не вникают, потому что им не интересно. Ну штош!
Получается, что большинство понимает всё довольно успешно. Варианта 2: либо я пишу очень понятно (круто); либо топики базовичковые (надо грузить вас чем-то более сложным).
Повторим когда-нибудь, когда вас станет гораздо больше. Будем думать, как это применять теперь мощно.
❤22👍7