feat(tradein): вся Свердловская область — 40 городов в city-sweep #2879

Merged
lekss361 merged 2 commits from feat/tradein-oblast-full into main 2026-08-13 19:08:45 +00:00
Owner

Зачем

Покрытие было 6 городов. По области — 3 229 объявлений против 20 111 по Екатеринбургу, причём 83% всех свежих данных приходится на ЕКБ. Заводим оставшиеся 40 городов области.

Данные добыты, а не выведены по правилу

Слаги площадок не выводятся транслитерацией — у Авито kamensk-uralskiy через дефис, verhnyaya_pyshma без «к». Поэтому каждый идентификатор добывался с площадки, и каждый метод сначала прогонялся на 5 городах с уже известными значениями. Собирали новые только после точного воспроизведения эталона.

Что Метод Валидация
Координаты прод-геокодер + сверка с медианой сделок Росреестра по городу воспроизвёл прошитые якоря (Каменск 56.4155 против 56.414)
cian_id geo-suggest, фильтр по «Свердловская область» 5/5
yandex_rgid region_suggest, дизамбигуация по label 5/5
avito_slug живой запрос страницы города 5/5 + контроль: ложные варианты дают чистый 404

Контрольная проверка на Авито важна отдельно — она доказывает, что метод различает верное и неверное, а не просто возвращает 200 на что угодно.

Сверка поймала три ошибки

Они бы молча уехали в прод:

  • Заречный — геокодер вернул микрорайон внутри Екатеринбурга. Обратное геокодирование: «улица Софьи Перовской, Екатеринбург». Взято из 777 сделок Росреестра.
  • Сысерть — промах на ~23 км. Взято из 403 сделок.
  • Бисерть — у Циана её нет, поиск на любой запрос отдаёт Сысерть (id 176028). Исключена целиком: это пгт, а не город области. Иначе развёртка собирала бы Сысерть дважды.

Строки только там, где идентификатор подтверждён

Источник Строк Пропуски
cian 40
yandex 39 Михайловска нет в гео-базе Яндекса вовсе (единственный там — ставропольский)
avito 23 17 не подтверждены: у 10 пул выдохся на 403/429 (кандидат не опровергнут), у 7 чистый 404 — нужна своя дизамбигуация омонимов

Итого 102 строки вместо 123. Развёртка с неподтверждённым идентификатором молча собирала бы Екатеринбург под меткой другого города — мусор в данных, который потом не отличить. Это хуже отсутствия развёртки.

Защита от тихого отката

  • CityLocation: avito_slug/yandex_rgid стали Optional; cian_region_id остался обязательным как единственный подтверждённый у всех.
  • Явный ValueError до любого I/O, если город есть в CITY_LOCATIONS без идентификатора. Без него _city_seg() откатывается на "ekaterinburg" — и сбор шёл бы по ЕКБ под меткой города-цели. _dispatch уже логирует исключения job'а, так что отказ и явный, и залогированный.
  • CITY_DISPLAY_NAMES на 40 городов — иначе listings.city записался бы как «Екатеринбург».
  • Попутно найден баг: _OBLAST_AVITO_URL_PATTERNS падала на импорте модуля при avito_slug=None.

Параметры

