This commit is contained in:
parent
5bf30570a5
commit
863577beb6
1 changed files with 270 additions and 0 deletions
270
data/sql/156_objective_mapping_corroborated_v4.sql
Normal file
270
data/sql/156_objective_mapping_corroborated_v4.sql
Normal file
|
|
@ -0,0 +1,270 @@
|
|||
-- 156_objective_mapping_corroborated_v4.sql
|
||||
-- Issue #307 OBJ-2 — РАУНД-2: КОРРОБОРИРОВАННЫЙ fuzzy (pg_trgm) backfill of
|
||||
-- objective_complex_mapping. Четвёртый проход (после 116/150/155). 155 (v3) был
|
||||
-- НАМЕРЕННО строгим: только near-exact distinctive-name матчи (nsim=1.000, +23).
|
||||
-- v4 безопасно добирает СРЕДНИЙ band (nsim 0.50-0.85), где имя одно не дотягивает
|
||||
-- до уверенности, но КОРРОБОРАЦИЯ застройщиком/районом добивает precision до ≈1.0.
|
||||
--
|
||||
-- CONTEXT
|
||||
-- objective_complex_mapping связывает Objective-ЖК (objective_complex_name) →
|
||||
-- domrf_kn_objects.obj_id. До этой миграции — 286 строк (group='Екатеринбург';
|
||||
-- match_method fuzzy/auto_fuzzy_v2/auto_trgm_v3/multi_feature_v1/manual; все
|
||||
-- is_reviewed=false кроме 2 manual). Verified prod 2026-06-14: 286/286/286
|
||||
-- (total/ekb/with_obj). 155 применён (_schema_migrations).
|
||||
--
|
||||
-- mv_layout_velocity JOIN-ит objective_corpus_room_month.project_name =
|
||||
-- objective_complex_mapping.objective_complex_name WHERE objective_group=
|
||||
-- 'Екатеринбург' AND domrf_obj_id IS NOT NULL. Unique-ключ матчинга — имя
|
||||
-- (project_name), не project_id: новые строки с этим именем + group='Екатеринбург'
|
||||
-- немедленно расширяют MV. Дедуп по project_name (как 150/155).
|
||||
--
|
||||
-- НОРМАЛИЗАЦИЯ (идентична 150/155): strip ЖК-boilerplate ('Жилой комплекс/квартал/
|
||||
-- район/дом','Клубный дом','Микрорайон','Квартал','ЖК','Дом'…), кавычки,
|
||||
-- stage-суффиксы ('N этап/очередь') ДО similarity. Strip-токены по word-boundary
|
||||
-- (\m…\M), чтобы не резать середину ('Домино'→'ино'). Dev нормализуется (strip
|
||||
-- юр.префиксов СЗ/ГК/ООО/Девелопмент/Development/Group…).
|
||||
--
|
||||
-- КОРРОБОРАЦИЯ (РАСШИРЕНА vs 155): три независимых сигнала равенства застройщика —
|
||||
-- dsim = trigram(norm dev) OR substring-containment (как 150/155)
|
||||
-- dev_tok = ОБЩИЙ distinctive dev-токен длиной ≥4 после нормализации
|
||||
-- dm = район Objective точно совпал с district_name DOM.РФ
|
||||
-- corroborated = (dsim >= 0.6) OR dev_tok OR (dm = 1)
|
||||
-- ПОЧЕМУ dev_tok: trigram/substring НЕ ловит аббревиатуру-vs-расшифровку. Реальный
|
||||
-- кейс — «АСЦ Правобережный» (Objective) vs «Архитектурно-строительный центр
|
||||
-- Правобережный» (DOM.РФ): dsim=0.326 (<0.6), substring fail, НО общий distinctive
|
||||
-- токен 'правобережный' (len 12) → тот же застройщик. Задача явно зовёт
|
||||
-- «Мкр. Центральный»→obj 47127 (АСЦ Правобережный, almost-certainly true);
|
||||
-- dev_tok ловит его И структурно-идентичный «Мкр. Садовый»→obj 35542 через ОДНО
|
||||
-- общее правило, а не hardcoded obj_id. (Калибровка prod read-only 2026-06-14:
|
||||
-- dev_tok НЕ добавил ни одного FP в band'е — он только разрешает abbreviation
|
||||
-- кейсы; все остальные 0.50-0.85 уже имели dsim=1.0 или district.)
|
||||
--
|
||||
-- ПРАВИЛО ПРИЁМКИ v4 — PRECISION OVER RECALL (P0: ложный mapping напрямую портит
|
||||
-- velocity/prices, держим precision≈1.0 на авто-наборе):
|
||||
-- accept = nsim >= 0.50 AND nsim <= 0.85 AND corroborated
|
||||
-- Band <0.50 НЕ берём (низкоконфидентный хвост). Band >0.85 здесь НЕ берём —
|
||||
-- он покрыт 155 (distinctive) либо остаётся generic-guarded (см. ниже). Этот
|
||||
-- проход — РОВНО средний band, где корроборация необходима и достаточна.
|
||||
--
|
||||
-- GENERIC-GUARDED (6 имён из v3): «Солнечный»,«Прекрасный»,«Невский»,«Стрелки»,
|
||||
-- «Жилой квартал Парковый»(→'парковый'),«Тихий центр». В band'е 1.0 они дают
|
||||
-- FP (одинаковое generic-имя, РАЗНЫЙ застройщик). Задача: принять ТОЛЬКО если
|
||||
-- developer корроборирует. ПРОВЕРЕНО (prod 2026-06-14): у ВСЕХ шести dsim=0,
|
||||
-- dev_tok=false, district НЕ совпал — застройщики реально РАЗНЫЕ (НКС vs Астра;
|
||||
-- Вавилон vs PRINZIP; Синара vs Актив; Вавилон vs Аркада; СКОН vs Фортис;
|
||||
-- Клевер vs Астра). Корроборации НЕТ → НИ ОДИН не принят. Guard держит; они
|
||||
-- остаются на manual. (Дополнительно их nsim=1.000 вне band'а 0.50-0.85, так что
|
||||
-- они и так не попали бы в accept — двойная защита.)
|
||||
--
|
||||
-- COLLISION HANDLING
|
||||
-- 1. Лучший domrf-obj на каждый objective project: DISTINCT ON (project_name)
|
||||
-- по nsim DESC, dsim DESC, obj_id.
|
||||
-- 2. Один obj_id ≤ один objective project: ROW_NUMBER per obj_id
|
||||
-- ORDER BY corroborated DESC, nsim DESC, project_name.
|
||||
-- 3. obj_id, уже занятые существующими 286 mapping (incl. 155), исключаются.
|
||||
-- Реальные коллизии, отсечённые guard'ом (prod 2026-06-14):
|
||||
-- • obj 58129 «Толк Академический» (уже в mapping) — на него целятся ТРИ
|
||||
-- разных Objective-проекта «Близкий/Eleven/Школьный (Академический)» лишь
|
||||
-- по district=Академический (dsim=0). Это РАЗНЫЕ дома в районе, не «Толк».
|
||||
-- obj_id-taken-guard убирает все три. (Урок: district-only корроборация в
|
||||
-- плотном районе шумит — но taken-guard + dedup её гасят.)
|
||||
-- • obj 31874 «Рио» (уже в mapping, 150) — «Рио-3» это ФАЗА того же ЖК,
|
||||
-- dsim=1.0 Стройтэк, но obj_id занят → исключён.
|
||||
--
|
||||
-- РЕЗУЛЬТАТ (verified prod, BEGIN…ROLLBACK 2026-06-14, snapshot 2026-06-10):
|
||||
-- 17 новых mapping (nsim 0.500-0.750, ВСЕ корроборированы dev/dev_tok/district,
|
||||
-- ВСЕ obj_id свободны). is_reviewed=FALSE. После: 286 → 303.
|
||||
-- Из 133 unmapped имён остаётся 116 на manual (Anton):
|
||||
-- • 19 без кандидата в DOM.РФ (города-спутники / нет ПД)
|
||||
-- • 6 generic-guarded near-exact (застройщик НЕ подтвердил)
|
||||
-- • 4 in-band corroborated, но obj_id уже занят (Близкий/Eleven/Школьный→58129,
|
||||
-- Рио-3→31874 — фазы/соседи уже-сматченных)
|
||||
-- • 16 in-band БЕЗ корроборации (другой застройщик, район не совпал)
|
||||
-- • 71 nsim<0.50 низкоконфидентный хвост
|
||||
-- Честно: Objective шире DOM.РФ (сателлиты, проекты без объявленной ПД) — 100%
|
||||
-- автоматом недостижимо by design; хвост — ручная разметка.
|
||||
--
|
||||
-- ПОЧЕМУ ОТДЕЛЬНЫЙ ПРОХОД, А НЕ ПЕРЕДЕЛКА 155
|
||||
-- 150 и 155 уже на prod (_schema_migrations, auto-apply once) — переписывать
|
||||
-- нельзя. 156 — аддитивный: только INSERT новых имён, НИКАКИХ DROP/UPDATE
|
||||
-- существующих 286 строк.
|
||||
--
|
||||
-- DEPENDENCIES: objective_lots, objective_complex_mapping, domrf_kn_objects,
|
||||
-- pg_trgm (1.6), mv_layout_velocity (UNIQUE pk mv_layout_velocity_pk на
|
||||
-- (obj_id, room_bucket) → REFRESH CONCURRENTLY валиден). GIN trgm индекс на
|
||||
-- domrf_kn_objects.comm_name создан в 155 (idx_domrf_kn_objects_comm_name_gin_trgm).
|
||||
-- Сам backfill использует similarity()>порог в CTE-join (не %-оператор), что НЕ
|
||||
-- триггерит trgm-индекс — но это one-shot Nested Loop (~4 s, как 150/155),
|
||||
-- приемлемо.
|
||||
-- DEPLOY: auto-applied by deploy.yml через _schema_migrations (ровно один раз по NN).
|
||||
-- Применяется ДО compose up -d. ORDER: schema/data first → backend deploy second.
|
||||
-- IDEMPOTENT: ON CONFLICT (objective_complex_name, objective_group) DO NOTHING.
|
||||
-- Re-apply добавит 0 строк (verified BEGIN/INSERT x2/ROLLBACK на prod:
|
||||
-- INSERT 0 17, затем INSERT 0 0).
|
||||
|
||||
BEGIN;
|
||||
|
||||
WITH
|
||||
-- 1. Latest DOM.РФ snapshot, регион 66 (Свердловская обл / ЕКБ-агломерация).
|
||||
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,
|
||||
-- \m…\M = PG word boundaries → токены матчатся как ЦЕЛЫЕ слова, не подстроки.
|
||||
btrim(regexp_replace(
|
||||
regexp_replace(
|
||||
regexp_replace(lower(coalesce(o.comm_name, '')),
|
||||
'\m(жилой комплекс|жилой квартал|жилой район|жилой дом|жилые башни|жилые кварталы|клубный дом|концепт-проект|семейный квартал|микрорайон|квартал|паркинг|жк|жр|жд|дом)\M',
|
||||
' ', 'g'),
|
||||
'\d+(\.\d+)?\s*(этап|очередь)', ' ', 'g'),
|
||||
'[«»"''.,()\-]+', ' ', 'g')) AS name_norm,
|
||||
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),
|
||||
-- исключая уже сматченные в 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,
|
||||
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.*,
|
||||
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,
|
||||
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,
|
||||
-- ОБЩИЙ distinctive dev-токен (len>=4): ловит аббревиатуру-vs-расшифровку
|
||||
-- (АСЦ Правобережный ↔ Архитектурно-строительный центр Правобережный).
|
||||
EXISTS (
|
||||
SELECT 1 FROM (
|
||||
SELECT unnest(regexp_split_to_array(btrim(regexp_replace(u.dev_norm, '\s+', ' ', 'g')), ' ')) AS t
|
||||
INTERSECT
|
||||
SELECT unnest(regexp_split_to_array(btrim(regexp_replace(d.dev_norm, '\s+', ' ', 'g')), ' ')) AS t
|
||||
) x
|
||||
WHERE char_length(t) >= 4
|
||||
) AS dev_tok_shared,
|
||||
(u.district IS NOT NULL AND d.district_name IS NOT NULL
|
||||
AND lower(btrim(u.district)) = lower(btrim(d.district_name)))::int AS district_match
|
||||
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
|
||||
),
|
||||
classified AS (
|
||||
SELECT
|
||||
b.*,
|
||||
(b.dsim >= 0.6 OR b.dev_tok_shared OR b.district_match = 1) AS corroborated
|
||||
FROM best_per_proj b
|
||||
),
|
||||
accepted AS (
|
||||
SELECT *
|
||||
FROM (
|
||||
SELECT
|
||||
c.*,
|
||||
ROW_NUMBER() OVER (
|
||||
PARTITION BY c.obj_id
|
||||
ORDER BY c.corroborated DESC, c.nsim DESC, c.project_name
|
||||
) AS rn_obj
|
||||
FROM classified c
|
||||
WHERE c.nsim >= 0.50 AND c.nsim <= 0.85 -- ровно средний band
|
||||
AND c.corroborated -- обязательная корроборация
|
||||
AND c.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_trgm_corroborated_v4',
|
||||
round(a.nsim::numeric, 3),
|
||||
'OBJ-2 corroborated trgm backfill #307 (round-2)'
|
||||
|| ' | nsim=' || round(a.nsim::numeric, 3)
|
||||
|| ' dsim=' || round(a.dsim::numeric, 3)
|
||||
|| ' corrob=' || CASE
|
||||
WHEN a.dsim >= 0.6 THEN 'dev'
|
||||
WHEN a.dev_tok_shared THEN 'dev_token'
|
||||
WHEN a.district_match = 1 THEN 'district'
|
||||
ELSE '?'
|
||||
END
|
||||
|| ' | 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 ВНЕ транзакции (после COMMIT): CONCURRENTLY требует non-tx-блок и
|
||||
-- UNIQUE index (mv_layout_velocity_pk на (obj_id, room_bucket) существует), не
|
||||
-- блокирует читателей. MV уже populated → CONCURRENTLY безопасен. deploy.yml
|
||||
-- прогоняет файл через `psql < file` без --single-transaction, поэтому COMMIT
|
||||
-- выше закрывает txn и этот стейтмент исполняется в собственной транзакции
|
||||
-- (тот же приём, что в 150/155).
|
||||
REFRESH MATERIALIZED VIEW CONCURRENTLY mv_layout_velocity;
|
||||
Loading…
Add table
Reference in a new issue