Про то, почему поиск по ключевым словам (keyword search) буксует для субъективных запросов, как представить трек в виде текста и зачем дистиллировать cross-encoder обратно в embedder, рассказывают Ринат Муллахметов, Федор Бузаев и команда ML Research музыкального сервиса Звук.
Попробуйте набрать в поиске музыкального сервиса Звук или любого другого стриминга «Романтическая прогулка с супругой по набережной Сочи». Скорее всего, вы не увидите музыкальные треки, полностью подходящие под ваше настроение, Происходит это потому, что традиционный поиск работает с сущностями: названиями, исполнителями, жанрами, плейлистами и ситуативными волнами. А «романтической прогулки с супругой по набережной Сочи» – не сущность. Это намерение.
Музыкальные платформы научились отлично обрабатывать прямые запросы: «Linkin Park», «хиты 90-х», «русский рэп». Но пользователи всё чаще формулируют запросы иначе – через контекст, настроение, ситуацию. «Что-нибудь тягучее и немного грустное для работы в воскресенье вечером». Или: «музыка как будто ты один в большом городе». Это не жанр. Это не тег. Это даже не настроение в понимании классических рекомендательных систем. И традиционный поиск здесь просто разводит руками.
С середины 2010-х в индустрии появился целый жанр – «спотифайкор»: музыка, намеренно заточенная под алгоритмы, а не под слушателя. Приглушённая, среднетемповая – зато легко попадает в плейлист «чилл вечером». Часть артистов пошла другим путём: перезаливает треки с названиями вроде «музыка для работы в кафе» или «романтический вечер дома», собирает плейлисты под ситуации, подкручивает метаданные под поисковые запросы.
По сути, это костыль. Он работает, пока пользователь формулирует запрос предсказуемо. Но люди так не делают.
Каталог Звука насчитывает десятки миллионов треков – и подавляющее большинство из них никак не оптимизированы под «правильные» слова. Поиск по ключевым словам справляется с прямыми запросами, но системно проигрывает на всём, что выражает намерение, а не называет конкретный объект. «Что-нибудь бодрое для утренней пробежки» – это не название трека и не имя артиста. Это ситуация. И именно такие запросы мы учились понимать. Решение, к которому пришли, оказалось интереснее, чем казалось на старте.
Три способа сгенерировать плейлист – и что с ними не так
Прежде чем рассказывать про архитектуру, стоит понять, почему она вообще появилась. У нас было три рабочих подхода, каждый со своими проблемами.
Генерация через LLM. Берёшь GPT-4.1, даёшь ему запрос – он генерирует список треков прямо в тексте: названия, исполнители. Потом сопоставляешь с внутренним каталогом. Звучит просто – и на популярных запросах действительно работает.
Но LLM галлюцинирует. Генерирует исполнителей и треки, которых в каталоге нет. Или есть, но не те: названия слегка перевраны, исполнители перепутаны. Если разбор ответа упал – приходится перезапускать с сообщением об ошибке в промпте. К этому добавляется задержка и стоимость внешних API: каждый запрос – это полноценный прогон LLM.
Есть и менее очевидная проблема. LLM знает музыку так, как она представлена в интернете – то есть хорошо знает хиты и плохо знает всё остальное. Попроси собрать плейлист по запросу «лучший рок» – получишь AC/DC, Queen и Metallica. Не потому что они объективно лучшие для конкретного пользователя, а потому что модель скатывается в ловушку собственных знаний: самые упоминаемые в обучающих данных исполнители доминируют в генерации. Плейлисты получаются предсказуемыми и однообразными – без учёта ни вкусов пользователя, ни глубины каталога.
И это ещё с поправкой на то, что несуществующие треки просто выбрасываются из подсчёта – реальная точность ещё ниже.
Фасетный поиск. Вместо того чтобы генерировать треки напрямую, LLM извлекает из запроса набор фильтров: жанр, настроение, темп, язык, год. Дальше – поиск по индексу с этими фильтрами. Идея разумная, но у неё есть жёсткий потолок: система работает ровно настолько хорошо, насколько полна схема фасетов.
Возьмём запрос «инди-треки, ощущение поздней ночной поездки». Жанр – инди – фасет есть. А «ощущение поздней ночной поездки» – это какой фасет? Правильно, никакого. LLM выберет жанр, возможно угадает настроение вроде «меланхоличный», но атмосферный образ потеряется. Как только запрос выходит за границы словаря тегов, всё, что касается атмосферы, нарративного контекста, ситуативных образов – попадает в серую зону, где фасеты покрывают лишь часть смысла.
И это ещё не всё. Система никак не защищена от ошибок на обоих концах: LLM может некорректно извлечь параметры из запроса или сгаллюцинировать, а разметка самих треков – содержать неверные или противоречивые теги. Оба источника ошибок тихие. Никакого резервного механизма, никакого сигнала, что что-то пошло не так – система просто возвращает результат, который выглядит правдоподобно, но построен на кривом фундаменте.
По метрикам – хуже, чем у LLM-генерации. Более сложная схема, а результат слабее.
Семантический поиск – то, что мы построили. Без галлюцинаций, с нормальной задержкой в продакшене. По качеству – существенно лучше обоих предыдущих подходов (конкретные цифры в разделе про оценку).

