
Привет! Меня зовут Дмитрий, я лид отдела семантики в Профи.ру. Мы отвечаем за то, чтобы сервис понимал, какую услугу ищет клиент: когда он вводит «репетитор по английскому», «починить стиральную машину» или «нужен электрик».
На первый взгляд, поиск в сервисе услуг — это довольно типовая задача. Что там может быть особенного? Под капотом лежит каталог услуг, Elasticsearch, нормализация запросов, ранжирование, кеширование. Ничего такого, чего не было бы в других продуктах. Но это только на первый взгляд.
Сейчас расскажу, как устроен поиск в Профи.ру, зачем мы добавили в него LLM и почему языковая модель у нас не заменила классический алгоритм, а стала его дополнительным слоем.
Что для нас значит поиск
Немного о том, как работает поиск на сайте и в приложениях Профи.ру.
Пользовательский путь часто начинается с поиска, но сам сервис поиска нужен не только для работы со строкой на главной странице.
У него есть три основных режима.
Первый и самый частый — определить услугу. Клиент пишет запрос «уроки английского», а мы должны понять, что речь идёт об услуге «репетитор по английскому языку». После этого подключаются другие системы. Они знают, какие вопросы нужно задать клиенту, как сформировать задачу и каким специалистам её показать.


Второй режим — поиск конкретного специалиста. Например, клиент вводит имя или фамилию специалиста и пытается найти его профиль.
Ищем:

Получаем страничку специалиста:

И третий — определить услугу, которую Профи.ру не оказывает. Это не обязательно что-то незаконное: некоторые работы мы не берём из-за требований к документам, рисков или специфики услуги. Например, няня для детей. Если начать искать няню, поиск распознает такой запрос и не поведёт человека по стандартному сценарию оформления заказа.

Кроме того, поиск работает не только на главной. Он вызывается внутри визардов — пошаговых сценариев создания задачи. Пока клиент описывает, что ему нужно, мы можем проверять свободный текст, предлагать более подходящую услугу или вовремя понимать, что запрос относится к категории, с которой сервис не работает.
То есть поиск — это у нас не отдельный экран. Это сервис, который сопровождает клиента на разных этапах пути.
Как устроен базовый поиск
С точки зрения архитектуры всё довольно предсказуемо. Веб, мобильный веб и приложения отправляют запрос в корпоративную шину данных — GraphQL-контур. Через gate он доходит до сервиса поиска, а дальше мы обращаемся к Elasticsearch — основному источнику данных для поиска по услугам.
Индекс обновляется раз в сутки. Это происходит во внутренней админке, в которой можно управлять всеми значениями, и к ней имеют доступ контентные менеджеры. Обычно нам этого достаточно: новая услуга не обязана появляться в поиске в ту же секунду. Но при необходимости, например во время работы с синонимами к какой-то услуге или для проверки изменений, переиндексацию можно запустить вручную. У нас был такой кейс, но не могу сказать, что мы часто пользуемся этой возможностью.
До поиска в Elastic запрос проходит нормализацию. Пользователь может написать одну и ту же потребность десятками способов: «уроки английского», «английский с преподавателем», «нужен репетитор англ», — а иногда ещё и переключить раскладку или ошибиться в одном-двух словах.
Поэтому мы:
приводим слова к базовой форме;
учитываем порядок слов для финального скоринга, но не делаем его жёстким ограничением;
пытаемся исправить неверную раскладку;
игнорируем часть опечаток;
используем синонимы и другие варианты написания услуг;
сравниваем запрос с формулировками из каталога.
Затем начинается основная часть — алгоритмический скоринг. У каждого найденного варианта есть внутренний вес: чем ближе текст пользователя к услуге в нашем каталоге, тем он выше. Полное совпадение получит максимальный вес, а совпадение только по части слов — более низкий.
Например, если пользователь вводит «репетитор по английскому языку», это почти идеальный мэтч. Если пишет просто «репетитор», то в выдачу могут попасть математика, английский, испанский, подготовка к экзаменам и другие направления. В таком случае, помимо текстового соответствия, мы учитываем частотность услуги. При прочих равных более востребованные варианты окажутся выше. Английский — выше испанского, математика — выше английского.

Наша цель проста: клиент должен увидеть нужный вариант в первых строках. Больше 90% выборов должны приходиться на первые пять результатов, а в идеале — на первые три.
Почему поиск должен быть душным и надёжным
Поиск в Профи.ру — не самый тяжёлый сервис в классическом понимании загрузки. В пике он обрабатывает около 150 тысяч запросов в час, то есть чуть более 60 запросов в секунду. При этом нагрузка днём и ночью различается больше чем в десять раз.
Но для нашего продукта он критичен, даже с учётом того, что найти специалиста можно и не только через поиск, а в категоризаторе.

