feat(tradein): ручка «дома, уехавшие от собственных объявлений» (#2996) #3010

Merged
bot-backend merged 1 commit from fix/2996-mislocated-houses-watchdog into main 2026-08-21 05:54:34 +00:00
Collaborator

Закрывает часть Находки 3 из #2996 — не гардом на приёме, а сторожем, который переживёт выход в Москву. Полный разбор с числами — в комментарии к #2996.

Что нашлось сверх постановки

Постановка говорила: «23 дома лежат вне области 66, гарда на приёме нет». Подтвердилось, плюс два недостающих куска.

Масштаб. К этим домам привязано 591 объявление. Адреса екатеринбургские («Ул. 8 Марта», «Крауля», «Амундсена»), координаты — Варшава, Белград, Таллин, Владивосток, Ижевск.

Причина. 22 из 23 несут в raw_payload один и тот же след:

{"yandex_geocode": {"batch": "full_backfill_2026-05-27",
                    "precision": "exact",
                    "address": "Россия, Удмуртская Республика, Ижевск, улица Шаумяна, 5"}}

Разовый бэкфилл 27.05 звал Yandex-геокодер напрямую, без резолва города. Живой путь при этом чист — проверил обе напрашивавшиеся гипотезы, и обе не подтвердились:

geocode_cache:  0 записей вне региона из 10 448
listings:       2 из 88 544 (обе неактивны)

Скрипта в репозитории нет — он не повторится сам. Значит гард нужен не столько живому пути, сколько следующему разовому скрипту: тот обошёл живую защиту, потому что звал API напрямую.

17 из 22 помечены precision: "exact". Точность отвечает на «нашёлся ли номер дома», а не «в том ли городе». Как критерий приёмки бэкфилла она бесполезна — все 17 «точных» неверны.

Инвариант выбран так, чтобы пережить Москву

Не «дом внутри рамки области», а «дом рядом со своими объявлениями». Рамка сломалась бы ровно при том расширении, ради которого заведена #2996.

Он же строго сильнее: ловит 25 домов против 23 у рамки, и оба лишних проверены вручную — настоящие («Ул. Белинского/Фурманова» за 207 км, 34 объявления).

Порог не подобран на глаз

Замер по 9 052 домам с привязанными объявлениями:

ближе 1 км     8 914   98.5 %
1–5 км            64
5–25 км           40
25–100 км          9   ← настоящие пригороды: Сарапулка, Кедровка, Чусовское
                         Озеро, Ревда, Первоуральск. Самый дальний — 46.7 км
дальше 100 км     25   ← ближайший 119.2 км, максимум 5 079 км

Между 46.7 и 119.2 км нет ни одного дома. Порог 100 лежит в середине пустого промежутка, а не на краю распределения.

Первая редакция была неверной, и её остановил замер

Я сначала добавил счётчик полем в /scraper/data-quality. Померил цену:

новый запрос                         ~445 мс (тёплый кэш; холодный 2.5 с)
обе существующие выборки той ручки     27 мс
фронт опрашивает её каждые             120 с

17-кратное удорожание опрашиваемой ручки ради числа, которое меняется раз в месяцы. Переделал в отдельную ручку по требованию; на возврат тяжёлого агрегата в горячий путь поставлен контроль-тест.

Пробовал и удешевить: вариант через min(ST_Distance(...)) оказался медленнее (724 мс — расстояние считается на каждую из 88 тыс. строк вместо одного на дом) и при этом ловит на два дома меньше. Индексы по house_id_fk есть — 445 мс это честная цена прохода.

Ручка отдаёт не только счётчик, но и масштаб: список домов с адресами и числом привязанных объявлений. По одному числу «25» решение об очистке не принять.

Как проверено

  • Двусторонне: против origin/main три теста красные, краснота везде по значению — ни одного ImportError/AttributeError. Сообщения называют фактическое состояние («порога нет вовсе», «есть ли обработчик: False»).
  • Контроль цены зелёный с обеих сторон: percentile_disc и слово mislocated не должны появляться в get_data_quality.
  • Контроль смысла: запрос обязан сравнивать дом со СВОИМИ объявлениями (l.house_id_fk = h.id, медиана координат) и не содержать координатной рамки ST_Y(...) BETWEEN — она не переживёт расширение региона.
  • Контроль порога: значение обязано лежать в измеренном промежутке (46.7, 119.2).
  • pytest tradein-mvp/backend4642 passed, 23 skipped.

Что осталось владельцу

Очистка 591 объявления: обнулить координаты 22 домов или удалить дома с пере-сопоставлением. Первое честнее (координат нет), второе чище (дом пересоздастся с правильными). Ручка теперь даёт список, по которому это решение можно принять.

Закрывает часть Находки 3 из #2996 — не гардом на приёме, а сторожем, который переживёт выход в Москву. Полный разбор с числами — в комментарии к #2996. ## Что нашлось сверх постановки Постановка говорила: «23 дома лежат вне области 66, гарда на приёме нет». Подтвердилось, плюс два недостающих куска. **Масштаб.** К этим домам привязано **591 объявление**. Адреса екатеринбургские («Ул. 8 Марта», «Крауля», «Амундсена»), координаты — Варшава, Белград, Таллин, Владивосток, Ижевск. **Причина.** 22 из 23 несут в `raw_payload` один и тот же след: ```json {"yandex_geocode": {"batch": "full_backfill_2026-05-27", "precision": "exact", "address": "Россия, Удмуртская Республика, Ижевск, улица Шаумяна, 5"}} ``` Разовый бэкфилл 27.05 звал Yandex-геокодер напрямую, без резолва города. **Живой путь при этом чист** — проверил обе напрашивавшиеся гипотезы, и обе не подтвердились: ``` geocode_cache: 0 записей вне региона из 10 448 listings: 2 из 88 544 (обе неактивны) ``` Скрипта в репозитории нет — он не повторится сам. Значит гард нужен не столько живому пути, сколько **следующему разовому скрипту**: тот обошёл живую защиту, потому что звал API напрямую. **17 из 22 помечены `precision: "exact"`.** Точность отвечает на «нашёлся ли номер дома», а не «в том ли городе». Как критерий приёмки бэкфилла она бесполезна — все 17 «точных» неверны. ## Инвариант выбран так, чтобы пережить Москву Не «дом внутри рамки области», а **«дом рядом со своими объявлениями»**. Рамка сломалась бы ровно при том расширении, ради которого заведена #2996. Он же строго сильнее: ловит **25** домов против 23 у рамки, и оба лишних проверены вручную — настоящие («Ул. Белинского/Фурманова» за 207 км, 34 объявления). ## Порог не подобран на глаз Замер по 9 052 домам с привязанными объявлениями: ``` ближе 1 км 8 914 98.5 % 1–5 км 64 5–25 км 40 25–100 км 9 ← настоящие пригороды: Сарапулка, Кедровка, Чусовское Озеро, Ревда, Первоуральск. Самый дальний — 46.7 км дальше 100 км 25 ← ближайший 119.2 км, максимум 5 079 км ``` **Между 46.7 и 119.2 км нет ни одного дома.** Порог 100 лежит в середине пустого промежутка, а не на краю распределения. ## Первая редакция была неверной, и её остановил замер Я сначала добавил счётчик полем в `/scraper/data-quality`. Померил цену: ``` новый запрос ~445 мс (тёплый кэш; холодный 2.5 с) обе существующие выборки той ручки 27 мс фронт опрашивает её каждые 120 с ``` 17-кратное удорожание опрашиваемой ручки ради числа, которое меняется раз в месяцы. Переделал в отдельную ручку по требованию; на возврат тяжёлого агрегата в горячий путь поставлен контроль-тест. Пробовал и удешевить: вариант через `min(ST_Distance(...))` оказался **медленнее** (724 мс — расстояние считается на каждую из 88 тыс. строк вместо одного на дом) и при этом ловит на два дома меньше. Индексы по `house_id_fk` есть — 445 мс это честная цена прохода. Ручка отдаёт не только счётчик, но и **масштаб**: список домов с адресами и числом привязанных объявлений. По одному числу «25» решение об очистке не принять. ## Как проверено - **Двусторонне:** против `origin/main` три теста красные, краснота **везде по значению** — ни одного ImportError/AttributeError. Сообщения называют фактическое состояние («порога нет вовсе», «есть ли обработчик: False»). - **Контроль цены** зелёный с обеих сторон: `percentile_disc` и слово `mislocated` не должны появляться в `get_data_quality`. - **Контроль смысла:** запрос обязан сравнивать дом со СВОИМИ объявлениями (`l.house_id_fk = h.id`, медиана координат) и не содержать координатной рамки `ST_Y(...) BETWEEN` — она не переживёт расширение региона. - **Контроль порога:** значение обязано лежать в измеренном промежутке (46.7, 119.2). - `pytest tradein-mvp/backend` — **4642 passed**, 23 skipped. ## Что осталось владельцу Очистка 591 объявления: обнулить координаты 22 домов или удалить дома с пере-сопоставлением. Первое честнее (координат нет), второе чище (дом пересоздастся с правильными). Ручка теперь даёт список, по которому это решение можно принять.
bot-backend added 1 commit 2026-08-20 20:29:42 +00:00
feat(tradein): ручка «дома, уехавшие от собственных объявлений» (#2996)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 10s
CI Trade-In / browser-tests (pull_request) Has been skipped
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 4m49s
fab0bc84c1
Находка 3 задачи («23 дома лежат вне области 66, гарда на приёме нет»)
подтвердилась, и к ней добавились масштаб и причина.

Масштаб: к этим домам привязано 591 объявление. Адреса екатеринбургские
(«Ул. 8 Марта», «Крауля», «Амундсена»), координаты — Варшава, Белград,
Таллин, Владивосток, Ижевск.

Причина: 22 из 23 несут в raw_payload след разового бэкфилла
`full_backfill_2026-05-27`, который звал Yandex-геокодер напрямую, без
резолва города. Живой путь при этом ЧИСТ — 0 записей вне региона в
geocode_cache из 10 448 и 2 из 88 544 у listings; скрипта в репозитории
нет. Это исторический осадок, а не текущая утечка, и гард нужен не
столько живому пути, сколько следующему разовому скрипту.

17 из 22 помечены геокодером `precision: "exact"`. Точность отвечает на
«нашёлся ли номер дома», а не «в том ли городе», и критерием приёмки быть
не может — поэтому проверяется ПРИНАДЛЕЖНОСТЬ.

Инвариант нарочно не географический: «дом рядом со своими объявлениями»,
а не «дом внутри рамки области». Рамка сломалась бы при выходе в Москву —
ровно то, ради чего заведена #2996. Он же строго сильнее: ловит 25 домов
против 23 у рамки, и оба лишних проверены («Ул. Белинского/Фурманова» за
207 км, 34 объявления).

Порог 100 км не подобран на глаз. Замер по 9 052 домам: ближе 1 км —
8 914 (98.5 %), 25-100 км — 9 настоящих пригородов (Сарапулка, Кедровка,
Чусовское Озеро, Ревда, Первоуральск; самый дальний 46.7 км), дальше
100 км — 25 (ближайший 119.2, максимум 5 079). Между 46.7 и 119.2 км нет
НИ ОДНОГО дома: порог лежит в середине пустого промежутка.

Первая редакция клала счётчик полем в /scraper/data-quality. Замер это
остановил: запрос стоит ~445 мс на тёплом кэше, а обе существующие
выборки той ручки вместе — 27 мс, при опросе фронтом каждые 120 с. То
есть 17-кратное удорожание ради числа, которое меняется раз в месяцы.
Проверка вынесена в отдельную ручку по требованию, и на возврат в горячий
путь поставлен контроль-тест.

Ручка отдаёт не только счётчик, но и масштаб (список домов + сколько
объявлений привязано) — по одному числу «25» решение об очистке не
принять.

Двусторонне: против origin/main три теста красные, краснота везде по
значению — ни одного ImportError/AttributeError. Контроль
test_check_stays_out_of_the_polled_endpoint зелёный с обеих сторон.

pytest tradein-mvp/backend — 4642 passed, 23 skipped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
bot-backend merged commit a003fdd37c into main 2026-08-21 05:54:34 +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#3010
No description provided.