fix(tradein): avito houses dedup через match_or_create_house + merge-миграция дублей (108) #1546

Merged
lekss361 merged 2 commits from fix/avito-houses-dedup-merge into main 2026-06-16 05:38:24 +00:00
Owner

Проблема

Таблица houses накопила ~4479 лишних строк (~33%): один физический дом создавался многократно. Корень — avito_houses.py::upsert_house/_upsert_house_with_url делали прямой INSERT INTO houses ON CONFLICT (source, ext_house_id), минуя match_or_create_house. Конфликт-ключ — avito building-id, поэтому дедуп по кадастру/адресу/гео никогда не запускался → каждый catalog-enrich плодил новый дом. Все остальные пути (listing-скраппер base.py, estimator) уже ходят через match_or_create_house корректно — avito_houses был единственным bypass.

Подтверждённая картина (живая БД): 3879 групп с одинаковым адресом + 1888 групп по координатам. Часть «у дома много объявлений» (listings.house_id_fk, avg 2.8/дом) работала и не трогается.

Изменения

1. Forward-fix avito_houses.py — новый _persist_house(db, h, house_url):

  • идёт через match_or_create_house(ext_source="avito", ext_id=str(h.ext_id), address=short_address, lat, lon, year_built, source_url) → canonical house_id (3-tier дедуп + pg_advisory_xact_lock);
  • затем UPDATE houses SET … со всеми 30+ enrichment-полями. avito-авторитетные (rating/developer/тех-хар-ки/map_pins/raw_characteristics) — прямое присвоение; address/lat/lon/year_built — через COALESCE(houses.<col>, …) чтобы не затирать canonical от cian/кадастра;
  • db.commit() только в save_house_catalog_enrichment (advisory lock держится весь pipeline).
  • address=short_address (не full): существующие house_address_aliases лежат в short-форме без регионального префикса — full дал бы промах по Tier 2 (review M1, проверено по проду).

2. Merge-миграция data/sql/108_merge_duplicate_houses.sql (идемпотентна, BEGIN/COMMIT, авто-применяется на прод):

  • cluster_key с приоритетом: кадастр → addr:lower(address)geo:round(lat/lon,4); строки без всех трёх пропускаются (консервативно — лучше недо-слить);
  • canonical = ROW_NUMBER() OVER (… ORDER BY geom IS NOT NULL DESC, field_count DESC, id ASC);
  • репойнт ВСЕХ 11 FK-зависимостей (listings.house_id_fk, house_sources, house_address_aliases, house_reviews, house_placement_history, houses_price_dynamics, house_reliability_checks, house_imv_evaluations, house_suggestions, external_valuations, address_mismatch_audit) до DELETE FROM houses — с обработкой UNIQUE-коллизий (delete-dup-before-repoint для таблиц с уникальными ключами);
  • backfill house_sources/house_address_aliases для канонов (ON CONFLICT DO NOTHING);
  • RAISE NOTICE before/after counts.

3. Тест tests/test_avito_houses_dedup.py — роутинг через matching + запись enrichment-полей.

Проверки

  • pytest: matching/same_building/dedup сьюты зелёные (46+ passed); единственный fail в полном прогоне — pre-existing test_cian_valuation::test_cache_hit (воспроизводится на main).
  • ruff: clean.
  • code-reviewer: ⚠️ MINOR → M1 исправлен (short_address), остальные minor by-design.

⚠️ Деплой

Миграция 108 деструктивна (удаляет ~тысячи строк houses) и авто-применяется на прод на деплое. Репойнт идёт до delete — данные зависимых таблиц сохраняются на канонических домах. Точный count смерженных кластеров — в RAISE NOTICE первого деплоя.

