Домклик: богатые поля SERP оседают в raw_payload и никуда не доезжают (100 % NULL в колонках) #3064

Open
opened 2026-08-23 21:15:32 +00:00 by lekss361 · 1 comment
Owner

Найдено аудитом скрапперов 24.08.2026. Не ошибка в смысле мониторинга — именно тот класс потери, который не виден ни в одной метрике, только чтением кода.

Что происходит

BFF-ответ Домклика (фикстура tradein-mvp/tests/fixtures/domclick_bff_offers_sample.json) отдаёт заметно больше, чем мы кладём в колонки: isRosreestrApproved, isSberCollateral, hasDiscount / discountValue, duplicatesOfferCount, flatComplex.{id,slug,name}, lastPriceHistoryState.

Всё это попадает только в raw_payload jsonb (tradein-mvp/packages/scraper-kit/src/scraper_kit/providers/domclick/serp.py:639-666) и не пробрасывается в ScrapedLot.

Замер на проде (1 338 активных domklik-листингов, 23-24.08)

Колонка NULL Комментарий
registry_match / is_rosreestr_checked 100 % (1338/1338) isRosreestrApproved — прямое 1:1 соответствие, уже парсится, просто не проброшено
house_source / house_ext_id / newbuilding_id 100 % flatComplex.id/slug не пробрасывается вовсе
is_pro_seller 100 % при том что agency_name заполнен у 100 % (1338/1338, _extract_agency_name, serp.py:194-218)
house_type (материал стен) 100 % задокументированный TODO Layer B (serp.py:25)
living_area_m2 99.8 % (1336/1338)

Почему flatComplex — не косметика

Без house_source / house_ext_id домклик-листинги физически не попадают в house-matching pipeline: нет привязки к домам и ЖК, нет дедупа на уровне дома, нет аналитики по ЖК. У yandex этот путь есть (site_id → house_ext_id), у домклика — нет.

Отдельно: house_type завис без пути реализации

