4 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
3fd6550a16 |
fix(tradein/matching): честность тиров сопоставления домов (#2674)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 7s
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 2m54s
Три находки эпика #2674 про верхние тиры матчинга домов. Замеры — прод tradein-postgres, 2026-08-05/06. Кадастр от площадок не приходит вообще. listings.cadastral_number (кадастр КВАРТИРЫ) — 0 из 93 408; единственный писатель, парсер Циана, читает offer["cadastralNumber"], которого в ответе нет. Все 28 504 заполненных building_cadastral_number на 100% пришли из локального гео-зеркала ЕГРН (tasks/cadastral_geo_match.py, KNN <=50 м) — проверено джойном к cad_buildings_local. Поэтому снят фильтр поиска has_kadastr: предикат `cadastral_number IS NOT NULL` мог вернуть только пустую выдачу. Колонка и писатель оставлены — заработают сами, если площадка начнёт отдавать кадастр. Tier 0 cadastr_exact оставлен, но не подключён к гео-кадастру. Он достижим по построению (ScrapedLot -> адаптер -> матчер), просто данных нет; подать туда KNN-заполнение НЕЛЬЗЯ: как ключ здания оно не инъективно — 656 из 3 260 значений накрывают >1 здание ГАР (20.1%), 751 из 2 864 зданий получают >1 значение (26.2%). Это был бы over-merge с confidence 1.0. Заодно исправлено ложное утверждение в шапке cadastral_geo_match.py, будто Tier 0 трактует эту колонку как подсказку. Tier 0.5 fias_exact удалён из match_or_create_house. Параметра house_fias_id не было ни в Protocol scraper_kit.contracts.HouseMatcher, ни в RealMatcherAdapter, ни у двух прямых вызывающих — передать его было некому. В match_house_readonly тир оставлен: у estimate-пути источник ФИАС есть (payload.target_fias_id / DaData). Что чинит сопоставление на самом деле: ключ идентичности в house_dedup_merge расширен с house_fias_id до COALESCE(house_fias_id, gar_house_guid). Это один и тот же UUID здания в ГАР (3 666 совпадений из 3 667 домов, где заполнены оба), но заполняют его разные источники, и половина в проход не входила. Read-only прогон отрендеренного mapping-SQL на проде: старый ключ — 0 пар, новый — 781 (8.3% таблицы houses, 6 389 объявлений на них). Канон-проход эти дома узнаёт (900 пар из 919 имеют один канон-адрес), но блокирует гео-стражем: 356 пар с NULL geom, 457 дальше 250 м (максимум 5 065 км — битый геокод). Ровно аргумент #2187: общий UUID здания старше близости. Качество сопоставления сейчас: 0 из 49 502 строк house_sources сматчены верхними тирами; fingerprint 58.97%, new 22.65%, geo_proximity 18.36%. Тесты: новый tests/test_matching_tier_reachability_2674.py сверяет параметры матчера с границей вызова (Protocol + адаптер) — ловит класс «ветка есть, передать некому», который обычный тест не видит, потому что зовёт функцию напрямую. Удалены два теста мёртвого fias-тира: они были зелёными ровно потому, что обходили границу вызова. Refs #2674 |
||
|
|
85059aeb1b |
refactor(tradein/scheduler): удалить legacy scheduler_loop + scraper-scheduling, kit единственный путь (#2397 Part C)
Топология подтверждена перед удалением (docker-compose.prod.yml): tradein-backend (uvicorn app.main:app) — SCHEDULER_ENABLE=false; tradein-scraper (python -m app.scheduler_main) — SCHEDULER_ENABLE=true + USE_KIT_SCHEDULER=true. Kit-путь (_run_kit_scheduler → scraper_kit.orchestration.scheduler + product_handlers) самодостаточен: не импортирует ничего из app.services.scheduler.scheduler_loop или app.services.scrape_pipeline. Все НЕ-sweep джобы, которые kit-scheduler диспетчерит через build_product_handlers, идут напрямую в app.tasks.*/ app.services.* (либо lazy-импортят import_rosreestr_dkp/_execute_cian_backfill из scheduler.py) — мимо удаляемой legacy-машинерии. app/services/scheduler.py: 2098 → 418 строк. Удалено: scheduler_loop, get_due_schedules, reap_zombies, _claim_run, _defer_next_run_at, _spawn_tracked/ _drain_inflight/_inflight_tasks, все 27 trigger_*_run-функций, импорт app.services.scrape_pipeline, константы SCHEDULER_TICK_SEC/ZOMBIE_THRESHOLD_HOURS (достижимы были только через удалённый scheduler_loop-путь). Оставлено (живые импортёры вне удалённого): compute_next_run_at + has_running_run (admin.py), import_rosreestr_dkp + _execute_cian_backfill (lazy-импорты в product_handlers.py — job-тела kit-handler'ов). main.py: убран `from app.services.scheduler import scheduler_loop` + lifespan-блок запуска (`if settings.scheduler_enable: asyncio.create_task(scheduler_loop())`); прод-backend всегда шёл с SCHEDULER_ENABLE=false, так что это был мёртвый код. scheduler_main.py: убрана ship-dark развилка #2192 (USE_KIT_SCHEDULER=false → legacy scheduler_loop fallback) — _run_kit_scheduler() теперь безусловный путь. Поле settings.use_kit_scheduler оставлено в конфиге (Settings extra="ignore" защищает от startup-краха на leftover env var), но на ветвление не влияет. app.services.scrape_pipeline: 0 runtime-импортёров в app/+scripts/+packages/ после этого PR (только тесты, которые Part E удалит вместе с самим файлом) — подтверждено grep. scrape_pipeline.py не тронут (Part E). Тесты: удалены test_house_imv_backfill_scheduler.py (100% legacy-триггер, backfill_house_imv сервис покрыт в test_house_imv_backfill_browser_flag.py / test_backfill_wave2.py) и test_kit_registry_completeness.py (parity-инвариант против удалённого dispatch, дублирует test_scraper_kit_scheduler_parity.py). Точечно вырезаны "Scheduler wiring" секции (trigger_fn_exists/dispatch_branch_ wired/runs_in_executor) из ~10 файлов, тестирующих сами task-функции — сами task-тесты (SQL-shape, миграции, fake-db поведение) оставлены нетронутыми. test_scheduler.py: 825 → ~90 строк (остались только compute_next_run_at-тесты). test_scraper_kit_scheduler_parity.py: убрана golden-parity секция против удалённого scheduler_loop (SOURCE_TO_OLD_TRIGGER/_drive_old_one_tick/ test_routing_parity_per_source), остальное (claim/reap_zombies/dispatch/ registry-shape тесты kit-модуля) сохранено — источник этих инвариантов не app.services.scheduler, а сам scraper_kit.orchestration.scheduler. test_scheduler_main.py: 2 теста, патчившие app.services.scheduler.scheduler_loop, переведены на монкипатч sm._run_kit_scheduler (единственный путь после этого PR). test_sweep_imv_phase.py:171-371 (6 прямых импортов run_avito_city_sweep из scrape_pipeline) намеренно НЕ тронуты — Part E. Verify: полный pytest 3179 passed / 6 skipped / 1 known-unrelated fail (test_search_cache_hit, #2208, не связан с этим PR); ruff 0.7.4 чист на всех изменённых файлах; `python -c "import app.main; import app.scheduler_main"` OK. |
||
| f9769571a3 |
fix(cadastral): single-statement match UPDATE — OFFSET chunk skipped rows
The chunked matcher paged the candidate set with OFFSET over the very predicate it mutates (building_cadastral_number IS NULL). Matched rows drop out, so OFFSET (reset to 0 OR advanced) skips un-processed rows: once >= batch_size unmatched listings (beyond threshold) accumulate at the low-id front, the chunk matches 0 and the loop breaks early, leaving higher-id matchable listings unfilled. Counterexample (batch=3, matchable ids 1,4,7,10): matches 1 and 4 then breaks, missing 7 and 10. Unit tests used single-chunk fixtures so never caught it. The full UPDATE is ~5s for ~41k listings, so replace the loop with ONE set-based LATERAL KNN UPDATE over all candidates — correct and fast. batch_size kept in the signature for schedule-param compat (now unused). |
|||
| edd187b47f |
feat(cadastral): geo-nearest building matcher via local cad mirror
Backfill listings.building_cadastral_number (was 0%) from the nearest cadastral building. gendesign_cad_buildings is a postgres_fdw foreign table with no geom — a per-listing FDW nearest query is ~1.16s/row (~13h for 43k listings). Instead materialize the FDW once into a LOCAL cad_buildings_local table (Point geom + GIST), then run a fast local KNN nearest-neighbour join. Perf: the distance gate uses geometry-space ST_DWithin(geom, point, deg) (GIST index, no geography cast) + geom <-> KNN order + ST_DistanceSphere metric recheck on the single nearest row. A geography-cast ST_DWithin in the WHERE defeated the index (58s+, full update never finished); the geometry-gate design does the whole ~41.9k-listing UPDATE in ~5.4s (refresh+match ~7s end-to-end), matching 16027 listings at 50m across 2229 distinct buildings. The match is GEO-NEAREST (approximate): a street-level-geocoded listing matches the nearest building within threshold_m (default 50m), not necessarily its exact cadastral building. Exact cadastral + parcel-containment deferred (cad_parcels FDW not exposed). Threshold is logged. - 124: cad_buildings_local table (empty) + GIST. Migration does not read the FDW (deploy-independent); populated by the refresh job. - 125: scrape_schedules seed source=cadastral_geo_match, enabled, next_run_at tomorrow 09:00 UTC (after geocode_missing). - tasks/cadastral_geo_match.py: refresh_cad_buildings_local (bulk FDW scan), match_listings_to_buildings (chunked LATERAL KNN UPDATE), run_cadastral_geo_match run-lifecycle wrapper. - scheduler: trigger_cadastral_geo_match_run + dispatch (sync DB-only, executor). |