All checks were successful
Deploy Trade-In / changes (push) Successful in 11s
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Successful in 1m36s
Deploy Trade-In / build-frontend (push) Successful in 4m43s
Deploy Trade-In / build-backend (push) Successful in 4m44s
Deploy Trade-In / deploy (push) Successful in 2m15s
112 lines
8.2 KiB
Markdown
112 lines
8.2 KiB
Markdown
# ГАР/ФИАС loader знаменателя «квартир на дом» (runbook)
|
||
|
||
Наполняет таблицу `gar_house_flats` числом помещений-квартир под каждым домом из
|
||
официального дампа ГАР/ФИАС и матчит к `houses` по нормализованному адресу, проставляя
|
||
`houses.gar_flat_count`. Это «знаменатель» для страницы **«доля квартир дома в продаже»**
|
||
(`active_vtorichka_listings / total_apartments_in_building`, view `v_building_sale_share`,
|
||
мигр. 143).
|
||
|
||
Код:
|
||
- `tradein-mvp/backend/app/services/gar_flats_loader.py` — стриминговый парсер + upsert + матчер.
|
||
- `tradein-mvp/backend/app/tasks/gar_flats_load.py` — CLI (`python -m app.tasks.gar_flats_load`).
|
||
|
||
## 1. Получить ГАР-дамп региона 66 (ops-шаг, вручную)
|
||
|
||
Лоадер потребляет **уже распакованные** XML локально — многогигабайтную загрузку/распаковку
|
||
делаем отдельно (это не часть лоадера).
|
||
|
||
1. Источник: ГАР (Государственный адресный реестр), портал ФНС — https://fias.nalog.ru/
|
||
(раздел «Скачать»). Берём «ГАР. Версия со всеми объектами» — формат XML, `gar_xml.zip`.
|
||
- Полный РФ-архив `gar_xml.zip` — десятки ГБ. Нам нужен **только регион 66** (Свердловская обл.).
|
||
- Внутри архива объекты разложены по папкам с кодом региона: распаковываем **только папку `66/`**.
|
||
Пример (извлечь одну папку без распаковки всего архива):
|
||
```bash
|
||
unzip gar_xml.zip '66/*' -d /data/gar
|
||
# → /data/gar/66/AS_ADDR_OBJ_*.XML, AS_HOUSES_*.XML, AS_APARTMENTS_*.XML,
|
||
# AS_MUN_HIERARCHY_*.XML, ...
|
||
```
|
||
- Размер распакованной папки `66/`: ориентир — несколько ГБ (крупнейшие файлы —
|
||
`AS_APARTMENTS_*` и `AS_MUN_HIERARCHY_*`, по сотни МБ — единицы ГБ). Поэтому парсер
|
||
стримит (`lxml.etree.iterparse` + очистка элементов), а не грузит дерево целиком.
|
||
|
||
Альтернатива: ГАР обновляется примерно еженедельно дельтами; нам достаточно полного
|
||
среза. Знаменатель «всего квартир в доме» почти статичен → **ре-прогон раз в квартал** ок.
|
||
|
||
Нужные файлы в папке региона (остальные лоадер игнорирует):
|
||
- `AS_ADDR_OBJ_*.XML` — адресные объекты (регион/город/улица).
|
||
- `AS_HOUSES_*.XML` — дома.
|
||
- `AS_APARTMENTS_*.XML` — помещения/квартиры.
|
||
- `AS_MUN_HIERARCHY_*.XML` — иерархия (приоритетна; при отсутствии используется `AS_ADM_HIERARCHY_*`).
|
||
|
||
## 2. Запуск лоадера
|
||
|
||
Где: в окружении с доступом к БД `tradein` (контейнер `tradein-backend` или локальный venv с
|
||
`DATABASE_URL` на tradein). Каталог `--dir` — путь к распакованной папке региона.
|
||
|
||
```bash
|
||
# в контейнере tradein-backend (DATABASE_URL уже выставлен)
|
||
docker exec -it tradein-backend \
|
||
python -m app.tasks.gar_flats_load --dir /data/gar/66 --region 66 --version 2026-06-01
|
||
```
|
||
|
||
Локально:
|
||
```bash
|
||
cd tradein-mvp/backend
|
||
DATABASE_URL=postgresql+psycopg://<user>:<pw>@localhost:<port>/tradein \
|
||
uv run python -m app.tasks.gar_flats_load --dir /data/gar/66 --region 66 --version 2026-06-01
|
||
```
|
||
|
||
Аргументы:
|
||
- `--dir` (обязательный) — каталог с распакованными ГАР XML региона.
|
||
- `--region` (умолч. `66`) — код региона; пишется в `gar_house_flats.region_code`.
|
||
- `--version` (умолч. сегодня, `YYYY-MM-DD`) — метка версии дампа (`gar_house_flats.gar_version`).
|
||
|
||
Что делает (две фазы, обе коммитятся):
|
||
1. **load** — стримит XML → считает помещения на дом (по иерархии) → собирает адрес →
|
||
UPSERT в `gar_house_flats` (`ON CONFLICT (house_guid)`, идемпотентно). `norm_address`
|
||
считает SQL-fn `tradein_normalize_short_addr` поверх «улица, номер».
|
||
2. **match** — `UPDATE houses SET gar_flat_count = …` по нормализованному адресу
|
||
(`gar_match_method='norm_address'`), идемпотентно (`IS DISTINCT FROM` gate).
|
||
|
||
В логах — сводка: `houses`, `apartments` (учтено под домами), `upserted`, `houses_matched`.
|
||
|
||
## 3. Проверка результата
|
||
|
||
```sql
|
||
-- сколько домов получили ГАР-знаменатель
|
||
SELECT count(*) FILTER (WHERE gar_flat_count IS NOT NULL) AS matched,
|
||
count(*) AS total
|
||
FROM houses;
|
||
|
||
-- топ домов по доле квартир в продаже (эффективный знаменатель из ГАР)
|
||
SELECT short_address, active_secondary, flat_count_effective, sale_share_pct, gar_match_method
|
||
FROM v_building_sale_share
|
||
WHERE gar_flat_count IS NOT NULL
|
||
ORDER BY sale_share_pct DESC NULLS LAST
|
||
LIMIT 20;
|
||
```
|
||
|
||
## 4. Семантика и ограничения
|
||
|
||
- **flat_count** = число активных (`ISACTUAL=1 AND ISACTIVE=1`) помещений ГАР, чей
|
||
`PARENTOBJID` в иерархии == `OBJECTID` дома. Тип помещения (`APARTTYPE`) НЕ фильтруется —
|
||
это «всего помещений в доме» (квартиры + нежилые), сознательное приближение знаменателя.
|
||
- **Адрес/норма.** `norm_address` = `tradein_normalize_short_addr(«<улица>, <номер>»)` —
|
||
тот же нормализатор, что и для `houses` при матче. Строка строится из «улица + номер»
|
||
(не из полной цепочки) для устойчивости: для ЕКБ эквивалентно `fn(full_address)` (fn срезает
|
||
регион/город), но не оставляет префикс для не-«г.» населённых пунктов. `full_address` хранится
|
||
отдельно как человекочитаемая provenance-цепочка (регион→город→улица→номер).
|
||
- **Tie-break.** Если несколько ГАР-строк дают один `norm_address`, матчер берёт строку с
|
||
**максимальным `flat_count`** (при равенстве — лексикографически меньший `house_guid`),
|
||
через `DISTINCT ON (norm_address) ORDER BY norm_address, flat_count DESC, house_guid`.
|
||
- **Дома с 0 квартир** грузятся (`flat_count=0`), но матчер их игнорирует (`flat_count > 0`).
|
||
- **Покрытие матча — точное равенство нормализованного адреса.** Расхождения формата типа
|
||
улицы (`ул` vs `ул.` vs `улица`) и корпусов (`5 к1` vs `5/1`) не совпадут точно. Это
|
||
ожидаемо: под `gar_house_flats.norm_address` уже создан GIN-trgm индекс (мигр. 143) для
|
||
будущего нечёткого (fuzzy) прохода — он вне scope этого лоадера.
|
||
|
||
## 5. Идемпотентность / ре-прогон
|
||
|
||
- `gar_house_flats` — `ON CONFLICT (house_guid) DO UPDATE` → повторный прогон обновляет, не дублит.
|
||
- Матчер — plain `UPDATE` с `IS DISTINCT FROM` → повторный прогон ничего не трогает.
|
||
- Ре-прогон с новым `--version` безопасен; рекомендуемая частота — раз в квартал.
|