## Проблема Таблица `houses` накопила ~4479 лишних строк (~33%): один физический дом создавался многократно. Корень — `avito_houses.py::upsert_house`/`_upsert_house_with_url` делали прямой `INSERT INTO houses ON CONFLICT (source, ext_house_id)`, минуя `match_or_create_house`. Конфликт-ключ — avito building-id, поэтому дедуп по кадастру/адресу/гео никогда не запускался → каждый catalog-enrich плодил новый дом. Все остальные пути (listing-скраппер `base.py`, estimator) уже ходят через `match_or_create_house` корректно — avito_houses был единственным bypass. Подтверждённая картина (живая БД): 3879 групп с одинаковым адресом + 1888 групп по координатам. Часть «у дома много объявлений» (`listings.house_id_fk`, avg 2.8/дом) работала и не трогается. ## Изменения **1. Forward-fix `avito_houses.py`** — новый `_persist_house(db, h, house_url)`: - идёт через `match_or_create_house(ext_source="avito", ext_id=str(h.ext_id), address=short_address, lat, lon, year_built, source_url)` → canonical `house_id` (3-tier дедуп + `pg_advisory_xact_lock`); - затем `UPDATE houses SET …` со всеми 30+ enrichment-полями. avito-авторитетные (rating/developer/тех-хар-ки/map_pins/raw_characteristics) — прямое присвоение; `address/lat/lon/year_built` — через `COALESCE(houses.<col>, …)` чтобы не затирать canonical от cian/кадастра; - `db.commit()` только в `save_house_catalog_enrichment` (advisory lock держится весь pipeline). - `address=short_address` (не full): существующие `house_address_aliases` лежат в short-форме без регионального префикса — full дал бы промах по Tier 2 (review M1, проверено по проду). **2. Merge-миграция `data/sql/108_merge_duplicate_houses.sql`** (идемпотентна, `BEGIN/COMMIT`, авто-применяется на прод): - cluster_key с приоритетом: кадастр → `addr:lower(address)` → `geo:round(lat/lon,4)`; строки без всех трёх пропускаются (консервативно — лучше недо-слить); - canonical = `ROW_NUMBER() OVER (… ORDER BY geom IS NOT NULL DESC, field_count DESC, id ASC)`; - репойнт ВСЕХ 11 FK-зависимостей (`listings.house_id_fk`, `house_sources`, `house_address_aliases`, `house_reviews`, `house_placement_history`, `houses_price_dynamics`, `house_reliability_checks`, `house_imv_evaluations`, `house_suggestions`, `external_valuations`, `address_mismatch_audit`) **до** `DELETE FROM houses` — с обработкой UNIQUE-коллизий (delete-dup-before-repoint для таблиц с уникальными ключами); - backfill `house_sources`/`house_address_aliases` для канонов (`ON CONFLICT DO NOTHING`); - `RAISE NOTICE` before/after counts. **3. Тест** `tests/test_avito_houses_dedup.py` — роутинг через matching + запись enrichment-полей. ## Проверки - pytest: matching/same_building/dedup сьюты зелёные (46+ passed); единственный fail в полном прогоне — pre-existing `test_cian_valuation::test_cache_hit` (воспроизводится на main). - ruff: clean. - code-reviewer: ⚠️ MINOR → M1 исправлен (`short_address`), остальные minor by-design. ## ⚠️ Деплой Миграция 108 **деструктивна** (удаляет ~тысячи строк houses) и **авто-применяется на прод** на деплое. Репойнт идёт до delete — данные зависимых таблиц сохраняются на канонических домах. Точный count смерженных кластеров — в `RAISE NOTICE` первого деплоя.
lekss361 added 2 commits 2026-06-16 05:36:32 +00:00
fix(tradein): avito houses dedup через match_or_create_house + merge-миграция дублей (108)
All checks were successful
CI / changes (push) Successful in 7s
CI / backend-tests (push) Has been skipped
CI / frontend-tests (push) Has been skipped
CI / openapi-codegen-check (push) Has been skipped
b72af1cc49
Корень проблемы: avito_houses.py обходил канонический matching-путь через
прямой INSERT ON CONFLICT(source, ext_house_id), создавая дубли для физически
одного дома (~33% таблицы houses = ~4479 лишних строк).

Forward fix: upsert_house/upsert_house_with_url → _persist_house(db, h, url),
который вызывает match_or_create_house (3-tier: source_exact→fingerprint→geo→new,
с pg_advisory_xact_lock) и затем UPDATE с enrichment-полями. avito-авторитетные
поля (rating, developer, технические хар-ки) перезаписываются напрямую;
address/lat/lon/year_built — через COALESCE чтобы не затирать canonical-данные.

Merge-миграция 108: cluster_key по cadastral/address/geo → canonical (max
не-NULL полей, geom присутствует, min(id)) → репойнт всех 11 зависимых таблиц
с обработкой UNIQUE-коллизий → DELETE дублей → backfill house_sources/aliases.
fix(tradein): M1 review — short_address для match_or_create_house (alias keyspace)
All checks were successful
CI / changes (push) Successful in 8s
CI / changes (pull_request) Successful in 6s
CI / backend-tests (push) Has been skipped
CI / frontend-tests (push) Has been skipped
CI / openapi-codegen-check (push) 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
bc504d9519
lekss361 added the
bug
scope/backend
scrapers
labels 2026-06-16 05:37:03 +00:00
lekss361 merged commit 91c2f6a6a8 into main 2026-06-16 05:38:24 +00:00
lekss361 deleted branch fix/avito-houses-dedup-merge 2026-06-16 05:38:24 +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#1546
No description provided.