[high] obj_class_filter (англ.) сравнивается с русским obj_class в БД — фильтр класса всегда даёт 0 конкурентов (competitors + best_layouts) #1340

Closed
opened 2026-06-14 18:08:41 +00:00 by bot-backend · 1 comment
Collaborator

Описание

При любом выбранном фильтре класса объекта (obj_class_filter) пользователь получает пустой список конкурентов в обоих модулях Site Finder. CompetitorsRequest.obj_class_filter и BestLayoutsRequest.obj_class_filter имеют тип Literal['economy','comfort','business'] (schemas/parcel.py:171,311) — английские значения. В БД domrf_kn_objects.obj_class scraper пишет каноническое РУССКОЕ значение ('Элит'/'Бизнес'/'Премиум'/'Комфорт'/'Стандарт'/'Типовой' — domrf_kn.py:158-172 _OBJ_CLASS_PATTERNS, backfill 106_*.sql). SQL делает точное равенство, поэтому 'comfort' = 'Комфорт' всегда false.

  • competitors.py: фильтр выключается только при obj_class_filter IS NULL; иначе результат пуст.
  • best_layouts.py: тот же пустой фильтр обнуляет all_obj_ids → возвращается _empty_response ("конкуренты не найдены"), хотя данные есть.

Корень

Единый корень — отсутствие нормализации EN→RU перед передачей параметра в SQL. В competitors.py уже есть _normalize_class/_CLASS_SYNONYMS (строки 116-127, 130-154), решающий ровно эту проблему, но он вызывается ТОЛЬКО для скоринга class_similarity/relevance_weight (строка 615), а НЕ перед db.execute. best_layouts.py использует тот же словарь только для скоринга выхода (строки 181, 249), но не для входного фильтра. Хендлер get_parcel_competitors (parcels.py:3142) передаёт body без перевода. Промежуточного слоя нормализации (validator/middleware) нет.

Evidence

competitors.py:422-426:

WHERE d.distance_m <= CAST(:radius_m AS float)
  AND (
      CAST(:obj_class_filter AS text) IS NULL
      OR d.obj_class = CAST(:obj_class_filter AS text)
  )
-- :obj_class_filter ∈ {'economy','comfort','business'}, d.obj_class ∈ {'Комфорт','Бизнес',...}

Параметр передаётся как есть на competitors.py:530 ("obj_class_filter": request.obj_class_filter).

best_layouts.py:100-103:

AND (
    CAST(:obj_class_filter AS text) IS NULL
    OR obj_class = CAST(:obj_class_filter AS text)
)
-- request.obj_class_filter='comfort', obj_class='Комфорт' → false

Параметр передаётся как есть на best_layouts.py:371.

Suggested fix

Нормализовать класс перед сравнением: маппить английский фильтр в набор русских канонических значений (например economy→{Стандарт,Эконом}, comfort→{Комфорт,Комфорт+}, business→{Бизнес,Бизнес+,Премиум,Элит} — границы на усмотрение продукта) и сравнивать через lower(obj_class) = ANY(:classes) или IN (...). Переиспользовать существующий словарь _CLASS_SYNONYMS из competitors.py в обоих модулях. Минимально: построить set русских значений для каждого фильтра и передавать как ANY(:classes).

Обоснование severity (high)

Два независимых пользовательских пути Site Finder полностью слепнут при включённом фильтре класса — ключевая аналитика конкурентов (и подбор лучших планировок) возвращает пустоту при наличии данных. Confidence обеих находок 0.97, путь достижим в проде на каждом запросе с непустым фильтром.


week ревью 1 · multi-agent аудит 2026-06-14 · severity=high · category=logic

Затронутые файлы:

  • /Users/anton/Птица/gendesign/backend/app/services/site_finder/competitors.py
  • /Users/anton/Птица/gendesign/backend/app/services/site_finder/best_layouts.py
