From 4ea35d5740bb5f4b01aff8698a16756b2d41474d Mon Sep 17 00:00:00 2001 From: TradeIn Deploy Date: Thu, 21 May 2026 20:04:19 +0300 Subject: [PATCH] =?UTF-8?q?feat(tradein):=20geocode-missing=20=D0=B4=D0=BB?= =?UTF-8?q?=D1=8F=20deals=20(#4=20=D0=B7=D0=B0=D0=B2=D0=B5=D1=80=D1=88?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit geocode-missing получил target=listings|deals. deals.geocode_tried_at добавлен в миграцию 005. cron геокодит и сделки Росреестра по улицам. --- tradein-mvp/backend/app/api/v1/admin.py | 32 +++++++++++-------- .../backend/data/sql/005_geocode_tracking.sql | 6 +++- tradein-mvp/deploy/cron-scrape.sh | 19 +++++++++-- 3 files changed, 40 insertions(+), 17 deletions(-) diff --git a/tradein-mvp/backend/app/api/v1/admin.py b/tradein-mvp/backend/app/api/v1/admin.py index d72e10eb..8946a303 100644 --- a/tradein-mvp/backend/app/api/v1/admin.py +++ b/tradein-mvp/backend/app/api/v1/admin.py @@ -140,28 +140,35 @@ def _clean_address_for_geocode(addr: str) -> str: async def geocode_missing( db: Annotated[Session, Depends(get_db)], limit: int = 100, + target: Literal["listings", "deals"] = "listings", ) -> 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 вызывает в цикле пока `remaining` > 0. geocode_tried_at: после КАЖДОЙ попытки (успех/провал) ставим NOW(). Failed- адреса не выбираются повторно 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( text( - """ + f""" SELECT id, address - FROM listings + FROM {target} WHERE lat IS NULL AND COALESCE(address, '') != '' - AND address NOT LIKE '%(Avito)%' - AND address NOT LIKE '%(N1)%' + {extra_filter} AND (geocode_tried_at IS NULL 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 """ ), @@ -176,7 +183,7 @@ async def geocode_missing( if result is None: # Помечаем что пробовали — иначе ретрай на каждом cron. 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"]}, ) db.commit() @@ -185,7 +192,7 @@ async def geocode_missing( # geom пересчитается триггером из lat/lon. db.execute( text( - "UPDATE listings SET lat = :lat, lon = :lon, " + f"UPDATE {target} SET lat = :lat, lon = :lon, " "geocode_tried_at = NOW() WHERE id = :id" ), {"lat": result.lat, "lon": result.lon, "id": row["id"]}, @@ -196,12 +203,11 @@ async def geocode_missing( # Сколько ещё не-геокоженных адресов ждут (для cron-loop'а). remaining = db.execute( text( - """ - SELECT count(*) FROM listings + f""" + SELECT count(*) FROM {target} WHERE lat IS NULL AND COALESCE(address, '') != '' - AND address NOT LIKE '%(Avito)%' - AND address NOT LIKE '%(N1)%' + {extra_filter} AND (geocode_tried_at IS NULL OR geocode_tried_at < NOW() - interval '7 days') """ diff --git a/tradein-mvp/backend/data/sql/005_geocode_tracking.sql b/tradein-mvp/backend/data/sql/005_geocode_tracking.sql index 75e68063..49537b20 100644 --- a/tradein-mvp/backend/data/sql/005_geocode_tracking.sql +++ b/tradein-mvp/backend/data/sql/005_geocode_tracking.sql @@ -11,8 +11,12 @@ -- Идемпотентно (ADD COLUMN IF NOT EXISTS) — безопасно при каждом деплое. 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 ON listings (geocode_tried_at NULLS FIRST) WHERE lat IS NULL; +CREATE INDEX IF NOT EXISTS deals_geocode_pending_idx + ON deals (geocode_tried_at NULLS FIRST) + WHERE lat IS NULL; diff --git a/tradein-mvp/deploy/cron-scrape.sh b/tradein-mvp/deploy/cron-scrape.sh index f71767bb..2b9398eb 100755 --- a/tradein-mvp/deploy/cron-scrape.sh +++ b/tradein-mvp/deploy/cron-scrape.sh @@ -71,13 +71,26 @@ done # Геокодинг свежих лотов без координат (Cian/N1 отдают только адрес). # Чанками по 100 (~200с/чанк, под таймаутом), пока remaining > 0. # Макс 15 чанков = 1500 адресов. geocode_tried_at не даёт зациклиться. -log "→ geocode-missing (чанками)" +log "→ geocode-missing listings (чанками)" for gi in $(seq 1 15); do 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" \ -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:-0}" -eq 0 ] && break sleep 2 -- 2.45.3