Как представить трек текстом – и зачем это вообще нужно
Если хочешь искать по смыслу, нужно уметь кодировать запрос и трек в одно векторное пространство. С запросом всё понятно – это уже текст. А вот трек – нет. И прежде чем объяснять, как мы это решили, стоит рассказать, откуда вообще берётся всё то, что мы о треке знаем.
Первый слой – из базы данных Звука: название трека, альбома, исполнителя, дата выхода, текст песни, наличие ненормативной лексики. Последний признак – не мелочь: для запроса «музыка на детский праздник» это – сигнал первостепенной важности.
Второй слой – из аудиодорожки. Нейросетевые модели типа VGGish и Whisper извлекли жанры, настроения, темп, инструменты, наличие и пол вокала, язык. Это объективные характеристики, которые невозможно получить из текстового описания – их нужно буквально услышать.
Третий слой – из внешних источников. У большого числа треков в Звуке текстов песен просто не было, а текст – признак, без которого семантика рассыпается. Мы собрали несколько ресурсов с исторической информацией о треках, музыкальными тегами, специфическими поджанрами и стилями. На основе этих данных через LLM генерируется поле about – семантическое описание трека в свободной форме.
И вот тут – ключевая инженерная идея. Всю эту мету мы нормализуем в JSON-подобный текстовый дескриптор, объединяющий четыре слоя: конкретные сущности (исполнитель, название, год), объективные музыкальные характеристики (темп, жанры, инструменты), субъективные дескрипторы (настроение, теги, поле about) и часть текста песни. JSON вместо плоского текста – не прихоть: так модель явно «видит» структуру документа и быстрее находит значимые признаки через механизм внимания, вместо того чтобы сканировать однородный поток токенов.

Поле about заслуживает отдельного внимания. Посмотрите на пример выше: для трека Nova Hale – Glass Walls там написано «эмоциональная уязвимость и страх быть по-настоящему увиденным; парадокс открытости, которая ведёт не к близости, а к невидимости». Это не тег. Это смысл. Запрос «что-нибудь про одиночество в толпе» попадёт сюда именно через это поле, а не через жанр Dream Pop. Без about система работала бы как продвинутый фасетный поиск – хорошо на явных тегах, плохо на образах и настроениях. Именно about превращает поиск из «найди совпадение по словам» в «пойми, о чём речь».
Ещё один неочевидный эффект – метаданные исполнителя. Даже если у нового трека мало описательного текста, ассоциация с известным артистом даёт модели сильные семантические ориентиры. А для малоизвестного исполнителя работает другой механизм: новый трек из жанра Dream Pop попадёт в нужную область векторного пространства просто потому, что модель уже понимает, как звучит этот жанр по другим трекам. Для длинного хвоста каталога это критично.
Наша мета богата: название, исполнитель, жанры, настроение, темп, инструменты, вокальность, язык, год выпуска и текст песни. Мы используем gte-Qwen2-7B-instruct, дообученный с функцией потерь InfoNCE на тройках (якорь, положительный пример, отрицательный пример). Механика такая: для каждого якорного запроса модель учится «притягивать» релевантный трек и «отталкивать» нерелевантные в векторном пространстве, максимизируя логарифмическую вероятность правильного выбора среди всех кандидатов в батче. Поиск – по ближайшим соседям в индексе векторных представлений, первые N кандидатов.

