128 lines
5.9 KiB
SQL
128 lines
5.9 KiB
SQL
-- Migration 025: Blog Self-Learning Loop (SLL v1.0)
|
||
-- Tracks LinkedIn engagement per post, extracts winning/losing patterns,
|
||
-- feeds insights back into the generation pipeline automatically.
|
||
|
||
-- ─────────────────────────────────────────────────────────
|
||
-- Blog Performance: LinkedIn/platform engagement per post
|
||
-- ─────────────────────────────────────────────────────────
|
||
CREATE TABLE IF NOT EXISTS blog_performance (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
blog_id UUID REFERENCES blog_drafts(id) ON DELETE CASCADE,
|
||
platform TEXT DEFAULT 'linkedin',
|
||
|
||
-- Raw engagement metrics (LinkedIn: saves > shares > comments > likes)
|
||
impressions INTEGER,
|
||
comments INTEGER DEFAULT 0,
|
||
shares INTEGER DEFAULT 0,
|
||
saves INTEGER DEFAULT 0,
|
||
likes INTEGER DEFAULT 0,
|
||
|
||
-- SLL score formula: (comments×3) + (shares×2) + (saves×2), likes = 0 weight
|
||
engagement_score INTEGER GENERATED ALWAYS AS (
|
||
COALESCE(comments,0)*3 + COALESCE(shares,0)*2 + COALESCE(saves,0)*2
|
||
) STORED,
|
||
|
||
-- Snapshot of article at time of measurement (for pattern extraction)
|
||
hook_text TEXT, -- first ~120 chars of the LinkedIn post
|
||
blog_type TEXT, -- tutorial | market_alert | comparison | …
|
||
topic TEXT, -- topic tag used during generation
|
||
word_count INTEGER,
|
||
pipeline_version TEXT,
|
||
|
||
-- Timing
|
||
posted_at TIMESTAMPTZ,
|
||
measured_at TIMESTAMPTZ DEFAULT NOW(),
|
||
notes TEXT,
|
||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||
);
|
||
|
||
CREATE INDEX IF NOT EXISTS idx_blog_perf_blog ON blog_performance(blog_id);
|
||
CREATE INDEX IF NOT EXISTS idx_blog_perf_score ON blog_performance(engagement_score DESC);
|
||
CREATE INDEX IF NOT EXISTS idx_blog_perf_type ON blog_performance(blog_type);
|
||
CREATE INDEX IF NOT EXISTS idx_blog_perf_posted ON blog_performance(posted_at DESC);
|
||
|
||
-- ─────────────────────────────────────────────────────────
|
||
-- Learned Patterns: extracted winning / losing patterns
|
||
-- ─────────────────────────────────────────────────────────
|
||
CREATE TABLE IF NOT EXISTS blog_learned_patterns (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
pattern_type TEXT NOT NULL, -- hook | structure | topic | length | opening | verb_style
|
||
pattern_value TEXT NOT NULL, -- "short hook + contrast", "lab vs production", "single scenario"
|
||
performance_class TEXT NOT NULL, -- winner | loser
|
||
avg_engagement NUMERIC, -- average engagement_score across samples
|
||
sample_count INTEGER DEFAULT 1,
|
||
example_post_id UUID REFERENCES blog_drafts(id),
|
||
active BOOLEAN DEFAULT TRUE,
|
||
extracted_at TIMESTAMPTZ DEFAULT NOW(),
|
||
last_updated TIMESTAMPTZ DEFAULT NOW()
|
||
);
|
||
|
||
CREATE INDEX IF NOT EXISTS idx_blog_patterns_type ON blog_learned_patterns(pattern_type);
|
||
CREATE INDEX IF NOT EXISTS idx_blog_patterns_class ON blog_learned_patterns(performance_class);
|
||
CREATE INDEX IF NOT EXISTS idx_blog_patterns_active ON blog_learned_patterns(active) WHERE active = TRUE;
|
||
|
||
-- ─────────────────────────────────────────────────────────
|
||
-- SLL State: current active learning snapshot (weekly)
|
||
-- ─────────────────────────────────────────────────────────
|
||
CREATE TABLE IF NOT EXISTS blog_sll_state (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
week_start DATE NOT NULL UNIQUE,
|
||
|
||
-- Top-performing patterns (JSON arrays of strings)
|
||
winner_patterns JSONB DEFAULT '[]',
|
||
loser_patterns JSONB DEFAULT '[]',
|
||
|
||
-- Topic performance
|
||
top_topics JSONB DEFAULT '[]',
|
||
avoid_topics JSONB DEFAULT '[]',
|
||
|
||
-- Length optimization
|
||
optimal_length_min INTEGER,
|
||
optimal_length_max INTEGER,
|
||
|
||
-- Hook patterns that convert
|
||
best_hook_patterns JSONB DEFAULT '[]',
|
||
|
||
-- Meta
|
||
posts_analyzed INTEGER DEFAULT 0,
|
||
avg_engagement_score NUMERIC,
|
||
generated_at TIMESTAMPTZ DEFAULT NOW(),
|
||
generated_by TEXT DEFAULT 'sll-auto'
|
||
);
|
||
|
||
-- ─────────────────────────────────────────────────────────
|
||
-- View: performance enriched with blog metadata
|
||
-- ─────────────────────────────────────────────────────────
|
||
CREATE OR REPLACE VIEW v_blog_performance AS
|
||
SELECT
|
||
p.id,
|
||
p.blog_id,
|
||
d.title,
|
||
d.topic,
|
||
d.pipeline_version,
|
||
d.word_count,
|
||
p.platform,
|
||
p.impressions,
|
||
p.comments,
|
||
p.shares,
|
||
p.saves,
|
||
p.likes,
|
||
p.engagement_score,
|
||
p.hook_text,
|
||
p.blog_type,
|
||
p.posted_at,
|
||
p.measured_at,
|
||
-- Performance tier based on engagement score
|
||
CASE
|
||
WHEN p.engagement_score >= 20 THEN 'gold'
|
||
WHEN p.engagement_score >= 10 THEN 'silver'
|
||
WHEN p.engagement_score >= 4 THEN 'bronze'
|
||
ELSE 'miss'
|
||
END AS performance_tier
|
||
FROM blog_performance p
|
||
JOIN blog_drafts d ON d.id = p.blog_id;
|
||
|
||
COMMENT ON TABLE blog_performance IS 'SLL v1.0: LinkedIn engagement tracking per blog post';
|
||
COMMENT ON TABLE blog_learned_patterns IS 'SLL v1.0: Extracted winning/losing content patterns';
|
||
COMMENT ON TABLE blog_sll_state IS 'SLL v1.0: Weekly learning state snapshot for pipeline injection';
|