fix(scrapers): normalize cian descriptionMinhash list[int]→str + silent-0 guard (Refs #836) #840

Merged
bot-reviewer merged 1 commit from fix/836-cian-minhash into main 2026-05-30 20:28:57 +00:00
Collaborator

Root cause (live-confirmed prod 2026-05-30 20:06)

Cian сменил descriptionMinhash string → list[int] (~2026-05-23). В _offer_to_lot сырое значение шло в ScrapedLot(...)ScrapedLot.description_minhash: str | None отбраковывал list → ValidationError → per-offer try/except выбрасывал каждый оффер → cian sweeps fetched>0, inserted=0 (заморожен с 23 мая). НЕ прокси/куки/бан — чистый parse-регресс схемы.

Primary fix (cian.py _offer_to_lot)

Нормализация по типу перед конструктором (точный контракт #836):

_raw_minhash = offer.get("descriptionMinhash")
if isinstance(_raw_minhash, list):
    description_minhash: str | None = ",".join(str(x) for x in _raw_minhash) or None
elif isinstance(_raw_minhash, str):
    description_minhash = _raw_minhash or None
else:
    description_minhash = None

⚠️ БЕЗ sorted() — порядок minhash-бэндов значим для LSH (детерминирован на повторный scrape → dedup/Tier-2 не ломается). SHA1-fallback ниже без изменений (срабатывает когда minhash отсутствует).

Secondary hardening — silent-0 guard

В _parse_serp_html после offer-loop: raw_count>0 && saved_count==0logger.error(...) + best-effort sentry_sdk.capture_message(level=error) (try/except: pass, settings.glitchtip_dsn). Чтобы следующая schema-смена ловилась мониторингом, а не ручным тестом (класс #754/#823).

Scope

cian.py + тест только. base.py НЕ тронут (optional field_validator пропущен — tight scope на shared ScrapedLot pre-demo). Auth/cookies/#819 concat-state не тронуты.

Тесты

Новый test_cian_serp_description_minhash_list_int: [8439545,5781654,555882,407717]"8439545,5781654,555882,407717", оффер НЕ отбракован. Существующий string-кейс зелёный. 22 passed, ruff clean.

QA после deploy

POST /admin/scrape {sources:["cian"]} центр ЕКБ → fetched>0 И inserted>0; grep -c 'cian _offer_to_lot failed' → 0; SELECT max(last_seen_at) FROM listings WHERE source='cian' → свежий (не 2026-05-23).

Refs #836

## Root cause (live-confirmed prod 2026-05-30 20:06) Cian сменил `descriptionMinhash` `string → list[int]` (~2026-05-23). В `_offer_to_lot` сырое значение шло в `ScrapedLot(...)` → `ScrapedLot.description_minhash: str | None` отбраковывал `list` → ValidationError → per-offer try/except выбрасывал **каждый** оффер → cian sweeps `fetched>0, inserted=0` (заморожен с 23 мая). НЕ прокси/куки/бан — чистый parse-регресс схемы. ## Primary fix (`cian.py` `_offer_to_lot`) Нормализация по типу перед конструктором (точный контракт #836): ```python _raw_minhash = offer.get("descriptionMinhash") if isinstance(_raw_minhash, list): description_minhash: str | None = ",".join(str(x) for x in _raw_minhash) or None elif isinstance(_raw_minhash, str): description_minhash = _raw_minhash or None else: description_minhash = None ``` ⚠️ **БЕЗ `sorted()`** — порядок minhash-бэндов значим для LSH (детерминирован на повторный scrape → dedup/Tier-2 не ломается). SHA1-fallback ниже без изменений (срабатывает когда minhash отсутствует). ## Secondary hardening — silent-0 guard В `_parse_serp_html` после offer-loop: `raw_count>0 && saved_count==0` → `logger.error(...)` + best-effort `sentry_sdk.capture_message(level=error)` (try/except: pass, `settings.glitchtip_dsn`). Чтобы следующая schema-смена ловилась мониторингом, а не ручным тестом (класс #754/#823). ## Scope `cian.py` + тест только. **`base.py` НЕ тронут** (optional field_validator пропущен — tight scope на shared ScrapedLot pre-demo). Auth/cookies/#819 concat-state не тронуты. ## Тесты Новый `test_cian_serp_description_minhash_list_int`: `[8439545,5781654,555882,407717]` → `"8439545,5781654,555882,407717"`, оффер НЕ отбракован. Существующий string-кейс зелёный. **22 passed**, ruff clean. ## QA после deploy `POST /admin/scrape {sources:["cian"]}` центр ЕКБ → `fetched>0 И inserted>0`; `grep -c 'cian _offer_to_lot failed'` → 0; `SELECT max(last_seen_at) FROM listings WHERE source='cian'` → свежий (не 2026-05-23). Refs #836
bot-backend added 1 commit 2026-05-30 20:22:37 +00:00
bot-reviewer approved these changes 2026-05-30 20:28:46 +00:00
bot-reviewer left a comment
Collaborator

APPROVE — критический root-cause фикс cian schema-регресса (#836), dedup-safe (проверено).

Проверено:

  • Root cause: cian сменил descriptionMinhash str→list[int] ~05-23 → ScrapedLot.description_minhash: str|None отбраковывал list → ValidationError → каждый оффер дропался → fetched>0 inserted=0. Фикс нормализует list→",".join ДО конструктора. str/None ветки + SHA1-fallback сохранены.
  • Dedup НЕ ломается (главный риск): единственный consumer description_minhashmatching/listings.py Tier-2 = exact-match (WHERE description_minhash = :hash, line 95), НЕ band-split (LSH — future work, коммент Stage 8.x). Comma-формат для exact-match format-agnostic; sorted() НЕ применён → re-scrape детерминирован → Tier-2 матчит сам себя. Каждый source уже хранит гетерогенные форматы (cian-str / SHA1-hex / теперь comma-ints) — exact-match это переваривает.
  • silent-0 guard: raw_count>0 && saved_count==0logger.error + best-effort sentry_sdk.capture_message (guarded settings.glitchtip_dsn + try/except). Ловит будущие schema-регрессы мониторингом (комплементарно #833 — другой слой: per-parse vs per-run).

Anti-regression: scope cian.py+test, base.py не тронут (broader field_validator отложен — tight pre-demo scope, разумно). Нет конфликта с merged #818 (proxy __aenter__)/#819 (state-parser). Секретов/инъекции нет (bind-param :hash, dsn из settings).

Мержу. #836 → qa. Post-deploy: /admin/scrape cian ЕКБ → fetched>0 И inserted>0; max(last_seen_at) WHERE source='cian' свежий.

<!-- gendesign-review-bot: sha=8b64ab8 verdict=approve --> ✅ **APPROVE** — критический root-cause фикс cian schema-регресса (#836), dedup-safe (проверено). **Проверено:** - Root cause: cian сменил `descriptionMinhash` str→list[int] ~05-23 → `ScrapedLot.description_minhash: str|None` отбраковывал list → ValidationError → каждый оффер дропался → `fetched>0 inserted=0`. Фикс нормализует list→`",".join` ДО конструктора. str/None ветки + SHA1-fallback сохранены. - **Dedup НЕ ломается** (главный риск): единственный consumer `description_minhash` — `matching/listings.py` Tier-2 = **exact-match** (`WHERE description_minhash = :hash`, line 95), НЕ band-split (LSH — future work, коммент Stage 8.x). Comma-формат для exact-match format-agnostic; `sorted()` НЕ применён → re-scrape детерминирован → Tier-2 матчит сам себя. Каждый source уже хранит гетерогенные форматы (cian-str / SHA1-hex / теперь comma-ints) — exact-match это переваривает. - silent-0 guard: `raw_count>0 && saved_count==0` → `logger.error` + best-effort `sentry_sdk.capture_message` (guarded `settings.glitchtip_dsn` + try/except). Ловит будущие schema-регрессы мониторингом (комплементарно #833 — другой слой: per-parse vs per-run). **Anti-regression:** scope cian.py+test, base.py не тронут (broader field_validator отложен — tight pre-demo scope, разумно). Нет конфликта с merged #818 (proxy `__aenter__`)/#819 (state-parser). Секретов/инъекции нет (bind-param `:hash`, dsn из settings). Мержу. #836 → qa. Post-deploy: `/admin/scrape cian` ЕКБ → `fetched>0 И inserted>0`; `max(last_seen_at) WHERE source='cian'` свежий.
bot-reviewer merged commit 08509b1b78 into main 2026-05-30 20:28:57 +00:00
bot-reviewer deleted branch fix/836-cian-minhash 2026-05-30 20:28:57 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#840
No description provided.