## Описание При любом выбранном фильтре класса объекта (`obj_class_filter`) пользователь получает пустой список конкурентов в обоих модулях Site Finder. `CompetitorsRequest.obj_class_filter` и `BestLayoutsRequest.obj_class_filter` имеют тип `Literal['economy','comfort','business']` (schemas/parcel.py:171,311) — английские значения. В БД `domrf_kn_objects.obj_class` scraper пишет каноническое РУССКОЕ значение (`'Элит'`/`'Бизнес'`/`'Премиум'`/`'Комфорт'`/`'Стандарт'`/`'Типовой'` — domrf_kn.py:158-172 `_OBJ_CLASS_PATTERNS`, backfill 106_*.sql). SQL делает точное равенство, поэтому `'comfort' = 'Комфорт'` всегда false. - **competitors.py**: фильтр выключается только при `obj_class_filter IS NULL`; иначе результат пуст. - **best_layouts.py**: тот же пустой фильтр обнуляет `all_obj_ids` → возвращается `_empty_response` ("конкуренты не найдены"), хотя данные есть. ## Корень Единый корень — отсутствие нормализации EN→RU перед передачей параметра в SQL. В `competitors.py` уже есть `_normalize_class`/`_CLASS_SYNONYMS` (строки 116-127, 130-154), решающий ровно эту проблему, но он вызывается ТОЛЬКО для скоринга `class_similarity`/relevance_weight (строка 615), а НЕ перед `db.execute`. `best_layouts.py` использует тот же словарь только для скоринга выхода (строки 181, 249), но не для входного фильтра. Хендлер `get_parcel_competitors` (parcels.py:3142) передаёт body без перевода. Промежуточного слоя нормализации (validator/middleware) нет. ## Evidence `competitors.py:422-426`: ```sql WHERE d.distance_m <= CAST(:radius_m AS float) AND ( CAST(:obj_class_filter AS text) IS NULL OR d.obj_class = CAST(:obj_class_filter AS text) ) -- :obj_class_filter ∈ {'economy','comfort','business'}, d.obj_class ∈ {'Комфорт','Бизнес',...} ``` Параметр передаётся как есть на competitors.py:530 (`"obj_class_filter": request.obj_class_filter`). `best_layouts.py:100-103`: ```sql AND ( CAST(:obj_class_filter AS text) IS NULL OR obj_class = CAST(:obj_class_filter AS text) ) -- request.obj_class_filter='comfort', obj_class='Комфорт' → false ``` Параметр передаётся как есть на best_layouts.py:371. ## Suggested fix Нормализовать класс перед сравнением: маппить английский фильтр в набор русских канонических значений (например economy→{Стандарт,Эконом}, comfort→{Комфорт,Комфорт+}, business→{Бизнес,Бизнес+,Премиум,Элит} — границы на усмотрение продукта) и сравнивать через `lower(obj_class) = ANY(:classes)` или `IN (...)`. Переиспользовать существующий словарь `_CLASS_SYNONYMS` из competitors.py в обоих модулях. Минимально: построить set русских значений для каждого фильтра и передавать как `ANY(:classes)`. ## Обоснование severity (high) Два независимых пользовательских пути Site Finder полностью слепнут при включённом фильтре класса — ключевая аналитика конкурентов (и подбор лучших планировок) возвращает пустоту при наличии данных. Confidence обеих находок 0.97, путь достижим в проде на каждом запросе с непустым фильтром. --- _week ревью 1 · multi-agent аудит 2026-06-14 · severity=**high** · category=`logic`_ Затронутые файлы: - `/Users/anton/Птица/gendesign/backend/app/services/site_finder/competitors.py` - `/Users/anton/Птица/gendesign/backend/app/services/site_finder/best_layouts.py`
bot-backend added the
week ревью 1
label 2026-06-14 18:08:41 +00:00
bot-backend added the
scope/backend
label 2026-06-15 11:01:49 +00:00
Owner

🔍 week ревью 1 — верификация (ПОДТВЕРЖДЕНО)

Текущее состояние кода: competitors.py:422-426 SQL сравнивает d.obj_class = CAST(:obj_class_filter AS text); параметр передаётся как есть на competitors.py:530 ("obj_class_filter": request.obj_class_filter). best_layouts.py:101-102 — тот же obj_class = CAST(:obj_class_filter AS text), параметр на best_layouts.py:368. Тип фильтра английский: schemas/parcel.py:176 ObjClassFilter = Literal["economy","comfort","business"] (competitors), :249 Literal["economy","comfort","business"] (best_layouts). В БД domrf_kn_objects.obj_class — русские значения ('Комфорт', 'Бизнес', ...).

