WB Parser Service — Quick Start

Архитектура взаимодействия

Бэкенд                          Парсер
   │                               │
   ├─ XADD parser:tasks ──────►   Worker читает задачу
   │   (WBParserTask)              │
   │                               ├─ Парсит WB
   │                               ├─ Сохраняет в PostgreSQL
   │                               │
   ◄── XREADGROUP parser:results ──┤
       (WBParserResult)            │
   │                               │
   ├─ SELECT из PostgreSQL         │
   │   (товары, позиции, история)  │

Бэкенд общается с парсером только через Redis Streams. HTTP API парсера — вспомогательный.


Сводка

Задачаtask_typeКлючевые поля requestГде результат
Полный сбор по keywordenrichsource_type: keyword, source_value, pagessummary в Redis, данные в БД
Полный сбор по категорииenrichsource_type: category, source_value, pagessummary в Redis, данные в БД
Быстрая позиция товараsearchsource_type: keyword, source_value, target, pagesQuickData в Redis + БД
Поиск по категорииsearchsource_type: category, source_value, target, pagesQuickData в Redis + БД
Рекомендации (знаю конкурентов)recomsource_type: nm_ids, nm_ids, targetsummary в Redis, детали в БД
Рекомендации (по keyword)recomsource_type: keyword, source_value, nm_ids, targetsummary в Redis, детали в БД
Дерево категорийGET /categories/?flat=true (HTTP)

Отправка задачи в Redis

Бэкенд формирует WBParserTask и публикует в stream parser:tasks:

XADD parser:tasks * data '{"task_id": "...", "task_type": "...", ...}'

Поле data — JSON-строка с WBParserTask.

Обязательные поля WBParserTask

ПолеТипОписание
task_idUUIDУникальный ID (генерирует бэкенд)
task_typestringsearch | enrich | recom | check
parse_methodstringtoken (рекомендуется) или html
request.source_typestringkeyword | category | nm_ids
request.source_valuestring | nullЗначение keyword или category_id
request.pagesint1..30
request.nm_idslist[int] | nullАртикулы (для recom/check)
request.targetobject | nullЦелевая конфигурация (для search/recom)

Сценарий 1: Полный сбор данных по keyword (enrich)

Цель: собрать все данные (позиции, цены, отзывы, состав, 3D-фото) по товарам в выдаче.

Шаг 1 — Отправить задачу в Redis

{
  "task_id": "550e8400-e29b-41d4-a716-446655440000",
  "task_type": "enrich",
  "parse_method": "token",
  "request": {
    "source_type": "keyword",
    "source_value": "термокружка",
    "pages": 5
  }
}

Шаг 2 — Получить результат из Redis

Читать из parser:results, фильтровать по task_id:

{
  "task_id": "550e8400-e29b-41d4-a716-446655440000",
  "task_type": "enrich",
  "parse_method": "token",
  "duration_seconds": 205.2,
  "request": {
    "source_type": "keyword",
    "source_value": "термокружка",
    "pages": 5,
    "target": null
  },
  "summary": {
    "items_found": 245,
    "items_saved": 245,
    "items_failed": 0,
    "pages_parsed": 5
  },
  "data": null,
  "details_in_db": true,
  "db_pointers": {"task_db_id": 456},
  "error": null
}

data = null всегда для enrich. Все данные — в PostgreSQL.

Шаг 3 — Забрать данные из БД

v_api_competitive_analytics — материализованная вьюха, объединяет v_product_card (все 68 полей) с позициями из search_positions. Обновляется автоматически после завершения enrich. Бэкенд фильтрует по keyword.

SELECT * FROM v_api_competitive_analytics
WHERE keyword = 'термокружка'
ORDER BY position;

Сценарий 2: Полный сбор данных по категории (enrich)

Шаг 0 — Узнать category_id

GET /categories/?flat=true
{"count": 1234, "categories": [{"id": 9468, "name": "Термосы и термокружки", ...}]}

Шаг 1 — Отправить задачу в Redis

{
  "task_id": "550e8400-e29b-41d4-a716-446655440001",
  "task_type": "enrich",
  "parse_method": "token",
  "request": {
    "source_type": "category",
    "source_value": "9468",
    "pages": 3
  }
}

