fix(sf-15): extract obj_class from aiDescription (DOM.RF list API never returns objClass field)
Add _extract_obj_class_from_ai() with regex patterns for Комфорт/Бизнес/Премиум/Элит/Стандарт. _norm_object() uses it as fallback; objClass from API still takes priority if ever returned. Add backfill SQL 105 to retroactively populate existing rows from raw JSON payloads. Coverage: ~1806/4548 rows will be filled; remainder NULL is expected (Нежилое/no AI desc).
This commit is contained in:
parent
f0589bcdff
commit
3daf2a1fb1
2 changed files with 113 additions and 2 deletions
|
|
@ -115,6 +115,36 @@ def _to_date(v: Any) -> date | None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
_OBJ_CLASS_PATTERNS: list[tuple[str, str]] = [
|
||||||
|
# Ordered from most to least specific to avoid 'бизнес' matching inside longer phrases.
|
||||||
|
# Each tuple: (regex pattern, canonical class name)
|
||||||
|
(r"элит", "Элит"),
|
||||||
|
(r"бизнес", "Бизнес"),
|
||||||
|
(r"премиум", "Премиум"),
|
||||||
|
(r"комфорт", "Комфорт"),
|
||||||
|
(r"стандарт|эконом", "Стандарт"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_obj_class_from_ai(ai_description: str | None) -> str | None:
|
||||||
|
"""Извлечь класс жилья из AI-описания DOM.РФ.
|
||||||
|
|
||||||
|
DOM.РФ не отдаёт `objClass` в /kn/object list — поле всегда NULL.
|
||||||
|
Однако `aiDescription` содержит текстовое упоминание класса ('комфорт-класса',
|
||||||
|
'«Комфорт»', 'класс «Бизнес»', 'Класс объекта — комфорт' и т.д.).
|
||||||
|
|
||||||
|
Возвращает каноническое название или None если описание пустое / класс не найден.
|
||||||
|
"""
|
||||||
|
import re
|
||||||
|
|
||||||
|
if not ai_description:
|
||||||
|
return None
|
||||||
|
for pattern, class_name in _OBJ_CLASS_PATTERNS:
|
||||||
|
if re.search(pattern, ai_description, re.IGNORECASE):
|
||||||
|
return class_name
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _problem_text(v: Any) -> str | None:
|
def _problem_text(v: Any) -> str | None:
|
||||||
"""Coerce problem flag (sometimes int 0/1, sometimes text) to text or None."""
|
"""Coerce problem flag (sometimes int 0/1, sometimes text) to text or None."""
|
||||||
if v is None:
|
if v is None:
|
||||||
|
|
@ -161,12 +191,25 @@ def _norm_object(row: dict[str, Any], region_cd: int | None = None) -> dict[str,
|
||||||
rpdRegionCd, objAddr, shortAddr, objCommercNm, objFloorMin, objFloorMax,
|
rpdRegionCd, objAddr, shortAddr, objCommercNm, objFloorMin, objFloorMax,
|
||||||
objElemLivingCnt, objSquareLiving, objReady100PercDt, objClass, latitude, longitude,
|
objElemLivingCnt, objSquareLiving, objReady100PercDt, objClass, latitude, longitude,
|
||||||
objProblemFlg, problemFlag, siteStatus, objGreenHouseFlg, objGuarantyEscrowFlg,
|
objProblemFlg, problemFlag, siteStatus, objGreenHouseFlg, objGuarantyEscrowFlg,
|
||||||
objStatus, freeFlatsInfo{priceMin, numberFlats}.
|
objStatus, aiDescription, freeFlatsInfo{priceMin, numberFlats}.
|
||||||
|
|
||||||
|
Note: DOM.РФ /kn/object list endpoint never populates `objClass` in API responses
|
||||||
|
(field absent from payload). Class is extracted from `aiDescription` text as fallback.
|
||||||
"""
|
"""
|
||||||
dev = row.get("developer") if isinstance(row.get("developer"), dict) else {}
|
dev = row.get("developer") if isinstance(row.get("developer"), dict) else {}
|
||||||
company_group = _g(dev, "companyGroup") if dev else None
|
company_group = _g(dev, "companyGroup") if dev else None
|
||||||
# Our DB convention: dev_id="<companyGroup>_0" matches v_developer_full_metrics.
|
# Our DB convention: dev_id="<companyGroup>_0" matches v_developer_full_metrics.
|
||||||
dev_id = f"{company_group}_0" if company_group else None
|
dev_id = f"{company_group}_0" if company_group else None
|
||||||
|
|
||||||
|
# objClass is absent from the list endpoint — extract from aiDescription instead.
|
||||||
|
obj_class = _g(row, "objClass") or _extract_obj_class_from_ai(_g(row, "aiDescription"))
|
||||||
|
if obj_class is None:
|
||||||
|
obj_id = _g(row, "objId", "obj_id", "id")
|
||||||
|
logger.debug(
|
||||||
|
"obj_class not found for obj_id=%s (no objClass field, no aiDescription class match)",
|
||||||
|
obj_id,
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"obj_id": _g(row, "objId", "obj_id", "id"),
|
"obj_id": _g(row, "objId", "obj_id", "id"),
|
||||||
"hobj_id": _g(row, "hobjId", "hobj_id"),
|
"hobj_id": _g(row, "hobjId", "hobj_id"),
|
||||||
|
|
@ -186,7 +229,7 @@ def _norm_object(row: dict[str, Any], region_cd: int | None = None) -> dict[str,
|
||||||
"site_status": _g(row, "siteStatus"),
|
"site_status": _g(row, "siteStatus"),
|
||||||
"green_house": _to_bool(_g(row, "objGreenHouseFlg")),
|
"green_house": _to_bool(_g(row, "objGreenHouseFlg")),
|
||||||
"escrow": _to_bool(_g(row, "objGuarantyEscrowFlg")),
|
"escrow": _to_bool(_g(row, "objGuarantyEscrowFlg")),
|
||||||
"obj_class": _g(row, "objClass"),
|
"obj_class": obj_class,
|
||||||
"wall_type": _g(row, "wallType"),
|
"wall_type": _g(row, "wallType"),
|
||||||
"energy_eff": _g(row, "energyEff"),
|
"energy_eff": _g(row, "energyEff"),
|
||||||
"latitude": _g(row, "latitude"),
|
"latitude": _g(row, "latitude"),
|
||||||
|
|
|
||||||
68
data/sql/105_backfill_obj_class_from_ai_description.sql
Normal file
68
data/sql/105_backfill_obj_class_from_ai_description.sql
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
-- 105_backfill_obj_class_from_ai_description.sql
|
||||||
|
--
|
||||||
|
-- Backfill obj_class for existing domrf_kn_objects rows using aiDescription
|
||||||
|
-- from the raw domrf_raw_endpoints payloads.
|
||||||
|
--
|
||||||
|
-- Root cause: DOM.РФ /kn/object list endpoint never returns `objClass` field
|
||||||
|
-- (absent from API payload). Class text is embedded in `aiDescription`.
|
||||||
|
-- Fix in scraper (domrf_kn.py _norm_object) extracts class on next sweep.
|
||||||
|
-- This script retroactively populates existing rows from stored raw payloads.
|
||||||
|
--
|
||||||
|
-- Coverage after backfill (region 66, snapshot 2026-05-05):
|
||||||
|
-- Комфорт: ~782, Бизнес: ~88, Премиум: ~13, Элит: ~12
|
||||||
|
-- class_not_found (нежилое / нет AI-описания): ~621 — remains NULL (expected)
|
||||||
|
--
|
||||||
|
-- Idempotent: only updates WHERE obj_class IS NULL. Safe to re-run.
|
||||||
|
|
||||||
|
BEGIN;
|
||||||
|
|
||||||
|
UPDATE domrf_kn_objects AS o
|
||||||
|
SET obj_class = CASE
|
||||||
|
WHEN raw_obj ->> 'aiDescription' ~* 'элит' THEN 'Элит'
|
||||||
|
WHEN raw_obj ->> 'aiDescription' ~* 'бизнес' THEN 'Бизнес'
|
||||||
|
WHEN raw_obj ->> 'aiDescription' ~* 'премиум' THEN 'Премиум'
|
||||||
|
WHEN raw_obj ->> 'aiDescription' ~* 'комфорт' THEN 'Комфорт'
|
||||||
|
WHEN raw_obj ->> 'aiDescription' ~* 'стандарт|эконом' THEN 'Стандарт'
|
||||||
|
ELSE NULL
|
||||||
|
END
|
||||||
|
FROM (
|
||||||
|
-- Unnest the raw JSON array payload; each element is one object row.
|
||||||
|
SELECT
|
||||||
|
(raw_obj ->> 'objId')::bigint AS obj_id,
|
||||||
|
e.snapshot_date,
|
||||||
|
raw_obj
|
||||||
|
FROM domrf_raw_endpoints AS e,
|
||||||
|
jsonb_array_elements(e.payload) AS raw_obj
|
||||||
|
WHERE e.section = 'kn_api'
|
||||||
|
AND jsonb_typeof(e.payload) = 'array'
|
||||||
|
AND raw_obj ->> 'aiDescription' IS NOT NULL
|
||||||
|
) AS src
|
||||||
|
WHERE o.obj_id = src.obj_id
|
||||||
|
AND o.snapshot_date = src.snapshot_date
|
||||||
|
AND o.obj_class IS NULL
|
||||||
|
-- Only update when we can actually extract a class
|
||||||
|
AND (
|
||||||
|
src.raw_obj ->> 'aiDescription' ~* 'элит'
|
||||||
|
OR src.raw_obj ->> 'aiDescription' ~* 'бизнес'
|
||||||
|
OR src.raw_obj ->> 'aiDescription' ~* 'премиум'
|
||||||
|
OR src.raw_obj ->> 'aiDescription' ~* 'комфорт'
|
||||||
|
OR src.raw_obj ->> 'aiDescription' ~* 'стандарт|эконом'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Report result
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
v_total bigint;
|
||||||
|
v_null bigint;
|
||||||
|
v_filled bigint;
|
||||||
|
BEGIN
|
||||||
|
SELECT COUNT(*), COUNT(*) FILTER (WHERE obj_class IS NULL),
|
||||||
|
COUNT(*) FILTER (WHERE obj_class IS NOT NULL)
|
||||||
|
INTO v_total, v_null, v_filled
|
||||||
|
FROM domrf_kn_objects;
|
||||||
|
|
||||||
|
RAISE NOTICE 'domrf_kn_objects after backfill: total=% filled=% still_null=%',
|
||||||
|
v_total, v_filled, v_null;
|
||||||
|
END$$;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
Loading…
Add table
Reference in a new issue