Корейские, китайские и европейские площадки хранят данные по-своему: разные названия топлива, коробки, кузова, разные валюты и статусы. Поиск поверх такой базы без предобработки просто не работает. Был построен слой нормализации, который привёл всё к единому виду — и только после этого фильтры стали давать предсказуемый результат.
CarHao
Пользователь пишет: «Хочу Cayenne до 8 млн под ключ в Москве» — система за секунду понимает запрос, ищет по базе из 4 миллионов автомобилей и выдаёт конкретный шортлист с ценами и ссылками. Построен пайплайн: от разбора свободного запроса до поддержания индекса в актуальном состоянии.
Какие сложности были у проекта
Человек пишет запрос своими словами. Система сама разбирает, что имеется в виду, накладывает фильтры по бюджету с учётом доставки и валюты, ищет по смыслу — и выдаёт карточки машин с ценами и ссылками. Менеджер получает тёплый лид, а не просто запрос «ищу машину».
Архитектура: как устроено изнутри
- База фактов и поисковый индекс разделены. PostgreSQL хранит исходные данные, Qdrant — только то, что нужно для поиска. Это позволяет обновлять одно, не ломая другое.
- LLM не принимает решений о данных. Модель переводит свободный запрос в структуру для QDRANT-фильтрации. Дальше работает детерминированная логика, а не «угадывание» нейросети. После полуения релевантных результатов осбирается финальынй ответ пользователю.
- Нормализация — это не опция, а обязательное условие. Без единого формата для всех источников фильтры по кузову, топливу или коробке просто не работают стабильно.
- 4 миллиона записей не пересчитываются каждый раз. Выгрузка новых записей → нормализация → перевекторизация, только для изменённых векторов.
Как выглядит результат?
Живые диалоги с ботом, логи работы системы, структура индекса и данные нормализации.
Диалоги с пользователями
Ассистент помнит контекст, уточняет бюджет под город, предлагает варианты с ценами и передаёт квалифицированный лид менеджеру — без разрыва между сообщениями.
Как система принимает решения — в логах
Ответ проходит через логический контур: запрос принимается и нормализуется, система решает, нужен ли семантический поиск, затем подбирает релевантную информацию и собирает финальный ответ.
4 000 000 записей в индексе — не просто тексты
Каждая запись содержит исходные данные из источника, нормализованные поля для фильтров и служебные метки для обновлений. Именно это позволяет фильтровать по многим критериям и затем по векторному поиску одновременно — быстро и точно.
Как из 5 источников получился один стандарт
Разные площадки называют «бензин», «АКПП» и «кроссовер» по-разному. Были созданы таблицы соответствий для каждого источника отдельно — и теперь фильтры работают стабильно вне зависимости от того, откуда пришла запись.
SQL-схема
Проект строился поверх живой и неидеальной структуры заказчика, а не поверх заранее удобной базы. Нужно было разобрать эту схему, понять ограничения и превратить ее в рабочий слой для поиска, нормализации и ответа.
Этапы разработки проекта
Шесть слоёв, которые вместе делают поиск по 4 млн+ автомобилям рабочим: индекс, нормализация, оркестрация, фильтрация и семантический поиск, продажная логика, обновление данных.
Backend / runtime
FastAPI-сервис с двумя входами: диалоговый чат для пользователя и прямой search API для программного доступа. Один сервис — два способа работы с индексом.
LLM-оркестрация
Свободный запрос пользователя превращается в поисковый контракт через LLM-нормализатор. Модель извлекает намерение — детерминированный код строит фильтры, ищет и считает цену.
Поисковый индекс
Семантический поиск по смыслу запроса плюс структурные фильтры по параметрам автомобиля. Если выдача пустая — система автоматически ослабляет фильтры и повторяет поиск.
Нормализация данных
5 источников с разными форматами — один стандарт поиска. Топливо, коробка, кузов, привод приведены к единым полям для каждого источника отдельно. Без этого слоя фильтры работают непредсказуемо.
Обновление индекса в QDRANT
Синхронизация данных из SQL, нормализация полей, пересчёт векторов — только для изменившихся записей. Без полного перестроения при каждом обновлении.
Диагностика
Для заказчика подготовлены аудиты покрытия полей, отчёты по пробелам в исходных данных, построена защита от разрушительных обновлений индекса и возобновляемые прогоны при сбоях.
Два решения, которые определили всю архитектуру
База данных и поисковый индекс — как разные вещи
PostgreSQL хранит правду об автомобилях. Qdrant хранит только то, что нужно для поиска. Если смешать эти роли в одном хранилище, система начинает падать при каждом обновлении источника. Разделение дало возможность обновлять и чинить части независимо.
Пересчитывать 4 миллиона записей каждый раз — плохая идея
Я разбил обновление на три шага: сначала синхронизация исходных данных, потом нормализация, потом пересчёт векторов — и только там, где что-то изменилось. Это сократило стоимость API-запросов, ускорило обновление и дало возможность продолжить с места остановки при любом сбое.
Sergey Shumov