ADR-001: Arquitetura de Dados Medallion com Feature Store JSONB¶
- Status: Proposta
- Data: 2026-03-01
- Autor: Nitai Bezerra
Contexto¶
A plataforma DGB está entrando numa fase de expansão onde novos metadados (features) serão criados para cada notícia — derivados de IA (sentimento, entidades, legibilidade), métricas analíticas (trending, engajamento), e relações entre artigos (similaridade, clusters temáticos).
Atualmente o PostgreSQL (Cloud SQL, db-f1-micro, ~300k artigos) concentra todas as responsabilidades: dados brutos, enriquecidos, embeddings e queries operacionais. A tabela news já possui ~25 colunas e cada feature nova exigiria ALTER TABLE ADD COLUMN, tornando o schema rígido.
Problemas identificados¶
- Schema acoplado a features: cada feature nova requer migração DDL na tabela
news - Sem separação analítico/operacional: o PG serve tanto o pipeline operacional quanto qualquer análise
- Sem Data Lake: dados brutos do scraping são transformados in-place, sem preservação do estado original
- Dados de usuário inexistentes: sem infraestrutura para subscriptions personalizadas, preferências ou histórico de leitura
Decisão¶
Adotar uma arquitetura em camadas Medallion (Bronze / Silver / Gold) adaptada à escala e orçamento do DGB, com um Feature Store leve baseado em JSONB no PostgreSQL.
Camadas¶
┌─────────────────────────────────────────────────────────────────────┐
│ BRONZE — Dados brutos imutáveis │
│ GCS bucket (gs://dgb-data-lake/bronze/) │
│ JSON/Parquet particionado por data │
├─────────────────────────────────────────────────────────────────────┤
│ SILVER — Dados operacionais (fonte de verdade) │
│ PostgreSQL: news, agencies, themes + news_features (JSONB) │
│ Typesense: índice de busca full-text + vetorial │
├─────────────────────────────────────────────────────────────────────┤
│ GOLD — Dados analíticos e agregados │
│ BigQuery: fato_noticias, dimensões, métricas, trending │
│ GCS: exports Parquet (HuggingFace, snapshots) │
└─────────────────────────────────────────────────────────────────────┘
Fluxo de dados alvo¶
flowchart TB
subgraph INGESTAO["Ingestão"]
S[Scraper API]
end
subgraph BRONZE["Bronze — GCS"]
BW[Bronze Writer] --> GCS["gs://dgb-data-lake/bronze/"]
end
subgraph SILVER["Silver — Operacional"]
PG[(PostgreSQL)]
NF[news_features<br/>JSONB]
TS[(Typesense)]
end
subgraph PROC["Processamento"]
EW[Enrichment Worker<br/>temas + resumo +<br/>sentimento + entidades]
EMB[Embeddings API]
FW[Feature Worker<br/>word_count, readability]
PUSH[Push Notifications]
FED[Federation]
end
subgraph GOLD["Gold — Analítico"]
BQ[(BigQuery)]
HF[(HuggingFace)]
end
subgraph FUTURO["Futuro"]
FS[(Firestore<br/>dados de usuário)]
end
S --> PG
S -->|Pub/Sub scraped| BW
S -->|Pub/Sub scraped| EW
EW -->|Pub/Sub enriched| EMB
EW -->|Pub/Sub enriched| FW
EW -->|Pub/Sub enriched| PUSH
EW -->|Pub/Sub enriched| FED
EW --> PG
EMB --> PG
FW --> NF
FW -->|Pub/Sub featured| TS
PG --> TS
PG -->|DAG diária| BQ
PG -->|DAG diária| HF
BQ -->|trending score| NF
style BRONZE fill:#fff3e0
style SILVER fill:#e8f5e9
style GOLD fill:#e3f2fd
style FUTURO fill:#f3e5f5
Feature Store: tabela news_features (JSONB)¶
Em vez de adicionar colunas ao news, criar uma tabela lateral com schema flexível:
CREATE TABLE news_features (
unique_id TEXT PRIMARY KEY REFERENCES news(unique_id),
features JSONB NOT NULL DEFAULT '{}',
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_news_features_gin ON news_features USING GIN (features);
Exemplo de documento features:
{
"sentiment": {"score": 0.72, "label": "positive"},
"readability": {"flesch_ease": 45.2, "grade_level": 12},
"entities": [
{"text": "MEC", "type": "ORG"},
{"text": "Lula", "type": "PER"}
],
"word_count": 523,
"trending_score": 8.5,
"similar_articles": ["abc123", "def456"],
"topic_cluster_id": 42
}
Feature Registry¶
Arquivo feature_registry.yaml versionado no repositório data-platform para controlar quais features existem, suas versões e como são computadas:
features:
sentiment:
version: "1.0"
model: "bedrock/claude-haiku"
compute: "enrichment-worker"
readability:
version: "1.0"
model: "local/textstat"
compute: "feature-worker"
entities:
version: "1.0"
model: "bedrock/claude-haiku"
compute: "enrichment-worker"
word_count:
version: "1.0"
model: "local/python"
compute: "feature-worker"
trending_score:
version: "1.0"
model: "bigquery/sql"
compute: "airflow-dag"
Alternativas consideradas¶
1. MongoDB/Firestore como banco principal de features¶
- Prós: schema-less nativo, escalabilidade horizontal
- Contras: custo de operar mais um banco, perda de JOINs transacionais com
news, complexidade operacional - Veredicto: descartado para o volume atual (~300k registros). JSONB no PostgreSQL oferece as mesmas vantagens de schema flexível com zero custo incremental
2. Adicionar colunas diretamente na tabela news¶
- Prós: simples, sem tabela extra
- Contras: schema cada vez mais largo, migrações DDL frequentes, acoplamento entre features e dados core
- Veredicto: descartado — não escala para dezenas de features com schemas heterogêneos
3. Vertex AI Feature Store¶
- Prós: solução gerenciada do GCP, integração nativa com ML pipelines
- Contras: over-engineering para o volume, custo alto, complexidade desnecessária
- Veredicto: descartado — o padrão PG + GCS + BQ é suficiente para o estágio atual
4. BigQuery como armazenamento único (sem GCS intermediário)¶
- Prós: menos componentes
- Contras: storage 10x mais caro que GCS, sem dados brutos imutáveis, BigQuery lê Parquet do GCS nativamente via external tables
- Veredicto: GCS como Data Lake bronze é mais econômico e flexível
Plano de implementação¶
Fase 0 — Fundação (~1 semana, custo: $0)¶
- Criar tabela
news_featurescom índice GIN - Criar bucket GCS
dgb-data-lakevia Terraform (lifecycle: Standard → Nearline 90d → Coldline 365d) - Criar
feature_registry.yaml - Habilitar BigQuery API
Fase 1 — Features locais + Bronze (~2-3 semanas, custo: +$1-2/mês)¶
- Bronze Writer (Cloud Function): subscriber de
dgb.news.scraped→ grava JSON no GCS - Feature Worker (Cloud Function): subscriber de
dgb.news.enriched→ computa features locais (word_count, readability, has_numbers, paragraph_count, publication_hour, day_of_week) - Estender Enrichment Worker com sentiment e entities (custo marginal, já paga pela chamada ao Haiku)
- Novo topic Pub/Sub:
dgb.news.featured
Fase 2 — BigQuery Analytics (~3-4 semanas, custo: +$0-5/mês no free tier)¶
- Dataset
dgb_goldcom tabelas fato e dimensão - DAG Airflow: PG → Parquet (GCS silver/) → BigQuery load (incremental diário)
- External tables sobre dados bronze do GCS
- Views materializadas: métricas diárias por agência, trending
Fase 3 — Features avançadas (~4-6 semanas, custo: +$2-5/mês)¶
- Trending score via BigQuery → atualização periódica em
news_features→ Typesense - Topic clusters via pgvector similarity + HDBSCAN
- Métricas de engajamento: portal → Pub/Sub → BigQuery
Fase 4 — Dados de usuário (~6-8 semanas, custo: +$0-2/mês no free tier)¶
- Firestore (Native mode) para
users,preferences,reading_history - API de preferências no portal
- Personalização baseada em histórico
Consequências¶
Positivas¶
- Extensibilidade: novas features são apenas novos campos JSONB — sem migrações DDL
- Separação de responsabilidades: operacional (PG/Typesense), analítico (BigQuery), histórico (GCS)
- Custo controlado: incremento de ~$2-4/mês sobre os ~$230-280/mês atuais, aproveitando free tiers
- Interoperabilidade: BigQuery lê GCS nativamente, Pub/Sub alimenta todas as camadas
- Preservação de dados: camada bronze garante dados brutos imutáveis para reprocessamento
Negativas¶
- Mais componentes para operar: GCS, BigQuery, Cloud Functions adicionados ao stack
- Latência para features analíticas: trending score atualizado a cada 6h, não em tempo real
- JSONB não é tipado: validação de features depende do Feature Registry (convenção, não enforcement do banco)
Riscos e mitigações¶
| Risco | Mitigação |
|---|---|
| JSONB fica lento com volume grande | Monitorar; se ultrapassar 2M registros, avaliar migração para Firestore |
| BigQuery free tier não ser suficiente | 1TB queries/mês é ~10x o necessário hoje; monitorar via billing alerts |
| Feature Registry ficar desatualizado | CI check: validar que toda feature em news_features está registrada no YAML |