Как мы обучали наш семантический поиск треков
Триплеты для обучения
Контрастивное обучение требует пар (запрос, релевантный трек) и (запрос, нерелевантный трек). Разумеется, для всех возможных треков у нас их нет. Как быть?
Решение элегантное: мы инвертировали задачу. Для каждого трека попросили GPT-4.1 сгенерировать пять позитивных запросов и пять сложных негативных запросов (hard negatives) – таких, которые похожи по форме, но не подходят треку по смыслу. Например, для меланхоличного инди-трека hard negative – это запрос вроде «энергичная музыка для утренней пробежки»: формально похоже по структуре, но семантически противоположно. Или запрос про того же исполнителя, но ориентированный на другой его альбом – граничный случай, который заставляет векторную модель учиться тонким различиям. Hard negatives критичны для качества: лёгкие негативные примеры модель и так отличит, лучше сконцентрироваться на пограничных случаях.
Это дало нам синтетические данные из 51 701 трека, все для обучения. Но у синтетики есть сдвиг распределения – реальные пользователи пишут не так как GPT. Запросы от пользователей короче, небрежней, содержат опечатки, культурные отсылки и разговорные формулировки, которые GPT не генерирует. Кроме того, GPT-запросы к музыке как правило слишком «правильные» – они точно описывают трек, тогда как живой пользователь может написать что-то косвенное или метафорическое.
Поэтому параллельно мы разметили данные с экспертами. Использовали такую схему: взяли топ-5 кандидатов от предыдущей продакшен-системы для каждого запроса, отдали на разметку с перекрытием по трём экспертам, финальная метка – голосование большинством. Затем такой процесс повторялся несколько раз на актуальном подходе (семантический поиск) – на нескольких версиях полученных после обучения уже размеченной части. Таким образом, выборка усложнялась наполняясь все более ценными примерами (hard-negatives).. Итого 32 087 пар для обучения и 7 982 для тестирования. Среди аннотаторов – 82% единогласных решений, что для субъективной задачи очень хорошо. Эти данные мы используем для промежуточной оценки в ходе обучения – они структурированные, размечены на уровне треков, подходят для быстрых экспериментов. Для финальной offline-оценки системы мы использовали отдельный датасет от музыкальных экспертов – об этом ниже.
Ни синтетика, ни экспертная разметка по отдельности не дали лучшего результата – нужны оба источника, и не как просто «больше данных», а как два принципиально разных сигнала. Синтетика учит модель музыкальному каталогу в ширину – 51 701 трек, включая малоизвестный и нишевый контент, на который у экспертов не хватило бы ресурсов. Экспертная разметка учит модель живому языку – коротким, небрежным, метафорическим запросам, которые GPT не умеет генерировать. Насколько именно каждый источник влияет на качество – подробно разбираем в разделе про выбор базовой модели.

