fix(tradein/geocode): геокодировать только активные объявления (#2604) #2605

Merged
lekss361 merged 1 commit from fix/tradein-geocode-queue-active-only into main 2026-07-31 22:26:03 +00:00
Owner

Проблема (замерено на проде)

geocode_missing_listings (nightly, app/tasks/geocode_missing.py) отбирал адреса по lat IS NULL + backoff по geocode_tried_at, но НЕ фильтровал is_active. Очередь на прод: 14294 строки, из них is_active=false — 14074 (98.5%), активных — всего 220. Мёртвые строки — объявления чужих регионов (Новосибирск/Казань/Челябинск/Тюмень/Ижевск…), адрес вида «Новосибирская обл.,Новосибирск» без улицы и дома (13693 из 14294 без единой цифры в адресе). ORDER BY listings_count DESC ставил такой мусор В НАЧАЛО очереди (у «Новосибирская обл.,Новосибирск» — 214 listings, у реального адреса — 1-2), поэтому весь Nominatim-бюджет (1 req/sec) съедался сверху и до настоящих адресов дело не доходило: 8 ночных прогонов подряд с 24 июля, checked 329-592, saved 0, каждый по 30-50 минут.

Фикс

Добавлен AND is_active в SELECT очереди (app/tasks/geocode_missing.py). До: очередь 14294 строки (220 активных / 14074 мёртвых). После фикса SELECT видит только эти ~220 активных строк — мусор чужих регионов больше не конкурирует за место в LIMIT batch_size.

Три решения (п.1-3 из задачи) — явно, с обоснованием

П.1 — нужен ли is_active в UPDATE (lat/lon)? Решение: НЕТ, оставлено без фильтра. Координаты — свойство физического адреса (address, city), а не свойство конкретного listing. is_active=false дубликат этой же пары никогда не будет независимо отобран SELECT'ом (он навсегда исключён оттуда новым фильтром) — без unfiltered UPDATE такой дубликат остался бы с NULL lat/lon НАВСЕГДА, хотя ответ уже получен и оплачен Nominatim-вызовом активного листинга (нулевая доп. стоимость, чистый выигрыш: при реактивации листинг уже с координатами). Довод «за фильтр» (консистентность с SELECT) — чисто эстетический, не устраняет никакой ошибки данных.

П.2 — та же логика для geocode_tried_at (обе ветки: geo is None и except). Решение: тоже без фильтра, тем же обоснованием: backoff-метка привязана к тексту (address, city), а не к конкретному listing; is_active=false дубликат и так навсегда исключён из будущих SELECT (фильтр там был бы no-op). Единственный случай где это имеет значение — реактивация листинга (is_active → true): backoff уже стоит и корректно защищает от немедленного повтора заведомо неудачного адреса.

П.3 — арифметика addresses_total / условие дренажа. 220 активных строк после GROUP BY address, city дают ≤220 уникальных пар (десятки на практике) — заведомо меньше batch_size=200 в подавляющем большинстве прогонов. run_geocode_missing_listings завершится по ветке res.addresses_total < batch_size уже на первой итерации — это ПРАВИЛЬНОЕ поведение (очередь разгребена), не баг. Деления там нет вообще (только int сравнение); единственное деление в файле — rate = (idx+1)/elapsed if elapsed > 0 else 0 — уже защищено guard'ом и не связано с этим изменением. Пустая очередь (addresses_total == 0) ловится отдельной веткой ВЫШЕ этой проверки.

Falsification-прогон (git stash impl-файла, тесты оставлены)

  • До фикса (impl застэшен, тест test_geocode_missing_select_filters_is_active активен): 1 failed, 27 passed.
  • После фикса (git stash pop): 28 passed.

Плюс 3 теста фиксируют решения по п.1/п.2 явно (test_geocode_missing_success_update_not_filtered_by_is_active, test_geocode_missing_notfound_tried_at_update_not_filtered_by_is_active, test_geocode_missing_exception_tried_at_update_not_filtered_by_is_active) — решение задокументировано тестом, не только комментарием.

Полный pytest (tradein-mvp/backend)

uv run pytest -q --deselect "tests/test_search_api.py::test_search_cache_hit"2937 passed, 9 skipped, 1 deselected (0 failed). Deselect — известный pre-existing фейл (401 от RBAC-мидлвари, ordering-only, не трогали).

Границы (не тронуто, по инструкции)

  • app/services/geocoder.py — не менялся.
  • region_code у чужих строк / скрапперы — отдельные пункты issue #2604 (2 и 3), не эта задача.
  • Данные не удалялись/не деактивировались — только сузилась выборка.
  • Миграций нет.

Refs #2604

## Проблема (замерено на проде) `geocode_missing_listings` (nightly, `app/tasks/geocode_missing.py`) отбирал адреса по `lat IS NULL` + backoff по `geocode_tried_at`, но НЕ фильтровал `is_active`. Очередь на прод: 14294 строки, из них `is_active=false` — 14074 (98.5%), активных — всего 220. Мёртвые строки — объявления чужих регионов (Новосибирск/Казань/Челябинск/Тюмень/Ижевск…), адрес вида «Новосибирская обл.,Новосибирск» без улицы и дома (13693 из 14294 без единой цифры в адресе). `ORDER BY listings_count DESC` ставил такой мусор В НАЧАЛО очереди (у «Новосибирская обл.,Новосибирск» — 214 listings, у реального адреса — 1-2), поэтому весь Nominatim-бюджет (1 req/sec) съедался сверху и до настоящих адресов дело не доходило: 8 ночных прогонов подряд с 24 июля, `checked` 329-592, `saved` **0**, каждый по 30-50 минут. ## Фикс Добавлен `AND is_active` в SELECT очереди (`app/tasks/geocode_missing.py`). До: очередь 14294 строки (220 активных / 14074 мёртвых). После фикса SELECT видит только эти ~220 активных строк — мусор чужих регионов больше не конкурирует за место в `LIMIT batch_size`. ## Три решения (п.1-3 из задачи) — явно, с обоснованием **П.1 — нужен ли `is_active` в UPDATE (lat/lon)?** Решение: **НЕТ, оставлено без фильтра.** Координаты — свойство физического адреса `(address, city)`, а не свойство конкретного listing. `is_active=false` дубликат этой же пары никогда не будет независимо отобран SELECT'ом (он навсегда исключён оттуда новым фильтром) — без unfiltered UPDATE такой дубликат остался бы с `NULL lat/lon` НАВСЕГДА, хотя ответ уже получен и оплачен Nominatim-вызовом активного листинга (нулевая доп. стоимость, чистый выигрыш: при реактивации листинг уже с координатами). Довод «за фильтр» (консистентность с SELECT) — чисто эстетический, не устраняет никакой ошибки данных. **П.2 — та же логика для `geocode_tried_at` (обе ветки: `geo is None` и `except`).** Решение: **тоже без фильтра**, тем же обоснованием: backoff-метка привязана к тексту `(address, city)`, а не к конкретному listing; `is_active=false` дубликат и так навсегда исключён из будущих SELECT (фильтр там был бы no-op). Единственный случай где это имеет значение — реактивация листинга (`is_active` → true): backoff уже стоит и корректно защищает от немедленного повтора заведомо неудачного адреса. **П.3 — арифметика `addresses_total` / условие дренажа.** 220 активных строк после `GROUP BY address, city` дают ≤220 уникальных пар (десятки на практике) — заведомо меньше `batch_size=200` в подавляющем большинстве прогонов. `run_geocode_missing_listings` завершится по ветке `res.addresses_total < batch_size` уже на первой итерации — это ПРАВИЛЬНОЕ поведение (очередь разгребена), не баг. Деления там нет вообще (только `int` сравнение); единственное деление в файле — `rate = (idx+1)/elapsed if elapsed > 0 else 0` — уже защищено guard'ом и не связано с этим изменением. Пустая очередь (`addresses_total == 0`) ловится отдельной веткой ВЫШЕ этой проверки. ## Falsification-прогон (git stash impl-файла, тесты оставлены) - **До фикса** (impl застэшен, тест `test_geocode_missing_select_filters_is_active` активен): `1 failed, 27 passed`. - **После фикса** (`git stash pop`): `28 passed`. Плюс 3 теста фиксируют решения по п.1/п.2 явно (`test_geocode_missing_success_update_not_filtered_by_is_active`, `test_geocode_missing_notfound_tried_at_update_not_filtered_by_is_active`, `test_geocode_missing_exception_tried_at_update_not_filtered_by_is_active`) — решение задокументировано тестом, не только комментарием. ## Полный pytest (`tradein-mvp/backend`) `uv run pytest -q --deselect "tests/test_search_api.py::test_search_cache_hit"` → **2937 passed, 9 skipped, 1 deselected** (0 failed). Deselect — известный pre-existing фейл (401 от RBAC-мидлвари, ordering-only, не трогали). ## Границы (не тронуто, по инструкции) - `app/services/geocoder.py` — не менялся. - `region_code` у чужих строк / скрапперы — отдельные пункты issue #2604 (2 и 3), не эта задача. - Данные не удалялись/не деактивировались — только сузилась выборка. - Миграций нет. Refs #2604
lekss361 added 1 commit 2026-07-31 22:11:29 +00:00
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
bd472b9b57
Ночная очередь 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
lekss361 merged commit 6868d489aa into main 2026-07-31 22:26:03 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
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#2605
No description provided.