gendesign/_wt-cadval/data/sql/150_objective_mapping_backfill.sql
bot-backend 542ff1c9ca docs(tradein/domclick): два комментария описывали пул, которого нет с миграции 253
Оба места утверждали, что у Домклика один выделенный резидентный прокси и
пула нет. Это перестало быть правдой ещё в #2800 (миграция 253 сняла
резервацию узла), но текст остался — и именно на него опирался тикет #3189,
поставленный под «калибровку» ограничения, которого не существует.
Свип к тому же ходит через пул давно (serp.py:359), а бэкфилл подключён к
нему в PR #3222.

Заодно докстринг называл не тот ограничитель: свип кладёт не счётчик
провалов lease, а break по первому DomClickBlockedError (#2854) — до
ротации дело не доходит ни при каком счётчике, отсюда buckets_completed=0.

Только комментарии, поведение не меняется. Миграция 175 уже применена, а
_schema_migrations трекает по имени файла без checksum — правка текста
её не перезапустит.
2026-08-29 18:57:25 +03:00

230 lines
14 KiB
PL/PgSQL
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

-- 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;