Шаг 2 — Получить результат из Redis

Аналогично сценарию 1. data = null, summary содержит counts.

Шаг 3 — Забрать данные из БД

-- Для категорий keyword хранится как 'category:{id}'
SELECT * FROM v_api_competitive_analytics
WHERE keyword = 'category:9468'
ORDER BY position;

Цель: узнать позицию своего артикула в выдаче — без полного enrich.

Шаг 1 — Отправить задачу в Redis

{
  "task_id": "550e8400-e29b-41d4-a716-446655440002",
  "task_type": "search",
  "parse_method": "token",
  "request": {
    "source_type": "keyword",
    "source_value": "термокружка",
    "pages": 3,
    "target": {
      "target_nm_id": 124203940,
      "top_n": 50
    }
  }
}

Шаг 2 — Получить результат из Redis

{
  "task_id": "550e8400-e29b-41d4-a716-446655440002",
  "task_type": "search",
  "summary": {
    "items_found": 250,
    "items_saved": 250,
    "items_failed": 0,
    "pages_parsed": 3
  },
  "data": {
    "target_results": [
      {
        "nm_id": 124203940,
        "found_in_listing": true,
        "position": 5,
        "page": 1,
        "position_on_page": 5,
        "is_promoted": false,
        "price": 643.0,
        "rating": 4.8,
        "feedbacks": 1200,
        "total_stock": 540
      }
    ],
    "top_products": [
      {
        "nm_id": 111222333,
        "position": 1,
        "page": 1,
        "is_promoted": false,
        "price": 590.0,
        "rating": 4.9,
        "feedbacks": 3200,
        "total_stock": 1200
      }
    ]
  },
  "details_in_db": true,
  "db_pointers": {"task_db_id": 123},
  "error": null
}

Данные в data — можно показать в UI сразу, без похода в БД. target_results — позиция и метрики целевого артикула. top_products — топ-N товаров из выдачи.


Шаг 1 — Отправить задачу в Redis

{
  "task_id": "550e8400-e29b-41d4-a716-446655440402",
  "task_type": "search",
  "parse_method": "token",
  "request": {
    "source_type": "category",
    "source_value": "8126",
    "pages": 30,
    "target": {
      "target_nm_id": 480853346,
      "top_n": 1500
    }
  }
}

Шаг 2 — Результат

Аналогично сценарию 3: data.target_results + data.top_products в Redis.


Сценарий 5: Рекомендательные полки (recom)

Цель: показывает ли WB мой товар на страницах конкурентов?

Вариант A — Знаю конкурентов (monitor)

{
  "task_id": "550e8400-e29b-41d4-a716-446655440003",
  "task_type": "recom",
  "parse_method": "token",
  "request": {
    "source_type": "nm_ids",
    "source_value": null,
    "nm_ids": [124203940, 94608652, 376929502],
    "pages": 1,
    "target": {
      "target_nm_id": 124203940,
      "target_nm_ids": [94608652, 376929502],
      "top_n": 10
    }
  }
}

nm_ids[0] = target, остальные — конкуренты. Target также дублируется в target.target_nm_id.

Вариант B — Не знаю конкурентов (cross_reference)

{
  "task_id": "550e8400-e29b-41d4-a716-446655440004",
  "task_type": "recom",
  "parse_method": "token",
  "request": {
    "source_type": "keyword",
    "source_value": "термокружка",
    "nm_ids": [124203940],
    "pages": 1,
    "target": {
      "target_nm_id": 124203940,
      "top_n": 10
    }
  }
}

Парсер сам найдёт конкурентов из поисковой выдачи по keyword.

Результат в Redis

{
  "task_id": "550e8400-e29b-41d4-a716-446655440003",
  "task_type": "recom",
  "summary": {
    "items_found": 2,
    "items_saved": 47,
    "items_failed": 1,
    "pages_parsed": 1
  },
  "data": null,
  "details_in_db": true,
  "db_pointers": {"task_db_id": 789},
  "error": null
}

items_found = у скольких конкурентов нас показывают. items_failed = у скольких НЕ показывают.

Детали в БД

