fix(tradein): чанковый геокодинг — cron не таймаутится (#1) #382

Merged
lekss361 merged 1 commit from fix/tradein-geocode-chunked into main 2026-05-21 16:43:47 +00:00
3 changed files with 71 additions and 13 deletions

View file

@ -124,12 +124,15 @@ def _clean_address_for_geocode(addr: str) -> str:
@router.post("/geocode-missing") @router.post("/geocode-missing")
async def geocode_missing( async def geocode_missing(
db: Annotated[Session, Depends(get_db)], db: Annotated[Session, Depends(get_db)],
limit: int = 500, limit: int = 100,
) -> dict: ) -> dict:
"""Геокодинг listings у которых нет lat/lon (используя address). """Геокодинг listings у которых нет lat/lon (используя address).
Запускать после scrape Cian/N1 не отдают координаты, только адрес. Чанк-обработка: limit=100 ~150-200с (Nominatim 1 req/sec). cron вызывает
Nominatim rate-limited 1 req/sec большой batch идёт ~limit секунд. в цикле пока `remaining` > 0.
geocode_tried_at: после КАЖДОЙ попытки (успех/провал) ставим NOW(). Failed-
адреса не выбираются повторно 7 дней cron-loop завершается, не зацикливается.
geom обновляется автоматически триггером listings_set_geom_trg. geom обновляется автоматически триггером listings_set_geom_trg.
""" """
rows = db.execute( rows = db.execute(
@ -137,11 +140,13 @@ async def geocode_missing(
""" """
SELECT id, address SELECT id, address
FROM listings FROM listings
WHERE (lat IS NULL OR lon IS NULL) WHERE lat IS NULL
AND COALESCE(address, '') != '' AND COALESCE(address, '') != ''
AND address NOT LIKE '%(Avito)%' AND address NOT LIKE '%(Avito)%'
AND address NOT LIKE '%(N1)%' AND address NOT LIKE '%(N1)%'
ORDER BY scraped_at DESC AND (geocode_tried_at IS NULL
OR geocode_tried_at < NOW() - interval '7 days')
ORDER BY geocode_tried_at NULLS FIRST, scraped_at DESC
LIMIT :limit LIMIT :limit
""" """
), ),
@ -154,14 +159,43 @@ async def geocode_missing(
clean = _clean_address_for_geocode(row["address"]) clean = _clean_address_for_geocode(row["address"])
result = await geocode(clean, db) result = await geocode(clean, db)
if result is None: if result is None:
# Помечаем что пробовали — иначе ретрай на каждом cron.
db.execute(
text("UPDATE listings SET geocode_tried_at = NOW() WHERE id = :id"),
{"id": row["id"]},
)
db.commit()
skipped += 1 skipped += 1
continue continue
# geom пересчитается триггером из lat/lon. # geom пересчитается триггером из lat/lon.
db.execute( db.execute(
text("UPDATE listings SET lat = :lat, lon = :lon WHERE id = :id"), text(
"UPDATE listings SET lat = :lat, lon = :lon, "
"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"]},
) )
db.commit() db.commit()
geocoded += 1 geocoded += 1
return {"checked": len(rows), "geocoded": geocoded, "skipped": skipped} # Сколько ещё не-геокоженных адресов ждут (для cron-loop'а).
remaining = db.execute(
text(
"""
SELECT count(*) FROM listings
WHERE lat IS NULL
AND COALESCE(address, '') != ''
AND address NOT LIKE '%(Avito)%'
AND address NOT LIKE '%(N1)%'
AND (geocode_tried_at IS NULL
OR geocode_tried_at < NOW() - interval '7 days')
"""
)
).scalar()
return {
"checked": len(rows),
"geocoded": geocoded,
"skipped": skipped,
"remaining": int(remaining or 0),
}

View file

@ -0,0 +1,18 @@
-- Trade-In MVP — отслеживание попыток геокодинга.
--
-- Проблема: geocode-missing раньше выбирал ВСЕ строки с lat IS NULL. Адреса,
-- которые Nominatim не может распознать (~12%), выбирались на каждом прогоне
-- → бесконечный ретрай + cron-таймаут.
--
-- Решение: колонка geocode_tried_at. После КАЖДОЙ попытки (успех или провал)
-- ставим NOW(). geocode-missing выбирает только не-пробованные ИЛИ
-- пробованные >7 дней назад. Failed-адреса ретраятся раз в неделю, не каждый cron.
--
-- Идемпотентно (ADD COLUMN IF NOT EXISTS) — безопасно при каждом деплое.
ALTER TABLE listings ADD COLUMN IF NOT EXISTS geocode_tried_at timestamptz;
-- Частичный индекс: ускоряет выборку «что ещё геокодить».
CREATE INDEX IF NOT EXISTS listings_geocode_pending_idx
ON listings (geocode_tried_at NULLS FIRST)
WHERE lat IS NULL;

View file

@ -64,11 +64,17 @@ for anchor in "${ANCHORS[@]}"; do
done done
# Геокодинг свежих лотов без координат (Cian/N1 отдают только адрес). # Геокодинг свежих лотов без координат (Cian/N1 отдают только адрес).
# Nominatim 1 req/sec → batch ~500 идёт до ~8 мин. # Чанками по 100 (~200с/чанк, под таймаутом), пока remaining > 0.
log "→ geocode-missing" # Макс 15 чанков = 1500 адресов. geocode_tried_at не даёт зациклиться.
geo=$(docker exec "$CONTAINER" curl -s -X POST \ log "→ geocode-missing (чанками)"
"http://localhost:8000/api/v1/admin/geocode-missing?limit=500" \ for gi in $(seq 1 15); do
-m 700 2>/dev/null || echo '{"error":"geocode_timeout"}') geo=$(docker exec "$CONTAINER" curl -s -X POST \
log "← geocode-missing $geo" "http://localhost:8000/api/v1/admin/geocode-missing?limit=100" \
-m 320 2>/dev/null || echo '{"error":"geocode_timeout","remaining":0}')
log "← geocode чанк $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 "=== cron-scrape done ===" log "=== cron-scrape done ==="