Выкатили векторный поиск на бою агрегатора под сотню тысяч уников в сутки, убрали старые фильтры, открыли шампанское. Через десять минут p99 улетел за шесть секунд, база легла под тяжестью HNSW-графов, а по запросу «красная камри до двух миллионов» выдача радостно показала красные гранты, потому что косинусное расстояние решило, что это тоже неплохие красные автомобили. Утром с позором откатились на Sphinx. Семантический поиск по объявлениям недвижимости и авто: находить по смыслу, а не по точному фильтру — это мантра, которую сейчас продают на каждой технической конференции. Но почти все делают это неправильно. В индустрии классифайдов сложился опасный карго-культ: берем готовый Sentence-Transformer из открытого доступа, втыкаем его в pgvector, называем это искусственным интеллектом нового поколения и ждем кратного роста конверсии. Иллюзия рушится при первом же столкновении с реальным трафиком, сложными запросами и деньгами пользователей.
Проблема, которую мы все пытаемся решить на этапе Middle of the Funnel, предельно понятна и осязаема бизнесом. Пользователь больше не хочет кликать по бесконечным чекбоксам и выпадающим спискам. Формат потребления изменился. Человек пишет в поисковую строку агрегатора «светлая двушка рядом со школой на севере» или «живой дизельный крузак не битый один владелец». Традиционные поисковые движки на базе инвертированного индекса здесь абсолютно бесполезны. Они ищут пересечения токенов. Нет токена «школа» в описании квартиры — карточка улетает в небытие, даже если школа буквально видна из окна на фотографиях и указана на карте. Выдача пустеет. Доля нулевых результатов растет по экспоненте. Пользователь закрывает вкладку и уходит к конкурентам. Конверсия в контакт падает на ровном месте просто потому, что машина не смогла перевести человеческий язык в набор жестких фильтров. Ответом на эту боль стала слепая вера инженеров в плотные эмбеддинги.
Векторный поиск действительно умеет ловить синонимы, прощать чудовищные опечатки и понимать разговорные формулировки. Многомерное пространство понимает, что «однушка», «единичка» и «однокомнатная» — это одна и та же смысловая точка. Но чистый эмбеддинг проседает на точных фильтрах с такой сокрушительной силой, что убивает весь пользовательский опыт на корню. Векторная математика ничего не знает про жесткие ограничения реального мира. Для нейросети квартира за одиннадцать миллионов и квартира за двенадцать миллионов находятся в одной тесной смысловой окрестности, они семантически практически идентичны. Для покупателя с жестко одобренным ипотечным лимитом ровно в одиннадцать миллионов эта разница критична и непреодолима. Чистый вектор радостно покажет ему то, что он физически не может купить. То же самое касается геопозиции. Вектор не понимает, что другой берег реки без моста — это два часа по пробкам, для него это просто соседние координаты с похожим описанием.
Единственный рабочий подход в суровых реалиях классифайдов — это гибридный поиск. Никаких компромиссов и никаких инноваций ради инноваций. Вы берете классический BM25 для точного лексического совпадения, добавляете плотные векторы для семантики и накрываете всю эту конструкцию железобетонными структурными фильтрами по гео, цене и базовым атрибутам. Это архитектура в два или три жестко разделенных этапа. На первом этапе, стадии первичного retrieval, вы безжалостно отсекаете корпус. Если человек ищет машину в пределах МКАД строго до трех миллионов рублей, векторный индекс даже не должен пытаться смотреть на премиальные внедорожники в Казани за пять миллионов. Вы фильтруете базу метаданными на уровне движка, получаете узкий кандидатский пул и только внутри него делаете поиск ближайших соседей по векторам, параллельно пробивая BM25. На выходе получаете два списка кандидатов, которые нужно слить воедино. Дальше в дело вступает ранжирование. Второй этап, reranking, где тяжелая ML-модель переупорядочивает эту солянку, учитывая кликовые вероятности.
Мусорные карточки означают мусорный поиск, и это аксиома. Если ленивый продавец написал в описании «уютное гнездышко для молодой семьи», но забыл заполнить площадь, этаж и материал стен, никакой передовой трансформер не вытащит из этого текста фактуру. Текст объявления часто содержит агрессивные ложные позитивы. Автодилеры обожают писать в самом низу текста огромные портянки тегов вроде «рассмотрим обмен на BMW, Audi, Mercedes, Lexus», продавая при этом битый десятилетний Солярис. Векторная модель с радостью подтянет этот Солярис по пользовательскому запросу «купить BMW», потому что контекстуальный фон совпал идеально. Когда мы перестраивали поисковую инфраструктуру для одного из крупных агрегаторов недвижимости в Morana Labs, нам пришлось убить несколько недель машинного времени просто на очистку обучающего датасета и тонкую настройку кастомной модели, чтобы она научилась распознавать такие SEO-спамные хвосты и не путала дом сталинской постройки с диктатором.
Готовые мультиязычные эмбеддинги не спасают. Они не знают, что кузов F10 — это конкретная генерация BMW, а аббревиатура VAG — это не ругательство, а концерн. Модель нужно дообучать на вашем специфичном, узком домене. На ваших кликовых логах, используя triplet loss, где позитивный пример — это карточка, по которой пользователь совершил звонок, а негативный — та, которую он просмотрел в выдаче, но проигнорировал. Вы буквально заставляете модель раздвигать в векторном пространстве объявления, которые внешне похожи текстово, но имеют разную бизнес-ценность для клиента.
Отдельная история — это разговорные формулировки и сленг, который меняется каждый год. Семантика обязана вытаскивать смысл из этого потока сознания. Покупатели квартир ищут «евродвушку», «распашонку», «видовую» или «бабушкин ремонт». Покупатели машин вбивают «беху», «на ручке», «не крашенную», «из-под деда». Лексический поиск здесь умирает сразу и навсегда, если вы не поддерживаете монструозные, бесконечные словари синонимов, которые невозможно обновлять вручную без армии асессоров. Векторное пространство решает это изящно, если модель видела миллионы текстов из вашей ниши. Она сама сближает векторы «механическая коробка» и «на ручке». Но тут же в полный рост встает проблема отрицаний. Запрос «не первый этаж» или «без ДТП». Большинство трансформеров отвратительно работают с лингвистическими отрицаниями. Слово «не» просто тонет при усреднении токенов в пулинге. Нейросеть видит яркий токен «первый этаж», видит «ДТП» и радостно тащит вам битые машины на первых этажах. Поэтому перед векторизацией запроса мы ставим отдельный легковесный классификатор намерений. Он парсит запрос на лету. Если классификатор видит отрицание, он вырезает его из текста и транслирует в жесткий структурный фильтр. Семантика ищет смыслы, детерминированная логика ставит границы. Это единственный путь не бесить пользователя.
Метрики тоже нужно уметь считать, а не просто смотреть на красивые дашборды. Оффлайн-метрики вроде NDCG или Recall@10 показывают температуру по больнице во время обучения. В суровом проде вас должны волновать ровно три вещи. Первая — доля поисковых сессий с нулевой выдачей. Это главный показатель того, что ваша семантика работает, спасает опечатки и вытаскивает длинный хвост неочевидных запросов. Вторая метрика — CTR поисковой выдачи. Пользователь должен кликать на первые три-пять карточек. Если он уныло листает до пятой страницы, ваш гибридный скоринг настроен криво и веса между лексикой и семантикой подобраны неверно. Третья, и самая важная бизнес-метрика, — конверсия в просмотр телефона или начало чата с продавцом. Если CTR растет, люди кликают, а реальные контакты стоят на месте, значит, вы показываете красивые, релевантные по тексту, но абсолютно не подходящие по бизнес-логике объявления. Вы продаете пользователю иллюзию выбора, а не реальный товар. Это классическая болезнь перекрученного веса векторной семантики относительно цены и качества самого объявления.
Теперь о суровой реальности инженерии и железа. Инференс под реальной нагрузкой ломает абсолютно все красивые и стройные концепции из лабораторных ноутбуков. Построить индекс на миллион записей для демонстрации легко. Держать этот индекс в оперативной памяти и обеспечивать p99 latency в пределах пятидесяти миллисекунд при тысячах запросов в секунду — это задача, на которой сыплются целые отделы. HNSW-индексы катастрофически прожорливы. Каждая нода в графе обрастает связями, индекс пухнет в памяти в геометрической прогрессии. Использовать pgvector удобно на старте, пока вся ваша база целиком помещается в кэш. Но когда база вырастает, а холодный старт вымывает память, база ложится наглухо, утягивая за собой весь бэкенд.
Начинаются танцы с бубном вокруг квантования. Проективное или скалярное квантование безжалостно сжимают векторы, экономят драгоценную память, но больно бьют по точности выдачи. Вы начинаете терять релевантность просто ради выживания серверов. Архитектурный выход только один — жесткое разделение вычислительных контуров. Векторный индекс должен жить отдельно, в специализированном движке, который изначально спроектирован под работу с векторами. Реляционные метаданные живут в своей базе. Быстрый in-memory кэш хранит предрассчитанные фильтры. А над всем этим стоит асинхронный оркестратор, который собирает этот распределенный пазл в единый пайплайн за десятки миллисекунд.
Архитектура гибридного поиска требует параноидальной дисциплины. Вы не можете просто насыпать модного ИИ в продукт, прикрутить векторную базу и ждать чудес в продуктовых метриках. Каждый этап этого конвейера — от первичного парсинга пользовательского запроса до финального переранжирования — должен быть обложен метриками, алертами и постоянными A/B-тестами. Вы буквально боретесь за миллисекунды на бэкенде, чтобы не убить таймауты, и за доли процентов конверсии на фронтенде, чтобы оправдать затраты на GPU. В тот момент, когда вы перестаете верить в магию единого универсального векторного пространства и начинаете методично собирать сложную систему из тупых проверенных фильтров, быстрых графов и тяжелых градиентных ранжировщиков, вы начинаете делать настоящую инженерию. Остальное — просто хайп, который не доживет до вечера пятницы под реальной нагрузкой.