fix(tradein/scraper): собирать вторичку Циана по области, а не только новостройки #2623

Merged
lekss361 merged 2 commits from fix/tradein-cian-oblast-secondary into main 2026-08-02 11:13:33 +00:00
Owner

Summary

  • Включить сбор вторички Циана для 4 областных city-sweep расписаний (Нижний Тагил, Каменск-Уральский, Первоуральск, Серов) — default_params.newbuilding_only: false через jsonb-мердж.
  • Проблема: областную вторичку Циана не собирал никто — городская развёртка (default newbuilding_only=True) отбрасывала её, а run_cian_full_load (единственный владелец вторички по докстрингу) хардкодит city=Екатеринбург и область не покрывает.
  • Екатеринбургское расписание (cian_city_sweep, без суффикса города) НЕ тронуто — для него дефолт корректен, вторичку ЕКБ авторитетно собирает cian_full_load.

⚠️ Верхняя Пышма ИСКЛЮЧЕНА из этой миграции (было 5, стало 4)

После первого варианта PR обнаружился риск: geo-проверка (ST_DWithin от центра ЕКБ) показала, что городская метка city у cian-строк с city="Верхняя Пышма" загрязнена екатеринбургскими объявлениями:

источник метка всего физически в 15 км от центра ЕКБ
cian Верхняя Пышма 22 5 (23%)
cian Нижний Тагил 8 0
cian Каменск-Уральский 5 0
cian Первоуральск 2 0

