feat(tradein-sql): bootstrap houses + link 98.8% of listings (Phase A+B) #527
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
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: lekss361/gendesign#527
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "feat/tradein-backfill-houses-link-listings"
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?
Problem
housestable was empty (0 rows). All 18,428 active listings hadhouse_id_fk=NULL. Cross-source listings (cian+avito+yandex для same building) не consolidate'ились в один дом.Fix
Idempotent migration
063_backfill_houses_and_link_listings.sqlwith 6 steps:house_source=avito+house_ext_id(preserve internal Avito IDs)ext_house_id). EXCLUDE addresses already covered by step 1 (no cross-source dupes).UPDATE listings.house_id_fkvia(source, ext_house_id)matchUPDATE listings.house_id_fkvia normalized address.COALESCEprefers avito-house if same addr exists (cian/yandex lots в known Avito building линкуются к одному house)house_placement_history.house_id(160 yandex_valuation orphans, raw_payload без address → 0 relinked; deferred to Phase C scraper-side)Normalizer function
tradein_normalize_short_addr(text)(CREATE OR REPLACE, IMMUTABLE, PARALLEL SAFE) stripsРоссия/обл./г./кв./корп./оф./подъезд.Live-run counters
housestotalsource=avitosource=derivedlistings.house_id_fkNOT NULLhouse_placement_historyrelinkedraw_payloadбезaddresskey — Phase C work)Idempotency
INSERT … ON CONFLICT DO NOTHING(UNIQUE on(source, ext_house_id))UPDATE … WHERE house_id_fk IS NULLVerified
Validated end-to-end via
BEGIN; \i 063…sql; ROLLBACK;on prod tradein-postgres (no persistence). Будет применено real на deploy черезapply migrationsstep.Roadmap
Follow-up to
[[Decision_TradeIn_DataQuality_8PR_Roadmap]]. Phase C (Avito IMVplacementHistory+suggestionsper house) deferred to separate Celery job. UI exposure of houses/history coming in PR 11.Deep Code Review — verdict APPROVE
Summary
Verified contracts
houses (source, ext_house_id)UNIQUE present (009_houses.sql) ->ON CONFLICT DO NOTHINGvalidhouses.short_addressexists (010_houses_alter.sql) -> Step 3 populates, Step 5 readspgcrypto.digest()extension required -> noted in header commentls -1 | sort)ON_ERROR_STOP=onon psql invocation -> BEGIN/COMMIT atomicity preservedIdempotency analysis
INSERT ... ON CONFLICT (source, ext_house_id) DO NOTHING-- re-run safesubstring(sha256(norm_addr), 1, 32)-- deterministic per addressUPDATE listings SET house_id_fk = ... WHERE house_id_fk IS NULL-- re-run is no-opCREATE OR REPLACE FUNCTION-- re-run safeDO $$ ... $$blocks read-only -- pure observabilityNOT IN (subquery filtered by address IS NOT NULL)-- confirmed normalizer cannot return NULL on non-NULL input (usestrim(... FROM regexp_replace(...))which yields empty string worst case, not NULL) ->NOT INnot poisoned by NULLDedup correctness (1.2% unlinked = 231 rows)
NOT INexclusion prevents split rows when same building has both avito-typed and other-source listingsCOALESCE(non-derived match, derived match)consolidates cross-source listings under single house -- semantically correct for the cross-source goalWHERE house_id_fk IS NULL AND address IS NOT NULLskips null-address listings; sub-5-char normalizations also skipped per Step 3 filter. No retry queue/log table for manual review -- minor gap but acceptable for one-shot backfill.Concurrency / advisory lock interaction (#501)
match_or_create_house()from #501 holdspg_advisory_xact_lock(42, hashtext(fingerprint))only inside Python-side INSERT pathUNIQUE (source, ext_house_id)+ON CONFLICT DO NOTHINGprovides safety net regardless of sourcematch_or_create_houselogicPerformance
tradein_normalize_short_addr(address)against 562 houses, exec'd per listings row -> IMMUTABLE function, plan-optimizable. ~5-10s.listings_house_ext_id_idx (source, house_ext_id)-- fastCREATE INDEX IF NOT EXISTS houses_short_addr_idxfor future re-runs at scale.listingstable held for duration of single transaction (~30-60s). Moderate impact during deploy -- readers blocked on UPDATE'd rows but scrapers throttled by Caddy reload anyway.Rollback plan
BEGIN; ... COMMIT;block --ROLLBACKreverts cleanlyBEGIN; \i 063...sql; ROLLBACK;on prod tradein-postgres per PR bodyDELETE FROM houses WHERE first_seen_at > '<deploy ts>'+UPDATE listings SET house_id_fk = NULL WHERE last_modified_at > '<deploy ts>'(manual, but doable since houses table started at 0 rows)tradein_normalize_short_addrfunction is CREATE OR REPLACE -- leaving it post-rollback is harmlessTest coverage / data quality assertions
Estimator / schema additions (cian_valuation)
CianValuationSummary-- purely additive, optional. Backward compat: existing frontend ignores unknown field._empty_estimatecorrectly initializescian_valuation=Noneestimate_quality: defensiveint(...) if cian_val.sale_price_rub else None,list(cian_val.chart or [])-- handles missing chart safelycian_valisCianValuationResult | Nonefrom existing scraper integration (line 702) -- contract verifiedVault cross-check
Decision_TradeIn_DataQuality_8PR_RoadmapPhase A+B per PR bodyplacementHistoryper house) deferred to separate Celery job -- appropriate scope disciplineRecommended next steps (non-blocker)
addressintohouse_placement_history.raw_payloadso future relink (Step 6) is more than 0-row no-ophouses_short_addr_idxpartial index before re-runComplexity / blast radius