fix(tradein/geocode): прошить city_hint в deals-скрипт + развести счётчики гейта (#2603) #2655
No reviewers
Labels
No labels
Fable 5 ревью
GG-форсайт
admin
analytics
auth
automation
bug
business
chore
ci
compliance
data
data-moat
docs
duplicate
dx
enhancement
feedback/max
generative
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
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: lekss361/gendesign#2655
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "fix/2603-geocode-city-hint-tails"
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?
Что было
Два хвоста после #2601 плюс две мелочи.
scripts/geocode_deals_nominatim.py— непрошитый sibling-caller. ГруппировкаGROUP BY address, вызовgeocode(address, db)без города.deals.city(миграция 177) заполнена на 100%, но выбрасывалась: один и тот же текст адреса из разных городов схлопывался в одну группу → один geocode-вызов → один UPDATE по тексту адреса, задевающий обе стороны. Ровно та форма, которую #2601 починил вapp/tasks/geocode_missing.py.skipped_non_ekbвapp/tasks/backfill_listings_coords_geoportal.pyсмешивал колоночный и текстовый гейты. Колоночный стоит ПЕРЕД парсером адреса, поэтому по мере раскатки областных развёрток (#2598) строки начнут перетекать изno_addressвskipped_non_ekb— счётчик поменял бы смысл ровно тогда, когда по нему хотят валидировать раскатку.test_admin_geocode_missing_passes_city_hintпокрывал толькоtarget="listings".geocode_missing.pyне печаталcity, хотя с #2594 это часть ключа группы.Что стало
1. deals-скрипт — та же форма, что в #2601:
SELECT address, city ... GROUP BY address, city, window/outerORDER BYсинхронизированы (..., address ASC, city ASC NULLS FIRST) — иначеrunning_rows-кап режет другой порядок.geocode(address, db, city_hint=...)._update_deals_geocoded,_mark_deals_tried) скоупятсяcity IS NOT DISTINCT FROM CAST(:city AS text)— обычное=никогда не истинно для NULL-города, NULL-группа не обновлялась бы вовсе и переотбиралась бы вечно._count_pending_totalсчитает ПАРЫ (знаменатель dry-run-проекции сходится с числителем).cityдобавлен в dry-run/warning-логи скрипта.2. Откуда взят набор городов и почему. Хинт передаётся ТОЛЬКО для значений из
geocoder.SVERDLOVSK_OBLAST_CITIES(_city_hint), иначеcity_hint=None. Это существующий источник истины в репозитории, а не новый список: тот же frozenset уже питает гейты самого геокодера (_names_non_ekb_city,_ekb_local_tiers_allowed,_has_oblast_marker) иestimator._resolve_target_city(city-scoping ДКП-коридора, см. миграцию 205). Свой список дрейфовал бы от гейтов, в которые этот же хинт и приходит.Почему фильтр обязателен:
deals.city— росреестровое поле, в хвосте распределения лежит мусор (Бессонова,Бердюгина,Билейский рыбопитомник). Любой не-ЕКБ хинт жёстко закрывает EKB-локальные тиры и подставляется префиксом в запрос провайдеру, т.е. мусорный хинт делает результат ХУЖЕ, чем его отсутствие. Нераспознанное значение → поведение как раньше (без хинта), но UPDATE всё равно скоупится по паре.3. Счётчики гейта. Добавлен
skipped_non_ekb_by_column(dataclass +to_counters()+ оба DONE-лога). Обратная совместимость:skipped_non_ekbпродолжает означать то же, что и раньше (гейт целиком), новый счётчик — его подмножество; вклад текстового гейта = разность, т.е. метрика #2583 остаётся вычислимой.4. Мелочи. Тест admin-эндпоинта параметризован на
["listings", "deals"](+ проверка, что SELECT адресован запрошенной таблице). Dry-run логgeocode_missing.pyпечатаетcity.Тесты
Прогон локально,
uv sync --frozen+uv run pytest(как в CIdeploy-tradein.yml):tests/scripts/test_geocode_deals_nominatim.py,tests/tasks/test_backfill_listings_coords_geoportal.py,tests/tasks/test_geocode_missing.py).tradein-mvp/backendс CI-деселектом: 3316 passed, 9 skipped, 1 deselected (tests/test_search_api.py::test_search_cache_hit— pre-existing, деселектится и в CI).tests/scripts/test_geocode_deals_nominatim.pyне собирается (ImportError: _city_hint),test_skipped_non_ekb_by_column_counts_only_column_gateиtest_to_counters_exposes_skipped_non_ekb_by_columnпадают. Честная оговорка: параметризация admin-теста наdealsпроходит и на старом коде — это добор покрытия ветки, а не фикс поведения (сам прокидcity_hintдля deals приехал ещё в #2601).Новые тесты: группировка по паре и
cityвAddressGroup;_city_hint(известный город → передаётся, мусор/пусто → None); один и тот же адрес в двух городах → 2 вызова со своими хинтами и 2 скоупленных UPDATE; miss помечает только свой город;IS NOT DISTINCT FROMв обоих writer'ах, включая NULL-группу; разделение счётчиков + ключ вto_counters().Что НЕ входит
app/services/geocoder.pyне тронут (только импорт константы) — как и в #2601.POST /admin/geocode-missing?target=deals(app/api/v1/admin.py, приехало в #2601) отдаётrow["city"]вcity_hintБЕЗ словарной валидации — тот же класс риска «мусорный росреестровый хинт», что чинится здесь в скрипте. Не трогал, чтобы не расширять скоуп задачи; если нужно — вынести_city_hintв общее место отдельным PR.COUNT(DISTINCT (address, COALESCE(city, '')))в dry-run-знаменателе:COALESCEнамеренный — чтобы не зависеть от спора о том, считает лиcount()row-выражение с NULL-полем; на живой БД не проверял (задача чисто кодовая, к проду не ходил).Refs #2603