fix(tradein/db): разовая чистка 1123 адресов Авито с приклеенным рейтингом (#2814) #2818
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
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: lekss361/gendesign#2818
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "fix/2814-address-backfill"
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?
Разовое лечение 1123 строк из #2814. Парсер починен в #2815 (merged, прод-verified), но уже записанные адреса сами не вылечатся:
base.py:614пишетaddress = COALESCE(listings.address, EXCLUDED.address)— при конфликте адрес осознанно НЕ перезаписывается (#2777). Миграция — единственный путь.Перемерено на проде 2026-08-10 (после деплоя #2815, по данным а не по релиз-метке)
source='avito',is_active)address ~ '·\s*\d')Ничего не «рассосалось» свипом после #2815:
max(last_seen_at)у этих 1123 = 2026-08-09 16:53, тронуто с 09:57 — 0 строк. Все 1123 — avito, все активные; в других источниках такого хвоста нет.Правило резки — дословно парсерное
Парсер (
providers/avito/serp.py):Миграция:
плюс
trim(both E' ,.\n\t')=strip(" ,.\n\t")иNULLIF(…,'')=return cleaned or None.Паритет проверен тем же кодом, а не по глазам: 1123 сырых адреса выгружены с прода и прогнаны через живой
_clean_addressв боевом контейнере —rows=1123 mismatches=0._deglue_house_markerв SQL не повторяется: замерено, что после резки хвоста слипшегося «29р-н» нет ни в одной из 1123 строк (0 совпадений_DEGLUE_RE).Районные хвосты (#1773) сохранены: резать по «·» можно только когда за ней цифра. 296 строк вида «улица Бебеля, 138 · р-н Железнодорожный» — 296 до, 296 после.
Dry-run на проде (BEGIN … ROLLBACK, экзактным файлом миграции)
UPDATE 1123· загрязнённых после —0· районных «·» —296/296·psql_exit=0ул. Ткачей,17·5,0 · 4 отзываул. Ткачей,17ул. Свердлова,32Б·4,2 · 5 отзывовул. Свердлова,32Бул. Шаумяна,93·5,0 · 4 отзываул. Шаумяна,93ул. Щорса,103·4,3 · 15 отзывовул. Щорса,103Уральская ул.,5·4,8 · 15 отзывовУральская ул.,5Селькоровская ул.,60·5,0 · 3 отзываСелькоровская ул.,60Волгоградская ул.,18·1 отзывВолгоградская ул.,18ул. Азина,22/2·4,6 · 17 отзывовул. Азина,22/2ул. 8 Марта,204Г/2·4,3 · 3 отзываул. 8 Марта,204Г/2мкр-н Широкая Речка, ул. Анатолия Муранова,18·4,7 · 11 отзывовмкр-н Широкая Речка, ул. Анатолия Муранова,18·3,1 · 11 отзывов(id 10377315)NULLПоследняя строка — единственная во всём наборе, где адрес состоял ИЗ рейтинга целиком. Парсер на ней возвращает
None; миграция делает то же черезNULLIF. Мусор в колонке хуже пустоты: NULL апсерт теперь дозаполняет (#2777), «·3,1 · 11 отзывов» — нет.Обратимость — без новой таблицы и колонки
Прежнее значение уже хранится:
raw_payload->>'address'пишется скрейпером на INSERT и отсутствует вON CONFLICT DO UPDATE SET(проверено поbase.py) — переживает любой свип. Замер: у 1123 из 1123raw_payload->>'address' = addressпобайтово, NULL-ов нет.Откат-предикат (полный текст в шапке миграции) проверен в том же dry-run:
restored = beforeпобайтово у 1123 из 1123;raw_payloadгрязный, аaddressуже перезаписанavito_detailполным «Свердловская обл., Первоуральск, …», он не берёт (0) — и это правильно.Геокодирование — отдельный вопрос, числа честные
Выборка
geocode_missing_listings:lat IS NULL AND is_active AND address IS NOT NULL AND length(trim(address)) >= 5 AND (geocode_tried_at IS NULL OR < 7 days), GROUP BY (address, city).geocode_tried_at = NULLв миграции обязателен: метка стоит у 711 строк (у 370 — свежее 7 суток), но стоит она на СТАРОМ негеокодируемом тексте. Цена сброса измерена: до 19 лишних запросов Nominatim (19 пар из 854 имеют соседа с недавним отказом).next_run_at = 2026-08-10 17:45 UTC, окно 0-23,batch_size=200,budget_sec=1800.geocode_cacheс координатами и не истекли (ключ считан той же формулой, что строит_cache_key) → 245 строк получат geom мгновенно. Остальное — как повезёт тирам (кадастровый FDW → Nominatim): последние 5 ночных прогонов дали 17-53% успеха на адрес.Критерий приёмки, записанный ДО факта (проверять 2026-08-11 утром):
SELECT count(*) FROM listings WHERE source='avito' AND address ~ '·\s*\d'→ 0, откат-предикат → 1123;lat IS NOT NULL(жёсткий низ из кэша). Меньше 245 = геокодер не подобрал их, и это баг, а не «не повезло».Чего эта миграция НЕ делает, сознательно
COALESCEв апсерте (#2777);address IS NULL— они лечатся сами (#2777). Замерено: пустых строк''среди них 0, все именно NULL;address+city. Но у 88 из 548 донорских пар соседи расходятся больше чем на 50 м, у 32 — больше 250 м, худший разброс 15 км. Выбор победителя между ними — новая политика, а не бэкфилл. Если решение «переносить» будет принято — это отдельная миграция, числа готовы.Гейт
scripts/check-migration-lock-timeout.py:selftest OK+✓ блокирующий DDL прикрыт lock_timeout (проверено новых миграций: 4).Closes #2814