Codex Lab 환경(iran-airstrike-replay-codex)에서 검증 완료된 어구 모선 자동 추론 + 검토 워크플로우 전체를 이식. ## Python (prediction/) - gear_parent_inference(1,428줄): 다층 점수 모델 (correlation + name + track + prior bonus) - gear_parent_episode(631줄): Episode 연속성 (Jaccard + 공간거리) - gear_name_rules: 모선 이름 정규화 + 4자 미만 필터 - scheduler: 추론 호출 단계 추가 (4.8) - fleet_tracker/kcgdb: SQL qualified_table() 동적화 - gear_correlation: timestamp 필드 추가 ## DB (database/migration/ 012~015) - 후보 스냅샷, resolution, episode, 라벨 세션, 제외 관리 테이블 9개 + VIEW 2개 ## Backend (Java) - 12개 DTO/Controller (ParentInferenceWorkflowController 등) - GroupPolygonService: parent_resolution LEFT JOIN + 15개 API 메서드 ## Frontend - ParentReviewPanel: 모선 검토 대시보드 - vesselAnalysis: 10개 신규 API 함수 + 6개 타입 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
112 lines
4.8 KiB
SQL
112 lines
4.8 KiB
SQL
-- 015: 어구 모선 추론 episode continuity + prior bonus
|
|
|
|
SET search_path TO kcg, public;
|
|
|
|
ALTER TABLE kcg.gear_group_parent_candidate_snapshots
|
|
ADD COLUMN IF NOT EXISTS normalized_parent_name VARCHAR(200);
|
|
|
|
ALTER TABLE kcg.gear_group_parent_candidate_snapshots
|
|
ADD COLUMN IF NOT EXISTS episode_id VARCHAR(64);
|
|
|
|
ALTER TABLE kcg.gear_group_parent_candidate_snapshots
|
|
ADD COLUMN IF NOT EXISTS episode_prior_bonus DOUBLE PRECISION NOT NULL DEFAULT 0;
|
|
|
|
ALTER TABLE kcg.gear_group_parent_candidate_snapshots
|
|
ADD COLUMN IF NOT EXISTS lineage_prior_bonus DOUBLE PRECISION NOT NULL DEFAULT 0;
|
|
|
|
ALTER TABLE kcg.gear_group_parent_candidate_snapshots
|
|
ADD COLUMN IF NOT EXISTS label_prior_bonus DOUBLE PRECISION NOT NULL DEFAULT 0;
|
|
|
|
UPDATE kcg.gear_group_parent_candidate_snapshots
|
|
SET normalized_parent_name = regexp_replace(upper(COALESCE(parent_name, '')), '[[:space:]_%-]+', '', 'g')
|
|
WHERE normalized_parent_name IS NULL;
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_ggpcs_episode_time
|
|
ON kcg.gear_group_parent_candidate_snapshots(episode_id, observed_at DESC);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_ggpcs_lineage_time
|
|
ON kcg.gear_group_parent_candidate_snapshots(normalized_parent_name, observed_at DESC);
|
|
|
|
ALTER TABLE kcg.gear_group_parent_resolution
|
|
ADD COLUMN IF NOT EXISTS episode_id VARCHAR(64);
|
|
|
|
ALTER TABLE kcg.gear_group_parent_resolution
|
|
ADD COLUMN IF NOT EXISTS continuity_source VARCHAR(32);
|
|
|
|
ALTER TABLE kcg.gear_group_parent_resolution
|
|
ADD COLUMN IF NOT EXISTS continuity_score DOUBLE PRECISION;
|
|
|
|
ALTER TABLE kcg.gear_group_parent_resolution
|
|
ADD COLUMN IF NOT EXISTS prior_bonus_total DOUBLE PRECISION NOT NULL DEFAULT 0;
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_ggpr_episode
|
|
ON kcg.gear_group_parent_resolution(episode_id);
|
|
|
|
ALTER TABLE kcg.gear_parent_label_sessions
|
|
ADD COLUMN IF NOT EXISTS normalized_parent_name VARCHAR(200);
|
|
|
|
UPDATE kcg.gear_parent_label_sessions
|
|
SET normalized_parent_name = regexp_replace(upper(COALESCE(group_key, '')), '[[:space:]_%-]+', '', 'g')
|
|
WHERE normalized_parent_name IS NULL;
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_gpls_lineage_active
|
|
ON kcg.gear_parent_label_sessions(normalized_parent_name, active_from DESC);
|
|
|
|
CREATE TABLE IF NOT EXISTS kcg.gear_group_episodes (
|
|
episode_id VARCHAR(64) PRIMARY KEY,
|
|
lineage_key VARCHAR(200) NOT NULL,
|
|
group_key VARCHAR(100) NOT NULL,
|
|
normalized_parent_name VARCHAR(200) NOT NULL,
|
|
current_sub_cluster_id SMALLINT NOT NULL DEFAULT 0,
|
|
status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
|
|
continuity_source VARCHAR(32) NOT NULL DEFAULT 'NEW',
|
|
continuity_score DOUBLE PRECISION,
|
|
first_seen_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
last_seen_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
last_snapshot_time TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
current_member_count INT NOT NULL DEFAULT 0,
|
|
current_member_mmsis JSONB NOT NULL DEFAULT '[]'::jsonb,
|
|
current_center_point geometry(Point, 4326),
|
|
split_from_episode_id VARCHAR(64),
|
|
merged_from_episode_ids JSONB NOT NULL DEFAULT '[]'::jsonb,
|
|
merged_into_episode_id VARCHAR(64),
|
|
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
CONSTRAINT chk_gge_status CHECK (status IN ('ACTIVE', 'MERGED', 'EXPIRED')),
|
|
CONSTRAINT chk_gge_continuity CHECK (continuity_source IN ('NEW', 'CONTINUED', 'SPLIT_CONTINUE', 'SPLIT_NEW', 'MERGE_NEW', 'DIRECT_PARENT_MATCH'))
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_gge_lineage_status_time
|
|
ON kcg.gear_group_episodes(lineage_key, status, last_snapshot_time DESC);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_gge_group_time
|
|
ON kcg.gear_group_episodes(group_key, current_sub_cluster_id, last_snapshot_time DESC);
|
|
|
|
CREATE TABLE IF NOT EXISTS kcg.gear_group_episode_snapshots (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
episode_id VARCHAR(64) NOT NULL REFERENCES kcg.gear_group_episodes(episode_id) ON DELETE CASCADE,
|
|
lineage_key VARCHAR(200) NOT NULL,
|
|
group_key VARCHAR(100) NOT NULL,
|
|
normalized_parent_name VARCHAR(200) NOT NULL,
|
|
sub_cluster_id SMALLINT NOT NULL DEFAULT 0,
|
|
observed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
member_count INT NOT NULL DEFAULT 0,
|
|
member_mmsis JSONB NOT NULL DEFAULT '[]'::jsonb,
|
|
center_point geometry(Point, 4326),
|
|
continuity_source VARCHAR(32) NOT NULL,
|
|
continuity_score DOUBLE PRECISION,
|
|
parent_episode_ids JSONB NOT NULL DEFAULT '[]'::jsonb,
|
|
top_candidate_mmsi VARCHAR(20),
|
|
top_candidate_score DOUBLE PRECISION,
|
|
resolution_status VARCHAR(40),
|
|
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
CONSTRAINT uq_gges_episode_observed UNIQUE (episode_id, observed_at)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_gges_lineage_observed
|
|
ON kcg.gear_group_episode_snapshots(lineage_key, observed_at DESC);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_gges_group_observed
|
|
ON kcg.gear_group_episode_snapshots(group_key, sub_cluster_id, observed_at DESC);
|