Бэкенд Парсер
│ │
├─ XADD parser:tasks ──────► Worker читает задачу
│ (WBParserTask) │
│ ├─ Парсит WB
│ ├─ Сохраняет в PostgreSQL
│ │
◄── XREADGROUP parser:results ──┤
(WBParserResult) │
│ │
├─ SELECT из PostgreSQL │
│ (товары, позиции, история) │
Бэкенд общается с парсером только через Redis Streams. HTTP API парсера — вспомогательный.
| Задача | task_type | Ключевые поля request | Где результат |
|---|---|---|---|
| Полный сбор по keyword | enrich | source_type: keyword, source_value, pages | summary в Redis, данные в БД |
| Полный сбор по категории | enrich | source_type: category, source_value, pages | summary в Redis, данные в БД |
| Быстрая позиция товара | search | source_type: keyword, source_value, target, pages | QuickData в Redis + БД |
| Поиск по категории | search | source_type: category, source_value, target, pages | QuickData в Redis + БД |
| Рекомендации (знаю конкурентов) | recom | source_type: nm_ids, nm_ids, target | summary в Redis, детали в БД |
| Рекомендации (по keyword) | recom | source_type: keyword, source_value, nm_ids, target | summary в Redis, детали в БД |
| Дерево категорий | — | — | GET /categories/?flat=true (HTTP) |
Бэкенд формирует WBParserTask и публикует в stream parser:tasks:
XADD parser:tasks * data '{"task_id": "...", "task_type": "...", ...}'
Поле data — JSON-строка с WBParserTask.
| Поле | Тип | Описание |
|---|---|---|
task_id | UUID | Уникальный ID (генерирует бэкенд) |
task_type | string | search | enrich | recom | check |
parse_method | string | token (рекомендуется) или html |
request.source_type | string | keyword | category | nm_ids |
request.source_value | string | null | Значение keyword или category_id |
request.pages | int | 1..30 |
request.nm_ids | list[int] | null | Артикулы (для recom/check) |
request.target | object | null | Целевая конфигурация (для search/recom) |
Цель: собрать все данные (позиции, цены, отзывы, состав, 3D-фото) по товарам в выдаче.
{
"task_id": "550e8400-e29b-41d4-a716-446655440000",
"task_type": "enrich",
"parse_method": "token",
"request": {
"source_type": "keyword",
"source_value": "термокружка",
"pages": 5
}
}
Читать из 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.
v_api_competitive_analytics— материализованная вьюха, объединяетv_product_card(все 68 полей) с позициями изsearch_positions. Обновляется автоматически после завершения enrich. Бэкенд фильтрует поkeyword.
SELECT * FROM v_api_competitive_analytics
WHERE keyword = 'термокружка'
ORDER BY position;
GET /categories/?flat=true
{"count": 1234, "categories": [{"id": 9468, "name": "Термосы и термокружки", ...}]}
{
"task_id": "550e8400-e29b-41d4-a716-446655440001",
"task_type": "enrich",
"parse_method": "token",
"request": {
"source_type": "category",
"source_value": "9468",
"pages": 3
}
}
Аналогично сценарию 1. data = null, summary содержит counts.
-- Для категорий keyword хранится как 'category:{id}'
SELECT * FROM v_api_competitive_analytics
WHERE keyword = 'category:9468'
ORDER BY position;
Цель: узнать позицию своего артикула в выдаче — без полного enrich.
{
"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
}
}
}
{
"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 товаров из выдачи.
{
"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
}
}
}
Аналогично сценарию 3: data.target_results + data.top_products в Redis.
Цель: показывает ли WB мой товар на страницах конкурентов?
{
"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.
{
"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.
{
"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;
parser:resultsXREADGROUP 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_type | data | summary.items_found | summary.items_saved | summary.items_failed |
|---|---|---|---|---|
search | QuickData (target_results + top_products) | Товаров найдено | Товаров сохранено в БД | С ошибками |
enrich | null | Товаров найдено | Товаров обогащено в БД | 0 (не трекается) |
recom | null | Конкурентов, где нас показывают | Записей рекомендаций в БД | Конкурентов, где НЕ показывают |
{
"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.
Объединяет
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;
Обычная вьюха с 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;
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;
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;
Эти эндпоинты доступны напрямую, но основной путь — через Redis.
| Метод | Путь | Описание |
|---|---|---|
| GET | /categories/?flat=true | Плоский список категорий (для получения category_id) |
| GET | /categories/ | Иерархическое дерево категорий |
| GET | /tasks/{task_id}/status | Статус задачи по UUID (альтернатива Redis) |
| GET | /tasks/streams/health | Здоровье Redis Streams |
| GET | /health | K8s liveness |
| GET | /readiness | K8s readiness |
products содержит актуальное состояние, а не снэпшот. Если товар попал в два разных enrich — в нём данные последнего обновления.*_history-таблицах по product_id.product_recommendations.tasks при получении задачи из Redis. db_task_id в результате — это ID этой записи.