Выбор базовой модели – и где на самом деле лежит прирост
Эволюция выбора базовой модели хорошо показывает, какие решения дают реальный прирост, а какие – косметический.
Начали мы с bge-m3 – мультиязычной модели, ориентированной на поиск, которая хорошо работает в RAG-сценариях. Без дообучения – 64.1% MAP. Дообучение на синтетических данных с InfoNCE подняло до 68.3%. Результат умеренный: +4.2 процентных пункта, и потолок уже виден.
Перелом случился при замене на gte-Qwen2-7B-instruct – авторегрессионную модель, которая занимает высокие места в рейтинге MTEB. Даже без дообучения она выдаёт 71.2% – то есть «из коробки» уже лучше, чем дообученный bge-m3. Стоит отметить, что сама архитектура на базе более крупной языковой модели Qwen2-7B и это принципиальное отличие от предыдущих подходов.
Вот как менялось качество по мере добавления данных и этапов обучения – всё на базе gte-Qwen2-7B-instruct:

Несколько наблюдений. Синтетика в одиночку даёт скромный прирост поверх сильной базы (+2.3 п.п.) – модель и так много знает. Экспертная разметка даёт больше (+6.9 п.п.), потому что закрывает сдвиг распределения между GPT-стилем и живой речью пользователей. Но лучший результат – при совместном обучении: ни один источник данных не заменяет другой. Это два разных сигнала: синтетика покрывает длинный хвост каталога, а экспертная разметка учит модель реальным формулировкам.
А дистилляция от cross-encoder добавляет ещё 5.4 п.п. – об этом подробнее в разделе ниже.
Отдельно – про то, чего не видно в таблице. Embedder на базе LLM переносит в векторное пространство не только обучающие данные, но и знания о мире. Пример: в нашей разметке нет жанра «шансон», но модель успешно находит треки Михаила Круга по запросу «русский шансон» – потому что эта связь уже живёт в весах. Модели, ориентированные только на поиск, так не умеют: они оперируют лишь тем, что явно прописано в метаданных.
Итого: от bge-m3 до финальной конфигурации – рост больше 20 процентных пунктов. Но главный скачок дала не хитрая функция потерь и не объём данных, а выбор базовой модели с богатыми знаниями о мире.
Проблема коротких запросов – и как мы её решили
Один из нетривиальных инсайтов, который вылез в процессе: короткие запросы плохо обучают модель.
Реальный пользовательский запрос по типу «рок для пробежки» или «что-то грустное» содержит лишь крохотную часть информации о желаемом треке – только один признак из десятков, которые есть в мете. Модель, обученная только на таких запросах, просто не видит полной картины и плохо обобщается на новые формулировки.
Решение: двухэтапные обучение. Сначала мы обучали модель на длинных синтетических запросах, которые GPT генерировал на основе полного JSON-дескриптора трека – такой запрос может описывать сразу и жанр, и настроение, и инструменты, и тематику текста. Модель учится на богатом сигнале и «видит» всю мету. Затем – дообучение на коротких запросах, которые ближе к реальным пользовательским. Первый этап обучения на длинных запросах даёт модели сильную базу, а второй адаптирует её к реальному распределению пользовательских запросов.
Но обучение на длинных запросах создало дополнительную проблему с генерацией hard negatives. Обычный способ – взять случайный трек другого жанра. Но для длинного запроса, который описывает трек со всех сторон, «случайный другой трек» может быть слишком лёгким или, наоборот, слишком похожим по нескольким признакам сразу. Мы решили это через модификацию функции потерь: на основе совпадающих полей в JSON мы считали близость двух треков и использовали её как мягкий лейбл для CosineSimilarityLoss. Два рок-трека близки – функция потерь это учитывает. Рок и рэп далеки – функция потерь это тоже учитывает. Модель учится на градуированных сигналах, а не на бинарном «похоже / не похоже». Важно: этот CosineSimilarityLoss используется только на первом этапе обучения на длинных запросах. На следующем этапе – дистилляция от cross-encoder’а – мы переходим на CoSENT функцию потерь, который работает иначе и решает другую задачу. Об этом подробнее в разделе про дистилляцию.
Cross-encoder: точнее, но дороже
После того как embedder возвращает топ-N кандидатов по ANN search, хочется уточнить ранжирование. Embedder видит запрос и трек по отдельности, кодирует их независимо, а мы считаем их cosine similarity в общем пространстве. Это быстро и масштабируется на миллионы треков – именно поэтому ANN search возможен. Но модель не видит взаимодействие между конкретным запросом и конкретным треком – только их независимые представления. Два трека могут иметь одинаковое cosine similarity с запросом, но один отвечает на него точно, а другой – лишь в одном из нескольких смыслов.
Cross-encoder видит пару (запрос, трек) целиком и потому может учесть тонкие взаимодействия: что именно в описании трека отвечает на конкретный запрос, насколько настроение трека совпадает с контекстом запроса, есть ли семантическое противоречие между словами запроса и тегами трека.

