Данные и поиск
Создайте REST-сервис поиска по каталогу товаров. У товара есть устойчивый ID, артикул, название, описание, category_id, brand_id, атрибуты, цена в целых копейках RUB, остаток в штуках и время изменения UTC. Публикуется только товар со status=published; available=true означает stock_quantity > 0, а неизвестный остаток считается недоступным. Импорт и изменение записи должны валидировать обязательные поля, запрещать отрицательные цену и остаток; отсутствие остатка хранится отдельно от нуля. Поиск по названию, артикулу и описанию должен работать без учёта регистра и корректно обрабатывать Unicode, включая русские буквы и составные символы.
GET /api/v1/products/search?q=&category_id=&brand_id=&min_price_kopecks=&max_price_kopecks=&available=&sort=&cursor=&limit= возвращает карточки; limit по умолчанию 20, максимум 50, q до 120 символов. Цена включительна на обеих границах; min больше max даёт 400. Разрешены sort relevance, price_asc, price_desc, updated_desc, каждый с ID как последний ключ сортировки. Cursor использует keyset и подписывается сервером. GET /api/v1/products/facets принимает эти фильтры без cursor и limit. Каждая фасета применяет текст и фильтры, кроме собственного: фасета бренда игнорирует brand_id, фасета категории игнорирует category_id. Остальные применяются все. Фасеты сортируются count DESC, value ASC и ограничены 100 значениями. GET /api/v1/products/{id} возвращает карточку; PUT /api/v1/admin/products/{id} создаёт или заменяет товар; DELETE /api/v1/admin/products/{id} снимает публикацию. Локальный seed создаёт catalog_admin с токеном только в Authorization header; этот токен даёт доступ к admin-маршрутам. Используйте PostgreSQL, ручной SQL, миграции и GIN tsvector индекс по русской конфигурации. Запросы ищут все токены, регистр не учитывается, исправления опечаток нет.
Пустой q без фильтра даёт 400; с фильтром разрешён. Неизвестная категория даёт пустую выдачу. Неверный диапазон и неподписанный cursor дают 400; отсутствующий или снятый товар — 404. PUT и DELETE пишут товар и событие в outbox в одной транзакции. Индексатор применяет не более 200 событий в секунду, контрольная позиция сохраняется после применения. Повтор события идемпотентен по product ID и revision. Очередь ограничена 100 000; при заполнении изменение не подтверждается и возвращает 503. Поиск сообщает index_updated_at и lag_seconds. Приёмочный lag — до 60 секунд на 10 000 товарах. SQL deadline — 3 секунды. Ошибки базы не раскрываются клиенту. Все SQL-запросы используют context.Context с deadline 3 секунды. Запись товара и outbox version фиксируются атомарно. Индексатор читает события последовательно, применяет их повторяемо и подтверждает контрольную позицию после фиксации. При перезапуске он продолжает с контрольной позицией; удаление скрывает товар после обработки tombstone.
Структура программы
Веб-слой разбирает параметры и формирует ответы; предметный пакет задаёт правила запроса, фасет и сортировки; SQL-пакет хранит каталог и индекс, отдельный цикл применяет outbox. main.go только загружает конфигурацию, связывает зависимости и управляет запуском. Пакеты без циклов, общих utils и интерфейсов без потребителя. README показывает зависимости; тесты ядра без HTTP/БД, интеграционные отдельно.
Приёмка должна запускаться локально с PostgreSQL и детерминированными тестовыми данными, без сети и платных сервисов. Набор из 10 000 товаров включает русский текст, одинаковые оценки и цены, отсутствие атрибутов, товары без остатка и обновления с удалением. Тесты проверяют регистр и Unicode, AND-семантику выбранных фильтров, точные фасетные количества, сортировку с равными ключами, границы страниц, валидацию и восстановление индекса после перезапуска. Измерьте p95 поиска и индексации и опубликуйте команду, конфигурацию, размер и фактический результат, не выдавая его за общий SLA. Критерии готовности: unavailable товар не проходит available=true; стабильный каталог не даёт дублей между страницами; lag виден и не больше 60 секунд; недоступная БД не подтверждает изменение; миграции применяются с чистой базы.