16 KiB
Спека: 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 живет отдельно от облачного runtime приложения.
Код хранится в этом же репозитории:
ingestion/
src/
migrations/
docker-compose.yml
Dockerfile
Стек ingestion деплоится отдельным docker-compose.yml.
Минимальные сервисы:
postgres
ingestion-worker
ingestion-admin
export-server
Основное Next.js-приложение не подключается напрямую к ingestion DB.
5. Source of Truth и Bridge
Source of truth для каталога: PostgreSQL ingestion stack.
Текущее примитивное хранилище приложения (data/videoreg.json) является только read-model cache.
Bridge работает со стороны приложения:
- ingestion публикует versioned HTTPS export;
- приложение вручную запускает operator import command;
- import command скачивает manifest;
- проверяет
schemaVersion,exportId,createdAt,sha256,itemCount; - скачивает catalog artifact;
- валидирует payload через shared Zod schema;
- атомарно заменяет
data/videoreg.json.
В MVP import запускается вручную оператором, не по расписанию.
6. Export Contract
Export доступен по HTTPS со статичным токеном:
Authorization: Bearer <token>
Токен передается через env import-команды. Токен не передается в query string.
Manifest
type CatalogExportManifest = {
schemaVersion: 1;
exportId: string;
createdAt: string;
artifactUrl: string;
sha256: string;
itemCount: number;
};
Catalog Artifact
type DashcamCatalogExport = {
schemaVersion: 1;
exportId: string;
createdAt: string;
products: ExportedDashcamProduct[];
};
type ExportedDashcamProduct = {
id: string;
brand: string;
model: string;
title: string;
specs: Record<string, string | number | boolean>;
images: ExportedProductImage[];
sources: ExportedProductSource[];
discoveredAt: string;
updatedAt: string;
};
type ExportedProductImage = {
url: string;
originalUrl: string;
source: "yandex_market";
sha256?: string;
width?: number;
height?: number;
};
type ExportedProductSource = {
source: "yandex_market";
sourceProductId?: string;
url: string;
stableContentHash: string;
fetchedAt: string;
};
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 должен явно детектить:
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.
type DiscoveredProduct = {
source: "yandex_market";
categoryUrl?: string;
productUrl: string;
sourceProductId?: string;
titlePreview?: string;
discoveredAt: string;
};
10. Fetch Queue
Очередь реализуется в PostgreSQL, без Redis/BullMQ в MVP.
State machine:
pending
-> fetching
-> fetched
-> parsed
-> normalized
-> deduplicated
-> needs_review
-> approved
-> exported
Ошибочные состояния:
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
Приоритет источников внутри страницы:
JSON-LD > embedded state > semantic DOM > fallback DOM
Не извлекать:
- prices
- sellers
- availability
- review texts
- review authors
type ParsedMarketProduct = {
source: "yandex_market";
sourceProductId?: string;
title: string;
brand?: string;
model?: string;
aggregateRating?: {
value: number;
scale?: number;
ratingCount?: number;
};
images: string[];
specs: Record<string, string | number | boolean>;
sourceUrl: string;
};
12. Snapshot и Checksum Model
Сырой snapshot хранить всегда. Парсер должен уметь переобрабатывать старые страницы.
Нельзя использовать hash всего HTML как сигнал "страница изменилась": динамическая верстка, цены, рекомендации, счетчики и timestamps будут постоянно шуметь.
Для каждого snapshot хранить два хэша:
type ProductSourceSnapshot = {
source: "yandex_market";
url: string;
fetchedAt: string;
httpStatus: number;
rawSnapshotHash: string;
stableContentHash?: string;
rawHtmlPath?: string;
parsedJson: Record<string, unknown>;
};
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
Нужно привести хаотичные характеристики к единому виду.
Пример:
{
"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
Статусы:
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-модель:
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.jsonvalidated 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
- protect endpoints with Bearer token
- verify checksum in app import
- atomic replace of
data/videoreg.json
19. Definition of Done
Система считается готовой, если:
- ingestion stack запускается отдельно через
docker-compose.yml - 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-specificstableContentHash - старые snapshots можно переобработать новым parser
- дубли попадают в moderation queue
- bulk approve работает по canonical products
- public export не содержит цены, продавцов, наличие и тексты отзывов
- public export не содержит aggregate rating в MVP
- approved images отдаются как versioned image artifacts