Верхняя Пышма физически ~15 км от Екатеринбурга — anchor-развёртка захватывает краевые екатеринбургские объявления и подписывает их не тем городом. listings.city — money-critical (её читает asking_to_sold_ratio.py для city-скоупа ASKING vs SOLD стороны, #2583 H2) — неверная метка двигает выкупные цены. Включение вторички умножило бы это 23%-загрязнение (22 строки → несколько сотен).

nizhniy_tagil/kamensk_uralskiy/pervouralsk/serov — загрязнение по той же проверке нулевое — включать безопасно.

Верхняя Пышма НЕ забыта — включат отдельной миграцией после починки городской разметки sweep'а (правится параллельно). Объяснение с цифрой 5/22 зафиксировано в шапке 204_cian_oblast_sweeps_secondary.sql и в regression-guard тесте, чтобы не решили «забыли».

Почему это безопасно (нагрузка / дубли / сегмент)

  • Нагрузка на источник НЕ растёт. fetch_around_multi_room (providers/cian/serp.py) не принимает newbuilding_only/secondary_only — SERP-фаза (все room×page запросы) идёт одинаково независимо от флага. Фильтр в pipeline.py:2436 применяется ПОСЛЕ фетча, ДО save — чисто in-memory отсечение уже оплаченных запросов. Растёт только объём save_listings (DB writes): ~2-9 сохранённых лотов/прогон → ~55-184 (весь fetched-пул, по 4 оставшимся источникам). detail_top_n=10 detail-запросов тоже не растёт (LIMIT константен).
  • Дублей с cian_full_load не будет. full_load всегда city_region_id=4743 (ЕКБ), областные sweeps используют CITY_LOCATIONS[].cian_region_id (4886/4781/4925/4982, все != 4743) — физически разные региональные SERP-выдачи. dedup_hash = sha256(source|source_id) — глобальный Cian offer_id, ON CONFLICT DO UPDATE даже в edge-case не создаёт дубль-строку.
  • listing_segment проставляется корректно. providers/cian/serp.py:892 — вторичка получает listing_segment="vtorichka" (НЕ NULL), проходит фильтр listing_segment IS NULL OR = vtorichka в asking_to_sold_ratio.py/buildings_query.py — новые лоты попадут в оценку без доп. кода.

Замеры (before/after, BEGIN...ROLLBACK на проде)

Прод-счётчики (scrape_runs.counters) до фикса:

sweep увидено выброшено сохранено
pervouralsk 55 53 2
kamensk_uralskiy 113 108 5
nizhniy_tagil 184 176 3 (avg по 14 runs: 154 увидено / 8 сохранено)
verkhnyaya_pyshma 38 16 9 — исключено из этого PR, см. секцию выше

jsonb-мердж перепроверен для финального (4-источникового) списка в транзакции с ROLLBACK на проде: UPDATE 4 строки, все соседние ключи (city/radius_m/detail_top_n/enrich_houses/pages_per_anchor/request_delay_sec) сохранены, cian_city_sweep (ЕКБ) и cian_city_sweep_verkhnyaya_pyshma не задеты.

Migration number

Main на момент ветки — до 202. Открытый PR #2622 (fix/tradein-geocoder-house-letter) уже занял 203 (203_purge_geocode_cache_house_letter.sql) — использован 204, чтобы избежать коллизии. Перепроверено дважды (включая перед финальным push — parallel windows активны): main всё ещё на 202, других претендентов на 204 среди открытых PR нет.

Test plan

  • tests/test_migration_204_cian_oblast_sweeps_secondary.py (9 тестов) — транзакционность, no-DDL, jsonb-мердж (не перезапись), ровно 4 таргет-source, ДВА regression-guard'а (bare cian_city_sweep ЕКБ И cian_city_sweep_verkhnyaya_pyshma отсутствуют в WHERE), no psycopg CAST trap, идемпотентность
  • Полный pytest в tradein-mvp/backend: 3090 passed, 9 skipped, 1 pre-existing fail (test_search_api.py::test_search_cache_hit, 401 RBAC — не в скоупе, известен заранее)

Границы

Только миграция + тест. Код скраперов/scheduler.py/pipeline.py не тронут — дефолт newbuilding_only=True в коде остаётся как правильный fallback. Развёртки вручную не запускались.

## Summary - Включить сбор вторички Циана для **4** областных city-sweep расписаний (Нижний Тагил, Каменск-Уральский, Первоуральск, Серов) — `default_params.newbuilding_only: false` через jsonb-мердж. - Проблема: областную вторичку Циана не собирал никто — городская развёртка (default newbuilding_only=True) отбрасывала её, а `run_cian_full_load` (единственный владелец вторички по докстрингу) хардкодит `city=Екатеринбург` и область не покрывает. - Екатеринбургское расписание (`cian_city_sweep`, без суффикса города) НЕ тронуто — для него дефолт корректен, вторичку ЕКБ авторитетно собирает `cian_full_load`. ## ⚠️ Верхняя Пышма ИСКЛЮЧЕНА из этой миграции (было 5, стало 4) После первого варианта PR обнаружился риск: geo-проверка (`ST_DWithin` от центра ЕКБ) показала, что городская метка `city` у cian-строк с `city="Верхняя Пышма"` **загрязнена екатеринбургскими объявлениями**: | источник | метка | всего | физически в 15 км от центра ЕКБ | |---|---|---|---| | cian | Верхняя Пышма | 22 | **5 (23%)** | | cian | Нижний Тагил | 8 | 0 | | cian | Каменск-Уральский | 5 | 0 | | cian | Первоуральск | 2 | 0 | Верхняя Пышма физически ~15 км от Екатеринбурга — anchor-развёртка захватывает краевые екатеринбургские объявления и подписывает их не тем городом. `listings.city` — money-critical (её читает `asking_to_sold_ratio.py` для city-скоупа ASKING vs SOLD стороны, #2583 H2) — неверная метка двигает выкупные цены. Включение вторички умножило бы это 23%-загрязнение (22 строки → несколько сотен). nizhniy_tagil/kamensk_uralskiy/pervouralsk/serov — загрязнение по той же проверке **нулевое** — включать безопасно. **Верхняя Пышма НЕ забыта** — включат отдельной миграцией после починки городской разметки sweep'а (правится параллельно). Объяснение с цифрой 5/22 зафиксировано в шапке `204_cian_oblast_sweeps_secondary.sql` и в regression-guard тесте, чтобы не решили «забыли». ## Почему это безопасно (нагрузка / дубли / сегмент) - **Нагрузка на источник НЕ растёт.** `fetch_around_multi_room` (providers/cian/serp.py) не принимает newbuilding_only/secondary_only — SERP-фаза (все room×page запросы) идёт одинаково независимо от флага. Фильтр в pipeline.py:2436 применяется ПОСЛЕ фетча, ДО save — чисто in-memory отсечение уже оплаченных запросов. Растёт только объём save_listings (DB writes): ~2-9 сохранённых лотов/прогон → ~55-184 (весь fetched-пул, по 4 оставшимся источникам). detail_top_n=10 detail-запросов тоже не растёт (LIMIT константен). - **Дублей с `cian_full_load` не будет.** full_load всегда `city_region_id=4743` (ЕКБ), областные sweeps используют CITY_LOCATIONS[<slug>].cian_region_id (4886/4781/4925/4982, все != 4743) — физически разные региональные SERP-выдачи. dedup_hash = sha256(source|source_id) — глобальный Cian offer_id, ON CONFLICT DO UPDATE даже в edge-case не создаёт дубль-строку. - **listing_segment проставляется корректно.** providers/cian/serp.py:892 — вторичка получает `listing_segment="vtorichka"` (НЕ NULL), проходит фильтр `listing_segment IS NULL OR = vtorichka` в asking_to_sold_ratio.py/buildings_query.py — новые лоты попадут в оценку без доп. кода. ## Замеры (before/after, BEGIN...ROLLBACK на проде) Прод-счётчики (scrape_runs.counters) до фикса: | sweep | увидено | выброшено | сохранено | |---|---|---|---| | pervouralsk | 55 | 53 | 2 | | kamensk_uralskiy | 113 | 108 | 5 | | nizhniy_tagil | 184 | 176 | 3 (avg по 14 runs: 154 увидено / 8 сохранено) | | ~~verkhnyaya_pyshma~~ | ~~38~~ | ~~16~~ | ~~9~~ — исключено из этого PR, см. секцию выше | jsonb-мердж перепроверен для финального (4-источникового) списка в транзакции с ROLLBACK на проде: `UPDATE 4` строки, все соседние ключи (city/radius_m/detail_top_n/enrich_houses/pages_per_anchor/request_delay_sec) сохранены, `cian_city_sweep` (ЕКБ) и `cian_city_sweep_verkhnyaya_pyshma` не задеты. ## Migration number Main на момент ветки — до 202. Открытый PR #2622 (fix/tradein-geocoder-house-letter) уже занял 203 (`203_purge_geocode_cache_house_letter.sql`) — использован **204**, чтобы избежать коллизии. Перепроверено дважды (включая перед финальным push — parallel windows активны): main всё ещё на 202, других претендентов на 204 среди открытых PR нет. ## Test plan - [x] `tests/test_migration_204_cian_oblast_sweeps_secondary.py` (9 тестов) — транзакционность, no-DDL, jsonb-мердж (не перезапись), ровно 4 таргет-source, ДВА regression-guard'а (bare `cian_city_sweep` ЕКБ И `cian_city_sweep_verkhnyaya_pyshma` отсутствуют в WHERE), no psycopg CAST trap, идемпотентность - [x] Полный `pytest` в `tradein-mvp/backend`: 3090 passed, 9 skipped, 1 pre-existing fail (`test_search_api.py::test_search_cache_hit`, 401 RBAC — не в скоупе, известен заранее) ## Границы Только миграция + тест. Код скраперов/scheduler.py/pipeline.py не тронут — дефолт `newbuilding_only=True` в коде остаётся как правильный fallback. Развёртки вручную не запускались.
lekss361 added 1 commit 2026-08-02 11:01:28 +00:00
fix(tradein/scraper): собирать вторичку Циана по области, а не только новостройки
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 8s
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 2m38s
034ab48cfe
bot-backend added 1 commit 2026-08-02 11:08:03 +00:00
fix(tradein/scraper): исключить Верхнюю Пышму из вторички Циана — грязная городская метка
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI / changes (pull_request) Successful in 8s
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 2m40s
6dda48fc6c
lekss361 merged commit 0d2d54a634 into main 2026-08-02 11:13:33 +00:00
Sign in to join this conversation.
No reviewers
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#2623
No description provided.