-- Где мой товар показывается в рекомендациях конкурентов
SELECT source_nm_id AS competitor, position, search_position
FROM product_recommendations
WHERE recommended_nm_id = 124203940
  AND checked_at = (
      SELECT MAX(checked_at) FROM product_recommendations
      WHERE recommended_nm_id = 124203940
  );

-- Полный список рекомендаций конкретного конкурента
SELECT recommended_nm_id, position
FROM product_recommendations
WHERE source_nm_id = 94608652
ORDER BY position;

Получение результата из Redis

Подписка на parser:results

XREADGROUP GROUP bidder-backend backend-1 COUNT 10 BLOCK 5000 STREAMS parser:results >

Каждое сообщение — JSON в поле data:

{"data": "{\"task_id\": \"...\", \"task_type\": \"enrich\", ...}"}

После обработки — подтвердить:

XACK parser:results bidder-backend <entry_id>

Что содержит результат по типу задачи

task_typedatasummary.items_foundsummary.items_savedsummary.items_failed
searchQuickData (target_results + top_products)Товаров найденоТоваров сохранено в БДС ошибками
enrichnullТоваров найденоТоваров обогащено в БД0 (не трекается)
recomnullКонкурентов, где нас показываютЗаписей рекомендаций в БДКонкурентов, где НЕ показывают

Ошибка (любой тип)

{
  "task_id": "...",
  "task_type": "enrich",
  "summary": {"items_found": 0, "items_saved": 0, "items_failed": 0, "pages_parsed": 0},
  "data": null,
  "error": {
    "type": "network",
    "message": "Connection timeout after 30s"
  }
}

error.type: network, captcha, rate_limit, parsing, validation, unknown.


API-вьюхи для бэкенда

v_api_competitive_analytics (MATERIALIZED VIEW)

Объединяет v_product_card (68 полей: сырые enrich-поля + расчётные метрики) с позициями из search_positions. Обновляется автоматически после каждого enrich (worker вызывает REFRESH MATERIALIZED VIEW). Индекс: (product_id, keyword) — для быстрой фильтрации.

Типичные запросы бэкенда

-- По keyword (конкурентный анализ)
SELECT * FROM v_api_competitive_analytics
WHERE keyword = :keyword
ORDER BY position;

-- По категории
SELECT * FROM v_api_competitive_analytics
WHERE keyword = 'category:' || :category_id
ORDER BY position;

v_product_card (VIEW)

Обычная вьюха с 68 полями продукта (без позиций). Читает данные из v_product_metrics и products на лету. Используется как основа для v_api_competitive_analytics и для прямых запросов без привязки к поиску.

-- Только товары с продажами
SELECT * FROM v_product_card WHERE sales_qty > 0 ORDER BY revenue DESC;

-- Топ по content_score
SELECT * FROM v_product_card ORDER BY content_score DESC LIMIT 50;

Дополнительные SQL-запросы

История цен товара

SELECT price_current, price_basic, discount_percent, spp, recorded_at
FROM price_history ph
JOIN products p ON p.id = ph.product_id
WHERE p.nm_id = :nm_id
ORDER BY ph.recorded_at DESC;

Список всех прогонов по keyword

SELECT keyword, recorded_at, COUNT(*) AS products_count
FROM search_positions
WHERE keyword = :keyword
GROUP BY keyword, recorded_at
ORDER BY recorded_at DESC;

Данные конкретного прогона (не последнего)

SELECT p.*, sp.position, sp.page_number, sp.is_promoted
FROM search_positions sp
JOIN products p ON p.id = sp.product_id
WHERE sp.keyword = :keyword
  AND sp.recorded_at = :target_recorded_at
ORDER BY sp.position;

HTTP API парсера (вспомогательный)

Эти эндпоинты доступны напрямую, но основной путь — через Redis.

МетодПутьОписание
GET/categories/?flat=trueПлоский список категорий (для получения category_id)
GET/categories/Иерархическое дерево категорий
GET/tasks/{task_id}/statusСтатус задачи по UUID (альтернатива Redis)
GET/tasks/streams/healthЗдоровье Redis Streams
GET/healthK8s liveness
GET/readinessK8s readiness

Важно