Схема работы нашего cross-encoder reranker:

Обучение Qwen-2.5-14B-Instruct на наших данных экспертной разметки даёт MAP 90.2% и ROC-AUC 87.5%. Для сравнения – GPT-4.1 без дообучения: 87.7% / 85.1%. Дообученная open-source модель обходит проприетарный LLM. Меньшие варианты деградируют: 7B даёт 87.3% / 83.4%, 1.5B – 82.9% / 79.7%. Здесь важен масштаб модели – 14B оказывается необходимым минимумом для качественного ранжирования.
Модель учится, как LLM, на предсказание следующего токена, но с ограничением – выдавать “Yes” или “No”, лосс – бинарная кросс-энтропия только по этим двум токенам. Просто и эффективно – никакой специальной ранжирующей головы поверх LLM не нужно.
Но есть проблема. Cross-encoder нужно прогнать по всем 500+ кандидатам для каждого запроса, чтобы получить парную метрику, один проход модели на пару. При высокой нагрузке это слишком дорого. Один прогон 14B-модели на пару (запрос, трек) занимает порядка 20–40 мс на A100. Звучит быстро – пока не умножишь на 500 кандидатов. Получается 10–20 секунд на один пользовательский запрос – это не продакшен, это исследовательский стенд. Для сравнения: embedder обрабатывает запрос за один проход и ищет по индексу – ответ за доли секунды. К тому же cross-encoder усложняет интеграцию с персонализацией: туда тоже нужно пробросить пользовательский контекст, а это ещё один уровень сложности в продакшен пайплайне.

