fix(scrapers): stable dedup_hash — drop price_rub + strip URL query (Refs #753) #761
No reviewers
Labels
No labels
admin
analytics
auth
automation
bug
business
chore
ci
compliance
data
data-moat
docs
duplicate
dx
enhancement
Fable 5 ревью
feedback/max
generative
GG-форсайт
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
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: lekss361/gendesign#761
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "fix/753-avito-dedup-stable"
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?
Что
ScrapedLot.compute_dedup_hashвключалprice_rubи волатильныйsource_url(?context=...) в ключ → смена цены / ре-скрейп того же объявления давали новый hash →INSERTдубля вместоON CONFLICT (dedup_hash) DO UPDATE→ дубли активных listings → искажениеasking_to_sold_ratio, sales-vs-listings, активного инвентаря.Как (
base.py.compute_dedup_hash)price_rubубран из ключа — смена цены того же объявления это UPDATE строки, не новый объект;source_url-fallback стрипает query-string (?...) — для источников безsource_id(и Avito-карточек безdata-item-id).⚠️ Конфликт со спекой (зафиксировано, не угадано)
DoD п.1 требовал «avito выставляет
source_id(grep → 0 совпадений)». Премиса устарела:avito.py:404уже делаетsource_id=card.attributes.get("data-item-id")(добавлено #726).avito.pyя не менял — DoD п.1 удовлетворён существующим кодом. Реальный live-баг — толькоprice_rubв ключеbase.py(срабатывал даже когдаsource_idесть). Его и чиню.Разовый эффект (как оговорено в issue)
Смена формулы → существующие строки получат новый hash → следующий скрейп upsert'нет их по новому ключу. Всплеск дублей разовый. Backfill/merge 72 существующих дубль-групп — out of scope (отдельный follow-up).
Тесты (
tests/test_base_save_listings.py, offline)test_dedup_hash_stable_across_price_and_context_query— тот жеsource_id, разная цена + разный?context=→ один hash.test_dedup_hash_strips_query_when_no_source_id— безsource_idquery-strip работает.test_dedup_hash_distinguishes_different_listings— разныеsource_id→ разный hash (не схлопывает несвязанное).test_dedup_hash_distinguishes_source_even_same_id—sourceостаётся частью ключа.pytest -k dedup→ 16 passed.test_base_save_listings.py+test_cian_serp_scraper.py→ 39 passed (нет регрессии).ruff checkclean.Refs #753
✅ APPROVE
Stable
dedup_hash(#753). Проверил тщательно —dedup_hashэтоON CONFLICTupsert-ключ listings.Correctness
price_rubиз ключа: смена цены того же объявления теперь хешируется одинаково →ON CONFLICT (dedup_hash) DO UPDATEобновляет строку, а не вставляет дубль. Это и есть корневой баг (цена в ключе ломала upsert). Верно.(source_url or "").split("?")[0]убирает волатильный?context=для источников безsource_id. Верно.source | (source_id or stripped_url): Avito (source_id из #726), Cian (offer_id), Yandex (stripped URL) — стабильны;sourceв ключе (тест на same-id-different-source). Нет риска коллизии (в source нет|).source_id=data-item-id(avito.py:404) — я ревьюил #726, подтверждаю — поэтому avito.py не тронут, починен только реальный баг в base.py. Ровно поведение «конфликт → коммент, не угадывай», задокументировано в PR. Хорошая cross-PR осведомлённость.source_idfalsy → fallback на URL (безопасно).Tests — 4 новых вкл. anti-over-collapse guards (разный source_id → разный hash; same id different source → разный hash). 16 dedup passed, 39 total без регрессии, ruff clean.
Разовый churn — смена формулы ре-хеширует существующие строки → разовый всплеск дублей на следующем скрейпе; backfill 72 групп — out-of-scope follow-up. Задокументировано и принято в issue.
Scope/security — base.py + тест (+ ruff-format churn); avito.py корректно не тронут; нет миграции (computed hash), нет secrets, нет self-extending триггера.
Verdict: ✅ APPROVE (Praktika #753). Post-deploy мониторинг дубль-групп → qa.