Commit graph

11 commits

Author SHA1 Message Date
bot-backend
8cdb195e18 fix(tradein/geocode): бюджет прогона не был потолком — проверялся только между батчами
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 9s
CI Trade-In / frontend-checks (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 4m45s
CI / backend-tests (pull_request) Has been skipped
`run_geocode_missing_listings` рекламирует `budget_sec` как максимальное время
прогона, но проверка стояла после возврата из батча. Внутри батча цикл шёл по
всем 200 адресам и часов не смотрел, то есть фактический потолок был
`budget_sec + один полный батч`.

Замер: прогон 5017 (27.08, `budget_sec=1800`) шёл 3150 с — 175 % бюджета, и
вышел не по бюджету, а по дренажу: бюджетная ветка за 52 минуты не выполнилась
ни разу.

Пока адрес стоил ~1.9 с это терялось в шуме. После общего ограничителя темпа
Nominatim (#2953) средняя цена 4.5 с, а на трудном хвосте (tier-1 + до 4
typo-вариантов под паузой 1 с, плюс retry×3) — до 33 с. Полный батч из таких
адресов уезжает на ~110 минут поверх бюджета, при окне расписания 06:00–09:00.

Дедлайн теперь передаётся В батч и проверяется на каждом адресе. Оборванный
батч — штатный исход: `geocode_tried_at` проставлен только у обработанных пар,
остальные попадут в выборку следующего прогона.

Отдельный флаг `budget_exhausted` нужен потому, что `addresses_total` на
оборванном батче равен размеру ВЫБОРКИ (== batch_size) — ветка дренажа
`addresses_total < batch_size` не сработала бы, и обёртка крутила бы цикл
дальше. Тест на это падает без флага (проверено снятием ветки).

Тесты: 5 новых, все три несущие проверки падают без соответствующей правки.
37 passed локально.

Closes #3151
2026-08-27 19:10:53 +03:00
5ecd5361fd fix(tradein/geocode): гейт мусорного города вынести в общий хелпер и прошить в admin-путь (#2603)
All checks were successful
CI / changes (pull_request) Successful in 7s
CI Trade-In / 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 2m42s
Первый коммит починил только scripts/geocode_deals_nominatim.py — ручной скрипт.
Тот же дефект оставался на живом пути: POST /admin/geocode-missing?target=deals
отдавал сырой row["city"] в city_hint, а deals.city росреестровое и в хвосте
распределения содержит не-города («Бессонова», «Билейский рыбопитомник»). Любой
не-ЕКБ хинт жёстко закрывает EKB-локальные тиры и уезжает префиксом в запрос
провайдеру, то есть мусорный хинт хуже отсутствия хинта.

Гейт вынесен в geocoder.known_city_hint (сверка с SVERDLOVSK_OBLAST_CITIES —
тем же набором, который уже питает _names_non_ekb_city / _ekb_local_tiers_allowed)
и переиспользуется всеми тремя потребителями city_hint: скриптом, admin-ручкой и
задачей geocode_missing. Копий функции нет — четвёртый потребитель, если появится,
получит гейт сам.

Тесты: мусорный город -> хинт не передаётся, валидный -> передаётся; проверено
фальсификацией (без фикса все три новых теста краснеют).
2026-08-05 20:20:41 +05:00
ec17886d60 fix(tradein/geocode): прошить city_hint в deals-скрипт + развести счётчики гейта (#2603)
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 2m46s
Хвосты после #2601 (замыкание петли «город → геокодер»).

1. scripts/geocode_deals_nominatim.py — непрошитый sibling-caller.
   Скрипт группировал `GROUP BY address` и звал `geocode(address, db)` без
   города, хотя deals.city (миграция 177) заполнена на 100%: один и тот же
   текст адреса из разных городов схлопывался в одну группу, один geocode-вызов
   и один UPDATE по тексту адреса. Теперь — та же форма, что в #2601:
   группировка по паре (address, city), city_hint в geocode(), UPDATE и
   mark-tried через `city IS NOT DISTINCT FROM` (обычное `=` не ловит NULL-город
   → NULL-группа не обновлялась бы вовсе).

   Хинт передаётся ТОЛЬКО для значений из geocoder.SVERDLOVSK_OBLAST_CITIES:
   deals.city росреестровое, в хвосте лежит мусор («Бессонова», «Бердюгина»,
   «Билейский рыбопитомник»), а любой не-ЕКБ хинт жёстко закрывает EKB-локальные
   тиры и подставляется в запрос провайдеру — мусорный хинт хуже отсутствия
   хинта. Словарь переиспользован, а не заведён свой: тот же набор уже питает
   гейты самого геокодера (_names_non_ekb_city / _ekb_local_tiers_allowed) и
   estimator._resolve_target_city.

2. tasks/backfill_listings_coords_geoportal.py — наблюдаемость городского гейта.
   Добавлен skipped_non_ekb_by_column (+ в to_counters и в DONE-логи): колоночный
   гейт стоит перед парсером адреса, поэтому по мере раскатки областных
   развёрток (#2598) строки потекут из no_address в skipped_non_ekb и общий
   счётчик поменяет смысл ровно тогда, когда по нему валидируют раскатку.
   Старый счётчик не тронут — остаётся суммой обоих гейтов, вклад текстового
   считается разностью.

3. tests: test_admin_geocode_missing_passes_city_hint параметризован на
   target="deals" (колонка city есть в обеих таблицах, ветка была не покрыта).

4. tasks/geocode_missing.py: dry-run лог печатает city — он с #2594 часть ключа
   группы, без него две строки dry-run неотличимы.

Refs #2603
2026-08-05 17:36:07 +05:00
bot-backend
bd472b9b57 fix(tradein/geocode): геокодировать только активные объявления (#2604)
All checks were successful
CI / changes (pull_request) Successful in 8s
CI Trade-In / changes (pull_request) Successful in 10s
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 2m29s
Ночная очередь geocode_missing_listings была на 98.5% забита is_active=false
объявлениями чужих регионов (Новосибирск/Казань/Челябинск/Тюмень/Ижевск...) без
улицы и дома. ORDER BY listings_count DESC ставил такой мусор в начало очереди
(у 'Новосибирская обл.,Новосибирск' — 214 listings, у реального адреса — 1-2),
поэтому Nominatim-бюджет (1 req/sec) съедался мусором и до активных адресов
дело не доходило: 8 ночных прогонов подряд saved=0.

Добавлен AND is_active в SELECT. UPDATE (lat/lon и оба tried_at) намеренно
оставлены без этого фильтра — координаты и backoff-метка принадлежат паре
(address, city) как тексту, не конкретному listing; is_active=false дубликат
той же пары и так навсегда исключён из будущих SELECT, а unfiltered UPDATE
проставляет ему ответ бесплатно (Nominatim-вызов уже оплачен активным
листингом) на случай реактивации.

Refs #2604
2026-08-01 01:09:50 +03:00
bot-backend
41e3cb906b fix(tradein/geocode): передавать город объявления как city_hint в геокодирование (#2594)
All checks were successful
CI / changes (pull_request) Successful in 11s
CI Trade-In / changes (pull_request) Successful in 11s
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
Замыкает петлю "город объявления -> геокодирование" (issue #2594, шаг 2/3).
listings.city (миграция 196) заполняется скрапером из контекста развёртки, но
три caller-места геокодера решали город по ТЕКСТУ адреса и игнорировали
колонку - голый адрес без города в тексте ("ул. Победы, 30", тагильский)
уходил в Екатеринбург.

- app/tasks/geocode_missing.py: группировка по (address, city) вместо
  address, city_hint в geocode(), UPDATE/tried_at-пометка по паре через
  city IS NOT DISTINCT FROM :city (обычный `=` не поймал бы NULL-город и
  не даёт нужной симметрии между группами).
- app/tasks/backfill_listings_coords_geoportal.py: гейт по колонке city
  ПЕРЕД матчем против EKB-only ekb_geoportal_buildings, ПЕРЕД текстовым
  гейтом _names_non_ekb_city (сохранён как fallback для city IS NULL).
  Это окно идёт раньше geocode_missing_listings, поэтому раньше успевало
  испортить координаты первым.
- app/api/v1/admin.py: per-ID endpoint /geocode-missing читает city из
  SELECT (listings.city / deals.city) и передаёт как city_hint.

geocoder.py не тронут (запрещено ТЗ).

Тесты: falsification-прогон (stash impl, тесты остаются) - 7 failed / 36
passed на старом коде, все 7 - новые тесты на новое поведение; после
stash pop - 43 passed / 0 failed. Полный pytest tradein-mvp/backend:
2970 passed, 1 failed (pre-existing tests/test_search_api.py::test_search_cache_hit,
несвязан), 9 skipped.
2026-07-31 23:47:26 +03:00
bot-backend
3d075632a9 chore(tradein/geocoder): удалить Яндекс-геокодер (#2593)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI / changes (pull_request) Successful in 10s
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 2m28s
Убирает ядро Yandex Geocoder (forward/reverse/suggest lookups + region-check
+ bias-хелперы + EKB_BBOX dict) из app/services/geocoder.py — Yandex demo-key
исчерпан, Nominatim/DaData/локальные ЕКБ-тиры (geoportal/cadastral) остаются
единственными живыми провайдерами. Цепочка тиров после удаления: кэш →
геопортал ЕКБ → кадастр (house-match) → кадастр (raw) → Nominatim; в
подсказках дополнительно DaData.

НЕ затронуто (намеренно): Yandex.Недвижимость как источник объявлений
(source='yandex', yandex_city_sweep*, providers/yandex/serp.py:geocoderAddress),
Avito geocoder (providers/avito/imv.py:_geocode), EKB_BBOX_TIGHT/WIDE,
_nominatim_region_ok, scripts/*_yandex_reverse.py и их тесты, tests/fixtures/
yandex_geocode_sample.json (всё ещё используется test_audit_address_mismatch.py).

_SNAP_PRECISIONS оставлен с "exact" (недостижимо без Yandex-tier, но дёшево
хранить — parity с frontend MapPicker.tsx SNAP_PRECISIONS и не ломает
test_snap_precision_useful_exact_and_number).
2026-07-31 21:22:09 +03:00
17fa6544a2 fix(geocode): set geocode_tried_at on successful geocode UPDATE
All checks were successful
CI / changes (push) Successful in 8s
CI / backend-tests (push) Has been skipped
CI / changes (pull_request) Successful in 6s
CI / frontend-tests (push) Has been skipped
CI / openapi-codegen-check (push) 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
Add geocode_tried_at = NOW() to the success UPDATE SET clause so
successfully-geocoded listings record their attempt time, matching
the per-ID admin endpoint contract and the column spec.
2026-06-16 10:07:51 +03:00
a56420bdfb fix(tradein): wire nightly geocode backfill для listings в in-app scheduler
All checks were successful
CI / changes (push) Successful in 7s
CI / backend-tests (push) Has been skipped
CI / frontend-tests (push) Has been skipped
CI / openapi-codegen-check (push) Has been skipped
Root cause: 7674 avito/yandex/n1 listings з null-geom мають ненульовий address,
але geocode_tried_at IS NULL — OS cron геокодує тільки deals, ніколи listings.

Parts:
- geocode_missing.py: WHERE додає geocode_tried_at IS NULL OR tried_at < 7d
  (loop-safe); при failure (geo=None / exception) позначаємо tried_at=NOW().
  Новий run_geocode_missing_listings: batch loop з wall-clock budget (30m),
  update_heartbeat / mark_done / mark_failed lifecycle через scrape_runs.
- scheduler.py: trigger_geocode_missing_listings_run + dispatch elif.
- migration 110: seed scrape_schedules row (enabled=true, window 06-09 UTC).
- tests: оновлено 2 тести (commit на tried_at UPDATE); +7 нових тестів.
  1794 passed.
2026-06-16 10:01:49 +03:00
b736d7b7e0 feat(estimator): exclude city-centroid listings from radius analogs (Refs #769 Part E)
Закрывает последнюю часть #769 (A1/A2/B/C/D в #798/#804). Finding #17:
bare-city адреса геокодились в city-центроид и сохранялись как точные lat/lon →
mislocated листинги участвовали в radius ST_DWithin-аналогах.

- 089: ALTER TABLE listings ADD COLUMN IF NOT EXISTS geo_precision text (idempot).
- ScrapedLot.geo_precision + проброс в save_listings.
- geocode_missing.py: geo_precision='city' через существующий _geocode_is_coarse.
- estimator _fetch_analogs Tier H+W: + AND (geo_precision IS DISTINCT FROM 'city').
  NULL проходит (консервативно, сдвига до backfill нет). Pricing не тронут.

7 тестов; 270 passed, ruff clean.
2026-05-30 21:22:21 +03:00
1ce9e10012 feat(tradein): persistent infra для backfill scripts (PR F) (#592)
All checks were successful
Deploy Trade-In / changes (push) Successful in 5s
Deploy Trade-In / build-frontend (push) Successful in 46s
Deploy Trade-In / build-backend (push) Successful in 1m38s
Deploy Trade-In / deploy (push) Successful in 42s
2026-05-27 06:59:54 +00:00
52716a46c6 fix(tradein): geocode backfill + remove Avito exclusion from estimator (#490)
All checks were successful
Deploy Trade-In / changes (push) Successful in 5s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-backend (push) Successful in 42s
Deploy Trade-In / deploy (push) Successful in 24s
2026-05-23 20:03:48 +00:00