Кросс-источниковый дедуп объявлений невозможен по построению: dedup_hash = source + source_id, матчер не вызывается ни разу #2995

Open
opened 2026-08-20 17:49:19 +00:00 by lekss361 · 1 comment
Owner

Эпик: #2989

Находка

Это не баг реализации, а следствие того, что идентичность объявления задана через площадку.

ScrapedLot.compute_dedup_hash() (packages/scraper-kit/src/scraper_kit/base.py:187-206) хэширует source + source_id, и именно по нему идёт ON CONFLICT. Один лот на Авито и ЦИАН по построению обязан стать двумя строками listings. Дедуп невозможен математически, а не «не настроен».

Симптомы, которые из этого следуют: v_cross_source_health возвращает 0 строк, v_data_quality.listings_dedup_2sources = 0, из 101 992 записей listing_sources ни одной с двумя ext_source.

Матчер написан, но мёртв

Трёхуровневый match_or_create_listing (backend/app/services/matching/listings.py:22) существует, но со стороны скрапера не вызывается ни разу: _link_listing_to_house (base.py:905-996) сразу дёргает upsert_listing_source(…, method="source_link").

Прод подтверждает: из 101 999 строк listing_sources у всех matched_method только source_link (91 214) или backfill (10 785). Ни одной minhash, composite или cadastr_exact. То есть listing_sources сегодня не таблица связей, а зеркало listings один к одному.

Это ровно случай из эпика #2674 — код написан, отревьюен, смержен и ни разу не сработал.

Дубли реально есть и измеримы

По грубому ключу (house_id_fk, rooms, floor, round(area_m2)) среди активных: 5 175 блоков с более чем одним источником на 12 536 строк, из них 7 361 строка избыточна. В 4 474 блоках (86,5 %) цены расходятся не более чем на 3 % — это один и тот же лот, а не разные квартиры.

Вред: один лот попадает в выборку аналогов дважды и получает двойной вес.

Готовый образец внутри проекта

Дедуп домов работает: house_address_aliases с вычисляемым fingerprint (35 977 матчей) плюс house_merge_log с jsonb-снимками для отката. 6 430 домов собраны из двух и более источников, 4 824 — из трёх.

Целевая модель для объявлений — тот же шаблон: «публикация» (первоисточник, одна площадка) и «лот» (кластерная голова, попадает в аналоги один раз). Двухступенчато: блокировка по грубому ключу → скоринг внутри блока. Результат — таблица связей с уверенностью, а не жёсткий merge, чтобы ошибочное слияние можно было откатить.

Поправка проверяющего: description_minhash заполнен только у ЦИАН и частично Домклика (23,6 %), так что нечёткое сравнение по описанию слепо по данным. И simhash в bigint сам по себе не решает задачу — расстояние Хэмминга не берётся ни btree, ни GIN; нужен banding (несколько колонок по 16 бит с btree по каждой), иначе поиск кандидатов выродится в seq scan.

Acceptance

  • Ключ идентичности публикации отделён от идентичности лота
  • Матчер реально вызывается на пути скрапера — проверяется по matched_method на проде
  • Связи хранятся с уверенностью, ошибочное слияние откатывается
  • v_cross_source_health перестаёт быть пустой
  • Выборка аналогов берёт лот один раз, а не каждую публикацию

Scope: packages/scraper-kit/src/scraper_kit/base.py, backend/app/services/matching/listings.py, backend/data/sql/.

