# Спека: Yandex Market Dashcam Catalog Ingestion ## 1. Цель Построить поддерживаемый ingestion engine для каталога автомобильных видеорегистраторов. Целевая категория: `Автомобильные видеорегистраторы` Публичное приложение должно получать только approved canonical catalog: * название * бренд * модель * нормализованные характеристики * изображения * source metadata * дату первого обнаружения * дату последнего обновления В MVP не парсим и не экспортируем: * цены * продавцов * наличие * тексты отзывов * авторов отзывов * review content Общий aggregate rating можно сохранять внутри ingestion как source signal, но он не входит в публичный export и не участвует в `stableContentHash`. Главная мысль: не "краулер, который пролезет", а каталоговый ingestion engine, который можно поддерживать годами. --- ## 2. Границы и запреты Не реализовывать: * обход rate limiter * маскировку под реальных пользователей * ротацию User-Agent / browser fingerprint * прокси-фермы * headless stealth * обход captcha * агрессивную параллельную загрузку * регулярный browser-based crawl В MVP fetcher работает только через обычный HTTP fetch. Headless browser допустим только как ручной diagnostic tool для анализа структуры страницы. Он не является fallback в регулярном crawl path. --- ## 3. Источники ### Primary * публичные category pages Яндекс Маркета * публичные product pages * JSON-LD, если доступен в HTML * embedded state, если доступен в HTML * sitemap, но только в ограниченных границах ### Preferred official fallback * Yandex Market Partner API, если доступен по условиям проекта * фиды продавцов, если доступны легально * фиды брендов * официальные сайты производителей --- ## 4. Deployment Model Ingestion деплоится в то же облако, что и web-приложение (один Swarm/Portainer environment), а не на отдельный сервер. Раньше предполагался отдельный cloud, но управлять двумя независимыми облаками оказалось слишком сложно. См. решение `videoreg-nnv.3`. Код хранится в этом же репозитории: ```text ingestion/ src/ migrations/ docker-compose.yml Dockerfile ``` Минимальные сервисы: ```text postgres ingestion-worker ingestion-admin ``` `export-server` больше не нужен: ingestion-worker пишет валидированный `catalog.json` + `manifest.json` в общий (shared) export-том, а web читает их напрямую с этого тома. Никакого HTTP-экспорта и Bearer-токена. Основное Next.js-приложение по-прежнему не подключается напрямую к ingestion DB — единственный контакт с каталогом идёт через export-артефакт на shared-томе. --- ## 5. Source of Truth и Bridge Source of truth для каталога: PostgreSQL ingestion stack. Текущее примитивное хранилище приложения (`data/videoreg.json`) является только read-model cache. Bridge работает со стороны приложения через общий export-том: 1. ingestion пишет versioned export (`manifest.json` + `catalog.json`) на shared-том; 2. приложение/сборка читает `manifest.json` с того же тома; 3. проверяет `schemaVersion`, `exportId`, `createdAt`, `sha256`, `itemCount`; 4. читает `catalog.json` с тома; 5. сверяет `sha256` артефакта; 6. валидирует payload через shared Zod schema; 7. атомарно заменяет `data/videoreg.json` (или используется как build-time источник). В MVP import запускается вручную оператором, не по расписанию. HTTP/токен в этом обмене не участвуют — оба сервиса в одном облаке и делят том. --- ## 6. Export Contract Export публикуется как файлы на общем (shared) export-томе: `manifest.json` и `artifacts//catalog.json`. HTTP-эндпоинт и Bearer-токен не используются — ingestion и web живут в одном облаке и делят том. Целостность гарантируется `sha256` в манифесте и shared Zod-схемой при чтении. ### Manifest ```ts type CatalogExportManifest = { schemaVersion: 1; exportId: string; createdAt: string; artifactPath: string; sha256: string; itemCount: number; }; ``` ### Catalog Artifact ```ts type DashcamCatalogExport = { schemaVersion: 1; exportId: string; createdAt: string; products: ExportedDashcamProduct[]; }; type ExportedDashcamProduct = { id: string; brand: string; model: string; canonicalName?: string; title: string; specs: Record; images: ExportedProductImage[]; sources: ExportedProductSource[]; discoveredAt: string; updatedAt: string; }; type ExportedProductImage = { url: string; originalUrl: string; source: MarketSource; sha256?: string; width?: number; height?: number; }; type ExportedProductSource = { source: MarketSource; sourceProductId?: string; url: string; stableContentHash: string; fetchedAt: string; }; type MarketSource = "yandex_market" | "mvideo"; ``` Runtime validation: shared Zod schema в репозитории. TypeScript-типы без runtime-валидации недостаточны. --- ## 7. Image Policy Ingestion кеширует approved images и публикует versioned image artifact URLs. Export содержит: * URL versioned image artifact * original source URL * source metadata * optional checksum/dimensions Приложение не должно hotlink-ить source images напрямую. Юридическое решение о хранении и показе изображений должно быть принято отдельно до production-публикации. --- ## 8. Crawl Policy Нагрузка: * 1 запрос за 5-15 секунд * максимум 100-300 страниц в сутки на источник * jitter между запросами * exponential backoff при 429/403/5xx * остановка источника при captcha * отдельный дневной budget на category pages, product pages, sitemap * не скачивать повторно страницы, если stable content не изменился HTTP-only fetcher должен явно детектить: ```text rate_limited blocked captcha not_found parse_failed needs_manual_review ``` При `captcha`, `403`, `429` источник ставится на паузу. Не пытаться обходить ограничение. --- ## 9. Discovery Strategy Discovery работает sitemap-first, но не обходит широкий sitemap всего Маркета. Границы discovery задают: * ручные seed category URLs * ручные seed product URL patterns * product URLs, найденные из category HTTP discovery * уже известные product URLs Sitemap используется для: * diff известных URL * refresh кандидатных URL * обнаружение URL рядом с уже известными границами Category pages используются вторично: * первая страница категории * пагинация, если доступна без JS * top listings * новые product URLs, если они есть в HTML Если category HTML бедный без JS, discovery не должен переходить в regular headless crawl. Такие случаи уходят в `needs_manual_review` или official fallback. ```ts type DiscoveredProduct = { source: MarketSource; categoryUrl?: string; productUrl: string; sourceProductId?: string; titlePreview?: string; discoveredAt: string; }; ``` --- ## 10. Fetch Queue Очередь реализуется в PostgreSQL, без Redis/BullMQ в MVP. State machine: ```text pending -> fetching -> fetched -> parsed -> normalized -> deduplicated -> needs_review -> approved -> exported ``` Ошибочные состояния: ```text rate_limited blocked captcha not_found parse_failed needs_manual_review ``` Для каждой задачи хранить: * source * URL * job type * state * attempts * nextRunAt * lastError * createdAt * updatedAt Backoff и daily budgets считаются в PostgreSQL. --- ## 11. Parser Parser извлекает только разрешенные поля: * title * brand * model * specs * images * sourceProductId * aggregate rating как internal-only signal Приоритет источников внутри страницы: ```text JSON-LD > embedded state > semantic DOM > fallback DOM ``` Не извлекать: * prices * sellers * availability * review texts * review authors ```ts type ParsedMarketProduct = { source: MarketSource; sourceProductId?: string; title: string; brand?: string; model?: string; aggregateRating?: { value: number; scale?: number; ratingCount?: number; }; images: string[]; specs: Record; sourceUrl: string; }; ``` --- ## 12. Snapshot и Checksum Model Сырой snapshot хранить всегда. Парсер должен уметь переобрабатывать старые страницы. Нельзя использовать hash всего HTML как сигнал "страница изменилась": динамическая верстка, цены, рекомендации, счетчики и timestamps будут постоянно шуметь. Для каждого snapshot хранить два хэша: ```ts type ProductSourceSnapshot = { source: MarketSource; url: string; fetchedAt: string; httpStatus: number; rawSnapshotHash: string; stableContentHash?: string; rawHtmlPath?: string; parsedJson: Record; }; ``` ### `rawSnapshotHash` Хэш точного raw snapshot для аудита и debug. ### `stableContentHash` Source-specific semantic hash. Для Яндекс Маркета в MVP включать: * sourceProductId * canonical product URL * title * brand * model * normalized specs * image identifiers / original image URLs Исключать: * price * sellers * availability * aggregate rating * review count * timestamps * layout * recommendations * counters * tracking data --- ## 13. Normalization Нужно привести хаотичные характеристики к единому виду. Пример: ```json { "resolution_front": "3840x2160", "channels": 2, "sensor_front": "Sony STARVIS 2", "gps": true, "wifi": true, "screen": true, "parking_mode": true, "memory_card_max_gb": 256, "view_angle_degrees": 140 } ``` Компоненты normalizer: * brand dictionary * model cleaner * spec mapper * unit converter * boolean detector * alias dictionary --- ## 14. Deduplication Дедупликация работает до модерации. Единица модерации: canonical product, уже сгруппированный из вариантов и source URLs. Сравнивать: * brand * normalized model name * aliases * sourceProductId * image perceptual hash * specs similarity * product URL history Статусы: ```text new_product same_product possible_duplicate variant oem_clone ``` `possible_duplicate`, `variant`, `oem_clone` не должны попадать в public export без ручного решения. --- ## 15. Moderation Admin Админка живет внутри отдельного ingestion stack. Она работает напрямую с ingestion PostgreSQL, snapshots, очередями и canonical products. MVP-экраны: * новые canonical products * bulk select * "разрешить все" * товары с `parse_failed` * возможные дубли * конфликтующие характеристики * история изменений * ручное объединение моделей * ручное редактирование canonical specs Публикация в export разрешена только после ручной модерации. Bulk approve допустим, но утверждает canonical products, а не отдельные source URLs. --- ## 16. Database Минимальная PostgreSQL-модель: ```sql products product_variants product_sources source_snapshots product_specs product_images source_ratings crawl_jobs crawl_errors moderation_queue export_runs ``` Принципы: * raw HTML/JSON snapshots и image artifacts хранить на mounted volume * в PostgreSQL хранить пути, checksum, metadata и связи * уникальность по `source + url` * ingestion DB является source of truth * `data/videoreg.json` приложения является replaceable read-model cache --- ## 17. Sync Schedule ### Частый легкий sync Каждые 6-12 часов: * первая страница категории * sitemap diff в пределах известных границ * новые product URLs * изменение top listings, если доступно без JS ### Глубокий sync Раз в 7-14 дней: * все известные карточки * характеристики * изображения * source metadata ### New product detection Каждый день: * category diff * bounded sitemap diff * known model aliases search * official fallback feeds, если доступны --- ## 18. MVP Phases ### Phase 1 - Legal discovery * проверить robots.txt * проверить sitemap * проверить доступность API/фидов * описать crawl budget * описать allowed URL patterns * зафиксировать image storage policy ### Phase 2 - Shared contract * добавить shared Zod schema для manifest/catalog export * добавить TypeScript-типы из схем * описать `schemaVersion` * добавить app import command * заменить `data/videoreg.json` validated catalog payload ### Phase 3 - Ingestion stack skeleton * создать `ingestion/` * добавить `docker-compose.yml` * поднять PostgreSQL * добавить migrations * добавить mounted volume для snapshots/images * добавить healthcheck ### Phase 4 - Queue and fetcher * реализовать PostgreSQL-backed queue * реализовать HTTP-only fetcher * добавить rate limits, retry, backoff * stop on captcha / 403 / 429 * сохранять raw snapshots * считать `rawSnapshotHash` ### Phase 5 - Parser * category parser * bounded sitemap discovery * product parser * specs parser * image parser * aggregate rating parser as internal-only * source-specific `stableContentHash` ### Phase 6 - Normalizer * brand dictionary * model cleaner * spec mapper * unit converter * boolean detector ### Phase 7 - Deduplication and moderation * exact match * fuzzy match * alias match * canonical product grouping * moderation queue * bulk approve ### Phase 8 - Export and import * publish versioned manifest * publish versioned catalog artifact * publish versioned image artifacts * write export artifacts to the shared export volume (no HTTP, no token) * verify checksum in app import * atomic replace of `data/videoreg.json` --- ## 19. Definition of Done Система считается готовой, если: * ingestion и web деплоятся в одно облако; export идёт через shared-том, без HTTP/токена * PostgreSQL является source of truth * приложение импортирует только approved canonical catalog * `data/videoreg.json` заменяется только после manifest/checksum/schema validation * crawler не создает заметной нагрузки на источник * crawler останавливается при ограничениях * crawler не использует regular headless/browser stealth * raw snapshots хранятся всегда * есть `rawSnapshotHash` и source-specific `stableContentHash` * старые snapshots можно переобработать новым parser * дубли попадают в moderation queue * bulk approve работает по canonical products * public export не содержит цены, продавцов, наличие и тексты отзывов * public export не содержит aggregate rating в MVP * approved images отдаются как versioned image artifacts