serp.py:25 планирует добирать renovation / wallType / priceHistory через detail-эндпоинт (Layer B). Но domclick_detail_backfill сейчас практически мёртв — за 15+ дней подряд каждый прогон упирается в max_consecutive_blocks=3 почти сразу ({"blocked": 3, "attempted": 3-4, "enriched": 0-1}), суммарно ~5-6 обогащённых карточек за две недели. Кука сессии при этом живая (domclick_session_cookies: uploaded 10.08, expires 09.09, свежий last_used_at) — то есть дело не в протухшей сессии, а в том, что QRATOR-обход (#2430 / #2433) перестал брать текущую защиту площадки. См. также #2764 (ban_kind='unknown' не различает «нас распознал QRATOR» и «наш browser-fetch сам упал»).

Значит на Layer B рассчитывать нельзя, и то, что доступно прямо из SERP, тем более стоит забирать.

Что делать

S/S, делать сейчас:

  1. isRosreestrApprovedis_rosreestr_checked / registry_match.
  2. is_pro_seller вывести из agency_name (данные уже есть у 100 % записей).

M/M, отдельным заходом:
3. flatComplex.id/slughouse_source / house_ext_id — нужна миграция и проверка house-matching.

Пункты 1-2 не требуют миграции: колонки уже существуют, значения уже разбираются, не хватает только присваивания в ScrapedLot.

Найдено аудитом скрапперов 24.08.2026. Не ошибка в смысле мониторинга — именно тот класс потери, который не виден ни в одной метрике, только чтением кода. ## Что происходит BFF-ответ Домклика (фикстура `tradein-mvp/tests/fixtures/domclick_bff_offers_sample.json`) отдаёт заметно больше, чем мы кладём в колонки: `isRosreestrApproved`, `isSberCollateral`, `hasDiscount` / `discountValue`, `duplicatesOfferCount`, `flatComplex.{id,slug,name}`, `lastPriceHistoryState`. Всё это попадает **только** в `raw_payload` jsonb (`tradein-mvp/packages/scraper-kit/src/scraper_kit/providers/domclick/serp.py:639-666`) и не пробрасывается в `ScrapedLot`. ## Замер на проде (1 338 активных domklik-листингов, 23-24.08) | Колонка | NULL | Комментарий | |---|---|---| | `registry_match` / `is_rosreestr_checked` | **100 %** (1338/1338) | `isRosreestrApproved` — прямое 1:1 соответствие, **уже парсится**, просто не проброшено | | `house_source` / `house_ext_id` / `newbuilding_id` | **100 %** | `flatComplex.id/slug` не пробрасывается вовсе | | `is_pro_seller` | **100 %** | при том что `agency_name` заполнен у **100 %** (1338/1338, `_extract_agency_name`, `serp.py:194-218`) | | `house_type` (материал стен) | **100 %** | задокументированный TODO Layer B (`serp.py:25`) | | `living_area_m2` | 99.8 % (1336/1338) | | ## Почему `flatComplex` — не косметика Без `house_source` / `house_ext_id` домклик-листинги **физически не попадают в house-matching pipeline**: нет привязки к домам и ЖК, нет дедупа на уровне дома, нет аналитики по ЖК. У yandex этот путь есть (`site_id → house_ext_id`), у домклика — нет. ## Отдельно: `house_type` завис без пути реализации `serp.py:25` планирует добирать `renovation` / `wallType` / `priceHistory` через detail-эндпоинт (Layer B). Но `domclick_detail_backfill` сейчас практически мёртв — за 15+ дней подряд каждый прогон упирается в `max_consecutive_blocks=3` почти сразу (`{"blocked": 3, "attempted": 3-4, "enriched": 0-1}`), суммарно ~5-6 обогащённых карточек за две недели. Кука сессии при этом живая (`domclick_session_cookies`: uploaded 10.08, expires 09.09, свежий `last_used_at`) — то есть дело не в протухшей сессии, а в том, что QRATOR-обход (#2430 / #2433) перестал брать текущую защиту площадки. См. также #2764 (`ban_kind='unknown'` не различает «нас распознал QRATOR» и «наш browser-fetch сам упал»). Значит на Layer B рассчитывать нельзя, и то, что доступно прямо из SERP, тем более стоит забирать. ## Что делать **S/S, делать сейчас:** 1. `isRosreestrApproved` → `is_rosreestr_checked` / `registry_match`. 2. `is_pro_seller` вывести из `agency_name` (данные уже есть у 100 % записей). **M/M, отдельным заходом:** 3. `flatComplex.id/slug` → `house_source` / `house_ext_id` — нужна миграция и проверка house-matching. Пункты 1-2 не требуют миграции: колонки уже существуют, значения уже разбираются, не хватает только присваивания в `ScrapedLot`.
Author
Owner

Взялся за пункт 3 и остановился до написания кода: поле, названное в задаче, — неверный ключ, а верный приходит по пути, который домами не занимается. Ниже замеры и три варианта; выбор за тобой, потому что это ценообразующий путь.

Как устроена привязка сейчас

Матчер (matching/houses.py, Tier 1, confidence 1.0):

SELECT house_id FROM house_sources WHERE ext_source=:s AND ext_id=:e   -- UNIQUE(ext_source, ext_id)

а вызывается он так (scrapers/base.py):

h_src = lot.house_source or lot.source          # у domklik сейчас -> 'domklik'
h_ext = lot.house_ext_id or (source_id or dedup_hash)

То есть у Домклика house_source пуст → ключом становится сам листинг, и каждое объявление образует собственный «дом». Отсюда 809 «домов» на 1351 активный листинг. Это и есть причина, по которой они не участвуют в дедупе — не отсутствие поля, а fallback на идентичность листинга.

Почему flatComplex — неверный ключ

Ключ Покрытие Различных значений Листингов на ключ
flatComplex.id 737 / 1351 (55 %) 116 ≈ 6.4
domclick_building_guid 619 / 1351 (46 %) 467 ≈ 1.3

И решающее: все 1351 активных — вторичка, новостроек ноль.

Прецеденты, на которые опирается задача (cian_newbuilding, yandex_realty_nb), применяют ЖК-идентификатор к новостройкам, где «один ЖК ≈ один объект продажи» — допустимое упрощение. Для вторички ЖК — это несколько разных зданий с разными адресами, возрастом и ценой.

Если взять flatComplex.id как house_ext_id, 737 листингов схлопнутся в 116 «домов», и якорь того же дома в оценщике (_compute_same_building_anchor) начнёт сравнивать квартиры из разных зданий как соседей по подъезду. Это прямая порча ценового механизма, а не улучшение дедупа.

Проверка domclick_building_guid на выборке подтверждает, что он настоящий идентификатор здания — у топовых guid'ов по одному адресу на ключ (9 листингов/1 адрес, 7/1, 5/1).

Но у верного ключа своя проблема

domclick_building_guid разбирается в providers/domclick/detail.py:461 (pc.address.guid) — то есть приходит detail-путём. А match_or_create_house вызывается в save_listings, то есть на SERP-пути, который guid'а не видит.

Поставка при этом живая, вопреки моему же опасению: 35 из 59 на этой неделе, 367 из 791 на прошлой, всего обогащено 6301, последнее 21.08. То есть ключ есть, он просто лежит не на той дороге.

Миграция не нужна вовсе — колонки house_source / house_ext_id / newbuilding_id уже существуют. Оценка «M/M, нужна миграция» в задаче завышена.

Три варианта

A. Читать guid из уже сохранённого raw_payload в SERP-пути. save_listings и так делает pre-read строки (гейт #2992). Если у строки уже есть guid от прошлого обогащения — использовать его как house_ext_id. Аккуратно, но связывает запись SERP с содержимым предыдущего payload.

B. Привязывать дом в самом detail-бэкфилле. Он уже делает UPDATE строки — добавить туда match_or_create_house. Локальнее и честнее по смыслу (guid пришёл — сразу и привязали), но раздваивает место, где создаются дома.

C. Оставить как есть. 54 % листингов guid'а не имеют, для них ничего не изменится в любом варианте.

Во всех вариантах предлагаю house_source = 'domclick_building' — с пространством имён, как cian_newbuilding / yandex_realty_nb, чтобы guid не столкнулся с per-listing идентичностью.

Отдельно и безрисково: flatComplex.slugnewbuilding_id. Это поле бэкендом сейчас не используется вообще (проверил), то есть чистый захват данных на будущее, а ЖК получает своё законное место вместо чужого.

Почему остановился

Задача сформулирована как проброс поля, а по факту это:

  1. смена ключа идентичности домов для источника,
  2. влияющая на якорь того же дома в оценщике — то есть на цену,
  3. затрагивающая 619 листингов (619 «домов» → 467).

Направление изменения, скорее всего, верное — настоящие соседи по дому вместо их отсутствия. Но это ценовой путь, и подтвердить эффект можно только замером на trade_in_estimates, как это делалось в #2661.

Скажи, какой вариант — сделаю с замером до/после. Вариант C тоже нормальный ответ: пробел реальный, но он не про переезд и не горит.

Взялся за пункт 3 и остановился до написания кода: **поле, названное в задаче, — неверный ключ**, а верный приходит по пути, который домами не занимается. Ниже замеры и три варианта; выбор за тобой, потому что это ценообразующий путь. ## Как устроена привязка сейчас Матчер (`matching/houses.py`, Tier 1, confidence 1.0): ```sql SELECT house_id FROM house_sources WHERE ext_source=:s AND ext_id=:e -- UNIQUE(ext_source, ext_id) ``` а вызывается он так (`scrapers/base.py`): ``` h_src = lot.house_source or lot.source # у domklik сейчас -> 'domklik' h_ext = lot.house_ext_id or (source_id or dedup_hash) ``` То есть у Домклика `house_source` пуст → ключом становится **сам листинг**, и каждое объявление образует собственный «дом». Отсюда 809 «домов» на 1351 активный листинг. Это и есть причина, по которой они не участвуют в дедупе — не отсутствие поля, а fallback на идентичность листинга. ## Почему `flatComplex` — неверный ключ | Ключ | Покрытие | Различных значений | Листингов на ключ | |---|---|---|---| | `flatComplex.id` | 737 / 1351 (55 %) | **116** | **≈ 6.4** | | `domclick_building_guid` | 619 / 1351 (46 %) | **467** | **≈ 1.3** | И решающее: **все 1351 активных — вторичка, новостроек ноль.** Прецеденты, на которые опирается задача (`cian_newbuilding`, `yandex_realty_nb`), применяют ЖК-идентификатор к **новостройкам**, где «один ЖК ≈ один объект продажи» — допустимое упрощение. Для вторички ЖК — это несколько разных зданий с разными адресами, возрастом и ценой. Если взять `flatComplex.id` как `house_ext_id`, 737 листингов схлопнутся в 116 «домов», и **якорь того же дома в оценщике** (`_compute_same_building_anchor`) начнёт сравнивать квартиры из разных зданий как соседей по подъезду. Это прямая порча ценового механизма, а не улучшение дедупа. Проверка `domclick_building_guid` на выборке подтверждает, что он настоящий идентификатор здания — у топовых guid'ов по одному адресу на ключ (9 листингов/1 адрес, 7/1, 5/1). ## Но у верного ключа своя проблема `domclick_building_guid` разбирается в `providers/domclick/detail.py:461` (`pc.address.guid`) — то есть приходит **detail-путём**. А `match_or_create_house` вызывается в `save_listings`, то есть на **SERP-пути**, который guid'а не видит. Поставка при этом живая, вопреки моему же опасению: 35 из 59 на этой неделе, 367 из 791 на прошлой, всего обогащено 6301, последнее 21.08. То есть ключ есть, он просто лежит не на той дороге. **Миграция не нужна вовсе** — колонки `house_source` / `house_ext_id` / `newbuilding_id` уже существуют. Оценка «M/M, нужна миграция» в задаче завышена. ## Три варианта **A. Читать guid из уже сохранённого `raw_payload` в SERP-пути.** `save_listings` и так делает pre-read строки (гейт #2992). Если у строки уже есть guid от прошлого обогащения — использовать его как `house_ext_id`. Аккуратно, но связывает запись SERP с содержимым предыдущего payload. **B. Привязывать дом в самом detail-бэкфилле.** Он уже делает UPDATE строки — добавить туда `match_or_create_house`. Локальнее и честнее по смыслу (guid пришёл — сразу и привязали), но раздваивает место, где создаются дома. **C. Оставить как есть.** 54 % листингов guid'а не имеют, для них ничего не изменится в любом варианте. Во всех вариантах предлагаю `house_source = 'domclick_building'` — с пространством имён, как `cian_newbuilding` / `yandex_realty_nb`, чтобы guid не столкнулся с per-listing идентичностью. Отдельно и безрисково: `flatComplex.slug` → `newbuilding_id`. Это поле бэкендом сейчас **не используется вообще** (проверил), то есть чистый захват данных на будущее, а ЖК получает своё законное место вместо чужого. ## Почему остановился Задача сформулирована как проброс поля, а по факту это: 1. смена ключа идентичности домов для источника, 2. **влияющая на якорь того же дома в оценщике** — то есть на цену, 3. затрагивающая 619 листингов (619 «домов» → 467). Направление изменения, скорее всего, верное — настоящие соседи по дому вместо их отсутствия. Но это ценовой путь, и подтвердить эффект можно только замером на `trade_in_estimates`, как это делалось в #2661. Скажи, какой вариант — сделаю с замером до/после. Вариант C тоже нормальный ответ: пробел реальный, но он не про переезд и не горит.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#3064
No description provided.