Эпик: #2989 ## Находка Это не баг реализации, а следствие того, что **идентичность объявления задана через площадку**. `ScrapedLot.compute_dedup_hash()` (`packages/scraper-kit/src/scraper_kit/base.py:187-206`) хэширует `source + source_id`, и именно по нему идёт `ON CONFLICT`. Один лот на Авито и ЦИАН **по построению обязан** стать двумя строками `listings`. Дедуп невозможен математически, а не «не настроен». Симптомы, которые из этого следуют: `v_cross_source_health` возвращает 0 строк, `v_data_quality.listings_dedup_2sources` = 0, из 101 992 записей `listing_sources` **ни одной** с двумя `ext_source`. ## Матчер написан, но мёртв Трёхуровневый `match_or_create_listing` (`backend/app/services/matching/listings.py:22`) существует, но со стороны скрапера **не вызывается ни разу**: `_link_listing_to_house` (`base.py:905-996`) сразу дёргает `upsert_listing_source(…, method="source_link")`. Прод подтверждает: из 101 999 строк `listing_sources` у всех `matched_method` только `source_link` (91 214) или `backfill` (10 785). Ни одной `minhash`, `composite` или `cadastr_exact`. То есть `listing_sources` сегодня не таблица связей, а зеркало `listings` один к одному. Это ровно случай из эпика #2674 — код написан, отревьюен, смержен и ни разу не сработал. ## Дубли реально есть и измеримы По грубому ключу `(house_id_fk, rooms, floor, round(area_m2))` среди активных: **5 175 блоков** с более чем одним источником на 12 536 строк, из них **7 361 строка избыточна**. В 4 474 блоках (86,5 %) цены расходятся не более чем на 3 % — это один и тот же лот, а не разные квартиры. Вред: один лот попадает в выборку аналогов дважды и получает двойной вес. ## Готовый образец внутри проекта Дедуп **домов** работает: `house_address_aliases` с вычисляемым fingerprint (35 977 матчей) плюс `house_merge_log` с jsonb-снимками для отката. 6 430 домов собраны из двух и более источников, 4 824 — из трёх. Целевая модель для объявлений — тот же шаблон: «публикация» (первоисточник, одна площадка) и «лот» (кластерная голова, попадает в аналоги один раз). Двухступенчато: блокировка по грубому ключу → скоринг внутри блока. Результат — таблица связей с уверенностью, а не жёсткий merge, чтобы ошибочное слияние можно было откатить. Поправка проверяющего: `description_minhash` заполнен только у ЦИАН и частично Домклика (23,6 %), так что нечёткое сравнение по описанию слепо по данным. И simhash в `bigint` сам по себе не решает задачу — расстояние Хэмминга не берётся ни btree, ни GIN; нужен banding (несколько колонок по 16 бит с btree по каждой), иначе поиск кандидатов выродится в seq scan. ## Acceptance - [ ] Ключ идентичности публикации отделён от идентичности лота - [ ] Матчер реально вызывается на пути скрапера — проверяется по `matched_method` на проде - [ ] Связи хранятся с уверенностью, ошибочное слияние откатывается - [ ] `v_cross_source_health` перестаёт быть пустой - [ ] Выборка аналогов берёт лот один раз, а не каждую публикацию Scope: `packages/scraper-kit/src/scraper_kit/base.py`, `backend/app/services/matching/listings.py`, `backend/data/sql/`.
lekss361 added the
data
priority/p2
scope/backend
scope/db
tech-debt
tradein
labels 2026-08-20 17:50:59 +00:00
Collaborator

Перепроверка 27.08.2026 (после пересборки базы 25-26.08 и переезда продукта на Poincare). Тело тикета писалось 20-24.08 по старой инфраструктуре.

Посылка держится без правок: compute_dedup_hash (packages/scraper-kit/src/scraper_kit/base.py:187-206) по-прежнему кладёт source первым полем в SHA256. Ни minhash, ни составного ключа не появилось. Инвентарь пересчитать от текущих listings, число в теле устарело.

**Перепроверка 27.08.2026** (после пересборки базы 25-26.08 и переезда продукта на Poincare). Тело тикета писалось 20-24.08 по старой инфраструктуре. Посылка держится без правок: `compute_dedup_hash` (`packages/scraper-kit/src/scraper_kit/base.py:187-206`) по-прежнему кладёт `source` первым полем в SHA256. Ни minhash, ни составного ключа не появилось. Инвентарь пересчитать от текущих `listings`, число в теле устарело.
Sign in to join this conversation.
No milestone
No project
No assignees
2 participants
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#2995
No description provided.