fix(tradein/ui): городской фильтр в витрине «сделки против объявлений» (#2583 H4) #2627
No reviewers
Labels
No labels
Fable 5 ревью
GG-форсайт
admin
analytics
auth
automation
bug
business
chore
ci
compliance
data
data-moat
docs
duplicate
dx
enhancement
feedback/max
generative
needs-discussion
needs-human
observability
pause-bots
performance
priority/p0
priority/p1
priority/p2
priority/p3
scope/backend
scope/db
scope/devops
scope/frontend
scope/qa
scrapers
security
site-finder
stage/1
stage/2
status/blocked
status/done
status/needs-analysis
status/needs-fix
status/qa
status/ready
status/review
status/wip
tech-debt
tradein
ux
week ревью 1
wontfix
ИРД
вторичка
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: lekss361/gendesign#2627
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "fix/tradein-sales-vs-listings-city"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
street_sales_vs_listings()(data/sql/067_v_street_sales_vs_listings.sql) строила пары ДКП-сделка/объявление только поstreet_pattern ILIKE+ rooms + area + дата — без корреляции города.deals.address/listings.addressхранят"<Город>, <Улица>", а street_pattern — голое имя улицы, которое одинаково матчит одноимённые улицы разных городов обл.66 ("Ленина", "Красноармейская", ...)./street-deals(#C1,trade_in.py:1717):target_cityрезолвится через_resolve_target_city(address)(тот же словарь ~30 городов обл.66) и передаётся в TVF седьмым параметромp_target_city text DEFAULT NULL.LOWER(d.city) = LOWER(p_target_city)) —deals.cityзаполнена на 100% (прод-замер). listings-сторона терпима кNULL(l.city IS NULL OR LOWER(l.city) = LOWER(p_target_city)) —listings.cityзаполнена частично (прод-замер: avito 63%, yandex 19%, cian 4.6%, domklik 0.6%, n1 0%), симметрично уже принятому паттернуasking_to_sold_ratio.py(#2583 H2, PR #2617).p_target_city IS NULL(адрес вне словаря городов, известная H1) → фильтр не применяется ни на одной стороне — тот же fallback, что уже принят в/street-deals, для консистентности между соседними виджетами на одной странице.Три решения (с обоснованием)
_resolve_target_city(address)— идентично/street-deals. Для адресов вне словаря (~30 городов обл.66) возвращаетNone→ фильтр не применяется (текущее поведение сохраняется как fallback). Альтернатива (пустой результат приNone) отклонена: создала бы расхождение с соседним/street-dealsдля одного и того же адреса на одной странице — хуже, чем редкий edge-case без фильтра. H1 (список городов) — отдельная известная находка, чинится отдельным PR.NULL(city заполнена частично: avito 63%, yandex 19%, cian 4.6%, domklik 0.6%, n1 0% — прод-замер ниже). Строгий фильтр безIS NULLвыбросил бы ~80-95% listings кроме avito.trade_in.py:1865.p_target_cityдобавлен СЕДЬМЫМ параметром сDEFAULT NULL. Т.к.CREATE OR REPLACE FUNCTIONс добавленным параметром создаёт НОВУЮ перегрузку (Postgres матчит по списку типов аргументов), миграция явно дропает старую 6-арг сигнатуру (DROP FUNCTION IF EXISTS ...— идемпотентно) ПЕРЕДCREATE OR REPLACEс новой. Convenience viewv_street_sales_vs_listingsиз 067 уже дропнута миграцией 068 (была без street-match, генерила 50k spurious pairs) — фиксить нечего.Прод-замер (read-only,
Нижний Тагил+Ленина, rooms=2, area≈44.3м², defaults)Шире (street="Ленина" по ВСЕМ городам обл.66 с этим именем улицы, rooms=2, area≈44.3): 352 total pairs, 244 с listing-match, из них 119 (48.8%) явно чужого города + 122 (50%) NULL-city (Циан/Домклик/Яндекс) — только ~10% подтверждённо того же города. median_discount_pct на смеси городов = -63.6% (тот же порядок, что заявленные в находке -59%; точное число зависит от конкретного адреса/выборки исходного аудита, которую не удалось восстановить бит-в-бит — воспроизведена качественно та же картина: подавляющее большинство пар — чужой город, median сильно завышен по модулю).
Контроль (Екатеринбург, та же улица/rooms/area): BEFORE и AFTER идентичны — 14 total, 7 with_listing, 0 explicit cross-city, 4 NULL-city, median +11.11% — без деградации (у ЕКБ и так не было явных cross-city, avito доминирует и покрывает EKB лучше всего).
EXPLAIN(без ANALYZE): city-предикат добавляется в существующийFilterна тех же Bitmap Heap Scan (поdeals_address_trgm_idx/deals_rooms_area_idx/listings_address_trgm_idx) — не меняет стратегию join, cost практически не меняется (2404→2383, в пределах шума оценки планировщика). Индекса наlistings.cityнет, но он не нужен: фильтр применяется постфактум на уже отобранных ILIKE-строках.Test plan
tests/test_migration_205_sales_vs_listings_city_filter.py— новый: транзакционность, DROP FUNCTION (старая сигнатура) идемпотентно, новая 7-арг сигнатура сp_target_city DEFAULT NULL, city-предикаты на обеих сторонах (deals строго / listings терпимо к NULL), отсутствие CAST-ловушки psycopg, RETURNS TABLE не менялся.tests/test_sales_vs_listings.py— 2 новых теста:target_cityрезолвится и прокидывается для распознанного города;target_city=Noneдля адреса вне словаря (H1 fallback).pytestвtradein-mvp/backend: 3112 passed, 9 skipped, 1 known pre-existing fail (test_search_api.py::test_search_cache_hit, 401 RBAC — не связан с этим PR, не чинится по заданию).Refs #2583