Przejdź do treści

02 — Data Model

Insurance Intelligence System — PostgreSQL Schema


Zasady projektowania bazy

  1. Surowe dane nigdy nie są nadpisywane — raw_signals to append-only
  2. Klasyfikacja jest oddzielna od danych — można reklasyfikować bez utraty oryginału
  3. Każdy rekord ma źródło i timestamp — pełna audytowalność
  4. JSONB dla danych surowych — elastyczność bez zmiany schematu przy nowych źródłach
  5. Segmenty jako enum — spójność i możliwość filtrowania

Schemat bazy danych

Tabela: raw_signals

Surowe dane ze wszystkich źródeł. Append-only — nigdy nie modyfikowana.

CREATE TABLE raw_signals (
    id              SERIAL PRIMARY KEY,
    source_type     VARCHAR(50) NOT NULL,
    -- 'youtube' | 'knf' | 'piu' | 'nbp' | 'podcast' | 'media'
    source_id       VARCHAR(200),
    -- np. video_id dla YouTube, URL dla stron
    collected_at    TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    published_at    DATE,
    title           TEXT,
    raw_data        JSONB NOT NULL,
    -- pełne dane z API / scraper w oryginalnej formie
    description_urls TEXT[],
    -- URL-e wyciągnięte z opisów (linki do raportów itp.)
    checksum        VARCHAR(64) UNIQUE
    -- SHA256 z source_id — zapobiega duplikatom
);

CREATE INDEX idx_raw_signals_source ON raw_signals(source_type, collected_at);
CREATE INDEX idx_raw_signals_published ON raw_signals(published_at);

Tabela: classified_signals

Wyniki klasyfikacji przez Agenta 2. Jeden rekord na każdy raw_signal.

CREATE TYPE signal_segment AS ENUM (
    'ceo_cfo_tu',
    'financials_tu',
    'regulators',
    'conferences',
    'bancassurance',
    'intermediaries',
    'expert_analysis',
    'eu_groups',
    'noise',
    'unclassified'
);

CREATE TYPE classification_method AS ENUM (
    'rules',        -- sklasyfikowany przez reguły
    'claude_api',   -- sklasyfikowany przez Claude API
    'manual'        -- poprawiony ręcznie przez właściciela projektu
);

CREATE TABLE classified_signals (
    id                      SERIAL PRIMARY KEY,
    raw_signal_id           INTEGER NOT NULL REFERENCES raw_signals(id),
    segment                 signal_segment NOT NULL,
    relevance_score         SMALLINT CHECK (relevance_score BETWEEN 0 AND 100),
    is_noise                BOOLEAN NOT NULL DEFAULT FALSE,
    classification_method   classification_method NOT NULL,
    classification_reason   TEXT,
    -- krótkie uzasadnienie (szczeg. dla claude_api i manual)
    companies_mentioned     TEXT[],
    -- ['PZU', 'Warta'] — TU wymienione w tytule/opisie
    roles_mentioned         TEXT[],
    -- ['prezes', 'dyrektor sprzedaży'] — stanowiska
    topics                  TEXT[],
    -- ['wyniki finansowe', 'strategia', 'AI']
    classified_at           TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    manually_reviewed_at    TIMESTAMPTZ,
    manually_reviewed_by    VARCHAR(100)
);

CREATE INDEX idx_classified_segment ON classified_signals(segment, relevance_score DESC);
CREATE INDEX idx_classified_noise ON classified_signals(is_noise);
CREATE INDEX idx_classified_companies ON classified_signals USING GIN(companies_mentioned);

Tabela: insights

Wyniki analizy przez Agenta 3. Jeden insight może łączyć wiele sygnałów.

CREATE TABLE insights (
    id              SERIAL PRIMARY KEY,
    signal_ids      INTEGER[] NOT NULL,
    -- powiązane classified_signals.id
    insight_type    VARCHAR(50),
    -- 'trend' | 'anomaly' | 'competitor_move' | 'regulatory_change'
    title           TEXT NOT NULL,
    summary         TEXT NOT NULL,
    companies       TEXT[],
    topics          TEXT[],
    importance      SMALLINT CHECK (importance BETWEEN 1 AND 5),
    -- 5 = krytyczny, 1 = informacyjny
    period_start    DATE,
    period_end      DATE,
    generated_at    TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    report_id       INTEGER
    -- opcjonalne powiązanie z raportem
);

Tabela: known_sources

Źródła które system już zidentyfikował jako wartościowe (kanały YouTube, strony itp.)

CREATE TABLE known_sources (
    id              SERIAL PRIMARY KEY,
    source_type     VARCHAR(50) NOT NULL,
    source_id       VARCHAR(200) NOT NULL UNIQUE,
    -- np. channel_id dla YouTube
    name            TEXT NOT NULL,
    description     TEXT,
    quality_score   SMALLINT DEFAULT 50,
    -- 0-100, rośnie z każdym wartościowym sygnałem
    segment         signal_segment,
    -- dominujący segment tego źródła
    is_monitored    BOOLEAN DEFAULT TRUE,
    first_seen_at   TIMESTAMPTZ DEFAULT NOW(),
    last_signal_at  TIMESTAMPTZ,
    signal_count    INTEGER DEFAULT 0
);

Tabela: reports

Wygenerowane raporty tygodniowe / miesięczne / na żądanie.

CREATE TABLE reports (
    id              SERIAL PRIMARY KEY,
    report_type     VARCHAR(50),
    -- 'weekly' | 'monthly' | 'adhoc' | 'benchmark'
    title           TEXT NOT NULL,
    period_start    DATE,
    period_end      DATE,
    content_html    TEXT,
    content_json    JSONB,
    generated_at    TIMESTAMPTZ DEFAULT NOW(),
    generated_by    VARCHAR(50) DEFAULT 'agent_reporter'
);

Przepływ danych

YouTube API
    └─► raw_signals (source_type='youtube', raw_data={...})
            └─► classified_signals (segment, relevance_score, companies[])
                    └─► insights (trend/anomaly/move)
                            └─► reports (weekly/monthly/adhoc)

known_sources ◄── aktualizowany gdy nowy kanał pojawia się wielokrotnie

Konwencje

Element Konwencja
Nazwy tabel snake_case, liczba mnoga
Nazwy kolumn snake_case
Timestampy zawsze TIMESTAMPTZ (UTC)
Daty publikacji DATE (bez czasu)
Dane surowe JSONB (nie TEXT)
Enumy zdefiniowane jako PostgreSQL TYPE
Indeksy na każdej kolumnie używanej w WHERE/JOIN