radius_m 3000 для avito/cian (не 1500 — миграция 179 сама предупреждает, что одного якоря на 1500 м может не хватить), 25000 для yandex. detail_top_n=0 у avito: детали сейчас отдают HTTP 439 firewall независимо от IP (#2827), 20 обречённых запросов на город только приблизили бы бан. interval_days=3.

Все 102 строки выключены — включение волнами, вручную, с проверкой банов между волнами.

Test plan

  • Полный прогон: 4366 passed, 21 skipped
  • ruff check / ruff format — чисто, pre-commit зелёный
  • Двустороннее соответствие CITY_LOCATIONS ↔ строки миграции по каждому источнику отдельно: строка есть тогда и только тогда, когда есть идентификатор
  • Тесты доказывают, что guard реально падает (revda→avito, mikhaylovsk→yandex), а не просто присутствует в коде
  • Идемпотентность: один INSERT ... ON CONFLICT (source) DO NOTHING
  • Post-deploy: включить первую волну и убедиться, что listings.city пишется верно, а не «Екатеринбург»

Хвост

16 городов с Авито стоит передобыть на отдохнувшем пуле — список в шапке миграции. Это отдельная миграция, не блокирует эту.

## Зачем Покрытие было 6 городов. По области — **3 229 объявлений против 20 111 по Екатеринбургу**, причём 83% всех свежих данных приходится на ЕКБ. Заводим оставшиеся 40 городов области. ## Данные добыты, а не выведены по правилу Слаги площадок не выводятся транслитерацией — у Авито `kamensk-uralskiy` через дефис, `verhnyaya_pyshma` без «к». Поэтому каждый идентификатор добывался с площадки, и **каждый метод сначала прогонялся на 5 городах с уже известными значениями**. Собирали новые только после точного воспроизведения эталона. | Что | Метод | Валидация | |---|---|---| | Координаты | прод-геокодер + сверка с медианой сделок Росреестра по городу | воспроизвёл прошитые якоря (Каменск 56.4155 против 56.414) | | `cian_id` | geo-suggest, фильтр по «Свердловская область» | **5/5** | | `yandex_rgid` | region_suggest, дизамбигуация по label | **5/5** | | `avito_slug` | живой запрос страницы города | **5/5** + контроль: ложные варианты дают чистый 404 | Контрольная проверка на Авито важна отдельно — она доказывает, что метод **различает** верное и неверное, а не просто возвращает 200 на что угодно. ## Сверка поймала три ошибки Они бы молча уехали в прод: - **Заречный** — геокодер вернул микрорайон **внутри Екатеринбурга**. Обратное геокодирование: «улица Софьи Перовской, Екатеринбург». Взято из 777 сделок Росреестра. - **Сысерть** — промах на ~23 км. Взято из 403 сделок. - **Бисерть** — у Циана её нет, поиск на любой запрос отдаёт Сысерть (id 176028). Исключена целиком: это пгт, а не город области. Иначе развёртка собирала бы Сысерть дважды. ## Строки только там, где идентификатор подтверждён | Источник | Строк | Пропуски | |---|---|---| | cian | **40** | — | | yandex | **39** | Михайловска нет в гео-базе Яндекса вовсе (единственный там — ставропольский) | | avito | **23** | 17 не подтверждены: у 10 пул выдохся на 403/429 (кандидат не опровергнут), у 7 чистый 404 — нужна своя дизамбигуация омонимов | **Итого 102 строки вместо 123.** Развёртка с неподтверждённым идентификатором молча собирала бы Екатеринбург под меткой другого города — мусор в данных, который потом не отличить. Это хуже отсутствия развёртки. ## Защита от тихого отката - `CityLocation`: `avito_slug`/`yandex_rgid` стали `Optional`; `cian_region_id` остался обязательным как единственный подтверждённый у всех. - **Явный `ValueError` до любого I/O**, если город есть в `CITY_LOCATIONS` без идентификатора. Без него `_city_seg()` откатывается на `"ekaterinburg"` — и сбор шёл бы по ЕКБ под меткой города-цели. `_dispatch` уже логирует исключения job'а, так что отказ и явный, и залогированный. - `CITY_DISPLAY_NAMES` на 40 городов — иначе `listings.city` записался бы как «Екатеринбург». - Попутно найден баг: `_OBLAST_AVITO_URL_PATTERNS` падала **на импорте модуля** при `avito_slug=None`. ## Параметры `radius_m` 3000 для avito/cian (не 1500 — миграция 179 сама предупреждает, что одного якоря на 1500 м может не хватить), 25000 для yandex. `detail_top_n=0` у avito: детали сейчас отдают HTTP 439 firewall независимо от IP (#2827), 20 обречённых запросов на город только приблизили бы бан. `interval_days=3`. **Все 102 строки выключены** — включение волнами, вручную, с проверкой банов между волнами. ## Test plan - [x] Полный прогон: **4366 passed, 21 skipped** - [x] `ruff check` / `ruff format` — чисто, pre-commit зелёный - [x] Двустороннее соответствие `CITY_LOCATIONS` ↔ строки миграции по каждому источнику отдельно: строка есть тогда и только тогда, когда есть идентификатор - [x] Тесты доказывают, что guard реально падает (`revda`→avito, `mikhaylovsk`→yandex), а не просто присутствует в коде - [x] Идемпотентность: один `INSERT ... ON CONFLICT (source) DO NOTHING` - [ ] Post-deploy: включить первую волну и убедиться, что `listings.city` пишется верно, а не «Екатеринбург» ## Хвост 16 городов с Авито стоит передобыть на отдохнувшем пуле — список в шапке миграции. Это отдельная миграция, не блокирует эту.
lekss361 added 1 commit 2026-08-13 18:49:12 +00:00
feat(tradein): вся Свердловская область — 40 городов в city-sweep
Some checks failed
CI Trade-In / changes (pull_request) Successful in 7s
CI / changes (pull_request) Successful in 9s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI Trade-In / browser-tests (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) Failing after 50s
76116f8869
Покрытие было 6 городов: по области 3229 объявлений против 20111 по ЕКБ.
Заводим оставшиеся 40 городов области.

Координаты и идентификаторы площадок добыты, а не выведены по правилу.
Каждый метод сначала прогонялся на 5 городах с уже известными значениями и
только после точного воспроизведения применялся к новым:
- координаты — прод-геокодер, сверка с медианой сделок Росреестра по городу
- cian_id — geo-suggest, фильтр по «Свердловская область» (тёзки реальны:
  Ревда в Мурманской, Заречный в Пензенской)
- yandex_rgid — region_suggest, дизамбигуация по label
- avito_slug — живой запрос страницы города; контроль: заведомо ложные
  варианты дают чистый 404, то есть метод различает, а не угадывает

Сверка поймала три ошибки, которые иначе уехали бы в прод:
- Заречный: геокодер вернул микрорайон ВНУТРИ Екатеринбурга (обратное
  геокодирование: «улица Софьи Перовской, Екатеринбург»)
- Сысерть: промах на ~23 км
- Бисерть: у Циана её нет, поиск отдаёт Сысерть — исключена целиком (пгт,
  а не город области)

Строки заводятся ТОЛЬКО там, где идентификатор подтверждён: cian 40,
yandex 39 (нет Михайловска — его нет в гео-базе Яндекса вовсе), avito 23.
Итого 102 строки. Развёртка с неподтверждённым идентификатором молча
собирала бы Екатеринбург под меткой другого города — это хуже отсутствия.

- CityLocation: avito_slug/yandex_rgid стали Optional, cian_region_id
  остался обязательным как единственный подтверждённый у всех
- явный ValueError до любого I/O, если город есть в CITY_LOCATIONS без
  идентификатора: _city_seg() иначе откатывается на "ekaterinburg"
- CITY_DISPLAY_NAMES на 40 городов, иначе listings.city = «Екатеринбург»
- попутно: _OBLAST_AVITO_URL_PATTERNS падала на импорте при avito_slug=None

Все 102 строки выключены — включение волнами, вручную.
bot-backend added 1 commit 2026-08-13 19:01:54 +00:00
fix(tradein/sql): окно 23→0 вместо 23→24 — миграция не применялась
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 9s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 4m21s
609768a3bd
CI поймал то, что статические проверки пропустили: пять строк получили
window_start_hour=23, window_end_hour=24, а констрейнт из 052_ требует
BETWEEN 0 AND 23. Весь INSERT падал, миграция не применялась.

Час 23 заворачивается в 0 — планировщик поддерживает окна через полночь
явно (window_end_hour <= window_start_hour → cross-midnight).

Тест test_migration_262_windows_are_one_hour и пропустил ошибку: он
требовал end == start + 1, что для 23 даёт ровно 24. Теперь сверяет
(start + 1) % 24, плюс отдельная проверка на диапазон 0..23 — то есть
на то самое условие прод-констрейнта.

Проверено сухим прогоном на боевой схеме: BEGIN → миграция → ROLLBACK,
psql exit 0, число строк не изменилось.
lekss361 merged commit 01b5e73ea4 into main 2026-08-13 19:08:45 +00:00
lekss361 deleted branch feat/tradein-oblast-full 2026-08-13 19:08:45 +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#2879
No description provided.