Корень: Нормализация EN→RU не применяется к входному фильтру. _normalize_class + _CLASS_SYNONYMS существуют в competitors.py:116-154 (economy→эконом, comfort→комфорт, business→бизнес, ...), но вызываются только в скоринге (competitors.py:615 _class_similarity, best_layouts.py скоринг выхода) — не перед db.execute. Итог: 'comfort' = 'Комфорт' всегда false → пустой список конкурентов / _empty_response.

Что править:

  • Построить set русских канонических значений на каждый EN-фильтр (границы класса согласовать с продуктом — economy→{Стандарт,Эконом}, comfort→{Комфорт,Комфорт+}, business→{Бизнес,Бизнес+,Премиум,Элит}).
  • Заменить точное равенство на lower(d.obj_class) = ANY(:classes) / IN (...); передавать список вместо одиночной строки.
  • Переиспользовать _CLASS_SYNONYMS из competitors.py в обоих модулях (вынести в общий хелпер).

Что посмотреть:

  • Код: competitors.py (SQL :422-426, bind :530, _normalize_class:130, _CLASS_SYNONYMS:116), best_layouts.py (SQL :101-102, bind :368), schemas/parcel.py:176,249. Добавить тест с непустым obj_class_filter + RU obj_class в БД (сейчас покрытия нет).
  • Vault: релевантных заметок не найдено (поиск «obj_class фильтр класса competitors» дал только Endpoint_Parcel_Analyze без привязки).
**🔍 week ревью 1 — верификация (ПОДТВЕРЖДЕНО)** **Текущее состояние кода:** `competitors.py:422-426` SQL сравнивает `d.obj_class = CAST(:obj_class_filter AS text)`; параметр передаётся как есть на `competitors.py:530` (`"obj_class_filter": request.obj_class_filter`). `best_layouts.py:101-102` — тот же `obj_class = CAST(:obj_class_filter AS text)`, параметр на `best_layouts.py:368`. Тип фильтра английский: `schemas/parcel.py:176` `ObjClassFilter = Literal["economy","comfort","business"]` (competitors), `:249` `Literal["economy","comfort","business"]` (best_layouts). В БД `domrf_kn_objects.obj_class` — русские значения ('Комфорт', 'Бизнес', ...). **Корень:** Нормализация EN→RU не применяется к входному фильтру. `_normalize_class` + `_CLASS_SYNONYMS` существуют в `competitors.py:116-154` (economy→эконом, comfort→комфорт, business→бизнес, ...), но вызываются только в скоринге (`competitors.py:615` `_class_similarity`, `best_layouts.py` скоринг выхода) — не перед `db.execute`. Итог: `'comfort' = 'Комфорт'` всегда false → пустой список конкурентов / `_empty_response`. **Что править:** - Построить set русских канонических значений на каждый EN-фильтр (границы класса согласовать с продуктом — economy→{Стандарт,Эконом}, comfort→{Комфорт,Комфорт+}, business→{Бизнес,Бизнес+,Премиум,Элит}). - Заменить точное равенство на `lower(d.obj_class) = ANY(:classes)` / `IN (...)`; передавать список вместо одиночной строки. - Переиспользовать `_CLASS_SYNONYMS` из `competitors.py` в обоих модулях (вынести в общий хелпер). **Что посмотреть:** - Код: `competitors.py` (SQL `:422-426`, bind `:530`, `_normalize_class:130`, `_CLASS_SYNONYMS:116`), `best_layouts.py` (SQL `:101-102`, bind `:368`), `schemas/parcel.py:176,249`. Добавить тест с непустым `obj_class_filter` + RU obj_class в БД (сейчас покрытия нет). - Vault: релевантных заметок не найдено (поиск «obj_class фильтр класса competitors» дал только `Endpoint_Parcel_Analyze` без привязки).
Sign in to join this conversation.
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#1340
No description provided.