Плюс клиенты привыкли к поиску. И если они не могут физически объяснить, какая услуга им нужна, то, скорее всего, не создадут задачу. Поэтому мы очень стремимся к стабильной и предсказуемой работе поиска. Спойлер: нам это удаётся.
Сейчас базовый поиск обычно отвечает примерно за 150 мс. Порог, после которого мы считаем, что с сервисом что-то не так, — 250 мс на 95-м перцентиле. Ошибки отслеживаем с нулевой терпимостью: уже одна ошибка создаёт warning, а при десяти в команду приходит алерт.
Мы смотрим на это через общую инфраструктуру мониторинга: Zabbix, Metabase, логи и продуктовую аналитику. Видим время ответа на разных перцентилях, число ошибок, клики по позициям выдачи, длину запросов и поведение пользователей в конкретных категориях.
У нас есть аналитика, по которой видно, что большинство клиентов выбирают услугу, не успев набрать десять символов: пик находится примерно в районе семи или даже меньше. Поэтому короткие запросы особенно важны: они частотны, повторяются и хорошо подходят для кеширования в Redis.
Нагрузку также снижает архитектура на фронтенде: мы хорошо организовали дебаунс и тротлинг. Пользователь вводит текст по символам, но не каждый промежуточный вариант улетает на бэкенд: мы отменяем незавершённые запросы. Если человек уже начал вводить следующий символ, то обрабатывать старый запрос часто уже не имеет смысла.
Наконец, у нашего поиска много fallback-сценариев. Если сервис не нашёл подходящую услугу, он не должен оставить пользователя перед пустым экраном. Например, если запрос не похож на название услуги, система может предположить, что клиент искал специалиста, и переключиться в соответствующий режим.
Поиск также работает и внутри конкретной вертикали. В разделе ремонта он не будет предлагать репетиторов, даже если текст запроса потенциально совпадает с чем-то из другой категории. Так мы повышаем предсказуемость выдачи для пользователя. Душно, зато работает.
Как изменился поиск из-за ИИ
Долгое время запросы, которые мы не могли уверенно сопоставить с услугой, были редкими. Для них даже существовал внутренний термин — «белоснежный сценарий»: если поиск не понимал, что именно требуется клиенту, мы предлагали уточнить потребность через дополнительные вопросы.
Несколько лет назад в этот сценарий попадало меньше 1% пользователей. Казалось, что проблема есть, но она не настолько велика, чтобы менять устойчивую архитектуру поиска. Но я постоянно об этом думал и понимал, что недалёк тот день, когда это число увеличится и нам придётся что-то менять.
Так и случилось. Люди начали привыкать к LLM и умным поисковым интерфейсам. Стали чаще описывать задачу своими словами, добавлять контекст, ограничения и детали, которые не укладываются в короткое название услуги.
Вместо «ремонт комнаты» писали «хочу привести в порядок комнату на даче, стены кривые, пол скрипит, а с проводкой, кажется, тоже что-то не так». Вместо «репетитор по английскому» — «ребёнку надо подтянуть английский перед экзаменом, но заниматься хочется индивидуально». Стало понятно: если мы хотим понимать такие запросы, нужна дополнительная логика.
Но отмечу то, что мы при этом не стали заменять Elasticsearch языковой моделью.
Алгоритмический поиск хорошо оптимизирован, предсказуемо работает, быстро отвечает и умеет ранжировать результаты по правилам, которые мы годами проверяли на реальном поведении пользователей. Отказываться от него ради модели было бы странно.
Поэтому LLM сейчас работает как интерпретатор между языком клиента и языком нашего каталога.

С кейсом несуществующей услуги моделька тоже справляется отлично
Каждая услуга в Профи.ру описывается набором признаков. В упрощённом виде это действие, объект и дополнительные свойства. Например:
«Репетитор по английскому языку». Действие — репетиторство. Объект — английский язык.
«Ремонт компьютера». Действие — ремонт. Объект — компьютер.
«Обучение вождению». Действие — обучение. Объект — вождение.
Когда обычный поиск не находит достаточно хорошего совпадения, подключается LLM. Она разбирает свободное описание на эти признаки, формирует гипотезу о нужной услуге и отправляет её обратно в привычный алгоритмический поиск.
Дальше Elasticsearch и наш скоринг делают то, что умеют лучше всего: находят варианты, считают веса, ранжируют выдачу. Модель получает эти результаты и при необходимости может уточнить гипотезу, а не принимать решение на глаз.
Упрощённо пайплайн выглядит так:
Запрос клиента ↓ Обычный алгоритмический поиск ↓ Есть уверенный мэтч? -- да → показываем результат ↓ нет LLM выделяет признаки услуги и строит гипотезу ↓ Алгоритмический поиск проверяет гипотезу ↓ Ранжируем результаты и ведём клиента дальше
Сейчас для этой части мы используем внешнюю общедоступную языковую модель. И хотя внутри у нас развивается и собственная инфраструктура, и модели на своих вычислительных ресурсах, поиск работает в текущем варианте. Пока что.
Что в итоге
Мы довольны тем, что поиск стал лучше понимать естественные формулировки, от которых сейчас никуда не деться. И рады, что для нас это не означало, что надо переписать весь старый сервис или отказаться от накопленной логики скоринга, кеширования, мониторинга и fallback-сценариев. Достаточно просто добавить ещё один слой поверх.
Но на этом точно не остановимся, расслабляться рано: кто знает, как изменится поведение пользователей в ближайшее время?