Knowledge distillation: забрать знания у «дорогого» ранкера
Классический трюк в ML: есть точная, но медленная модель (учитель) – дистиллируй её знания в быструю (ученик).
Наша схема: берём обученный cross-encoder, прогоняем его по большому числу пар (запрос, трек), собираем непрерывные оценки релевантности – но уже не бинарные метки, а вещественные числа от 0 до 1 – оценка для токена Yes, отражающая уверенность модели в релевантности. Потом дообучаем embedder на этих скорах с помощью CoSENT функции потерь.
Что такое CoSENT. Стандартная контрастивная функция потерь типа InfoNCE обучает модель выбирать правильный трек из набора кандидатов – по сути, это softmax-классификация по косинусным сходствам. Модель знает: «вот позитивный пример, всё остальное в наборе – негативные». Это нормально для обучения с нуля, но плохо подходит для дистилляции: у нас есть непрерывный сигнал от модели-учителя, который говорит не «релевантно / нерелевантно», а «насколько релевантно». Эту подробную оценку InfoNCE просто выбрасывает, сводя всё к бинарному сигналу.
CoSENT (Consistent Sentence Embedding) решает именно эту проблему. Его идея: если у нас есть набор пар с известными оценками релевантности, то косинусная близость в векторном пространстве должна сохранять их порядок. Формально: для любой пары (A, B), где score(A) > score(B), должно выполняться cos(query, A) > cos(query, B). Функция потерь штрафует за нарушения этого порядка по всем парам в наборе.
Это принципиально отличается от бинарного подхода. InfoNCE спрашивает: «embedder, ты вообще умеешь отличать релевантное от нерелевантного?» CoSENT спрашивает: «embedder, ты умеешь ранжировать по степени релевантности?» Для задачи плейлист-генерации второй вопрос важнее – нам нужно не просто найти релевантные треки, а правильно их упорядочить.
Дополнительный эффект: прогон cross-encoder по большому числу пар даёт плотный сигнал по сравнению с бинарной разметкой. Больше обучающего сигнала → лучше генерализация при контрастивном обучении.
Результат: MAP нашего embedder’а вырастает с 81.5% до 86.9%. Это почти догоняет cross-encoder (90.2%) при стоимости одного прохода embedder’а против двух для cross-encoder. Разрыв в 3.3% – это цена за то, чтобы не гонять 500+ пар через тяжёлый ранкер на каждый запрос в реальном времени. Для продакшена – это обмен более чем оправданный.
Мы проверили и альтернативные базовые модели для embedder’а после дистилляции: e5-mistral-7b-instruct даёт 83.7%, NV-Embed-v2 – 84.2%. Оба хуже, чем gte-Qwen2-7B-instruct после дистилляции (86.9%). Мы объясняем это не принципиальной разницей в архитектуре, а тем, что gte-Qwen2-7B-instruct лучше приспособлен к конкретным характеристикам нашей дистилляционной схемы и обучающих данных – в частности, к русскоязычным запросам и специфике музыкального домена.
Параллельно с Qwen мы попробовали Giga-Embeddings-instruct – и вот тут стало интереснее. Большинство мультиязычных моделей знают русский примерно как хороший переводчик: технически грамотно, но без чутья. GigaEmbeddings строилась на базе GigaChat-3B, который с самого начала обучался на русскоязычных текстах – не как один из языков, а как основной. На бенчмарке ruMTEB из 23 задач это даёт 69.1 средний балл, и она обходит модели с заметно большим числом параметров.
Мы ощутили разницу именно на живых пользовательских запросах – разговорных, с культурными отсылками, иногда просто небрежно написанных. «Что-нибудь под Новый год как в советских фильмах» или «музыка как из фильмов Балабанова» – это не жанр и не тег, это образ, который нужно понять. Там, где Qwen иногда спотыкался, GigaEmbeddings держался увереннее.
Результат после дистилляции: MAP 88.1% против 86.9% у Qwen. Разница в 1.2 процентных пункта целиком объясняется русскоязычными запросами – на англоязычных двe модели идут практически вровень. Это конкретное преимущество в конкретном домене: русский язык, разговорный стиль, культурный контекст. Для нашей задачи – именно то, что нужно.

Offline-оценка: откуда взялись 81%, и почему это честно
MAP (Mean Average Precision) – наша основная метрика. Она учитывает не только наличие релевантных треков в выдаче, но и их позицию. Также она устойчива к дисбалансу классов – а он здесь огромный: в каталоге из миллионов треков, релевантных для конкретного запроса может быть десяток.
Но метрика – это половина вопроса. Вторая половина – на каких данных считать.
Тестовая выборка из 7 982 пар – нормальный инструмент для промежуточных экспериментов. Но за время разработки к ней неизбежно подстраиваешься: выбираешь гиперпараметры, сравниваешь конфигурации, принимаешь решения – и тестовая выборка постепенно перестаёт быть независимой. Поэтому для финальной оценки мы привлекли команду музыкальных экспертов, которые независимо составили около 500 уникальных запросов и для каждого вручную подобрали релевантные и нерелевантные треки. Этот датасет не участвовал ни в обучении, ни в выборе архитектуры – на нём и считались финальные метрики.
Итого:

