feat(tradein): geocode-missing для deals (#4 завершение)

geocode-missing получил target=listings|deals. deals.geocode_tried_at
добавлен в миграцию 005. cron геокодит и сделки Росреестра по улицам.
This commit is contained in:
TradeIn Deploy 2026-05-21 20:04:19 +03:00
parent 9ac4f35abc
commit 4ea35d5740
3 changed files with 40 additions and 17 deletions

View file

@ -140,28 +140,35 @@ def _clean_address_for_geocode(addr: str) -> str:
async def geocode_missing( async def geocode_missing(
db: Annotated[Session, Depends(get_db)], db: Annotated[Session, Depends(get_db)],
limit: int = 100, limit: int = 100,
target: Literal["listings", "deals"] = "listings",
) -> dict: ) -> dict:
"""Геокодинг listings у которых нет lat/lon (используя address). """Геокодинг listings ИЛИ deals у которых нет lat/lon (используя address).
target=listings (по умолч.) объявления Cian/N1; target=deals сделки Росреестра.
Чанк-обработка: limit=100 ~150-200с (Nominatim 1 req/sec). cron вызывает Чанк-обработка: limit=100 ~150-200с (Nominatim 1 req/sec). cron вызывает
в цикле пока `remaining` > 0. в цикле пока `remaining` > 0.
geocode_tried_at: после КАЖДОЙ попытки (успех/провал) ставим NOW(). Failed- geocode_tried_at: после КАЖДОЙ попытки (успех/провал) ставим NOW(). Failed-
адреса не выбираются повторно 7 дней cron-loop завершается, не зацикливается. адреса не выбираются повторно 7 дней cron-loop завершается, не зацикливается.
geom обновляется автоматически триггером listings_set_geom_trg. geom обновляется автоматически триггером.
""" """
# Доп. фильтр для listings — у Avito/N1 встречаются плейсхолдер-адреса.
extra_filter = (
"AND address NOT LIKE '%(Avito)%' AND address NOT LIKE '%(N1)%'"
if target == "listings"
else ""
)
rows = db.execute( rows = db.execute(
text( text(
""" f"""
SELECT id, address SELECT id, address
FROM listings FROM {target}
WHERE lat IS NULL WHERE lat IS NULL
AND COALESCE(address, '') != '' AND COALESCE(address, '') != ''
AND address NOT LIKE '%(Avito)%' {extra_filter}
AND address NOT LIKE '%(N1)%'
AND (geocode_tried_at IS NULL AND (geocode_tried_at IS NULL
OR geocode_tried_at < NOW() - interval '7 days') OR geocode_tried_at < NOW() - interval '7 days')
ORDER BY geocode_tried_at NULLS FIRST, scraped_at DESC ORDER BY geocode_tried_at NULLS FIRST
LIMIT :limit LIMIT :limit
""" """
), ),
@ -176,7 +183,7 @@ async def geocode_missing(
if result is None: if result is None:
# Помечаем что пробовали — иначе ретрай на каждом cron. # Помечаем что пробовали — иначе ретрай на каждом cron.
db.execute( db.execute(
text("UPDATE listings SET geocode_tried_at = NOW() WHERE id = :id"), text(f"UPDATE {target} SET geocode_tried_at = NOW() WHERE id = :id"),
{"id": row["id"]}, {"id": row["id"]},
) )
db.commit() db.commit()
@ -185,7 +192,7 @@ async def geocode_missing(
# geom пересчитается триггером из lat/lon. # geom пересчитается триггером из lat/lon.
db.execute( db.execute(
text( text(
"UPDATE listings SET lat = :lat, lon = :lon, " f"UPDATE {target} SET lat = :lat, lon = :lon, "
"geocode_tried_at = NOW() WHERE id = :id" "geocode_tried_at = NOW() WHERE id = :id"
), ),
{"lat": result.lat, "lon": result.lon, "id": row["id"]}, {"lat": result.lat, "lon": result.lon, "id": row["id"]},
@ -196,12 +203,11 @@ async def geocode_missing(
# Сколько ещё не-геокоженных адресов ждут (для cron-loop'а). # Сколько ещё не-геокоженных адресов ждут (для cron-loop'а).
remaining = db.execute( remaining = db.execute(
text( text(
""" f"""
SELECT count(*) FROM listings SELECT count(*) FROM {target}
WHERE lat IS NULL WHERE lat IS NULL
AND COALESCE(address, '') != '' AND COALESCE(address, '') != ''
AND address NOT LIKE '%(Avito)%' {extra_filter}
AND address NOT LIKE '%(N1)%'
AND (geocode_tried_at IS NULL AND (geocode_tried_at IS NULL
OR geocode_tried_at < NOW() - interval '7 days') OR geocode_tried_at < NOW() - interval '7 days')
""" """

View file

@ -11,8 +11,12 @@
-- Идемпотентно (ADD COLUMN IF NOT EXISTS) — безопасно при каждом деплое. -- Идемпотентно (ADD COLUMN IF NOT EXISTS) — безопасно при каждом деплое.
ALTER TABLE listings ADD COLUMN IF NOT EXISTS geocode_tried_at timestamptz; ALTER TABLE listings ADD COLUMN IF NOT EXISTS geocode_tried_at timestamptz;
ALTER TABLE deals ADD COLUMN IF NOT EXISTS geocode_tried_at timestamptz;
-- Частичный индекс: ускоряет выборку «что ещё геокодить». -- Частичные индексы: ускоряют выборку «что ещё геокодить».
CREATE INDEX IF NOT EXISTS listings_geocode_pending_idx CREATE INDEX IF NOT EXISTS listings_geocode_pending_idx
ON listings (geocode_tried_at NULLS FIRST) ON listings (geocode_tried_at NULLS FIRST)
WHERE lat IS NULL; WHERE lat IS NULL;
CREATE INDEX IF NOT EXISTS deals_geocode_pending_idx
ON deals (geocode_tried_at NULLS FIRST)
WHERE lat IS NULL;

View file

@ -71,13 +71,26 @@ done
# Геокодинг свежих лотов без координат (Cian/N1 отдают только адрес). # Геокодинг свежих лотов без координат (Cian/N1 отдают только адрес).
# Чанками по 100 (~200с/чанк, под таймаутом), пока remaining > 0. # Чанками по 100 (~200с/чанк, под таймаутом), пока remaining > 0.
# Макс 15 чанков = 1500 адресов. geocode_tried_at не даёт зациклиться. # Макс 15 чанков = 1500 адресов. geocode_tried_at не даёт зациклиться.
log "→ geocode-missing (чанками)" log "→ geocode-missing listings (чанками)"
for gi in $(seq 1 15); do for gi in $(seq 1 15); do
geo=$(docker exec "$CONTAINER" curl -s -X POST \ geo=$(docker exec "$CONTAINER" curl -s -X POST \
"http://localhost:8000/api/v1/admin/geocode-missing?limit=100" \ "http://localhost:8000/api/v1/admin/geocode-missing?limit=100&target=listings" \
-H "$ADMIN_HDR" \ -H "$ADMIN_HDR" \
-m 320 2>/dev/null || echo '{"error":"geocode_timeout","remaining":0}') -m 320 2>/dev/null || echo '{"error":"geocode_timeout","remaining":0}')
log "← geocode чанк $gi: $geo" log "← listings чанк $gi: $geo"
remaining=$(echo "$geo" | grep -oE '"remaining":[0-9]+' | grep -oE '[0-9]+' || echo 0)
[ "${remaining:-0}" -eq 0 ] && break
sleep 2
done
# Сделки Росреестра тоже без координат (импорт отдаёт только улицу).
log "→ geocode-missing deals (чанками)"
for gi in $(seq 1 30); do
geo=$(docker exec "$CONTAINER" curl -s -X POST \
"http://localhost:8000/api/v1/admin/geocode-missing?limit=100&target=deals" \
-H "$ADMIN_HDR" \
-m 320 2>/dev/null || echo '{"error":"geocode_timeout","remaining":0}')
log "← deals чанк $gi: $geo"
remaining=$(echo "$geo" | grep -oE '"remaining":[0-9]+' | grep -oE '[0-9]+' || echo 0) remaining=$(echo "$geo" | grep -oE '"remaining":[0-9]+' | grep -oE '[0-9]+' || echo 0)
[ "${remaining:-0}" -eq 0 ] && break [ "${remaining:-0}" -eq 0 ] && break
sleep 2 sleep 2