fix(tradein/data): yandex url-«дубли» — каноникализация URL (скрейпер + миграция 164) + manifest 162 (#2235, #2216) #2271

Merged
lekss361 merged 2 commits from fix/tradein-yandex-url-dedup into main 2026-07-03 06:53:27 +00:00
Owner

Root cause (verified на данных, :35432 SELECT-only)

Все 204 дубль-группы (1253 активных строк) — yandex novostroyki. В каждой группе строки с разными source_id и ценами = реальные разные квартиры, делящие URL застройщика/редиректа (na100.pro/go.php — 732 строки, atlasdevelopment.su — 279→1 URL, unistroyrf.ru — 127→1 и т.д.): gate-API отдаёт entity["url"] = лендинг ЖК, _entity_to_lot сохранял его verbatim. Row-level dedup (dedup_hash = sha256(source|source_id)) работает корректно — дублировалась только колонка source_url. Vtorichka (4193 строки) несёт канонические realty.yandex.ru/offer/<id> и дублей не даёт.

Решение: вариант Б из issue (ETL-уникальность, БЕЗ unique index)

Partial unique index на (source, source_url) WHERE is_active отвергнут — верифицировано ревьюером по коду: conflict target upsert'а = dedup_hash (base.py:455), конфликт по URL не имеет арбитра → IntegrityError → reconcile по (source, source_id) не найдёт новую строку → легитимное объявление молча дропается. «Схлопнуть до свежайшего» деактивировало бы ~1049 реальных объявлений.

Вместо этого:

  • Корень: _canonical_source_url() в yandex_realty.py + зеркало в scraper-kit (golden-parity #2133) — не-realty.yandex URL заменяется на https://realty.yandex.ru/offer/<source_id>/.
  • Хил: недеструктивная идемпотентная миграция 164_yandex_url_canonicalize_active_dups.sql — переписывает shared URL у активных yandex-строк, is_active не трогает. Upsert/reconcile/enrichment не пишут source_url → хил переживает ре-скрейпы (проверено ревьюером по всем трём write-путям).
  • Попутно (#2216 gap из верификационного аудита): дописан пропущенный 162_seed_deals_freshness_monitor.sql в _manifest_applied.txt (stale manifest из #2228); manifest 159→164 без дыр.

Как проверено

  • Полный uv run pytest (deselect только test_search_cache_hit): 3033 passed, 6 skipped; test_migrations_manifest.py 4 passed.
  • Read-only dry-run миграции против прод-БД: 0 остаточных дубль-групп после применения.
  • Тесты: каноникализация (verbatim vtorichka / rewrite partner/redirect/empty; два оффера с одним dev-URL → разные канонические), golden-parity _ENTITY_PARTNER_URL, статические гарды миграции (транзакционность/идемпотентность/недеструктивность).
  • code-reviewer: APPROVE (арбитр dedup_hash, формат URL migration==scraper, NULL-safety, vtorichka untouched — верифицированы).

Post-deploy: проверить на проде дубль-группы 204 → 0 и что инвентарь yandex не упал.

Closes #2235

## Root cause (verified на данных, :35432 SELECT-only) Все 204 дубль-группы (1253 активных строк) — yandex `novostroyki`. В каждой группе строки с **разными source_id и ценами** = реальные разные квартиры, делящие **URL застройщика/редиректа** (na100.pro/go.php — 732 строки, atlasdevelopment.su — 279→1 URL, unistroyrf.ru — 127→1 и т.д.): gate-API отдаёт `entity["url"]` = лендинг ЖК, `_entity_to_lot` сохранял его verbatim. Row-level dedup (`dedup_hash = sha256(source|source_id)`) работает корректно — дублировалась только колонка `source_url`. Vtorichka (4193 строки) несёт канонические `realty.yandex.ru/offer/<id>` и дублей не даёт. ## Решение: вариант Б из issue (ETL-уникальность, БЕЗ unique index) Partial unique index на `(source, source_url) WHERE is_active` отвергнут — верифицировано ревьюером по коду: conflict target upsert'а = `dedup_hash` (base.py:455), конфликт по URL не имеет арбитра → IntegrityError → reconcile по `(source, source_id)` не найдёт новую строку → **легитимное объявление молча дропается**. «Схлопнуть до свежайшего» деактивировало бы ~1049 реальных объявлений. Вместо этого: - **Корень:** `_canonical_source_url()` в `yandex_realty.py` + зеркало в scraper-kit (golden-parity #2133) — не-realty.yandex URL заменяется на `https://realty.yandex.ru/offer/<source_id>/`. - **Хил:** недеструктивная идемпотентная миграция `164_yandex_url_canonicalize_active_dups.sql` — переписывает shared URL у активных yandex-строк, `is_active` не трогает. Upsert/reconcile/enrichment не пишут `source_url` → хил переживает ре-скрейпы (проверено ревьюером по всем трём write-путям). - **Попутно (#2216 gap из верификационного аудита):** дописан пропущенный `162_seed_deals_freshness_monitor.sql` в `_manifest_applied.txt` (stale manifest из #2228); manifest 159→164 без дыр. ## Как проверено - Полный `uv run pytest` (deselect только `test_search_cache_hit`): **3033 passed, 6 skipped**; `test_migrations_manifest.py` 4 passed. - Read-only dry-run миграции против прод-БД: 0 остаточных дубль-групп после применения. - Тесты: каноникализация (verbatim vtorichka / rewrite partner/redirect/empty; два оффера с одним dev-URL → разные канонические), golden-parity `_ENTITY_PARTNER_URL`, статические гарды миграции (транзакционность/идемпотентность/недеструктивность). - code-reviewer: ✅ APPROVE (арбитр dedup_hash, формат URL migration==scraper, NULL-safety, vtorichka untouched — верифицированы). Post-deploy: проверить на проде дубль-группы 204 → 0 и что инвентарь yandex не упал. Closes #2235
lekss361 added 2 commits 2026-07-03 06:48:40 +00:00
fix(tradein/migrations): дописать 162 в _manifest_applied — stale manifest из #2228 (#2216)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI / changes (pull_request) Successful in 9s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 1m44s
dd8b5769e5
lekss361 merged commit bbe596747d into main 2026-07-03 06:53:27 +00:00
lekss361 deleted branch fix/tradein-yandex-url-dedup 2026-07-03 06:53:28 +00:00
Sign in to join this conversation.
No reviewers
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#2271
No description provided.