fix(tradein/scraper): строка с пустым сегментом самочинится при пересборе #2911
No reviewers
Labels
No labels
Fable 5 ревью
GG-форсайт
admin
analytics
auth
automation
bug
business
chore
ci
compliance
data
data-moat
docs
duplicate
dx
enhancement
feedback/max
generative
needs-discussion
needs-human
observability
pause-bots
performance
priority/p0
priority/p1
priority/p2
priority/p3
scope/backend
scope/db
scope/devops
scope/frontend
scope/qa
scrapers
security
site-finder
stage/1
stage/2
status/blocked
status/done
status/needs-analysis
status/needs-fix
status/qa
status/ready
status/review
status/wip
tech-debt
tradein
ux
week ревью 1
wontfix
ИРД
вторичка
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: lekss361/gendesign#2911
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "fix/tradein-upsert-segment-selfheal"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Причина, а не симптом
Upsert объявлений (
ON CONFLICT (dedup_hash) DO UPDATE) не обновлялlisting_segment. Строка, единожды родившаяся с пустым сегментом, оставалась такой навсегда — даже при живой ежедневной досдаче, потому что повторный сбор шёл по ветке UPDATE, а этой колонки в SET не было.Отсюда пул «активных» строк без сегмента, который якорил оценку. Миграция 266 и джобы
deactivate_stale_{cian,yandex}_null_segment(#2908) лечили симптом — снимали протухшие строки с пустым сегментом. Этот PR закрывает причину: строка чинит себя сама при следующем же сборе.Замер на проде 15.08.2026 — активных строк с пустым сегментом: cian 224, yandex 544. Из них старше 60 суток (то есть подпадающих под деактивацию) — 211 и 523; остальные 34 живые и после этой правки получат сегмент вместо того, чтобы дожидаться TTL.
Правка
Два места, обе —
tradein-mvp/packages/scraper-kit/src/scraper_kit/base.py, функцияsave_listings():base.py:648— основной upsert:base.py:756— reconcile-UPDATE (запасной путь при дрейфеdedup_hash, когда ловитсяUniqueViolationпо(source, source_id)):Второе место формально за рамками «ON CONFLICT DO UPDATE», но болезнь та же: строки, прошедшие через reconcile, остались бы незалеченными.
COALESCE, а не голыйEXCLUDED: если сбор пришёл без сегмента, уже известное значение обязано выжить. Тот же идиом, что рядом применён к городу.Проверка
tradein-mvp/backend/tests/test_listing_segment_upsert_selfheal.py— 5 тестов: INSERT-путь, обе стороны COALESCE в upsert (сегмент проставляется / известный не затирается), reconcile-UPDATE. Все проходят.ruff checkчисто;grep -nE ':[a-z_]+::[a-z]'пуст (ловушка приведения типов psycopg v3 не задета).Отдельно, не чинилось
В том же upsert только при INSERT пишутся и никогда не обновляются:
lat,lon,region_code,rooms,floor,total_floors,year_built,house_type,repair_state,has_balcony,house_source,house_ext_id,house_url,photo_urls,raw_payload,geo_precision. То есть исправление параметров площадкой и свежие фото после первой вставки теряются. Это тот же класс, но отдельная задача — трогать здесь не стал.Критерий приёмки на проде
После деплоя и следующего свипа cian/yandex число активных строк с
listing_segment IS NULLиlast_seen_atсвежее 7 суток должно упасть с 13 (cian 9 + yandex 4) до 0.Refs #2908
Раньше listing_segment писался ТОЛЬКО при INSERT в save_listings — строка, единожды родившаяся с NULL-сегментом (классификатор не сработал на первом скрейпе), никогда не получала сегмент на повторных сборах, даже когда он становился определим. Симптом лечила отдельная джоба деактивации (deactivate_stale_{cian,yandex}_null_segment, миграция 266, PR #2908) — чистит мусор, но не саму запись. Добавлен listing_segment = COALESCE(EXCLUDED.listing_segment, listings.listing_segment) в ON CONFLICT DO UPDATE, и тот же идиом в reconcile-UPDATE (dedup_hash-drift fallback path) — иначе self-heal был бы неполным для строк, прошедших через этот путь. COALESCE, а не голый EXCLUDED, чтобы повторный скрейп без определённого сегмента не затирал уже известное значение пустым (тот же паттерн, что уже применён для city/kitchen/ceiling). Тесты: tests/test_listing_segment_upsert_selfheal.py — INSERT-путь, ON CONFLICT COALESCE (обе стороны), reconcile-UPDATE COALESCE.