feat(tradein): hook matching в save_listings — fill listing_sources (PR I) #595

Merged
Light1YT merged 1 commit from feat/tradein-hook-listing-matching into main 2026-05-27 10:02:38 +00:00
Owner

Summary

Закрывает критичную gap: listing_sources table в проде имеет 0 rows при том что listings имеет 18,428 rows — matching service никогда не вызывался из scrapers. Новые scraped lots теперь автоматически линкуются к canonical houses.

Implementation

Single hook point: base.py:save_listings() — единственная точка INSERT для всех 4 scrapers (Avito/Cian/Yandex/N1). После каждого UPSERT в listings — call _link_listing_to_house():

  1. match_or_create_house() resolves canonical houses.id (Tier 0-3 как у estimator)
  2. upsert_listing_source() пишет (listing_id, ext_source, ext_id, house_id) в junction с method='source_link', confidence=1.0

Fault tolerance: try/except + db.begin_nested() SAVEPOINT — match failure не abortит batch.
Idempotency: ON CONFLICT (ext_source, ext_id) DO UPDATE в upsert_listing_source.

Files (+340 / -19)

  • app/services/scrapers/base.py +109/-8 (hook + RETURNING id + UA noqa cleanup)
  • tests/test_base_save_listings.py +231/-7 (14 tests, 8 new)

Tests

  • 14 pass в test_base_save_listings.py
  • 89 wider tests passing
  • ruff clean

Effect post-deploy

  • Следующий scraper sweep автоматически заполнит listing_sources для новых lots
  • Existing 18k listings остаются без linkage — нужен PR J backfill (отдельно)
  • Estimator (geo-proximity) пока что не affected — он использует raw listings.lat/lon, не junction

Out of scope (PR J)

Backfill existing 18k listings — отдельный operation script. Будет использовать тот же match_or_create_house + upsert_listing_source flow, в batches по 500-1k, с progress logs.

Open question для PR J

matching/listings.py Tier 2-3 references listings.house_id но 011_listings_alter.sql имеет только house_id_fk. Либо undocumented migration, либо Tier 2-3 silently возвращают 0 results. PR I использует только Tier 0-1 (source_link path) — не affected. PR J должен resolve канонический column через \d listings на проде.

Refs #582.

## Summary Закрывает критичную gap: `listing_sources` table в проде имеет **0 rows** при том что `listings` имеет **18,428 rows** — matching service никогда не вызывался из scrapers. Новые scraped lots теперь автоматически линкуются к canonical houses. ## Implementation **Single hook point**: `base.py:save_listings()` — единственная точка INSERT для всех 4 scrapers (Avito/Cian/Yandex/N1). После каждого UPSERT в listings — call `_link_listing_to_house()`: 1. `match_or_create_house()` resolves canonical `houses.id` (Tier 0-3 как у estimator) 2. `upsert_listing_source()` пишет `(listing_id, ext_source, ext_id, house_id)` в junction с method='source_link', confidence=1.0 **Fault tolerance**: `try/except + db.begin_nested()` SAVEPOINT — match failure не abortит batch. **Idempotency**: `ON CONFLICT (ext_source, ext_id) DO UPDATE` в `upsert_listing_source`. ## Files (+340 / -19) - `app/services/scrapers/base.py` +109/-8 (hook + RETURNING id + UA noqa cleanup) - `tests/test_base_save_listings.py` +231/-7 (14 tests, 8 new) ## Tests - 14 pass в `test_base_save_listings.py` - 89 wider tests passing - ruff clean ## Effect post-deploy - Следующий scraper sweep автоматически заполнит `listing_sources` для новых lots - Existing 18k listings остаются без linkage — нужен **PR J backfill** (отдельно) - Estimator (geo-proximity) пока что не affected — он использует raw `listings.lat/lon`, не junction ## Out of scope (PR J) Backfill existing 18k listings — отдельный operation script. Будет использовать тот же `match_or_create_house` + `upsert_listing_source` flow, в batches по 500-1k, с progress logs. ## Open question для PR J `matching/listings.py` Tier 2-3 references `listings.house_id` но `011_listings_alter.sql` имеет только `house_id_fk`. Либо undocumented migration, либо Tier 2-3 silently возвращают 0 results. PR I использует только Tier 0-1 (`source_link` path) — не affected. PR J должен resolve канонический column через `\d listings` на проде. Refs #582.
Light1YT added 1 commit 2026-05-27 10:02:36 +00:00
Wire app.services.matching в base.save_listings: после каждого INSERT,
resolve canonical house через match_or_create_house и register
(ext_source, ext_id) в listing_sources через upsert_listing_source.

Закрывает gap который оставлял listing_sources пустым (0 rows в проде)
при том что listings имеет 18k+ rows. Каждый scraped lot теперь создаёт/
обновляет listing_sources row и линкует к canonical houses — enabling
per-house price history и de-duplication через avito/cian/yandex/n1.

Implementation:
- save_listings RETURNING id добавлен (был только `(xmax = 0) AS inserted`)
- Новый helper _link_listing_to_house():
  * match_or_create_house() для resolve canonical houses.id из address/lat/lon
    (Tier 0–3 как у estimator.py)
  * Использует lot.house_source/house_ext_id если scraper extract'нул
    (Avito Houses Catalog, Cian newbuilding); fallback на (lot.source, ext_id)
  * upsert_listing_source с method='source_link', confidence=1.0
- Fault-tolerant: try/except + db.begin_nested() SAVEPOINT
  — match failure не abortит INSERT batch
- Idempotent: ON CONFLICT (ext_source, ext_id) UPDATE
- House-match failure отдельно handled — listing_sources всё равно upsert'ит
  (без house_id) чтобы row для later backfill existed
- 4 UA strings помечены noqa: E501 (pre-existing, нельзя split)

Tests: 14 pass в test_base_save_listings.py:
- 6 existing updated (_get_execute_params reads call_args_list[0], _mock_db
  returns id from RETURNING, autouse fixture патчит matching service)
- 8 new: RETURNING id, hook firing once, listing context passed, ON CONFLICT
  path, failure non-abort, dedup_hash fallback, house-match failure path,
  empty lots skip
- Wider 89 tests passing

Out of scope: backfill existing 18k listings (PR J) — для new scrapes
этот hook предотвратит regression.

Open Q для PR J: matching/listings.py Tier 2-3 references listings.house_id
но schema только house_id_fk. Backfill PR должен resolve canonical column.
Light1YT merged commit 7e24ccbf2c into main 2026-05-27 10:02:38 +00:00
Light1YT deleted branch feat/tradein-hook-listing-matching 2026-05-27 10:02:38 +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#595
No description provided.