All checks were successful
CI / changes (push) Successful in 7s
CI / frontend-tests (push) Has been skipped
CI / changes (pull_request) Successful in 6s
CI / frontend-tests (pull_request) Has been skipped
CI / backend-tests (push) Successful in 6m31s
CI / backend-tests (pull_request) Successful in 6m29s
Deploy / changes (push) Successful in 5s
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Successful in 2m16s
Deploy / build-worker (push) Successful in 3m34s
Deploy / deploy (push) Successful in 1m28s
P0 goldmine: objective_lots (1.12M, 81% цен) связывается с domrf для velocity/ layout-аналитики. Mapping было 148 → +115 (107 high + 8 mid) = 263. Fuzzy-match objective(project_name) ↔ domrf_kn_objects(comm_name) LATEST snapshot region_cd=66 через pg_trgm на НОРМАЛИЗОВАННЫХ именах (strip ЖК-boilerplate с \m\M word-boundary). Правило приёмки dev-corroborated: nsim≥0.62 AND dsim≥0.5 — precision over recall для P0-цен (generic-имена с разным застройщиком, напр. Солнечный/Прекрасный/Стрелки dsim=0.0, отклонены). match_method='auto_fuzzy_v2', is_reviewed=false (для human-review топ-кандидатов), confidence-tier в note. Idempotent ON CONFLICT (objective_complex_name, objective_group). REFRESH CONCURRENTLY mv_layout_velocity после COMMIT. Refs #307
230 lines
14 KiB
PL/PgSQL
230 lines
14 KiB
PL/PgSQL
-- 150_objective_mapping_backfill.sql
|
||
-- Issue #307 OBJ-2 (P0 objective goldmine) — massive fuzzy backfill of
|
||
-- objective_complex_mapping so Site Finder velocity/layout analytics covers a
|
||
-- much larger share of Objective prices.
|
||
--
|
||
-- CONTEXT
|
||
-- objective_complex_mapping связывает Objective-ЖК (objective_complex_name)
|
||
-- c domrf_kn_objects.obj_id. До этой миграции — 148 строк (все is_reviewed=true,
|
||
-- objective_group='Екатеринбург'). objective_lots содержит 419 distinct
|
||
-- project_name → 271 ещё не сматчены.
|
||
--
|
||
-- mv_layout_velocity JOIN-ит objective_corpus_room_month.project_name =
|
||
-- objective_complex_mapping.objective_complex_name WHERE objective_group='Екатеринбург'.
|
||
-- То есть новые mapping-строки с group='Екатеринбург' автоматически расширяют MV.
|
||
--
|
||
-- ПОДХОД (калибровка проведена на prod read-only 2026-06-13, latest snapshot 2026-06-10):
|
||
-- Сырой trigram similarity по comm_name занижается boilerplate-префиксами
|
||
-- ('Жилой комплекс "..."', 'ЖК', 'Квартал', 'Дом', кавычки, '… N этап'),
|
||
-- из-за чего идентичные core-имена дают similarity 0.40-0.44 и теряются.
|
||
-- Поэтому нормализуем ОБА имени (strip boilerplate + кавычки + stage-суффиксы)
|
||
-- ДО similarity. Strip-токены матчатся по word-boundary (\m…\M), чтобы не
|
||
-- резать середину слова ('Домино'→'ино', 'Доминанта', 'Кварталы…').
|
||
--
|
||
-- Developer-строки шумные (Latin vs Cyrillic: 'FORTIS Development' vs
|
||
-- 'Фортис Девелопмент'; юр.префиксы 'СЗ/ГК/Группа/ООО'), поэтому dev
|
||
-- нормализуем (strip юр.префиксов) и считаем dsim = trigram ИЛИ
|
||
-- substring-containment.
|
||
--
|
||
-- ПРАВИЛО ПРИЁМКИ — PRECISION OVER RECALL (P0: ложные mapping напрямую портят
|
||
-- velocity/prices, поэтому берём ТОЛЬКО dev-corroborated матчи):
|
||
-- accept = (nsim >= 0.92 AND dsim >= 0.5) (near-exact name + dev agreement)
|
||
-- OR (nsim >= 0.62 AND dsim >= 0.5) (good name + dev agreement)
|
||
-- == эффективно (nsim >= 0.62 AND dsim >= 0.5): КАЖДЫЙ авто-матч обязан иметь
|
||
-- согласие застройщика. Standalone nsim>=0.92 БЕЗ dev убран намеренно —
|
||
-- он ловил generic-имена с РАЗНЫМ девелопером (Солнечный/Прекрасный/Стрелки/
|
||
-- Невский/Тихий центр → FP). Цена: ~28 distinctive, но dev-noisy true-матчей
|
||
-- (Основа/Тактика/Самоцветы Урала) НЕ замапятся авто — Anton добавит вручную.
|
||
-- Низкоконфидентный хвост (nsim<0.62) тоже НЕ вставляется.
|
||
--
|
||
-- COLLISION HANDLING
|
||
-- 1. Лучший domrf-obj на каждый objective-проект: DISTINCT ON (project_name)
|
||
-- по similarity DESC.
|
||
-- 2. Если два objective-проекта целятся в один obj_id, оставляем лучший
|
||
-- (ROW_NUMBER per obj_id ORDER BY dsim DESC, nsim DESC). Пример obj 12789
|
||
-- = 'Жилой комплекс "Парковый квартал"' (dev PRINZIP): и 'Парковый квартал'
|
||
-- (PRINZIP, dsim высокий), и 'Жилой квартал Парковый' (ГК Вавилон) после
|
||
-- нормализации дают core 'парковый'. Побеждает PRINZIP по dsim; вариант
|
||
-- Вавилона имеет dsim=0.0 (другой застройщик) → отбрасывается. С новым
|
||
-- правилом dsim>=0.5 он и так не прошёл бы глобальный accept-gate.
|
||
-- (Аналогично 'Рио'/'Рио-3' Стройтэк целятся в один obj 31874.)
|
||
-- 3. obj_id, уже занятые существующими 148 mapping, исключаются (NOT IN).
|
||
--
|
||
-- РЕЗУЛЬТАТ (verified prod, dry-run BEGIN…ROLLBACK 2026-06-13): ~114 новых mapping
|
||
-- (107 confidence=high + 7 confidence=mid, все dev-corroborated). Все
|
||
-- is_reviewed=FALSE — Anton ревьюит вручную; note содержит confidence-tier,
|
||
-- matched comm_name и dev. Часть имён имеет строки в objective_corpus_room_month
|
||
-- → немедленно расширяют mv_layout_velocity.
|
||
-- Честно: не все 271 unmapped замапятся — Objective шире domrf (satellite-города,
|
||
-- проекты без объявленной ПД в наш.дом.рф) + намеренно отброшены ~28 dev-noisy
|
||
-- и низкоконфидентный хвост. Остаток — для ручной разметки Anton'ом.
|
||
--
|
||
-- objective_group='Екатеринбург' для ВСЕХ кандидатов: это региональный label
|
||
-- ЕКБ-агломерации (region_cd=66), не строго город — существующие 148 включают
|
||
-- 1 проект не из самого ЕКБ под тем же label. Так новые mapping тоже кормят MV.
|
||
--
|
||
-- EXPLAIN (prod): Nested Loop с similarity() Join Filter по 419×1392 парам,
|
||
-- Execution ~4 s. GIN/GIST индекс НЕ нужен и НЕ используется: предикат
|
||
-- similarity()>порог (а не оператор %) через CTE не триггерит trgm-индекс;
|
||
-- существующий idx_domrf_kn_objects_comm_name_trgm partial WHERE is_ekb=true,
|
||
-- а latest snapshot имеет is_ekb=false → бесполезен. 4 с для one-shot backfill
|
||
-- приемлемо (старый timeout миграции 116 был 89M-cost из-за CROSS JOIN LATERAL).
|
||
--
|
||
-- DEPENDENCIES: objective_lots, objective_complex_mapping, domrf_kn_objects,
|
||
-- pg_trgm (1.6, установлен), mv_layout_velocity (matview, UNIQUE pk → CONCURRENTLY).
|
||
-- DEPLOY: auto-applied by deploy.yml via _schema_migrations (ровно один раз по NN order).
|
||
-- ORDER: schema/data first → backend deploy second.
|
||
-- IDEMPOTENT: ON CONFLICT (objective_complex_name, objective_group) DO NOTHING.
|
||
-- Повторный запуск ничего не добавит (имена уже в mapping). Никакого DROP/мутаций
|
||
-- существующих 148 строк.
|
||
|
||
BEGIN;
|
||
|
||
WITH
|
||
-- 1. Latest DOM.РФ snapshot, регион 66 (Свердловская обл / ЕКБ-агломерация).
|
||
-- (obj_id, snapshot_date) UNIQUE → одна строка на obj_id; DISTINCT ON — страховка.
|
||
latest AS (
|
||
SELECT MAX(snapshot_date) AS sd FROM domrf_kn_objects
|
||
),
|
||
domrf AS (
|
||
SELECT DISTINCT ON (o.obj_id)
|
||
o.obj_id,
|
||
o.comm_name,
|
||
o.dev_name,
|
||
o.district_name,
|
||
-- normalized commercial name: strip ЖК-boilerplate, quotes, stage suffixes.
|
||
-- \m…\M = PG word boundaries → токены матчатся как ЦЕЛЫЕ слова, не подстроки
|
||
-- (иначе over-strip: «Домино»→«ино», «Доминанта»→«инанта», «Кварталы…»→«ы…»).
|
||
btrim(regexp_replace(
|
||
regexp_replace(
|
||
regexp_replace(lower(coalesce(o.comm_name, '')),
|
||
'\m(жилой комплекс|жилой квартал|жилой район|жилой дом|жилые башни|жилые кварталы|клубный дом|концепт-проект|семейный квартал|микрорайон|квартал|паркинг|жк|жр|жд|дом)\M',
|
||
' ', 'g'),
|
||
'\d+(\.\d+)?\s*(этап|очередь)', ' ', 'g'),
|
||
'[«»"''.,()\-]+', ' ', 'g')) AS name_norm,
|
||
-- normalized developer: strip legal/group prefixes
|
||
btrim(regexp_replace(
|
||
regexp_replace(lower(coalesce(o.dev_name, '')),
|
||
'(специализированный застройщик|сз|ооо|оао|зао|пао|ао|гк|группа|холдинг|ск|жск|фонд|девелопмент|девелопер[а-я]*|development|group|developm[a-z]*)',
|
||
' ', 'g'),
|
||
'[«»"''.,()\-]+', ' ', 'g')) AS dev_norm
|
||
FROM domrf_kn_objects o
|
||
WHERE o.snapshot_date = (SELECT sd FROM latest)
|
||
AND o.region_cd = 66
|
||
AND o.comm_name IS NOT NULL
|
||
ORDER BY o.obj_id
|
||
),
|
||
-- 2. Distinct Objective projects (один ряд на project_name; dominant dev/district/city),
|
||
-- исключая уже сматченные в group='Екатеринбург'.
|
||
obj AS (
|
||
SELECT
|
||
l.project_name,
|
||
mode() WITHIN GROUP (ORDER BY l.developer) AS developer,
|
||
mode() WITHIN GROUP (ORDER BY l.district) AS district,
|
||
mode() WITHIN GROUP (ORDER BY l.city) AS city,
|
||
max(l.objective_project_id) AS objective_project_id
|
||
FROM objective_lots l
|
||
WHERE l.project_name IS NOT NULL
|
||
GROUP BY l.project_name
|
||
),
|
||
obj_unmapped AS (
|
||
SELECT
|
||
o.*,
|
||
-- symmetric нормализация project_name (см. domrf.name_norm выше: \m…\M boundaries)
|
||
btrim(regexp_replace(
|
||
regexp_replace(
|
||
regexp_replace(lower(coalesce(o.project_name, '')),
|
||
'\m(жилой комплекс|жилой квартал|жилой район|жилой дом|жилые башни|жилые кварталы|клубный дом|концепт-проект|семейный квартал|микрорайон|квартал|паркинг|жк|жр|жд|дом)\M',
|
||
' ', 'g'),
|
||
'\d+(\.\d+)?\s*(этап|очередь)', ' ', 'g'),
|
||
'[«»"''.,()\-]+', ' ', 'g')) AS name_norm,
|
||
btrim(regexp_replace(
|
||
regexp_replace(lower(coalesce(o.developer, '')),
|
||
'(специализированный застройщик|сз|ооо|оао|зао|пао|ао|гк|группа|холдинг|ск|жск|фонд|девелопмент|девелопер[а-я]*|development|group|developm[a-z]*)',
|
||
' ', 'g'),
|
||
'[«»"''.,()\-]+', ' ', 'g')) AS dev_norm
|
||
FROM obj o
|
||
WHERE NOT EXISTS (
|
||
SELECT 1 FROM objective_complex_mapping m
|
||
WHERE m.objective_complex_name = o.project_name
|
||
AND m.objective_group = 'Екатеринбург'
|
||
)
|
||
),
|
||
-- 3. Best DOM.РФ candidate per objective project (нормализованный trigram).
|
||
pairs AS (
|
||
SELECT
|
||
u.project_name,
|
||
u.developer AS obj_dev,
|
||
u.district AS obj_district,
|
||
u.objective_project_id,
|
||
d.obj_id,
|
||
d.comm_name,
|
||
d.dev_name AS domrf_dev,
|
||
d.district_name AS domrf_district,
|
||
similarity(u.name_norm, d.name_norm) AS nsim,
|
||
-- dev agreement: trigram OR substring containment (после нормализации)
|
||
GREATEST(
|
||
similarity(u.dev_norm, d.dev_norm),
|
||
(char_length(btrim(u.dev_norm)) > 2
|
||
AND char_length(btrim(d.dev_norm)) > 2
|
||
AND (u.dev_norm ILIKE '%' || d.dev_norm || '%'
|
||
OR d.dev_norm ILIKE '%' || u.dev_norm || '%'))::int::double precision
|
||
) AS dsim
|
||
FROM obj_unmapped u
|
||
JOIN domrf d
|
||
ON similarity(u.name_norm, d.name_norm) > 0.2 -- prefilter
|
||
),
|
||
best_per_proj AS (
|
||
SELECT DISTINCT ON (project_name) *
|
||
FROM pairs
|
||
ORDER BY project_name, nsim DESC, dsim DESC, obj_id
|
||
),
|
||
-- 4. Accept rule + resolve obj_id collisions (keep best objective per domrf obj).
|
||
accepted AS (
|
||
SELECT *
|
||
FROM (
|
||
SELECT
|
||
b.*,
|
||
ROW_NUMBER() OVER (
|
||
PARTITION BY b.obj_id
|
||
ORDER BY b.dsim DESC, b.nsim DESC, b.project_name
|
||
) AS rn_obj
|
||
FROM best_per_proj b
|
||
WHERE ((b.nsim >= 0.92 AND b.dsim >= 0.5) OR (b.nsim >= 0.62 AND b.dsim >= 0.5))
|
||
AND b.obj_id NOT IN (
|
||
SELECT domrf_obj_id FROM objective_complex_mapping
|
||
WHERE domrf_obj_id IS NOT NULL
|
||
)
|
||
) s
|
||
WHERE s.rn_obj = 1
|
||
)
|
||
INSERT INTO objective_complex_mapping
|
||
(objective_complex_name, objective_project_id, objective_group,
|
||
domrf_obj_id, is_reviewed, match_method, match_score, note)
|
||
SELECT
|
||
a.project_name,
|
||
a.objective_project_id,
|
||
'Екатеринбург',
|
||
a.obj_id,
|
||
FALSE, -- ВСЕ авто-матчи на ручной review (Anton)
|
||
'auto_fuzzy_v2',
|
||
round(a.nsim::numeric, 3),
|
||
'OBJ-2 #307 auto_fuzzy_v2 2026-06-13 (norm-trgm). '
|
||
|| CASE
|
||
WHEN a.nsim >= 0.92 THEN 'confidence=high (near-exact name + dev)'
|
||
ELSE 'confidence=mid (good name + dev)'
|
||
END
|
||
|| ' | nsim=' || round(a.nsim::numeric, 3)
|
||
|| ' dsim=' || round(a.dsim::numeric, 3)
|
||
|| ' | domrf="' || coalesce(a.comm_name, '') || '"'
|
||
|| ' dev_obj="' || coalesce(a.obj_dev, '') || '"'
|
||
|| ' dev_domrf="' || coalesce(a.domrf_dev, '') || '"'
|
||
FROM accepted a
|
||
ON CONFLICT (objective_complex_name, objective_group) DO NOTHING;
|
||
|
||
COMMIT;
|
||
|
||
-- REFRESH вне транзакции: CONCURRENTLY требует non-tx и UNIQUE index
|
||
-- (mv_layout_velocity_pk на (obj_id, room_bucket) существует). Не блокирует читателей.
|
||
-- Initial MV уже populated (~554 строк) — CONCURRENTLY безопасен.
|
||
REFRESH MATERIALIZED VIEW CONCURRENTLY mv_layout_velocity;
|