[HIGH] tradein/avito: рейтинг дома подклеен в адрес объявления — у всех 1 123 таких строк нет координат (100%), 12.7% аналогов невидимы радиусу #2814

Closed
opened 2026-08-10 09:32:10 +00:00 by bot-backend · 3 comments
Collaborator

Найдено 2026-08-10 при измерении цены простоя Яндекс-геокодера (#2585). К геокодеру отношения не имеет — дефект в парсере адреса Авито.

Что происходит

С 27.07.2026 Авито рендерит рейтинг дома и число отзывов внутри того же <p> в data-marker="item-location", откуда serp.py:1948 берёт адрес. _clean_address этот хвост не режет, и он уезжает в listings.address:

Авиационная ул.,10·4,9 · 11 отзывов
ул. 8 Марта,204Г/2·4,3 · 3 отзыва
ул. Академика Шварца,18к2·1 отзыв
ул. Фурманова,59·2 отзыва

Масштаб — прод, 2026-08-10 09:40 UTC, read-only

1 123 активных объявления Авито с таким адресом, и у 1 123 из них нет координат. Доля 100%.

класс адреса (активное Авито) всего без координат
загрязнён рейтингом 1 123 1 123 (100%)
пустой 360 360 (100%)
без номера дома 2 177 832
чистый 5 715 949

Контрольная группа в своих же данных: у объявлений с чистым адресом координаты есть у 4 766 из 5 715 (83%), у загрязнённых — 0 из 1 123. 711 из 1 123 геокодер уже пробовал и не смог; такую строку не берёт ни один геокодер.

Появление совпадает с изменением вёрстки, а не с нашими правками:

неделя scraped_at загрязнённых всего Авито
≤ 2026-07-20 0 31 109
2026-07-27 91 989
2026-08-03 1 032 7 849

Цена

Объявление без geom молча выпадает из comp-пула эстиматора: Tier W отбирает через ST_DWithin(geom::geography, …), а geom IS NULL не проходит предикат и не считается нигде. Из 3 270 активных объявлений без координат 2 446 попадают в свежий пул аналогов (is_active, scraped_at < 30 сут, сегмент вторичка/NULL) — это 12.7% аналогов, невидимых радиусному поиску, из них 1 123 по этой причине.

Побочно: те же строки не резолвятся в дом (house_id_fk), то есть не питают ни домовой якорь, ни house_imv.

Что сделано (PR ниже)

Один хвост в _NOT_ADDRESS_TAIL_RE (providers/avito/serp.py:2100): режем по «·», за которой идёт цифра — рейтинг «·4,9» и счётчик «·2 отзыва». По любой «·» резать нельзя: разделитель района пишется как «, 59 · р-н Академический», за точкой буква, и этот хвост _deglue_house_marker намеренно сохраняет (#1773). Тест на обе стороны.

Что НЕ сделано — нужен бэкфилл, это к database-expert

Правка чинит только новые вставки. Существующие 1 123 строки не вылечатся сами: base.py:614 пишет address = COALESCE(listings.address, EXCLUDED.address) — адрес при конфликте никогда не перезаписывается, и это осознанно (#2777: свежий сырой адрес откатил бы чистку миграций 062/108/124).

Нужен разовый UPDATE listings SET address = <адрес до «·цифра»>, geocode_tried_at = NULL WHERE source='avito' AND address ~ '·\s*\d' — 1 123 строки, после чего geocode_missing_listings их подберёт. Сам не пишу: миграции не моя зона.

Найдено 2026-08-10 при измерении цены простоя Яндекс-геокодера (#2585). К геокодеру отношения не имеет — дефект в парсере адреса Авито. ## Что происходит С 27.07.2026 Авито рендерит рейтинг дома и число отзывов **внутри того же `<p>`** в `data-marker="item-location"`, откуда `serp.py:1948` берёт адрес. `_clean_address` этот хвост не режет, и он уезжает в `listings.address`: ``` Авиационная ул.,10·4,9 · 11 отзывов ул. 8 Марта,204Г/2·4,3 · 3 отзыва ул. Академика Шварца,18к2·1 отзыв ул. Фурманова,59·2 отзыва ``` ## Масштаб — прод, 2026-08-10 09:40 UTC, read-only **1 123 активных объявления Авито с таким адресом, и у 1 123 из них нет координат. Доля 100%.** | класс адреса (активное Авито) | всего | без координат | |---|---|---| | **загрязнён рейтингом** | **1 123** | **1 123 (100%)** | | пустой | 360 | 360 (100%) | | без номера дома | 2 177 | 832 | | чистый | 5 715 | 949 | Контрольная группа в своих же данных: у объявлений с чистым адресом координаты есть у **4 766 из 5 715 (83%)**, у загрязнённых — **0 из 1 123**. 711 из 1 123 геокодер уже пробовал и не смог; такую строку не берёт ни один геокодер. Появление совпадает с изменением вёрстки, а не с нашими правками: | неделя `scraped_at` | загрязнённых | всего Авито | |---|---|---| | ≤ 2026-07-20 | **0** | 31 109 | | 2026-07-27 | 91 | 989 | | 2026-08-03 | **1 032** | 7 849 | ## Цена Объявление без `geom` **молча выпадает** из comp-пула эстиматора: Tier W отбирает через `ST_DWithin(geom::geography, …)`, а `geom IS NULL` не проходит предикат и не считается нигде. Из 3 270 активных объявлений без координат **2 446 попадают в свежий пул аналогов** (`is_active`, `scraped_at` < 30 сут, сегмент вторичка/NULL) — это **12.7% аналогов, невидимых радиусному поиску**, из них 1 123 по этой причине. Побочно: те же строки не резолвятся в дом (`house_id_fk`), то есть не питают ни домовой якорь, ни `house_imv`. ## Что сделано (PR ниже) Один хвост в `_NOT_ADDRESS_TAIL_RE` (`providers/avito/serp.py:2100`): режем по «·», **за которой идёт цифра** — рейтинг «·4,9» и счётчик «·2 отзыва». По любой «·» резать нельзя: разделитель района пишется как «, 59 · р-н Академический», за точкой буква, и этот хвост `_deglue_house_marker` намеренно сохраняет (#1773). Тест на обе стороны. ## Что НЕ сделано — нужен бэкфилл, это к database-expert Правка чинит только **новые** вставки. Существующие 1 123 строки не вылечатся сами: `base.py:614` пишет `address = COALESCE(listings.address, EXCLUDED.address)` — адрес при конфликте **никогда не перезаписывается**, и это осознанно (#2777: свежий сырой адрес откатил бы чистку миграций 062/108/124). Нужен разовый `UPDATE listings SET address = <адрес до «·цифра»>, geocode_tried_at = NULL WHERE source='avito' AND address ~ '·\s*\d'` — 1 123 строки, после чего `geocode_missing_listings` их подберёт. Сам не пишу: миграции не моя зона.
Author
Collaborator

PR #2815 смержен и проверен в живом прод-контейнере (не по релиз-метке, а по коду), 2026-08-10 09:57 UTC:

docker exec tradein-scraper python -c "from scraper_kit.providers.avito.serp import _clean_address as c, _NOT_ADDRESS_TAIL_RE as r; ..."
→ pattern: \s*(Площадь \d|от \d+\s?мин\.|css-[a-z0-9_-]+|·\s*\d)
→ _clean_address("ул. Ткачей,17·5,0 · 4 отзыва") == "ул. Ткачей,17"

Новые вставки чистые начиная с ближайшего свипа. Задачу оставляю открытой ради бэкфилла 1 123 существующих строк — их address при конфликте не перезаписывается осознанно (#2777), сами они не вылечатся. Критерий приёмки: SELECT count(*) FROM listings WHERE source='avito' AND is_active AND address LIKE '%·%' → 0, и доля активного Авито без координат уходит с 34.9% ниже 25%.

Оговорка по 360 строкам с пустым адресом из таблицы выше: они address IS NULL (не пустая строка), поэтому лечатся сами — #2777 как раз сегодня научил upsert дозаполнять NULL-адрес. Бэкфилл нужен только загрязнённым.

PR #2815 смержен и **проверен в живом прод-контейнере** (не по релиз-метке, а по коду), 2026-08-10 09:57 UTC: ``` docker exec tradein-scraper python -c "from scraper_kit.providers.avito.serp import _clean_address as c, _NOT_ADDRESS_TAIL_RE as r; ..." → pattern: \s*(Площадь \d|от \d+\s?мин\.|css-[a-z0-9_-]+|·\s*\d) → _clean_address("ул. Ткачей,17·5,0 · 4 отзыва") == "ул. Ткачей,17" ``` Новые вставки чистые начиная с ближайшего свипа. **Задачу оставляю открытой ради бэкфилла 1 123 существующих строк** — их `address` при конфликте не перезаписывается осознанно (#2777), сами они не вылечатся. Критерий приёмки: `SELECT count(*) FROM listings WHERE source='avito' AND is_active AND address LIKE '%·%'` → 0, и доля активного Авито без координат уходит с 34.9% ниже 25%. Оговорка по 360 строкам с пустым адресом из таблицы выше: они `address IS NULL` (не пустая строка), поэтому лечатся сами — #2777 как раз сегодня научил upsert дозаполнять NULL-адрес. Бэкфилл нужен только загрязнённым.
Author
Collaborator

Бэкфилл сделан: PR #2818 смержен, миграция 254_listings_backfill_avito_rating_glued_address.sql применена на проде 2026-08-10 10:27:33 UTC (_schema_migrations), эффект проверен по данным.

Прод после применения

проверка значение
source='avito' AND address ~ '·\s*\d' 0 (было 1123)
вычищено строк (откат-предикат) 1123
из них стало address IS NULL 1 (·3,1 · 11 отзывов — адрес состоял ИЗ рейтинга; парсер тут тоже возвращает None)
geocode_tried_at сброшен 1123
районные хвосты «· р-н …» целы 296/296 (все cian, avito среди них 0)
из них уже с координатами 0 — координат чистка не даёт, см. ниже

Примеры с прода: ул. Ткачей,17·5,0 · 4 отзываул. Ткачей,17; ул. Свердлова,32Б·4,2 · 5 отзывовул. Свердлова,32Б; мкр-н Широкая Речка, ул. Анатолия Муранова,18·4,7 · 11 отзывовмкр-н Широкая Речка, ул. Анатолия Муранова,18.

Правило резки — дословно _NOT_ADDRESS_TAIL_RE из serp.py, и это проверено тем же кодом: 1123 сырых адреса выгружены с прода и прогнаны через живой _clean_address в боевом tradein-scraperrows=1123 mismatches=0. Плюс dry-run экзактным файлом миграции (BEGIN … ROLLBACK) до мержа: UPDATE 1123, после — 0.

Обратимость без новой таблицы: прежнее значение уже лежит в raw_payload->>'address' (пишется на INSERT, отсутствует в ON CONFLICT DO UPDATE SET, совпадает с address побайтово у 1123/1123). Полный откат-предикат — в шапке миграции; в dry-run он дал ровно 1123 совпадения по всей таблице, все наши, восстановление побайтовое.

Геокодирование — критерий приёмки записан ДО факта

Выборку geocode_missing_listings эти строки проходят: 1122 из 1123 (854 уникальные пары address+city; 1123-я — та, что стала NULL, её дозаполнит апсерт по #2777). Очередь: 1938 строк / 1370 пар (была 1569 / 1241) — рост всего +369, потому что 753 из 1123 и раньше стояли в очереди со своим грязным адресом и жгли бюджет Nominatim впустую; этот расход снят.

Ближайший прогон: next_run_at = 2026-08-10 17:45 UTC (окно 0-23, batch_size=200, budget_sec=1800).

Критерий приёмки на утро 2026-08-11: из 1122 вычищенных строк ≥ 245 имеют lat IS NOT NULL. 245 — не прикидка, а жёсткий низ: 138 из 854 пар уже лежат в живом geocode_cache с координатами и не истекли (ключ считан той же формулой, что строит _cache_key), это 245 строк, закрываемых без единого внешнего запроса. Меньше 245 = геокодер их не подобрал, и это баг, а не «не повезло». Сверх этого — как ответят тиры (кадастровый FDW → Nominatim): последние 5 ночных прогонов давали 17-53% успеха на адрес, точнее гадать не буду.

Проверка: SELECT count(*) FILTER (WHERE lat IS NOT NULL), count(*) FROM listings WHERE source='avito' AND raw_payload->>'address' ~ '·\s*\d' AND address IS NOT NULL;

Две поправки к формулировкам выше по треду

  1. Критерий из моего же комментария — address LIKE '%·%' → 0 — как общий предикат неверен: 296 строк вида «улица Бебеля, 138 · р-н Железнодорожный» обязаны сохранить «·» (#1773). Сегодня он всё же выполняется, потому что все 296 — cian, а avito среди них 0; но правильная формулировка — address ~ '·\s*\d'.
  2. Второй критерий — «доля активного Авито без координат уходит с 34.9% ниже 25%» — чисткой адреса не достигается и не может быть: сейчас 34.8%, и сдвинет её только геокодер. Порог 25% требует ~920 новых геокодов; жёсткий низ, который я готов подписать, — 245.

Остаток, который не закрыт сознательно

789 из 1123 строк имеют соседа с координатами по тому же вычищенному address+city — их можно было бы закрыть мгновенно копированием. Не сделано: у 88 из 548 донорских пар соседи расходятся между собой больше чем на 50 м, у 32 — больше 250 м, худший разброс 15 км. Выбор победителя между ними — новая политика, а не бэкфилл. Если решение «переносить» будет принято — отдельная миграция, числа готовы.

Бэкфилл сделан: PR #2818 смержен, миграция `254_listings_backfill_avito_rating_glued_address.sql` **применена на проде 2026-08-10 10:27:33 UTC** (`_schema_migrations`), эффект проверен по данным. ## Прод после применения | проверка | значение | |---|---| | `source='avito' AND address ~ '·\s*\d'` | **0** (было 1123) | | вычищено строк (откат-предикат) | **1123** | | из них стало `address IS NULL` | 1 (`·3,1 · 11 отзывов` — адрес состоял ИЗ рейтинга; парсер тут тоже возвращает `None`) | | `geocode_tried_at` сброшен | 1123 | | районные хвосты «· р-н …» целы | **296/296** (все cian, avito среди них 0) | | из них уже с координатами | **0** — координат чистка не даёт, см. ниже | Примеры с прода: `ул. Ткачей,17·5,0 · 4 отзыва` → `ул. Ткачей,17`; `ул. Свердлова,32Б·4,2 · 5 отзывов` → `ул. Свердлова,32Б`; `мкр-н Широкая Речка, ул. Анатолия Муранова,18·4,7 · 11 отзывов` → `мкр-н Широкая Речка, ул. Анатолия Муранова,18`. Правило резки — дословно `_NOT_ADDRESS_TAIL_RE` из `serp.py`, и это проверено **тем же кодом**: 1123 сырых адреса выгружены с прода и прогнаны через живой `_clean_address` в боевом `tradein-scraper` → `rows=1123 mismatches=0`. Плюс dry-run экзактным файлом миграции (`BEGIN … ROLLBACK`) до мержа: `UPDATE 1123`, после — 0. Обратимость без новой таблицы: прежнее значение уже лежит в `raw_payload->>'address'` (пишется на INSERT, отсутствует в `ON CONFLICT DO UPDATE SET`, совпадает с `address` побайтово у 1123/1123). Полный откат-предикат — в шапке миграции; в dry-run он дал ровно 1123 совпадения по всей таблице, все наши, восстановление побайтовое. ## Геокодирование — критерий приёмки записан ДО факта Выборку `geocode_missing_listings` эти строки проходят: 1122 из 1123 (854 уникальные пары `address+city`; 1123-я — та, что стала NULL, её дозаполнит апсерт по #2777). Очередь: 1938 строк / 1370 пар (была 1569 / 1241) — рост всего +369, потому что **753 из 1123 и раньше стояли в очереди со своим грязным адресом** и жгли бюджет Nominatim впустую; этот расход снят. **Ближайший прогон: `next_run_at = 2026-08-10 17:45 UTC`** (окно 0-23, `batch_size=200`, `budget_sec=1800`). Критерий приёмки на утро **2026-08-11**: из 1122 вычищенных строк **≥ 245** имеют `lat IS NOT NULL`. 245 — не прикидка, а жёсткий низ: 138 из 854 пар уже лежат в живом `geocode_cache` с координатами и не истекли (ключ считан той же формулой, что строит `_cache_key`), это 245 строк, закрываемых без единого внешнего запроса. Меньше 245 = геокодер их не подобрал, и это баг, а не «не повезло». Сверх этого — как ответят тиры (кадастровый FDW → Nominatim): последние 5 ночных прогонов давали 17-53% успеха на адрес, точнее гадать не буду. Проверка: `SELECT count(*) FILTER (WHERE lat IS NOT NULL), count(*) FROM listings WHERE source='avito' AND raw_payload->>'address' ~ '·\s*\d' AND address IS NOT NULL;` ## Две поправки к формулировкам выше по треду 1. Критерий из моего же комментария — `address LIKE '%·%'` → 0 — **как общий предикат неверен**: 296 строк вида «улица Бебеля, 138 · р-н Железнодорожный» обязаны сохранить «·» (#1773). Сегодня он всё же выполняется, потому что все 296 — cian, а avito среди них 0; но правильная формулировка — `address ~ '·\s*\d'`. 2. Второй критерий — «доля активного Авито без координат уходит с 34.9% ниже 25%» — чисткой адреса **не достигается и не может быть**: сейчас 34.8%, и сдвинет её только геокодер. Порог 25% требует ~920 новых геокодов; жёсткий низ, который я готов подписать, — 245. ## Остаток, который не закрыт сознательно 789 из 1123 строк имеют соседа с координатами по тому же вычищенному `address+city` — их можно было бы закрыть мгновенно копированием. Не сделано: у 88 из 548 донорских пар соседи расходятся между собой больше чем на 50 м, у 32 — больше 250 м, худший разброс 15 км. Выбор победителя между ними — новая политика, а не бэкфилл. Если решение «переносить» будет принято — отдельная миграция, числа готовы.
Author
Collaborator

Критерий выполнен вчетверо — и поправка к моему чтению

Записанный ДО факта критерий: из 1122 вычищенных адресов ≥ 245 получат координаты к утру 11.08. Замер 12.08:

бывших битых (raw_payload ~ '·цифра')   1133
теперь с координатами                    962   (85%)

962, а не 245 — геокодер подобрал почти все. Прогоны geocode_missing_listings это подтверждают: 971 сохранено 10.08 17:46 + 323 сохранено 11.08 18:17.

Поправка к тому, как я это чуть не прочитал

Сегодня при обзоре я посмотрел на долю покрытия Авито — 77.4% и до, и после — и едва не отчитался «геокодирование не сдвинулось». Это неверно: доля стоит на месте потому, что знаменатель вырос от новых объявлений, а абсолютное число координат выросло на полторы тысячи.

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

Закрыто по существу. Осталось владельцу разовое — не требуется: чистка и геокодирование прошли автоматически.

## Критерий выполнен вчетверо — и поправка к моему чтению Записанный ДО факта критерий: из 1122 вычищенных адресов ≥ **245** получат координаты к утру 11.08. Замер 12.08: ``` бывших битых (raw_payload ~ '·цифра') 1133 теперь с координатами 962 (85%) ``` **962**, а не 245 — геокодер подобрал почти все. Прогоны `geocode_missing_listings` это подтверждают: 971 сохранено 10.08 17:46 + 323 сохранено 11.08 18:17. ### Поправка к тому, как я это чуть не прочитал Сегодня при обзоре я посмотрел на **долю** покрытия Авито — 77.4% и до, и после — и едва не отчитался «геокодирование не сдвинулось». Это неверно: доля стоит на месте потому, что знаменатель вырос от новых объявлений, а **абсолютное** число координат выросло на полторы тысячи. Это ровно та ошибка, про которую в этом продукте уже есть заметка: смотреть на процент, когда меняются обе величины. Поймал на себе, отмечаю. Закрыто по существу. Осталось владельцу разовое — не требуется: чистка и геокодирование прошли автоматически.
Sign in to join this conversation.
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#2814
No description provided.