Разрыв между фасетным поиском и нашим семантическим – 17 процентных пунктов. С генерацией через LLM – 7 пунктов, но при этом наша система не галлюцинирует и не требует прогона LLM на каждый запрос. После дистилляции разрыв вырастает до 13 пунктов – при той же стоимости обслуживания. Точнее, быстрее и дешевле одновременно. Так бывает редко.
Продакшен: умные колонки и ATS
A/B-тест мы проводили на голосовых запросах с умных колонок. Выбор не случайный: там нет экрана, нет возможности скроллить и переслушивать. Пользователь произносит запрос, слышит первые треки – и либо остаётся слушать, либо нет. Система должна попасть с первого раза. Это самый жёсткий из наших сценариев, и именно поэтому мы начали с него.
Целевая метрика – Average Time Spent (ATS), отражает как вовлеченность, так и релевантность контента. Для экспериментальной группы запросы отправляются в нашу систему. Для контрольной группы – в текущий продакшен, базовое решение на основе поиска готовых плейлистов. Тест проводился на полной пользовательской базе до развертывания.
Результат: стабильный двузначный прирост в процентах ATS – основание для масштабирования системы на большую долю трафика.
Что в итоге
Три вещи, которые работают вместе и которые можно взять отдельно.
Первое – представление трека как структурированного текста с богатыми метаданными и семантическим about полем. Это позволяет LLM-embedder’у кодировать трек так, чтобы абстрактные запросы по типу «музыка для позднего вечера» находили правильные треки не по ключевым словам, а по смыслу. Отдельно стоит отметить, что embedder на базе LLM привносит знания о мире как самостоятельный сигнал ранжирования: даже трек с минимальной или вовсе отсутствующей метой может быть корректно найден – если модель знает, кто этот исполнитель, в каком контексте существует его музыка, и с какими образами она ассоциируется. Принципиальный момент: наша система работает исключительно к текстовом режиме и не требует аудио-обработки. Мы проверяли модель CLAP-based retrieval с общим векторным пространством для аудио и текста, которая сопоставляет запрос напрямую с аудио трека. Звучало привлекательно, но на практике текстовое базовое решение обогнало CLAP на абстрактных запросах: аудио хорошо кодирует тембр и темп, но плохо – смысловые образы. Плюс инженерная сложность с агрегацией сегментных эмбеддингов и обработкой аудио в продакшене. В итоге наш подход дешевле и проще в поддержке.
Второе – гибридный датасет: синтетика от GPT для охвата + экспертная разметка для качества. Ни то, ни другое в одиночку не даёт лучшего результата. Синтетика покрывает длинный хвост каталога, где у экспертов не хватило бы ресурсов. Разметка реальных запросов закрывает сдвиг распределения между GPT-стилем и живой речью пользователей. Совместное обучение на обоих источниках дали нам два разных, но по-своему полезных сигнала.
Третье – дистилляция знания от cross-encoder в embedder через CoSENT функцию потерь с непрерывной оценкой релевантности. Это позволяет получить большую часть прироста от переранжирования без его фактического применения в продакшене. Один проход embedder’а вместо 500+ вызовов cross-encoder. И при этом непрерывные оценки дают embedder’у более тонкий обучающий сигнал, чем бинарная разметка – модель учится не просто разделять релевантное от нерелевантного, а точно ранжировать по степени соответствия.
Фасетный поиск умеет отфильтровывать контент по заготовленным заранее тегам из базы. За его пределами начинается территория, где нужно понимать смысл, а не сопоставлять теги. И эта территория – большая часть того, что пользователи на самом деле хотят от музыкального поиска.
Но здесь мы не думаем останавливаться! Следующее важное для нас направление – персонализация. Причём хотелось бы, чтобы система сразу учитывала историю пользователя, а не подключала коллаборативный сигнал на этапе ранжирования.
Отдельная открытая проблема – обогащение новых треков: сейчас качество поиска напрямую зависит от полноты меты, а значит новый трек без разметки выпадает из системы до тех пор, пока мета не появится. Здесь может пригодиться audio-embedder: в отличие от текстового подхода он способен построить осмысленное представление трека из сырого аудио, без какой-либо предварительной разметки, и тем самым закрыть проблему холодного старта для нового контента.
Автор: Rinatum


