diff --git a/_plan/atualizacao-arquitetura-postgresql.md b/_plan/atualizacao-arquitetura-postgresql.md
new file mode 100644
index 0000000..be24c2f
--- /dev/null
+++ b/_plan/atualizacao-arquitetura-postgresql.md
@@ -0,0 +1,246 @@
+# Plano de Atualização da Documentação - DestaquesGovBr
+
+## Contexto da Mudança Arquitetural
+
+### Antes (Documentação Atual)
+- **Source of Truth**: HuggingFace Dataset
+- **Repositórios**: `scraper`, `typesense` (separados)
+- **Fluxo**: Sites → Scraper → HuggingFace → Cogfy → HuggingFace → Typesense
+
+### Agora (Nova Arquitetura)
+- **Source of Truth**: PostgreSQL (Cloud SQL)
+- **Repositórios**: `data-platform` (unificado) - repos antigos arquivados
+- **Fluxo**: Sites → Scraper → PostgreSQL (+HF dual-write) → Cogfy → PostgreSQL → Typesense / HuggingFace (sync)
+- **Novos componentes**: Embeddings API, Cloud Composer (Airflow), DAGs
+
+### Decisões do Usuário
+- [x] Criar novos arquivos + atualizar existentes
+- [x] Remover todas referências aos repos arquivados (scraper, typesense)
+- [x] Atualizar completamente as trilhas de onboarding
+
+---
+
+## Arquivos a Criar (Novos)
+
+| Arquivo | Conteúdo |
+|---------|----------|
+| `docs/modulos/data-platform.md` | Visão geral do repo unificado, estrutura, CLI |
+| `docs/arquitetura/postgresql.md` | Schema detalhado (5 tabelas), índices, normalização |
+| `docs/workflows/airflow-dags.md` | DAGs do Cloud Composer (sync HF, etc) |
+
+---
+
+## Arquivos a Modificar
+
+### 1. Arquitetura (Alta Prioridade)
+
+| Arquivo | Mudanças |
+|---------|----------|
+| `docs/index.md` | Novo diagrama com PostgreSQL central, atualizar descrição |
+| `docs/arquitetura/visao-geral.md` | Redesenhar diagramas Mermaid, PostgreSQL como camada 3 |
+| `docs/arquitetura/fluxo-de-dados.md` | Reescrever 8 etapas (incluindo embeddings e sync HF) |
+| `docs/arquitetura/componentes-estruturantes.md` | HuggingFace → "distribuição", nova seção PostgreSQL |
+
+### 2. Módulos (Alta Prioridade)
+
+| Arquivo | Mudanças |
+|---------|----------|
+| `docs/modulos/scraper.md` | Repo → data-platform, CLI, StorageAdapter, PostgresManager |
+| `docs/modulos/typesense-local.md` | Repo → data-platform, lê do PostgreSQL, embeddings |
+| `docs/modulos/cogfy-integracao.md` | Enrichment escreve no PostgreSQL |
+
+### 3. Workflows (Média Prioridade)
+
+| Arquivo | Mudanças |
+|---------|----------|
+| `docs/workflows/scraper-pipeline.md` | Novo repo, dual-write PostgreSQL |
+| `docs/workflows/typesense-data.md` | Sync lê do PostgreSQL, inclui embeddings |
+
+### 4. Infraestrutura (Média Prioridade)
+
+| Arquivo | Mudanças |
+|---------|----------|
+| `docs/infraestrutura/arquitetura-gcp.md` | Adicionar Cloud SQL, Composer, Embeddings API, custos |
+
+### 5. Onboarding (23 arquivos)
+
+| Arquivo | Mudanças Necessárias |
+|---------|---------------------|
+| `setup-backend.md` | Atualizar refs ao data-platform |
+| `setup-datascience.md` | Atualizar conexão PostgreSQL |
+| `setup-devvm.md` | Verificar refs a repos |
+| `airflow-tutorial.md` | Verificar se alinhado com Composer 3 |
+| `roteiro-onboarding.md` | Atualizar links e estrutura |
+| `troubleshooting.md` | Adicionar PostgreSQL |
+| `ds/explorando-dataset/*.md` | Atualizar fonte de dados (PostgreSQL vs HF) |
+| `ds/nlp-pipeline/*.md` | Verificar embeddings e busca semântica |
+| `ds/ml-classificacao/*.md` | Verificar pipeline |
+| `ds/qualidade-dados/*.md` | Verificar pipeline |
+
+---
+
+## Detalhamento dos Novos Arquivos
+
+### `docs/modulos/data-platform.md`
+
+```markdown
+# Data Platform
+
+Repositório unificado para toda a infraestrutura de dados do DestaquesGovBr.
+
+## Componentes Principais
+- **Scraper**: Coleta de notícias (gov.br + EBC)
+- **Enrichment**: Integração com Cogfy (temas + resumos)
+- **Embeddings**: Geração de vetores 768-dim
+- **Typesense Sync**: Indexação para busca
+- **HuggingFace Sync**: Distribuição de dados abertos
+
+## CLI
+- `data-platform scrape`
+- `data-platform enrich`
+- `data-platform sync-typesense`
+- `data-platform generate-embeddings`
+
+## Storage Adapter
+Modos: POSTGRES, HUGGINGFACE, DUAL_WRITE
+```
+
+### `docs/arquitetura/postgresql.md`
+
+```markdown
+# PostgreSQL - Fonte de Verdade
+
+## Schema (5 tabelas)
+- agencies (158 registros)
+- themes (200+ registros, 3 níveis)
+- news (300k+ registros)
+- sync_log (auditoria)
+
+## Configuração Cloud SQL
+- PostgreSQL 15
+- 1 vCPU, 3.75GB RAM
+- 50GB SSD (auto-resize até 500GB)
+- Backups diários, PITR 7 dias
+```
+
+### `docs/workflows/airflow-dags.md`
+
+```markdown
+# DAGs do Cloud Composer
+
+## sync_postgres_to_huggingface
+- Execução diária
+- Incremental via parquet shards
+- Consulta IDs existentes via Dataset Viewer API
+```
+
+---
+
+## Mudanças Detalhadas nos Diagramas
+
+### Diagrama Principal (`index.md`)
+
+**Atual:**
+```
+160+ Sites gov.br → Scraper → Cogfy/LLM → HuggingFace → Typesense → Portal
+```
+
+**Novo:**
+```mermaid
+graph LR
+ A[160+ Sites gov.br] --> B[Scraper]
+ B --> C[(PostgreSQL)]
+ C --> D[Cogfy/LLM]
+ D --> C
+ C --> E[Embeddings API]
+ E --> C
+ C --> F[Typesense]
+ F --> G[Portal Next.js]
+ C --> H{DAG Airflow}
+ H --> I[(HuggingFace)]
+```
+
+### Diagrama de Arquitetura (`visao-geral.md`)
+
+**Camadas atualizadas:**
+1. **Coleta**: Scraper (data-platform)
+2. **Enriquecimento**: Cogfy LLM
+3. **Armazenamento**: PostgreSQL (fonte de verdade)
+4. **Processamento**: Embeddings API
+5. **Indexação**: Typesense (com vectors)
+6. **Distribuição**: HuggingFace (dados abertos)
+7. **Apresentação**: Portal Next.js
+
+---
+
+## Ordem de Execução
+
+### Fase 1 - Novos Arquivos
+1. Criar `docs/modulos/data-platform.md`
+2. Criar `docs/arquitetura/postgresql.md`
+3. Criar `docs/workflows/airflow-dags.md`
+
+### Fase 2 - Arquitetura Core
+1. `docs/index.md`
+2. `docs/arquitetura/visao-geral.md`
+3. `docs/arquitetura/fluxo-de-dados.md`
+4. `docs/arquitetura/componentes-estruturantes.md`
+
+### Fase 3 - Módulos
+1. `docs/modulos/scraper.md`
+2. `docs/modulos/typesense-local.md`
+3. `docs/modulos/cogfy-integracao.md`
+
+### Fase 4 - Workflows e Infra
+1. `docs/workflows/scraper-pipeline.md`
+2. `docs/workflows/typesense-data.md`
+3. `docs/infraestrutura/arquitetura-gcp.md`
+
+### Fase 5 - Onboarding
+1. `docs/onboarding/roteiro-onboarding.md`
+2. `docs/onboarding/setup-backend.md`
+3. `docs/onboarding/setup-datascience.md`
+4. Trilha DS (explorando-dataset, nlp-pipeline, etc.)
+5. Demais arquivos
+
+---
+
+## Atualizar `mkdocs.yml`
+
+Adicionar novos arquivos na navegação:
+
+```yaml
+nav:
+ - Arquitetura:
+ - PostgreSQL: arquitetura/postgresql.md # NOVO
+ - Módulos:
+ - Data Platform: modulos/data-platform.md # NOVO
+ - Workflows:
+ - Airflow DAGs: workflows/airflow-dags.md # NOVO
+```
+
+---
+
+## Verificação Final
+
+1. `mkdocs serve` - verificar renderização local
+2. Validar todos os links internos
+3. Testar diagramas Mermaid
+4. Buscar referências restantes a "scraper" ou "typesense" (repos antigos)
+5. Verificar consistência: "PostgreSQL" como "fonte de verdade"
+6. Verificar que "HuggingFace" está descrito como "distribuição"
+
+---
+
+## Arquivos Críticos (Leitura Recomendada Durante Implementação)
+
+### data-platform
+- `src/data_platform/managers/postgres_manager.py` - Acesso PostgreSQL
+- `src/data_platform/managers/storage_adapter.py` - Dual-write
+- `src/data_platform/jobs/typesense/sync_job.py` - Sync Typesense
+- `src/data_platform/dags/sync_postgres_to_huggingface.py` - DAG HF
+
+### infra
+- `terraform/cloud_sql.tf` - Config PostgreSQL
+- `terraform/composer.tf` - Config Airflow
+- `terraform/embeddings-api.tf` - Config Embeddings
diff --git a/docs/arquitetura/componentes-estruturantes.md b/docs/arquitetura/componentes-estruturantes.md
index 862457d..36163e6 100644
--- a/docs/arquitetura/componentes-estruturantes.md
+++ b/docs/arquitetura/componentes-estruturantes.md
@@ -1,10 +1,11 @@
# Componentes Estruturantes
-O DestaquesGovbr possui três componentes estruturantes que definem a organização dos dados:
+O DestaquesGovbr possui quatro componentes estruturantes que definem a organização dos dados:
1. **Árvore Temática** - Classificação hierárquica de notícias
-2. **Catálogo de Órgãos** - Lista de 156 agências governamentais
-3. **Dataset HuggingFace** - Fonte de verdade dos dados
+2. **Catálogo de Órgãos** - Lista de 158 agências governamentais
+3. **PostgreSQL** - Fonte de verdade dos dados
+4. **HuggingFace** - Distribuição de dados abertos
## 1. Árvore Temática
@@ -54,14 +55,15 @@ Nível 1 (Tema) → Nível 2 (Subtema) → Nível 3 (Tópico)
### Arquivos
-A árvore temática está duplicada em dois repositórios (sincronização manual por enquanto):
+A árvore temática está armazenada no PostgreSQL e duplicada em repositórios (sincronização via pipeline):
-| Repositório | Arquivo | Formato |
-|-------------|---------|---------|
-| scraper | `src/enrichment/themes_tree.yaml` | YAML plano |
+| Local | Arquivo/Tabela | Formato |
+|-------|----------------|---------|
+| PostgreSQL | tabela `themes` | Normalizado (200+ registros) |
+| data-platform | `src/data_platform/enrichment/themes_tree.yaml` | YAML plano |
| portal | `src/lib/themes.yaml` | YAML estruturado |
-#### Formato no Scraper (`themes_tree.yaml`)
+#### Formato no Data Platform (`themes_tree.yaml`)
```yaml
01 - Economia e Finanças:
01.01 - Política Econômica:
@@ -122,13 +124,14 @@ Cada órgão possui:
### Arquivos
-| Repositório | Arquivo | Conteúdo |
-|-------------|---------|----------|
-| agencies | `agencies.yaml` | Dados dos 156 órgãos |
+| Local | Arquivo/Tabela | Conteúdo |
+|-------|----------------|----------|
+| PostgreSQL | tabela `agencies` | Dados dos 158 órgãos (normalizado) |
+| agencies | `agencies.yaml` | Dados fonte dos órgãos |
| agencies | `hierarchy.yaml` | Árvore hierárquica |
| portal | `src/lib/agencies.yaml` | Cópia (sincronização manual) |
-| scraper | `src/scraper/agencies.yaml` | Mapeamento ID → Nome |
-| scraper | `src/scraper/site_urls.yaml` | URLs de raspagem |
+| data-platform | `src/data_platform/scrapers/agencies.yaml` | Mapeamento ID → Nome |
+| data-platform | `src/data_platform/scrapers/site_urls.yaml` | URLs de raspagem |
### Exemplo de Entrada
@@ -192,21 +195,42 @@ Automatizar sincronização:
---
-## 3. Dataset HuggingFace
+## 3. PostgreSQL (Fonte de Verdade)
### Visão Geral
-O dataset no HuggingFace é a **fonte de verdade** de todos os dados do sistema.
+O PostgreSQL (Cloud SQL) é a **fonte de verdade central** de todos os dados do sistema.
-**URL**: [huggingface.co/datasets/nitaibezerra/govbrnews](https://huggingface.co/datasets/nitaibezerra/govbrnews)
+**Instância**: `destaquesgovbr-postgres` (southamerica-east1)
### Características
| Característica | Valor |
|----------------|-------|
| Documentos | ~300.000+ |
+| Tabelas | 4 (agencies, themes, news, sync_log) |
| Atualização | Diária (4AM UTC) |
-| Formato | Parquet + CSV |
+| Backup | Diário com PITR 7 dias |
+
+→ Veja detalhes completos em [postgresql.md](postgresql.md)
+
+---
+
+## 4. HuggingFace (Distribuição)
+
+### Visão Geral
+
+O dataset no HuggingFace é a **camada de distribuição** de dados abertos, sincronizada diariamente a partir do PostgreSQL.
+
+**URL**: [huggingface.co/datasets/nitaibezerra/govbrnews](https://huggingface.co/datasets/nitaibezerra/govbrnews)
+
+### Características
+
+| Característica | Valor |
+|----------------|-------|
+| Documentos | ~300.000+ |
+| Sincronização | Diária (6AM UTC via Airflow) |
+| Formato | Parquet (shards incrementais) |
| Versionamento | Automático pelo HF |
### Schema do Dataset
@@ -285,11 +309,10 @@ govbrnews/
### Uso no Sistema
-1. **Scraper**: Insere e atualiza registros
-2. **Typesense**: Indexa para busca full-text
-3. **Portal**: Consome via Typesense
-4. **Streamlit**: Análises e visualizações
-5. **Pesquisadores**: Download para análises externas
+1. **Comunidade**: Download para análises externas
+2. **Pesquisadores**: Estudos acadêmicos
+3. **Streamlit**: Visualizações públicas
+4. **Dados abertos**: Acesso transparente aos dados governamentais
### Dataset Reduzido
@@ -305,40 +328,50 @@ Existe também uma versão reduzida com apenas 4 colunas para análises rápidas
graph TB
subgraph "Componentes Estruturantes"
AT[Árvore Temática
25 temas × 3 níveis]
- CO[Catálogo de Órgãos
156 agências]
- DS[Dataset HuggingFace
~300k docs]
+ CO[Catálogo de Órgãos
158 agências]
+ PG[(PostgreSQL
Fonte de Verdade)]
+ HF[(HuggingFace
Distribuição)]
end
subgraph "Uso no Sistema"
SC[Scraper]
CF[Cogfy]
- PO[Portal]
+ EMB[Embeddings]
TS[Typesense]
+ PO[Portal]
end
CO -->|URLs de raspagem| SC
AT -->|Classificação| CF
- SC -->|Insere dados| DS
- CF -->|Enriquece| DS
- DS -->|Indexa| TS
+ SC -->|Insere dados| PG
+ CF -->|Enriquece| PG
+ EMB -->|Vetores| PG
+ PG -->|Indexa| TS
+ PG -->|Sync diário| HF
TS -->|Busca| PO
AT -->|Filtros| PO
CO -->|Filtros| PO
```
-## Sincronização Atual vs Futura
+## Sincronização
-### Atual (Manual)
+### Dados (Automatizado)
+```
+Scraper → PostgreSQL → Typesense (sync diário)
+ → HuggingFace (DAG Airflow 6AM UTC)
+```
+
+### Componentes Estruturantes (Manual)
```
agencies.yaml → Cópia manual → portal/agencies.yaml
- → scraper/agencies.yaml
- → scraper/site_urls.yaml
+ → data-platform/agencies.yaml
themes_tree.yaml → Cópia manual → portal/themes.yaml
+ → PostgreSQL (via script)
```
-### Futuro (Automatizado)
+### Meta Futura
```
-agencies → GitHub Action → portal + scraper
-destaquesgovbr-themes → GitHub Action → portal + scraper
+agencies repo → GitHub Action → portal + data-platform + PostgreSQL
+themes repo → GitHub Action → portal + data-platform + PostgreSQL
```
diff --git a/docs/arquitetura/fluxo-de-dados.md b/docs/arquitetura/fluxo-de-dados.md
index d7df0ab..9dbe9a5 100644
--- a/docs/arquitetura/fluxo-de-dados.md
+++ b/docs/arquitetura/fluxo-de-dados.md
@@ -12,9 +12,12 @@ sequenceDiagram
participant SC as Scraper Container
participant GOV as Sites gov.br
participant EBC as Sites EBC
- participant HF as HuggingFace
+ participant PG as PostgreSQL
participant CF as Cogfy API
+ participant EMB as Embeddings API
participant TS as Typesense
+ participant AF as Airflow (6AM UTC)
+ participant HF as HuggingFace
rect rgb(227, 242, 253)
Note over GH,SC: ETAPA 1: Scraping gov.br
@@ -23,7 +26,7 @@ sequenceDiagram
GOV-->>SC: HTML das páginas
SC->>SC: Parse HTML → Markdown
SC->>SC: Gera unique_id (MD5)
- SC->>HF: dataset.insert(new_articles)
+ SC->>PG: postgres.insert(new_articles)
end
rect rgb(255, 243, 224)
@@ -31,13 +34,13 @@ sequenceDiagram
SC->>EBC: Requisições HTTP (sites EBC)
EBC-->>SC: HTML das páginas
SC->>SC: Parse especializado EBC
- SC->>HF: dataset.insert(ebc_articles, allow_update=True)
+ SC->>PG: postgres.insert(ebc_articles, allow_update=True)
end
rect rgb(255, 253, 231)
Note over SC,CF: ETAPA 3: Upload para Cogfy
- SC->>HF: dataset.get(date_range)
- HF-->>SC: DataFrame com artigos
+ SC->>PG: get_news(date_range)
+ PG-->>SC: Registros
SC->>CF: POST /records (batch 1000)
CF-->>SC: IDs dos registros
end
@@ -50,21 +53,38 @@ sequenceDiagram
end
rect rgb(252, 228, 236)
- Note over SC,HF: ETAPA 5: Enriquecimento
+ Note over SC,PG: ETAPA 5: Enriquecimento
SC->>CF: GET /records (busca por unique_id)
CF-->>SC: themes + summary
SC->>SC: Mapeia códigos → labels
SC->>SC: Calcula most_specific_theme
- SC->>HF: dataset.update(enriched_df)
+ SC->>PG: postgres.update(enriched_data)
+ end
+
+ rect rgb(255, 248, 225)
+ Note over SC,EMB: ETAPA 6: Embeddings
+ SC->>PG: get_news_without_embeddings()
+ PG-->>SC: Notícias sem vetores
+ SC->>EMB: POST /embed (batch 100)
+ EMB-->>SC: Vetores 768-dim
+ SC->>PG: postgres.update(embeddings)
end
rect rgb(243, 229, 245)
- Note over TS,HF: ETAPA 6: Indexação Typesense
- GH->>TS: Trigger typesense-daily-load
- TS->>HF: Download dataset
- HF-->>TS: Parquet files
+ Note over TS,PG: ETAPA 7: Indexação Typesense
+ GH->>TS: Trigger typesense-sync
+ TS->>PG: iter_news_for_typesense()
+ PG-->>TS: Batches de 5000
TS->>TS: Upsert documentos
end
+
+ rect rgb(225, 245, 254)
+ Note over AF,HF: ETAPA 8: Sync HuggingFace
+ AF->>PG: Query novos registros
+ PG-->>AF: Registros do dia anterior
+ AF->>AF: Cria parquet shard
+ AF->>HF: Upload shard
+ end
```
## Etapas Detalhadas
@@ -74,19 +94,19 @@ sequenceDiagram
**Workflow**: `main-workflow.yaml` → job `scraper`
```bash
-python src/main.py scrape --start-date YYYY-MM-DD --end-date YYYY-MM-DD
+data-platform scrape --start-date YYYY-MM-DD --end-date YYYY-MM-DD
```
**Processo**:
-1. Carrega URLs de `src/scraper/site_urls.yaml` (~160+ URLs)
+1. Carrega URLs de `src/data_platform/scrapers/site_urls.yaml` (~160+ URLs)
2. Para cada URL, instancia `WebScraper`
3. Navega por páginas com paginação (`?b_start:int=N`)
4. Extrai campos: title, date, url, image, category, tags
5. Faz fetch do conteúdo completo de cada notícia
6. Converte HTML → Markdown com `markdownify`
7. Gera `unique_id = MD5(agency + published_at + title)`
-8. Insere no HuggingFace via `DatasetManager.insert()`
+8. Insere no PostgreSQL via `PostgresManager.insert()`
**Retry Logic**:
```python
@@ -99,7 +119,7 @@ def fetch_page(url): ...
**Workflow**: `main-workflow.yaml` → job `ebc-scraper`
```bash
-python src/main.py scrape-ebc --start-date YYYY-MM-DD --end-date YYYY-MM-DD --allow-update
+data-platform scrape-ebc --start-date YYYY-MM-DD --end-date YYYY-MM-DD --allow-update
```
**Diferenças**:
@@ -113,12 +133,12 @@ python src/main.py scrape-ebc --start-date YYYY-MM-DD --end-date YYYY-MM-DD --al
**Workflow**: `main-workflow.yaml` → job `upload-to-cogfy`
```bash
-python src/upload_to_cogfy_manager.py --start-date YYYY-MM-DD --end-date YYYY-MM-DD
+data-platform upload-cogfy --start-date YYYY-MM-DD --end-date YYYY-MM-DD
```
**Processo**:
-1. Carrega artigos do HuggingFace por intervalo de datas
+1. Carrega artigos do PostgreSQL por intervalo de datas
2. Converte campos para formato Cogfy:
- `published_at` → datetime UTC
@@ -142,7 +162,7 @@ O Cogfy executa:
**Workflow**: `main-workflow.yaml` → job `enrich-themes`
```bash
-python src/enrichment_manager.py --start-date YYYY-MM-DD --end-date YYYY-MM-DD
+data-platform enrich --start-date YYYY-MM-DD --end-date YYYY-MM-DD
```
**Processo**:
@@ -156,21 +176,51 @@ python src/enrichment_manager.py --start-date YYYY-MM-DD --end-date YYYY-MM-DD
- `theme_1_level_3` (text) → código e label
- `summary` (text)
4. Calcula `most_specific_theme` (prioridade: L3 > L2 > L1)
-5. Atualiza dataset no HuggingFace
+5. Atualiza PostgreSQL
-### Etapa 6: Indexação Typesense
+### Etapa 6: Embeddings
-**Workflow**: `typesense-daily-load.yml` (10AM UTC)
+**Workflow**: `main-workflow.yaml` → job `generate-embeddings`
```bash
-python python/scripts/load_data.py --mode incremental --days 7
+data-platform generate-embeddings --start-date YYYY-MM-DD
+```
+
+**Processo**:
+
+1. Busca notícias sem embeddings no PostgreSQL
+2. Prepara texto: `title + summary` (fallback para `content`)
+3. Envia para Embeddings API em batches de 100
+4. Recebe vetores 768-dim do modelo `paraphrase-multilingual-mpnet-base-v2`
+5. Atualiza `content_embedding` no PostgreSQL
+
+### Etapa 7: Indexação Typesense
+
+**Workflow**: `typesense-maintenance-sync.yaml` (10AM UTC)
+
+```bash
+data-platform sync-typesense --start-date YYYY-MM-DD
```
**Processo**:
1. Conecta ao Typesense em produção
-2. Baixa dataset do HuggingFace (últimos 7 dias)
-3. Faz upsert dos documentos na collection `news`
+2. Lê dados do PostgreSQL em batches de 5000
+3. Faz upsert dos documentos na collection `news` (incluindo embeddings)
+
+### Etapa 8: Sync HuggingFace
+
+**DAG Airflow**: `sync_postgres_to_huggingface` (6AM UTC)
+
+**Processo**:
+
+1. Query notícias do dia anterior no PostgreSQL
+2. Consulta IDs existentes no HuggingFace via Dataset Viewer API
+3. Filtra apenas novos registros
+4. Cria parquet shard com novos dados
+5. Upload do shard para HuggingFace
+
+→ Veja detalhes em [workflows/airflow-dags.md](../workflows/airflow-dags.md)
## Dados de Entrada e Saída
@@ -188,12 +238,15 @@ python python/scripts/load_data.py --mode incremental --days 7
```
-### Saída (HuggingFace Dataset)
+### Saída (PostgreSQL / News)
```json
{
+ "id": 123456,
"unique_id": "abc123def456",
- "agency": "gestao",
+ "agency_id": 45,
+ "agency_key": "gestao",
+ "agency_name": "Ministério da Gestão",
"published_at": "2024-12-02T10:00:00Z",
"updated_datetime": "2024-12-02T14:30:00Z",
"extracted_at": "2024-12-02T07:00:00Z",
@@ -202,19 +255,17 @@ python python/scripts/load_data.py --mode incremental --days 7
"editorial_lead": "Linha fina com contexto",
"url": "https://www.gov.br/gestao/...",
"content": "# Título\n\nConteúdo em Markdown...",
- "image": "https://www.gov.br/.../imagem.jpg",
+ "image_url": "https://www.gov.br/.../imagem.jpg",
"video_url": null,
"category": "Notícias",
"tags": ["tag1", "tag2"],
- "theme_1_level_1_code": "01",
- "theme_1_level_1_label": "Economia e Finanças",
- "theme_1_level_2_code": "01.01",
- "theme_1_level_2_label": "Política Econômica",
- "theme_1_level_3_code": "01.01.01",
- "theme_1_level_3_label": "Política Fiscal",
- "most_specific_theme_code": "01.01.01",
- "most_specific_theme_label": "Política Fiscal",
- "summary": "Resumo gerado por AI..."
+ "theme_l1_id": 1,
+ "theme_l2_id": 5,
+ "theme_l3_id": 15,
+ "most_specific_theme_id": 15,
+ "summary": "Resumo gerado por AI...",
+ "content_embedding": [0.123, -0.456, ...], // 768 dimensões
+ "embedding_generated_at": "2024-12-02T08:00:00Z"
}
```
@@ -229,9 +280,14 @@ python python/scripts/load_data.py --mode incremental --days 7
- Verificação de status antes de buscar resultados
- Fallback para valores vazios se inferência falhar
-### HuggingFace
-- Retry em push (5 tentativas)
-- Deduplicação por `unique_id`
+### PostgreSQL
+- Connection pooling com retry
+- Deduplicação por `unique_id` (ON CONFLICT)
+- Transações para operações batch
+
+### HuggingFace (Sync)
+- Incremental via parquet shards
+- Deduplicação via Dataset Viewer API
## Monitoramento
@@ -249,18 +305,35 @@ python python/scripts/load_data.py --mode incremental --days 7
### Scraping de período específico
```bash
+# Via CLI
+data-platform scrape --start-date 2024-01-01 --end-date 2024-01-31
+
# Via GitHub Actions
-gh workflow run scraper-dispatch.yaml \
- -f min-date=2024-01-01 \
- -f max-date=2024-01-31
+gh workflow run main-workflow.yaml \
+ -f start-date=2024-01-01 \
+ -f end-date=2024-01-31
```
-### Resync com Cogfy
+### Enriquecimento manual
```bash
-gh workflow run cogfy-sync-dispatch.yaml
+data-platform enrich --start-date 2024-01-01 --force
+```
+
+### Geração de embeddings
+```bash
+data-platform generate-embeddings --start-date 2024-01-01
+```
+
+### Sync Typesense
+```bash
+# Incremental
+data-platform sync-typesense --start-date 2024-01-01
+
+# Full reload
+data-platform sync-typesense --full-sync
```
-### Reload completo do Typesense
+### Reload completo do Typesense (via GitHub Actions)
```bash
gh workflow run typesense-full-reload.yaml \
-f confirm=DELETE
diff --git a/docs/arquitetura/postgresql.md b/docs/arquitetura/postgresql.md
new file mode 100644
index 0000000..e00a749
--- /dev/null
+++ b/docs/arquitetura/postgresql.md
@@ -0,0 +1,308 @@
+# PostgreSQL - Fonte de Verdade
+
+O PostgreSQL (Cloud SQL) é a **fonte de verdade central** do sistema DestaquesGovBr, armazenando todas as notícias coletadas, enriquecidas e processadas.
+
+!!! info "Cloud SQL"
+ **Instância**: `destaquesgovbr-postgres`
+ **Região**: `southamerica-east1` (São Paulo)
+ **Versão**: PostgreSQL 15
+
+## Schema
+
+O banco de dados possui **4 tabelas principais** com normalização parcial.
+
+```mermaid
+erDiagram
+ agencies ||--o{ news : "has"
+ themes ||--o{ news : "theme_l1"
+ themes ||--o{ news : "theme_l2"
+ themes ||--o{ news : "theme_l3"
+ themes ||--o{ news : "most_specific"
+
+ agencies {
+ int id PK
+ string key UK
+ string name
+ string type
+ string parent_key
+ string url
+ timestamp created_at
+ }
+
+ themes {
+ int id PK
+ string code UK
+ string label
+ string full_name
+ int level
+ string parent_code
+ timestamp created_at
+ }
+
+ news {
+ int id PK
+ string unique_id UK
+ int agency_id FK
+ int theme_l1_id FK
+ int theme_l2_id FK
+ int theme_l3_id FK
+ int most_specific_theme_id FK
+ string title
+ string url
+ text content
+ string summary
+ vector content_embedding
+ timestamp published_at
+ timestamp created_at
+ }
+
+ sync_log {
+ int id PK
+ string operation
+ string status
+ json details
+ timestamp created_at
+ }
+```
+
+### Tabela `agencies`
+
+Dados mestres de agências governamentais (158 registros).
+
+| Coluna | Tipo | Descrição |
+|--------|------|-----------|
+| `id` | `SERIAL` | Chave primária |
+| `key` | `VARCHAR(100)` | Identificador único (ex: `mec`, `saude`) |
+| `name` | `VARCHAR(255)` | Nome completo da agência |
+| `type` | `VARCHAR(50)` | Tipo (ministério, autarquia, etc) |
+| `parent_key` | `VARCHAR(100)` | Chave da agência pai (hierarquia) |
+| `url` | `VARCHAR(500)` | URL do portal da agência |
+| `created_at` | `TIMESTAMP` | Data de criação |
+
+**Índices**: `key` (unique), `parent_key`
+
+### Tabela `themes`
+
+Taxonomia hierárquica de temas em 3 níveis (200+ registros).
+
+| Coluna | Tipo | Descrição |
+|--------|------|-----------|
+| `id` | `SERIAL` | Chave primária |
+| `code` | `VARCHAR(20)` | Código hierárquico (ex: `01`, `01.01`, `01.01.01`) |
+| `label` | `VARCHAR(255)` | Nome curto do tema |
+| `full_name` | `VARCHAR(500)` | Nome completo incluindo hierarquia |
+| `level` | `INTEGER` | Nível hierárquico (1, 2 ou 3) |
+| `parent_code` | `VARCHAR(20)` | Código do tema pai |
+| `created_at` | `TIMESTAMP` | Data de criação |
+
+**Índices**: `code` (unique), `level`, `parent_code`
+
+**Exemplo de hierarquia**:
+```
+01 - Economia e Finanças (level 1)
+ 01.01 - Política Econômica (level 2)
+ 01.01.01 - Política Fiscal (level 3)
+```
+
+### Tabela `news`
+
+Notícias coletadas e enriquecidas (300k+ registros).
+
+| Coluna | Tipo | Descrição |
+|--------|------|-----------|
+| **Chaves** | | |
+| `id` | `SERIAL` | Chave primária |
+| `unique_id` | `VARCHAR(32)` | MD5(agency + published_at + title) |
+| **Foreign Keys** | | |
+| `agency_id` | `INTEGER` | FK para `agencies.id` |
+| `theme_l1_id` | `INTEGER` | Tema nível 1 |
+| `theme_l2_id` | `INTEGER` | Tema nível 2 |
+| `theme_l3_id` | `INTEGER` | Tema nível 3 |
+| `most_specific_theme_id` | `INTEGER` | Tema mais específico (L3 > L2 > L1) |
+| **Conteúdo** | | |
+| `title` | `VARCHAR(500)` | Título da notícia |
+| `url` | `VARCHAR(1000)` | URL original |
+| `image_url` | `VARCHAR(1000)` | URL da imagem |
+| `video_url` | `VARCHAR(1000)` | URL do vídeo |
+| `category` | `VARCHAR(100)` | Categoria original do site |
+| `tags` | `TEXT[]` | Tags originais do site |
+| `content` | `TEXT` | Conteúdo completo em Markdown |
+| `editorial_lead` | `TEXT` | Lead editorial |
+| `subtitle` | `VARCHAR(500)` | Subtítulo |
+| `summary` | `TEXT` | Resumo gerado pelo Cogfy |
+| **Timestamps** | | |
+| `published_at` | `TIMESTAMP` | Data de publicação |
+| `updated_datetime` | `TIMESTAMP` | Última atualização no site |
+| `extracted_at` | `TIMESTAMP` | Data de extração |
+| `created_at` | `TIMESTAMP` | Data de inserção no BD |
+| `updated_at` | `TIMESTAMP` | Última atualização no BD |
+| **Denormalizados** | | |
+| `agency_key` | `VARCHAR(100)` | Key da agência (performance) |
+| `agency_name` | `VARCHAR(255)` | Nome da agência (performance) |
+| **Embeddings** | | |
+| `content_embedding` | `VECTOR(768)` | Embedding do conteúdo |
+| `embedding_generated_at` | `TIMESTAMP` | Data de geração do embedding |
+
+**Índices**:
+
+- `unique_id` (unique)
+- `published_at DESC` (ordenação)
+- `agency_id` (filtro por agência)
+- `most_specific_theme_id` (filtro por tema)
+- `(agency_id, published_at DESC)` (composto)
+- `agency_key` (filtro rápido)
+- Full-text search: `tsvector('portuguese', title || ' ' || content)`
+
+### Tabela `sync_log`
+
+Log de operações de sincronização (auditoria).
+
+| Coluna | Tipo | Descrição |
+|--------|------|-----------|
+| `id` | `SERIAL` | Chave primária |
+| `operation` | `VARCHAR(50)` | Tipo (scrape, enrich, typesense_sync, hf_sync) |
+| `status` | `VARCHAR(20)` | Status (started, completed, failed) |
+| `details` | `JSONB` | Metadados (count, errors, duration) |
+| `created_at` | `TIMESTAMP` | Timestamp da operação |
+
+## Configuração Cloud SQL
+
+### Instância
+
+| Configuração | Valor |
+|--------------|-------|
+| **Nome** | `destaquesgovbr-postgres` |
+| **Versão** | PostgreSQL 15 |
+| **Tier** | `db-custom-1-3840` (1 vCPU, 3.75GB RAM) |
+| **Storage** | 50GB SSD (auto-resize até 500GB) |
+| **Região** | `southamerica-east1` |
+| **Availability** | ZONAL |
+| **Deletion Protection** | Habilitado |
+
+### Rede
+
+```mermaid
+graph LR
+ subgraph "VPC Network"
+ PG[(Cloud SQL
Private IP)]
+ CR[Cloud Run
Portal]
+ CE[Compute Engine
Typesense]
+ GHA[GitHub Actions
Cloud SQL Proxy]
+ end
+
+ CR -.->|VPC Connector| PG
+ CE -->|Internal IP| PG
+ GHA -->|SQL Proxy| PG
+```
+
+- **IP Privado**: Via VPC peering
+- **Acesso externo**: Apenas via Cloud SQL Proxy
+
+### Backup
+
+| Configuração | Valor |
+|--------------|-------|
+| **Backups automáticos** | Diários às 3AM UTC |
+| **Point-in-time recovery** | Habilitado (7 dias) |
+| **Retenção** | 30 backups |
+| **Auto-resize** | Até 500GB |
+
+### Performance Flags
+
+```sql
+-- Conexões
+max_connections = 100
+
+-- Memória
+shared_buffers = 256MB
+effective_cache_size = 336MB
+work_mem = 2.5MB
+maintenance_work_mem = 64MB
+
+-- Logging
+log_min_duration_statement = 1000 -- Log queries > 1s
+```
+
+### Query Insights
+
+- **Habilitado**: Sim
+- **Query string length**: 1024 caracteres
+- **Application tags**: Habilitadas
+
+## Secrets
+
+As credenciais são armazenadas no Secret Manager:
+
+| Secret ID | Conteúdo |
+|-----------|----------|
+| `govbrnews-postgres-connection-string` | `postgresql://user:pass@host:5432/govbrnews` |
+| `govbrnews-postgres-host` | IP privado da instância |
+| `govbrnews-postgres-password` | Senha do usuário `govbrnews_app` |
+
+## Acesso
+
+### Pelo Código (data-platform)
+
+```python
+from data_platform.managers.postgres_manager import PostgresManager
+
+pm = PostgresManager() # Lê DATABASE_URL ou Secret Manager
+pm.load_cache()
+
+# Operações
+news = pm.get(filters={"agency_key": "mec"}, limit=100)
+pm.insert(news_list, allow_update=True)
+```
+
+### Variáveis de Ambiente
+
+```bash
+# Conexão direta (desenvolvimento)
+DATABASE_URL=postgresql://govbrnews_app:xxx@10.x.x.x:5432/govbrnews
+
+# Ou via Secret Manager (produção)
+# A aplicação busca automaticamente se DATABASE_URL não estiver definida
+```
+
+### Via Cloud SQL Proxy (local)
+
+```bash
+# Instalar proxy
+curl -o cloud_sql_proxy https://dl.google.com/cloudsql/cloud_sql_proxy.linux.amd64
+chmod +x cloud_sql_proxy
+
+# Conectar
+./cloud_sql_proxy -instances=inspire-7-finep:southamerica-east1:destaquesgovbr-postgres=tcp:5432
+
+# Em outro terminal
+psql -h localhost -U govbrnews_app -d govbrnews
+```
+
+## Custos Estimados
+
+| Componente | Custo/mês |
+|------------|-----------|
+| Instância (db-custom-1-3840) | ~$35 |
+| Storage (50GB SSD) | ~$8.50 |
+| Backups (30 dias) | ~$5 |
+| **Total** | **~$48.50** |
+
+## Normalização Parcial
+
+O schema adota normalização **parcial** para balancear integridade e performance:
+
+### Normalizado
+
+- **agencies**: Tabela de lookup separada
+- **themes**: Taxonomia hierárquica com self-reference
+
+### Denormalizado (performance)
+
+- `agency_key` e `agency_name` em `news`: Evita JOINs em queries frequentes
+- Índice composto `(agency_id, published_at)`: Otimiza filtros combinados
+
+### Vantagens
+
+1. **Integridade**: FKs garantem consistência
+2. **Performance**: Campos denormalizados evitam JOINs
+3. **Flexibilidade**: Normalização permite queries complexas quando necessário
diff --git a/docs/arquitetura/visao-geral.md b/docs/arquitetura/visao-geral.md
index 0ff26bc..422694d 100644
--- a/docs/arquitetura/visao-geral.md
+++ b/docs/arquitetura/visao-geral.md
@@ -2,70 +2,86 @@
## Resumo
-O DestaquesGovbr é uma plataforma de agregação e enriquecimento de notícias governamentais composta por 5 camadas principais:
+O DestaquesGovbr é uma plataforma de agregação e enriquecimento de notícias governamentais composta por 7 camadas principais:
1. **Coleta** - Raspagem automatizada de ~160+ sites gov.br
-2. **Enriquecimento** - Classificação temática e sumarização via AI/LLM
-3. **Armazenamento** - Dataset no HuggingFace como fonte de verdade
-4. **Indexação** - Typesense para busca full-text
-5. **Apresentação** - Portal Next.js e apps de análise
+2. **Armazenamento** - PostgreSQL (Cloud SQL) como fonte de verdade
+3. **Enriquecimento** - Classificação temática e sumarização via Cogfy/LLM
+4. **Embeddings** - Geração de vetores 768-dim para busca semântica
+5. **Indexação** - Typesense para busca full-text e vetorial
+6. **Distribuição** - HuggingFace para dados abertos
+7. **Apresentação** - Portal Next.js e apps de análise
## Diagrama de Arquitetura
```mermaid
flowchart TB
subgraph COLETA["1. Coleta"]
- A[160+ Sites gov.br] -->|Raspagem diária 4AM UTC| B[WebScraper Python]
+ A[160+ Sites gov.br] -->|Raspagem diária 4AM UTC| B[Scraper Python]
A2[Sites EBC] -->|Raspagem| B
- B --> C[DatasetManager]
end
- subgraph ENRIQUECIMENTO["2. Enriquecimento AI"]
+ subgraph ARMAZENAMENTO["2. Armazenamento - Fonte de Verdade"]
+ B -->|Insert| C[(PostgreSQL
Cloud SQL)]
+ end
+
+ subgraph ENRIQUECIMENTO["3. Enriquecimento AI"]
C -->|Upload batch| D[Cogfy API]
D -->|LLM Inference| E[Classificação Temática]
D -->|LLM Inference| F[Resumo Automático]
E --> G[EnrichmentManager]
F --> G
+ G -->|Update| C
+ end
+
+ subgraph EMBEDDINGS["4. Embeddings"]
+ C -->|Textos| H[Embeddings API]
+ H -->|Vetores 768-dim| C
end
- subgraph ARMAZENAMENTO["3. Armazenamento ~300k docs"]
- G -->|Push| H[(HuggingFace Dataset)]
+ subgraph INDEXACAO["5. Indexação - Busca"]
+ C -->|Sync| I[(Typesense)]
end
- subgraph INDEXACAO["4. Indexação - Busca full-text"]
- H -->|Sync diário| I[(Typesense)]
+ subgraph DISTRIBUICAO["6. Distribuição"]
+ C -->|DAG Airflow| J[(HuggingFace
Dados Abertos)]
end
- subgraph APRESENTACAO["5. Apresentação"]
- I -->|API Search| J[Portal Next.js]
- H -->|Análise dados| K[Streamlit App]
+ subgraph APRESENTACAO["7. Apresentação"]
+ I -->|API Search| K[Portal Next.js]
+ J -->|Análise dados| L[Streamlit App]
end
subgraph INFRA["Infraestrutura GCP"]
- L[Cloud Run] -.-> J
- M[Compute Engine] -.-> I
- N[Artifact Registry]
- O[Secret Manager]
+ M[Cloud Run] -.-> K
+ M2[Cloud Run] -.-> H
+ N[Compute Engine] -.-> I
+ O[Cloud SQL] -.-> C
+ P[Cloud Composer] -.-> J
+ Q[Secret Manager]
end
style COLETA fill:#e3f2fd
- style ENRIQUECIMENTO fill:#fff3e0
style ARMAZENAMENTO fill:#e8f5e9
+ style ENRIQUECIMENTO fill:#fff3e0
+ style EMBEDDINGS fill:#fff8e1
style INDEXACAO fill:#fce4ec
+ style DISTRIBUICAO fill:#e1f5fe
style APRESENTACAO fill:#f3e5f5
style INFRA fill:#eceff1
```
## Componentes por Camada
-### 1. Coleta (`scraper`)
+### 1. Coleta (`data-platform`)
| Componente | Arquivo | Responsabilidade |
|------------|---------|------------------|
-| WebScraper | `src/scraper/webscraper.py` | Raspagem genérica de sites gov.br |
-| EBC Scraper | `src/scraper/ebc_webscraper.py` | Raspagem especializada da EBC |
-| ScrapeManager | `src/scraper/scrape_manager.py` | Orquestração paralela/sequencial |
-| DatasetManager | `src/dataset_manager.py` | Insert/Update no HuggingFace |
+| WebScraper | `src/data_platform/scrapers/webscraper.py` | Raspagem genérica de sites gov.br |
+| EBCScraper | `src/data_platform/scrapers/ebc_webscraper.py` | Raspagem especializada da EBC |
+| ScrapeManager | `src/data_platform/scrapers/scrape_manager.py` | Orquestração paralela/sequencial |
+| PostgresManager | `src/data_platform/managers/postgres_manager.py` | Insert/Update no PostgreSQL |
+| StorageAdapter | `src/data_platform/managers/storage_adapter.py` | Abstração de storage (PG/HF/dual) |
**Dados extraídos por notícia:**
@@ -86,13 +102,26 @@ flowchart TB
| `category` | Categoria original do site |
| `tags` | Tags/keywords do site |
-### 2. Enriquecimento (`scraper` + Cogfy)
+### 2. Armazenamento (PostgreSQL)
+
+**Instância Cloud SQL**: `destaquesgovbr-postgres` (PostgreSQL 15)
+
+| Tabela | Registros | Descrição |
+|--------|-----------|-----------|
+| `agencies` | 158 | Órgãos governamentais |
+| `themes` | 200+ | Taxonomia temática (3 níveis) |
+| `news` | 300k+ | Notícias coletadas e enriquecidas |
+| `sync_log` | - | Log de operações |
+
+→ Veja detalhes em [postgresql.md](postgresql.md)
+
+### 3. Enriquecimento (`data-platform` + Cogfy)
| Componente | Arquivo | Responsabilidade |
|------------|---------|------------------|
-| CogfyManager | `src/cogfy_manager.py` | Cliente da API Cogfy |
-| UploadToCogfy | `src/upload_to_cogfy_manager.py` | Envio de notícias para inferência |
-| EnrichmentManager | `src/enrichment_manager.py` | Busca resultados e atualiza dataset |
+| CogfyManager | `src/data_platform/cogfy/cogfy_manager.py` | Cliente da API Cogfy |
+| UploadManager | `src/data_platform/cogfy/upload_manager.py` | Envio de notícias para inferência |
+| EnrichmentManager | `src/data_platform/cogfy/enrichment_manager.py` | Busca resultados e atualiza PostgreSQL |
**Campos enriquecidos:**
@@ -102,26 +131,49 @@ flowchart TB
- `most_specific_theme_code/label` - Tema mais específico disponível
- `summary` - Resumo gerado por LLM
-### 3. Armazenamento (HuggingFace)
+### 4. Embeddings (`data-platform`)
-**Dataset principal**: [nitaibezerra/govbrnews](https://huggingface.co/datasets/nitaibezerra/govbrnews)
+| Componente | Arquivo | Responsabilidade |
+|------------|---------|------------------|
+| EmbeddingGenerator | `src/data_platform/jobs/embeddings/embedding_generator.py` | Geração de vetores |
+| Embeddings API | Cloud Run | Modelo `paraphrase-multilingual-mpnet-base-v2` |
-- ~300.000+ documentos
-- Atualização diária automatizada
-- Exportação em CSV por agência e ano
-- Versionamento automático pelo HuggingFace
+**Características:**
+- Vetores de 768 dimensões
+- Input: `title + summary` (fallback para `content`)
+- Armazenados em `news.content_embedding` (tipo `VECTOR`)
-### 4. Indexação (Typesense)
+### 5. Indexação (Typesense)
+
+| Componente | Arquivo | Responsabilidade |
+|------------|---------|------------------|
+| TypesenseClient | `src/data_platform/typesense/client.py` | Conexão com Typesense |
+| TypesenseIndexer | `src/data_platform/typesense/indexer.py` | Indexação de documentos |
+| SyncJob | `src/data_platform/jobs/typesense/sync_job.py` | Sincronização PostgreSQL → Typesense |
**Collection**: `news`
Configurado para:
- Busca full-text em `title` e `content`
+- Busca vetorial via `content_embedding` (768-dim)
- Filtros facetados por `agency`, `theme_*`, `published_at`
- Ordenação por relevância e data
-### 5. Apresentação
+### 6. Distribuição (HuggingFace)
+
+**Dataset principal**: [nitaibezerra/govbrnews](https://huggingface.co/datasets/nitaibezerra/govbrnews)
+
+- ~300.000+ documentos
+- Sincronização diária via DAG Airflow
+- Abordagem incremental (parquet shards)
+- Versionamento automático pelo HuggingFace
+
+**DAG**: `sync_postgres_to_huggingface` (6 AM UTC)
+
+→ Veja detalhes em [workflows/airflow-dags.md](../workflows/airflow-dags.md)
+
+### 7. Apresentação
| App | Tecnologia | URL |
|-----|------------|-----|
@@ -134,37 +186,49 @@ Configurado para:
sequenceDiagram
participant Cron as GitHub Actions (4AM UTC)
participant Scraper as Scraper Python
+ participant PG as PostgreSQL
participant Cogfy as Cogfy API
- participant HF as HuggingFace
+ participant EMB as Embeddings API
participant TS as Typesense
+ participant Airflow as Cloud Composer (6AM UTC)
+ participant HF as HuggingFace
Note over Cron: Trigger diário
Cron->>Scraper: main-workflow.yaml
Scraper->>Scraper: scrape gov.br (160+ sites)
Scraper->>Scraper: scrape EBC
- Scraper->>HF: Insert novos artigos
+ Scraper->>PG: Insert novos artigos
Scraper->>Cogfy: Upload para inferência
Note over Cogfy: Aguarda 20 min para processamento
Cogfy-->>Scraper: Themes + Summary
- Scraper->>HF: Update com enrichment
+ Scraper->>PG: Update com enrichment
+
+ Scraper->>EMB: Gerar embeddings
+ EMB-->>Scraper: Vetores 768-dim
+ Scraper->>PG: Update com embeddings
- Cron->>TS: typesense-daily-load.yml
- TS->>HF: Fetch dataset
+ Cron->>TS: typesense-sync
+ TS->>PG: Fetch dados
TS->>TS: Index documentos
- Note over TS: Portal consome dados atualizados
+ Airflow->>PG: Query novos registros
+ Airflow->>HF: Upload parquet shard
+
+ Note over TS,HF: Portal e comunidade consomem dados atualizados
```
## Tecnologias Principais
-### Backend (Scraper)
+### Backend (Data Platform)
-- **Python 3.12+** com Poetry
+- **Python 3.11+** com Poetry
+- **PostgreSQL 15** (Cloud SQL) com psycopg2
- **BeautifulSoup4** para parsing HTML
-- **datasets** (HuggingFace) para gerenciamento de dados
+- **datasets** + **huggingface_hub** para sync HF
- **requests** com retry logic
+- **Apache Airflow 3** (Cloud Composer)
### Frontend (Portal)
@@ -176,18 +240,21 @@ sequenceDiagram
### Infraestrutura
-- **GCP** - Cloud Run, Compute Engine, VPC
+- **GCP** - Cloud Run, Compute Engine, Cloud SQL, Cloud Composer, VPC
- **Terraform** - IaC
- **Docker** - Containerização
- **GitHub Actions** - CI/CD
+- **Apache Airflow** - Orquestração de pipelines
## Custos Estimados
| Componente | Custo/mês |
|------------|-----------|
-| Compute Engine (Typesense) | ~$55 |
-| Cloud Run (Portal) | ~$12-17 |
-| **Total** | **~$70** |
+| Cloud SQL (PostgreSQL) | ~$48 |
+| Compute Engine (Typesense) | ~$64 |
+| Cloud Run (Portal + Embeddings) | ~$15 |
+| Cloud Composer (Airflow) | ~$100-150 |
+| **Total** | **~$230-280** |
## Próximos Passos
diff --git a/docs/index.md b/docs/index.md
index 3171c0d..6c230e6 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -8,8 +8,8 @@ O **DestaquesGovbr** é uma plataforma integrada de notícias e informações do
- **Centraliza** ~160+ portais governamentais em uma plataforma única
- **Classifica** automaticamente notícias usando AI/LLM em 25 temas e 3 níveis hierárquicos
-- **Disponibiliza** dados abertos no HuggingFace (~300k+ notícias)
-- **Oferece** portal web moderno com busca semântica
+- **Armazena** dados em PostgreSQL (fonte de verdade) e distribui via HuggingFace (~300k+ notícias)
+- **Oferece** portal web moderno com busca semântica e vetorial
## Quick Start
@@ -58,10 +58,14 @@ flowchart LR
```mermaid
flowchart LR
A[160+ Sites gov.br] -->|Raspagem| B[Scraper]
- B -->|Enriquecimento| C[Cogfy/LLM]
- C -->|Armazenamento| D[(HuggingFace)]
- D -->|Indexação| E[(Typesense)]
- E -->|Busca| F[Portal Next.js]
+ B -->|Armazenamento| C[(PostgreSQL)]
+ C -->|Enriquecimento| D[Cogfy/LLM]
+ D --> C
+ C -->|Embeddings| E[Embeddings API]
+ E --> C
+ C -->|Indexação| F[(Typesense)]
+ F -->|Busca| G[Portal Next.js]
+ C -->|Sync diário| H[(HuggingFace)]
```
→ Veja detalhes em [arquitetura/visao-geral.md](arquitetura/visao-geral.md)
@@ -70,11 +74,11 @@ flowchart LR
| Repositório | Descrição | Tecnologia |
|-------------|-----------|------------|
-| [scraper](https://github.com/destaquesgovbr/scraper) | Scraper + Pipeline de dados | Python/Poetry |
+| [data-platform](https://github.com/destaquesgovbr/data-platform) | Pipeline de dados (scraper, sync, enrichment) | Python/Poetry |
| [portal](https://github.com/destaquesgovbr/portal) | Portal web principal | Next.js 15 |
| [infra](https://github.com/destaquesgovbr/infra) | Infraestrutura como código | Terraform/GCP |
-| [typesense](https://github.com/destaquesgovbr/typesense) | Typesense para dev local | Docker |
| [agencies](https://github.com/destaquesgovbr/agencies) | Dados dos órgãos | YAML |
+| [themes](https://github.com/destaquesgovbr/themes) | Taxonomia temática | YAML |
## Recursos Externos
diff --git a/docs/infraestrutura/arquitetura-gcp.md b/docs/infraestrutura/arquitetura-gcp.md
index 63f1c44..0172430 100644
--- a/docs/infraestrutura/arquitetura-gcp.md
+++ b/docs/infraestrutura/arquitetura-gcp.md
@@ -13,15 +13,24 @@ flowchart TB
subgraph "Google Cloud Platform"
subgraph "Networking"
VPC[VPC Network]
- SUB[Subnet us-east1]
+ SUB[Subnet southamerica-east1]
FW[Firewall Rules]
end
+ subgraph "Databases"
+ PG[(Cloud SQL
PostgreSQL 15)]
+ end
+
subgraph "Compute"
CR[Cloud Run
Portal Next.js]
+ EMB[Cloud Run
Embeddings API]
CE[Compute Engine
Typesense]
end
+ subgraph "Orchestration"
+ CC[Cloud Composer 3
Airflow 3.x]
+ end
+
subgraph "Storage & Registry"
AR[Artifact Registry
Docker Images]
SM[Secret Manager
Credentials]
@@ -36,22 +45,66 @@ flowchart TB
GH[GitHub Actions] -->|OIDC| WIF
WIF -->|Impersonate| SA
SA -->|Deploy| CR
+ SA -->|Deploy| EMB
SA -->|Push| AR
CR -->|VPC Connector| VPC
+ EMB -->|VPC Connector| VPC
+ CC -->|VPC| VPC
VPC --> CE
+ VPC --> PG
```
---
## Componentes
-### 1. Cloud Run (Portal)
+### 1. Cloud SQL (PostgreSQL) - Fonte de Verdade
+
+| Propriedade | Valor |
+|-------------|-------|
+| Instância | `destaquesgovbr-postgres` |
+| Versão | PostgreSQL 15 |
+| Região | `southamerica-east1` |
+| Tier | `db-f1-micro` (upgrade planejado) |
+| vCPU | 1 |
+| Memória | 3.75 GB |
+| Disco | 50GB SSD (auto-resize até 500GB) |
+| Backup | Diário com PITR 7 dias |
+
+**Características:**
+- Fonte de verdade central do sistema
+- Private IP (acesso apenas via VPC)
+- Backups automáticos diários
+- Point-in-time recovery (7 dias)
+
+→ Veja detalhes em [PostgreSQL](../arquitetura/postgresql.md)
+
+### 2. Cloud Composer (Airflow)
+
+| Propriedade | Valor |
+|-------------|-------|
+| Ambiente | `destaquesgovbr-composer` |
+| Versão | Composer 3 / Airflow 3.x |
+| Região | `southamerica-east1` |
+| Tipo | Small |
+
+**DAGs:**
+- `sync_postgres_to_huggingface` - Sync diário para HuggingFace (6AM UTC)
+
+**Características:**
+- Orquestração de pipelines de dados
+- Monitoramento e alertas integrados
+- Retry automático de tarefas
+
+→ Veja detalhes em [Airflow DAGs](../workflows/airflow-dags.md)
+
+### 3. Cloud Run (Portal)
| Propriedade | Valor |
|-------------|-------|
| Serviço | `portal` |
-| Região | `us-east1` |
+| Região | `southamerica-east1` |
| CPU | 1 |
| Memória | 512Mi |
| Min instances | 0 |
@@ -63,50 +116,77 @@ flowchart TB
- VPC Connector para acesso ao Typesense
- HTTPS automático
-### 2. Compute Engine (Typesense)
+### 4. Cloud Run (Embeddings API)
+
+| Propriedade | Valor |
+|-------------|-------|
+| Serviço | `embeddings-api` |
+| Região | `southamerica-east1` |
+| CPU | 2 |
+| Memória | 4Gi |
+| Min instances | 0 |
+| Max instances | 5 |
+
+**Características:**
+- Modelo: `paraphrase-multilingual-mpnet-base-v2`
+- Vetores 768-dim
+- VPC Connector para acesso ao PostgreSQL
+- GPU opcional para maior throughput
+
+### 5. Compute Engine (Typesense)
| Propriedade | Valor |
|-------------|-------|
| Nome | `typesense-server` |
| Tipo | `e2-medium` |
-| Região | `us-east1-b` |
+| Região | `southamerica-east1-a` |
| Disco | 50GB SSD |
| IP | Interno (VPC) |
**Características:**
- VM dedicada para Typesense
+- Suporte a busca vetorial (768-dim)
- Persistência de dados em disco
- Acesso via VPC (não exposto à internet)
-### 3. Artifact Registry
+### 6. Artifact Registry
| Propriedade | Valor |
|-------------|-------|
| Repositório | `destaquesgovbr` |
-| Região | `us-east1` |
+| Região | `southamerica-east1` |
| Formato | Docker |
**Imagens:**
- `portal` - Imagem do portal Next.js
+- `data-platform` - Imagem do data platform
+- `embeddings-api` - Imagem da API de embeddings
-### 4. Secret Manager
+### 7. Secret Manager
Secrets armazenadas:
-- `typesense-api-key` - API Key do Typesense
-- Outras credenciais sensíveis
+| Secret | Descrição |
+|--------|-----------|
+| `postgres-host` | IP privado do Cloud SQL |
+| `postgres-password` | Senha do banco |
+| `typesense-api-key` | API Key do Typesense |
+| `cogfy-api-key` | API Key do Cogfy |
+| `cogfy-collection-id` | ID da collection Cogfy |
+| `hf-token` | Token HuggingFace (write) |
-### 5. VPC Network
+### 8. VPC Network
| Propriedade | Valor |
|-------------|-------|
| Nome | `destaquesgovbr-vpc` |
| Subnet | `10.0.0.0/24` |
-| Região | `us-east1` |
+| Região | `southamerica-east1` |
**Firewall Rules:**
- SSH interno
- Typesense (8108) interno
+- PostgreSQL (5432) interno
- HTTPS (443) externo para Cloud Run
---
@@ -117,19 +197,27 @@ Secrets armazenadas:
flowchart LR
subgraph "Internet"
U[Usuários]
+ HF[(HuggingFace)]
end
- subgraph "GCP - us-east1"
+ subgraph "GCP - southamerica-east1"
subgraph "VPC 10.0.0.0/24"
CR[Cloud Run
Portal]
+ EMB[Cloud Run
Embeddings API]
VC[VPC Connector]
TS[Compute Engine
Typesense
10.0.0.x]
+ PG[(Cloud SQL
PostgreSQL
10.0.0.y)]
+ CC[Cloud Composer
Airflow]
end
end
U -->|HTTPS| CR
CR -->|VPC Connector| VC
VC -->|:8108| TS
+ VC -->|:5432| PG
+ EMB -->|VPC Connector| VC
+ CC -->|:5432| PG
+ CC -->|Sync| HF
```
---
@@ -138,11 +226,15 @@ flowchart LR
| Componente | Custo/mês |
|------------|-----------|
-| Compute Engine (Typesense) | ~$55 |
-| Cloud Run (Portal) | ~$12-17 |
+| Cloud SQL (PostgreSQL) | ~$48 |
+| Cloud Composer (Airflow) | ~$100-150 |
+| Compute Engine (Typesense) | ~$64 |
+| Cloud Run (Portal) | ~$10 |
+| Cloud Run (Embeddings API) | ~$5 |
| Artifact Registry | ~$1 |
| VPC Connector | ~$2 |
-| **Total** | **~$70-75** |
+| Secret Manager | ~$1 |
+| **Total** | **~$230-280** |
> Valores aproximados. Podem variar com uso.
@@ -153,15 +245,17 @@ flowchart LR
| Recurso | Região/Zona |
|---------|-------------|
| VPC | global |
-| Subnet | us-east1 |
-| Cloud Run | us-east1 |
-| Compute Engine | us-east1-b |
-| Artifact Registry | us-east1 |
-
-**Por que us-east1?**
-- Menor latência para Brasil
+| Subnet | southamerica-east1 |
+| Cloud SQL | southamerica-east1 |
+| Cloud Run | southamerica-east1 |
+| Cloud Composer | southamerica-east1 |
+| Compute Engine | southamerica-east1-a |
+| Artifact Registry | southamerica-east1 |
+
+**Por que southamerica-east1 (São Paulo)?**
+- Menor latência para usuários brasileiros
+- Conformidade com dados do governo
- Disponibilidade de recursos
-- Custo competitivo
---
@@ -190,7 +284,7 @@ sequenceDiagram
```bash
# Via gcloud
-gcloud compute ssh typesense-server --zone=us-east1-b
+gcloud compute ssh typesense-server --zone=southamerica-east1-a
# Verificar Typesense
curl http://localhost:8108/health
@@ -198,65 +292,133 @@ curl http://localhost:8108/health
---
+## Acesso ao PostgreSQL
+
+```bash
+# Via Cloud SQL Proxy (desenvolvimento local)
+cloud_sql_proxy -instances=PROJECT_ID:southamerica-east1:destaquesgovbr-postgres=tcp:5432
+
+# Conectar
+psql -h localhost -U admin -d destaquesgovbr
+
+# Via gcloud (admin)
+gcloud sql connect destaquesgovbr-postgres --user=admin
+```
+
+---
+
+## Acesso ao Composer (Airflow)
+
+```bash
+# Abrir UI do Airflow
+gcloud composer environments describe destaquesgovbr-composer \
+ --location=southamerica-east1 \
+ --format='value(config.airflowUri)'
+
+# Listar DAGs
+gcloud composer environments run destaquesgovbr-composer \
+ --location=southamerica-east1 \
+ dags list
+
+# Trigger manual de DAG
+gcloud composer environments run destaquesgovbr-composer \
+ --location=southamerica-east1 \
+ dags trigger -- sync_postgres_to_huggingface
+```
+
+---
+
## Monitoramento
+### Cloud SQL
+
+```bash
+# Status da instância
+gcloud sql instances describe destaquesgovbr-postgres
+
+# Métricas (via Console)
+# Console > SQL > destaquesgovbr-postgres > Monitoring
+```
+
### Cloud Run
```bash
# Status do serviço
-gcloud run services describe portal --region=us-east1
+gcloud run services describe portal --region=southamerica-east1
# Logs
-gcloud run services logs read portal --region=us-east1
+gcloud run services logs read portal --region=southamerica-east1
+```
-# Métricas (via Console)
-# Console > Cloud Run > portal > Metrics
+### Cloud Composer
+
+```bash
+# Status do ambiente
+gcloud composer environments describe destaquesgovbr-composer \
+ --location=southamerica-east1
+
+# Logs (via Console)
+# Console > Composer > destaquesgovbr-composer > Logs
```
### Compute Engine
```bash
# Status da VM
-gcloud compute instances describe typesense-server --zone=us-east1-b
+gcloud compute instances describe typesense-server --zone=southamerica-east1-a
# Logs do sistema
-gcloud compute ssh typesense-server --zone=us-east1-b -- sudo journalctl -u typesense
-
-# Métricas (via Console)
-# Console > Compute Engine > typesense-server > Monitoring
+gcloud compute ssh typesense-server --zone=southamerica-east1-a -- sudo journalctl -u typesense
```
---
## Backup e Recuperação
+### PostgreSQL (Cloud SQL)
+
+```bash
+# Backups são automáticos (diários)
+# Listar backups
+gcloud sql backups list --instance=destaquesgovbr-postgres
+
+# Restaurar de um backup
+gcloud sql backups restore BACKUP_ID \
+ --restore-instance=destaquesgovbr-postgres \
+ --backup-instance=destaquesgovbr-postgres
+```
+
### Typesense Data
```bash
# SSH no servidor
-gcloud compute ssh typesense-server --zone=us-east1-b
+gcloud compute ssh typesense-server --zone=southamerica-east1-a
# Backup do diretório de dados
sudo tar -czvf /tmp/typesense-backup.tar.gz /var/lib/typesense/data
# Download do backup
-gcloud compute scp typesense-server:/tmp/typesense-backup.tar.gz . --zone=us-east1-b
+gcloud compute scp typesense-server:/tmp/typesense-backup.tar.gz . --zone=southamerica-east1-a
```
-### Recuperação
-
-1. Parar Typesense
-2. Restaurar dados do backup
-3. Reiniciar Typesense
-4. Verificar integridade
-
---
## Escalabilidade
-### Cloud Run (Portal)
+### Cloud SQL
+
+- **Vertical**: Upgrade de tier (db-f1-micro → db-g1-small → db-custom)
+- **Read replicas**: Disponível se necessário
+- **Disk auto-resize**: Habilitado (até 500GB)
+
+### Cloud Composer
+
+- **Horizontal**: Adicionar workers
+- **Vertical**: Upgrade de tipo de ambiente
-- **Automático**: Escala de 0 a 10 instâncias
+### Cloud Run
+
+- **Automático**: Escala de 0 a N instâncias
- **Cold start**: ~2-3 segundos
- **Ajustes**: Via Terraform ou Console
@@ -273,6 +435,7 @@ gcloud compute scp typesense-server:/tmp/typesense-backup.tar.gz . --zone=us-eas
### Rede
- Typesense **não exposto** à internet
+- PostgreSQL **não exposto** à internet
- Acesso apenas via VPC Connector
- Firewall restritivo
@@ -288,11 +451,35 @@ gcloud compute scp typesense-server:/tmp/typesense-backup.tar.gz . --zone=us-eas
- Versionamento automático
- Acesso auditado
+### Dados
+
+- Backups automáticos do Cloud SQL
+- Criptografia em repouso (padrão GCP)
+- Criptografia em trânsito (TLS)
+
+---
+
+## Terraform (Arquivos Principais)
+
+| Arquivo | Conteúdo |
+|---------|----------|
+| `main.tf` | Providers e configurações globais |
+| `network.tf` | VPC, subnets, firewall |
+| `cloud_sql.tf` | PostgreSQL |
+| `composer.tf` | Cloud Composer |
+| `cloud_run.tf` | Portal e Embeddings API |
+| `compute.tf` | Typesense VM |
+| `iam.tf` | Service accounts e Workload Identity |
+| `secrets.tf` | Secret Manager |
+| `artifact_registry.tf` | Registry Docker |
+
---
## Links Relacionados
+- [PostgreSQL](../arquitetura/postgresql.md) - Banco de dados central
- [Terraform Guide](./terraform-guide.md) - Como gerenciar a infraestrutura
- [Secrets e IAM](./secrets-iam.md) - Permissões e credenciais
- [Deploy Portal](../workflows/portal-deploy.md) - Workflow de deploy
- [Typesense Data](../workflows/typesense-data.md) - Carga de dados
+- [Airflow DAGs](../workflows/airflow-dags.md) - Pipelines de orquestração
diff --git a/docs/modulos/cogfy-integracao.md b/docs/modulos/cogfy-integracao.md
index 011353e..7396c2e 100644
--- a/docs/modulos/cogfy-integracao.md
+++ b/docs/modulos/cogfy-integracao.md
@@ -11,12 +11,12 @@ O **Cogfy** é uma plataforma SaaS que fornece inferência LLM para:
```mermaid
flowchart LR
- SC[Scraper] -->|Upload| CF[Cogfy API]
+ PG[(PostgreSQL)] -->|Upload| CF[Cogfy API]
CF -->|LLM Inference| CL[Classificação]
CF -->|LLM Inference| SU[Sumarização]
CL --> EN[EnrichmentManager]
SU --> EN
- EN -->|Update| HF[(HuggingFace)]
+ EN -->|Update| PG
```
---
@@ -26,11 +26,11 @@ flowchart LR
### 1. Upload para Cogfy
```python
-# upload_to_cogfy_manager.py
+# src/data_platform/cogfy/upload_manager.py
def upload_to_cogfy(start_date: date, end_date: date):
"""Envia notícias para processamento no Cogfy."""
- # 1. Carregar artigos do HuggingFace
- df = dataset_manager.load_by_date_range(start_date, end_date)
+ # 1. Carregar artigos do PostgreSQL
+ articles = postgres_manager.get_news_by_date_range(start_date, end_date)
# 2. Converter para formato Cogfy
records = [
@@ -39,7 +39,7 @@ def upload_to_cogfy(start_date: date, end_date: date):
"title": row["title"],
"content": row["content"][:5000], # Limite de caracteres
}
- for _, row in df.iterrows()
+ for row in articles
]
# 3. Enviar em batches
@@ -56,20 +56,36 @@ O Cogfy processa via LLM, o que leva aproximadamente **20 minutos** para um batc
run: sleep 1200 # 20 minutos
```
-### 3. Buscar Resultados
+### 3. Buscar Resultados e Atualizar PostgreSQL
```python
-# enrichment_manager.py
-def fetch_enrichment(unique_id: str) -> dict:
- """Busca resultados processados do Cogfy."""
- record = cogfy_manager.get_record(unique_id)
-
- return {
- "theme_1_level_1": record.get("theme_1_level_1"),
- "theme_1_level_2": record.get("theme_1_level_2"),
- "theme_1_level_3": record.get("theme_1_level_3"),
- "summary": record.get("summary"),
- }
+# src/data_platform/cogfy/enrichment_manager.py
+def enrich(start_date: date, end_date: date) -> int:
+ """Busca resultados do Cogfy e atualiza PostgreSQL."""
+ # 1. Buscar artigos sem enriquecimento
+ articles = postgres_manager.get_news_without_enrichment(start_date, end_date)
+
+ # 2. Para cada artigo, buscar no Cogfy
+ for article in articles:
+ cogfy_data = cogfy_manager.get_record(article["unique_id"])
+
+ # 3. Mapear códigos para labels
+ theme_l1_code, theme_l1_label = parse_theme(cogfy_data.get("theme_1_level_1"))
+ theme_l2_code, theme_l2_label = parse_theme(cogfy_data.get("theme_1_level_2"))
+ theme_l3_code, theme_l3_label = parse_theme(cogfy_data.get("theme_1_level_3"))
+
+ # 4. Calcular most_specific_theme (L3 > L2 > L1)
+ most_specific = theme_l3_code or theme_l2_code or theme_l1_code
+
+ # 5. Atualizar PostgreSQL
+ postgres_manager.update_enrichment(
+ unique_id=article["unique_id"],
+ theme_l1_id=get_theme_id(theme_l1_code),
+ theme_l2_id=get_theme_id(theme_l2_code),
+ theme_l3_id=get_theme_id(theme_l3_code),
+ most_specific_theme_id=get_theme_id(most_specific),
+ summary=cogfy_data.get("summary")
+ )
```
---
@@ -110,6 +126,30 @@ class CogfyManager:
---
+## CLI do data-platform
+
+```bash
+# Upload para Cogfy
+data-platform upload-cogfy --start-date YYYY-MM-DD --end-date YYYY-MM-DD
+
+# Buscar enriquecimento (após ~20 min)
+data-platform enrich --start-date YYYY-MM-DD --end-date YYYY-MM-DD
+```
+
+---
+
+## Arquivos Principais
+
+| Componente | Localização |
+|------------|-------------|
+| CogfyManager | `src/data_platform/cogfy/cogfy_manager.py` |
+| UploadManager | `src/data_platform/cogfy/upload_manager.py` |
+| EnrichmentManager | `src/data_platform/cogfy/enrichment_manager.py` |
+| ThemeMapper | `src/data_platform/enrichment/theme_mapper.py` |
+| themes_tree.yaml | `src/data_platform/enrichment/themes_tree.yaml` |
+
+---
+
## Configuração no Cogfy
> **Nota**: A configuração do Cogfy é feita via interface web. Screenshots serão adicionados futuramente.
@@ -156,21 +196,21 @@ Resumo:
## Mapeamento de Campos
-### Entrada (Scraper → Cogfy)
+### Entrada (PostgreSQL → Cogfy)
-| Campo Scraper | Campo Cogfy | Tipo |
-|---------------|-------------|------|
+| Campo PostgreSQL | Campo Cogfy | Tipo |
+|------------------|-------------|------|
| unique_id | unique_id | string |
| title | title | string |
| content | content | string (max 5000 chars) |
-### Saída (Cogfy → Dataset)
+### Saída (Cogfy → PostgreSQL)
-| Campo Cogfy | Campo Dataset | Tipo |
-|-------------|---------------|------|
-| theme_1_level_1 | theme_1_level_1_code + label | string → split |
-| theme_1_level_2 | theme_1_level_2_code + label | string → split |
-| theme_1_level_3 | theme_1_level_3_code + label | string → split |
+| Campo Cogfy | Campo PostgreSQL | Tipo |
+|-------------|------------------|------|
+| theme_1_level_1 | theme_l1_id (FK) | string → ID lookup |
+| theme_1_level_2 | theme_l2_id (FK) | string → ID lookup |
+| theme_1_level_3 | theme_l3_id (FK) | string → ID lookup |
| summary | summary | string |
### Processamento de Temas
@@ -184,6 +224,12 @@ def parse_theme(theme_str: str) -> tuple[str, str]:
return None, None
parts = theme_str.split(" - ", 1)
return parts[0].strip(), parts[1].strip()
+
+def get_theme_id(code: str) -> int | None:
+ """Busca ID do tema no PostgreSQL pelo código."""
+ if not code:
+ return None
+ return postgres_manager.get_theme_id_by_code(code)
```
---
@@ -210,17 +256,18 @@ COGFY_COLLECTION_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
### Notícia não classificada
```python
-def enrich_article(row: dict) -> dict:
+def enrich_article(article: dict) -> bool:
"""Enriquece artigo com dados do Cogfy."""
try:
- cogfy_data = cogfy_manager.get_record(row["unique_id"])
+ cogfy_data = cogfy_manager.get_record(article["unique_id"])
if not cogfy_data:
- logger.warning(f"No Cogfy data for {row['unique_id']}")
- return row # Retorna sem enriquecimento
+ logger.warning(f"No Cogfy data for {article['unique_id']}")
+ return False
# ... processar enriquecimento
+ return True
except Exception as e:
- logger.error(f"Error enriching {row['unique_id']}: {e}")
- return row
+ logger.error(f"Error enriching {article['unique_id']}: {e}")
+ return False
```
### Retry em falhas de API
@@ -234,24 +281,52 @@ def get_record(self, unique_id: str) -> dict:
---
+## Fluxo no Pipeline Diário
+
+```mermaid
+sequenceDiagram
+ participant GH as GitHub Actions
+ participant PG as PostgreSQL
+ participant CF as Cogfy
+ participant EN as EnrichmentManager
+
+ GH->>PG: get_news(date_range)
+ PG-->>GH: Registros
+ GH->>CF: Upload para inferência
+ Note over CF: Processa via LLM (~20 min)
+ CF-->>EN: Themes + Summary prontos
+ EN->>CF: GET records por unique_id
+ EN->>EN: Mapeia código → ID (tabela themes)
+ EN->>EN: Calcula most_specific_theme_id
+ EN->>PG: UPDATE news SET theme_l1_id, summary, ...
+```
+
+---
+
## Métricas e Monitoramento
### Taxa de classificação
-```python
-def calculate_classification_rate(df: pd.DataFrame) -> float:
- """Calcula % de notícias classificadas."""
- classified = df["theme_1_level_1_code"].notna().sum()
- total = len(df)
- return classified / total * 100
+```sql
+-- % de notícias classificadas
+SELECT
+ COUNT(*) FILTER (WHERE theme_l1_id IS NOT NULL) * 100.0 / COUNT(*) as classification_rate
+FROM news
+WHERE published_at >= NOW() - INTERVAL '7 days';
```
### Distribuição por tema
-```python
-def theme_distribution(df: pd.DataFrame) -> dict:
- """Retorna distribuição de notícias por tema."""
- return df["theme_1_level_1_label"].value_counts().to_dict()
+```sql
+-- Top 10 temas mais frequentes
+SELECT
+ t.label,
+ COUNT(*) as count
+FROM news n
+JOIN themes t ON n.most_specific_theme_id = t.id
+GROUP BY t.label
+ORDER BY count DESC
+LIMIT 10;
```
---
@@ -278,27 +353,6 @@ O Cogfy cobra por:
---
-## Fluxo no Pipeline Diário
-
-```mermaid
-sequenceDiagram
- participant SC as Scraper
- participant HF as HuggingFace
- participant CF as Cogfy
- participant EN as Enrichment
-
- SC->>HF: Insert novos artigos
- SC->>CF: Upload para inferência
- Note over CF: Processa via LLM (~20 min)
- CF-->>EN: Themes + Summary prontos
- EN->>CF: GET records por unique_id
- EN->>EN: Mapeia código → label
- EN->>EN: Calcula most_specific_theme
- EN->>HF: Update com enriquecimento
-```
-
----
-
## Placeholder para Screenshots
> **TODO**: Adicionar screenshots da interface do Cogfy:
@@ -312,7 +366,8 @@ sequenceDiagram
## Links Relacionados
+- [Data Platform](data-platform.md) - Repositório unificado
+- [PostgreSQL](../arquitetura/postgresql.md) - Fonte de verdade
- [Fluxo de Dados](../arquitetura/fluxo-de-dados.md) - Pipeline completo
- [Árvore Temática](./arvore-tematica.md) - Taxonomia usada
-- [Módulo Scraper](./scraper.md) - Upload e enriquecimento
- [Pipeline Scraper](../workflows/scraper-pipeline.md) - Workflow diário
diff --git a/docs/modulos/data-platform.md b/docs/modulos/data-platform.md
new file mode 100644
index 0000000..7d9d277
--- /dev/null
+++ b/docs/modulos/data-platform.md
@@ -0,0 +1,274 @@
+# Data Platform
+
+Repositório centralizado para toda a infraestrutura de dados do DestaquesGovBr.
+
+!!! info "Repositório"
+ **GitHub**: [destaquesgovbr/data-platform](https://github.com/destaquesgovbr/data-platform)
+
+## Visão Geral
+
+O **data-platform** unifica toda a lógica de dados que anteriormente estava distribuída em múltiplos repositórios (`scraper`, `typesense`). Ele é responsável por:
+
+- **Coleta**: Scrapers para gov.br e EBC
+- **Armazenamento**: Gerenciamento do PostgreSQL (fonte de verdade) e HuggingFace (distribuição)
+- **Enriquecimento**: Integração com Cogfy para classificação temática e sumários
+- **Embeddings**: Geração de vetores para busca semântica
+- **Indexação**: Sincronização com Typesense
+
+## Arquitetura
+
+```mermaid
+graph TB
+ subgraph "Coleta"
+ S1[Scraper Gov.br]
+ S2[Scraper EBC]
+ end
+
+ subgraph "Armazenamento"
+ PG[(PostgreSQL
Fonte de Verdade)]
+ HF[(HuggingFace
Dados Abertos)]
+ end
+
+ subgraph "Processamento"
+ COG[Cogfy
Enriquecimento]
+ EMB[Embeddings API
Vetores 768-dim]
+ end
+
+ subgraph "Indexação"
+ TS[Typesense
Busca]
+ end
+
+ S1 --> PG
+ S2 --> PG
+ PG --> COG
+ COG --> PG
+ PG --> EMB
+ EMB --> PG
+ PG --> TS
+ PG -->|DAG Airflow| HF
+```
+
+## Estrutura do Repositório
+
+```
+data-platform/
+├── src/data_platform/
+│ ├── managers/ # Gerenciadores de storage
+│ │ ├── postgres_manager.py # Acesso ao PostgreSQL
+│ │ ├── dataset_manager.py # Acesso ao HuggingFace
+│ │ └── storage_adapter.py # Abstração dual-write
+│ ├── scrapers/ # Scrapers de notícias
+│ │ ├── scrape_manager.py # Gov.br
+│ │ └── ebc_scrape_manager.py
+│ ├── cogfy/ # Integração Cogfy
+│ │ ├── cogfy_manager.py
+│ │ ├── upload_manager.py
+│ │ └── enrichment_manager.py
+│ ├── typesense/ # Módulo Typesense
+│ │ ├── client.py
+│ │ ├── collection.py
+│ │ └── indexer.py
+│ ├── jobs/ # Jobs de processamento
+│ │ ├── typesense/sync_job.py
+│ │ ├── embeddings/embedding_generator.py
+│ │ └── hf_sync/
+│ ├── models/ # Modelos Pydantic
+│ │ └── news.py
+│ ├── dags/ # DAGs Airflow
+│ │ └── sync_postgres_to_huggingface.py
+│ └── cli.py # Interface de linha de comando
+├── tests/
+├── scripts/
+└── pyproject.toml
+```
+
+## CLI - Comandos Disponíveis
+
+### Scraping
+
+```bash
+# Raspar sites gov.br
+data-platform scrape --start-date 2025-01-01 --end-date 2025-01-31
+
+# Raspar sites EBC (Agência Brasil, TV Brasil)
+data-platform scrape-ebc --start-date 2025-01-01
+```
+
+### Enriquecimento
+
+```bash
+# Upload para Cogfy
+data-platform upload-cogfy --start-date 2025-01-01
+
+# Buscar enriquecimento do Cogfy
+data-platform enrich --start-date 2025-01-01 --force
+```
+
+### Embeddings
+
+```bash
+# Gerar embeddings para notícias
+data-platform generate-embeddings --start-date 2025-01-01
+```
+
+### Typesense
+
+```bash
+# Sincronizar PostgreSQL → Typesense
+data-platform sync-typesense --start-date 2025-01-01
+
+# Sincronização completa (full reload)
+data-platform sync-typesense --full-sync
+
+# Listar collections
+data-platform typesense-list
+
+# Deletar collection
+data-platform typesense-delete --confirm
+```
+
+### HuggingFace
+
+```bash
+# Sincronizar PostgreSQL → HuggingFace
+data-platform sync-hf --start-date 2025-01-01
+```
+
+## Storage Adapter
+
+O **StorageAdapter** é a camada de abstração que permite transição gradual entre backends de armazenamento.
+
+### Modos de Operação
+
+| Modo | Descrição |
+|------|-----------|
+| `POSTGRES` | Escreve apenas no PostgreSQL |
+| `HUGGINGFACE` | Escreve apenas no HuggingFace (legado) |
+| `DUAL_WRITE` | Escreve em ambos para transição segura |
+
+### Configuração
+
+```bash
+# Variáveis de ambiente
+STORAGE_BACKEND=postgres # postgres | huggingface | dual_write
+STORAGE_READ_FROM=postgres # De onde ler os dados
+```
+
+### Uso no Código
+
+```python
+from data_platform.managers.storage_adapter import StorageAdapter, StorageBackend
+
+adapter = StorageAdapter(
+ backend=StorageBackend.DUAL_WRITE,
+ read_from=StorageBackend.POSTGRES
+)
+
+# Insert transparente - escreve em ambos
+inserted = adapter.insert(news_list, allow_update=False)
+
+# Read - sempre do backend configurado em read_from
+news = adapter.get(filters={"agency_key": "mec"}, limit=100)
+```
+
+## PostgresManager
+
+Gerenciador de acesso ao PostgreSQL com connection pooling e cache.
+
+### Características
+
+- **Connection Pooling**: Min 1, Max 10 conexões
+- **Cache em Memória**: Agências e temas carregados na inicialização
+- **Batch Operations**: Inserts otimizados com `execute_values()`
+- **Conflict Handling**: Suporte a `ON CONFLICT DO UPDATE`
+
+### Exemplo de Uso
+
+```python
+from data_platform.managers.postgres_manager import PostgresManager
+
+pm = PostgresManager()
+pm.load_cache() # Carrega agências/temas em memória
+
+# Inserir notícias
+news_list = [NewsInsert(...), ...]
+inserted = pm.insert(news_list, allow_update=True)
+
+# Buscar notícias
+news = pm.get(
+ filters={"agency_key": "mec"},
+ limit=100,
+ order_by="published_at DESC"
+)
+
+pm.close_all()
+```
+
+## Variáveis de Ambiente
+
+```bash
+# PostgreSQL
+DATABASE_URL=postgresql://user:pass@host:5432/govbrnews
+
+# HuggingFace
+HF_TOKEN=hf_xxx
+HF_REPO_ID=destaquesgovbr/govbrnews
+
+# Typesense
+TYPESENSE_HOST=34.39.186.38
+TYPESENSE_PORT=8108
+TYPESENSE_API_KEY=xxx
+
+# Cogfy
+COGFY_API_KEY=xxx
+COGFY_COLLECTION_ID=xxx
+
+# Embeddings
+EMBEDDINGS_API_URL=https://embeddings-api-xxx.run.app
+EMBEDDINGS_API_KEY=xxx
+
+# Storage
+STORAGE_BACKEND=postgres
+STORAGE_READ_FROM=postgres
+```
+
+## Workflows GitHub Actions
+
+| Workflow | Trigger | Descrição |
+|----------|---------|-----------|
+| `main-workflow.yaml` | Diário (4AM UTC) | Pipeline completo: scrape → enrich → embed → sync |
+| `typesense-maintenance-sync.yaml` | Diário (10AM UTC) | Sync incremental Typesense |
+| `composer-deploy-dags.yaml` | Push | Deploy de DAGs no Airflow |
+
+## Instalação e Desenvolvimento
+
+```bash
+# Clonar
+git clone https://github.com/destaquesgovbr/data-platform.git
+cd data-platform
+
+# Instalar com Poetry
+poetry install
+
+# Rodar testes
+poetry run pytest
+
+# Rodar linters
+poetry run black src/ tests/
+poetry run ruff check src/ tests/
+poetry run mypy src/
+```
+
+## Documentação Adicional
+
+O repositório possui documentação interna em `docs/`:
+
+- `docs/architecture/overview.md` - Arquitetura do sistema
+- `docs/database/schema.md` - Schema PostgreSQL
+- `docs/typesense/` - Documentação do Typesense
+- `docs/development/setup.md` - Setup de desenvolvimento
+
+## Recursos Externos
+
+- **HuggingFace Dataset**: [nitaibezerra/govbrnews](https://huggingface.co/datasets/nitaibezerra/govbrnews)
+- **Portal**: [destaques.gov.br](https://destaques.gov.br)
diff --git a/docs/modulos/scraper.md b/docs/modulos/scraper.md
index 28dab87..cffeff3 100644
--- a/docs/modulos/scraper.md
+++ b/docs/modulos/scraper.md
@@ -1,443 +1,54 @@
-# Módulo: Scraper (scraper)
+# Módulo: Scraper (Arquivado)
-> Pipeline de coleta e enriquecimento de notícias governamentais.
+!!! warning "Módulo Migrado"
+ Este módulo foi migrado para o repositório **data-platform**.
+ O repositório `scraper` foi arquivado.
-**Repositório**: [github.com/destaquesgovbr/scraper](https://github.com/destaquesgovbr/scraper)
-
-## Visão Geral
-
-O scraper é responsável por:
-
-1. **Raspar** notícias de ~160+ sites gov.br e EBC
-2. **Armazenar** no dataset HuggingFace
-3. **Enviar** para classificação no Cogfy
-4. **Enriquecer** com temas e resumos gerados por LLM
-
-```mermaid
-flowchart LR
- A[Sites gov.br] -->|Raspagem| B[WebScraper]
- B -->|Insert| C[(HuggingFace)]
- C -->|Upload| D[Cogfy]
- D -->|Themes + Summary| E[EnrichmentManager]
- E -->|Update| C
-```
-
----
-
-## Stack Tecnológico
-
-| Tecnologia | Versão | Uso |
-|------------|--------|-----|
-| Python | 3.12+ | Linguagem principal |
-| Poetry | 1.7+ | Gerenciamento de dependências |
-| BeautifulSoup4 | 4.x | Parsing HTML |
-| datasets | HuggingFace | Gerenciamento de dados |
-| requests | 2.x | Requisições HTTP |
-| retry | - | Lógica de retry |
-
----
-
-## Estrutura do Repositório
-
-```
-scraper/
-├── src/
-│ ├── main.py # CLI principal (Typer)
-│ ├── dataset_manager.py # Gerenciador HuggingFace
-│ ├── cogfy_manager.py # Cliente API Cogfy
-│ ├── upload_to_cogfy_manager.py # Upload para inferência
-│ ├── enrichment_manager.py # Busca e aplica enriquecimento
-│ ├── enrichment/
-│ │ ├── themes_tree.yaml # Árvore temática (25 temas)
-│ │ └── theme_mapper.py # Mapeamento código → label
-│ └── scraper/
-│ ├── webscraper.py # Scraper genérico gov.br
-│ ├── ebc_webscraper.py # Scraper especializado EBC
-│ ├── scrape_manager.py # Orquestração de scrapers
-│ ├── agencies.yaml # Mapeamento ID → Nome
-│ └── site_urls.yaml # URLs de raspagem (~160+)
-├── tests/ # Testes unitários
-├── .github/workflows/
-│ ├── main-workflow.yaml # Pipeline diário (4AM UTC)
-│ ├── scraper-dispatch.yaml # Trigger manual
-│ └── docker-build.yaml # Build de imagem
-├── pyproject.toml # Dependências Poetry
-├── Dockerfile # Build Docker
-└── README.md
-```
+ **Novo repositório**: [github.com/destaquesgovbr/data-platform](https://github.com/destaquesgovbr/data-platform)
---
-## Componentes Principais
-
-### 1. WebScraper (`webscraper.py`)
-
-Scraper genérico para sites gov.br.
-
-```python
-class WebScraper:
- """Raspa notícias de sites gov.br padrão."""
-
- def __init__(self, base_url: str, agency: str):
- self.base_url = base_url
- self.agency = agency
-
- def scrape(self, start_date: date, end_date: date) -> list[dict]:
- """Raspa notícias no intervalo de datas."""
- ...
-
- def fetch_article_content(self, url: str) -> str:
- """Busca conteúdo completo e converte para Markdown."""
- ...
-```
-
-**Campos extraídos:**
-
-| Campo | Descrição |
-|-------|-----------|
-| `title` | Título da notícia |
-| `subtitle` | Subtítulo (quando disponível) |
-| `editorial_lead` | Lead editorial / linha fina |
-| `url` | URL original |
-| `published_at` | Data/hora de publicação (ISO 8601, UTC) |
-| `updated_datetime` | Data/hora de atualização |
-| `image` | URL da imagem principal |
-| `video_url` | URL de vídeo incorporado |
-| `content` | Conteúdo em Markdown |
-| `category` | Categoria original do site |
-| `tags` | Tags/keywords |
-
-**Retry Logic:**
-```python
-@retry(tries=5, delay=2, backoff=3, jitter=(1,3))
-def fetch_page(url: str) -> Response:
- """Fetch com retry exponencial."""
- ...
-```
-
-### 2. EBCWebScraper (`ebc_webscraper.py`)
-
-Scraper especializado para sites da EBC (Agência Brasil, etc).
-
-```python
-class EBCWebScraper(WebScraper):
- """Scraper para sites EBC com estrutura HTML diferente."""
-
- def parse_article(self, html: str) -> dict:
- """Parser específico para estrutura EBC."""
- ...
-```
-
-### 3. DatasetManager (`dataset_manager.py`)
+## Documentação Atualizada
-Gerencia operações com o dataset HuggingFace.
+A funcionalidade de scraping agora faz parte do repositório unificado `data-platform`. Consulte:
-```python
-class DatasetManager:
- """Gerenciador de dataset HuggingFace."""
+- **[Data Platform](data-platform.md)** - Visão geral do repositório unificado
+- **[Fluxo de Dados](../arquitetura/fluxo-de-dados.md)** - Pipeline completo de coleta e enriquecimento
+- **[PostgreSQL](../arquitetura/postgresql.md)** - Banco de dados central (fonte de verdade)
- def __init__(self, repo_id: str = "nitaibezerra/govbrnews"):
- self.repo_id = repo_id
+## Principais Mudanças
- def load_dataset(self) -> pd.DataFrame:
- """Carrega dataset completo."""
- ...
+| Antes (scraper) | Agora (data-platform) |
+|-----------------|----------------------|
+| Repositório separado | Repositório unificado |
+| HuggingFace como fonte de verdade | PostgreSQL como fonte de verdade |
+| `python src/main.py scrape` | `data-platform scrape` |
+| DatasetManager (HF) | PostgresManager + StorageAdapter |
- def insert(self, articles: list[dict]) -> int:
- """Insere novos artigos (deduplica por unique_id)."""
- ...
-
- def update(self, df: pd.DataFrame, key: str = "unique_id") -> int:
- """Atualiza registros existentes."""
- ...
-
- def push(self) -> None:
- """Faz push para HuggingFace."""
- ...
-```
-
-**Geração de unique_id:**
-```python
-def generate_unique_id(agency: str, published_at: int, title: str) -> str:
- """Gera ID único: MD5(agency + published_at + title)"""
- content = f"{agency}{published_at}{title}"
- return hashlib.md5(content.encode()).hexdigest()
-```
-
-### 4. CogfyManager (`cogfy_manager.py`)
-
-Cliente para API do Cogfy.
-
-```python
-class CogfyManager:
- """Cliente da API Cogfy para classificação e sumarização."""
-
- def __init__(self, api_key: str, collection_id: str):
- self.api_key = api_key
- self.collection_id = collection_id
-
- def upload_records(self, records: list[dict]) -> list[str]:
- """Envia registros para inferência LLM."""
- ...
-
- def get_record(self, unique_id: str) -> dict:
- """Busca registro processado por unique_id."""
- ...
-```
-
-### 5. EnrichmentManager (`enrichment_manager.py`)
-
-Busca resultados do Cogfy e atualiza dataset.
-
-```python
-class EnrichmentManager:
- """Gerencia enriquecimento de dados via Cogfy."""
-
- def enrich(self, start_date: date, end_date: date) -> int:
- """Enriquece artigos no intervalo de datas."""
- # 1. Busca artigos no HuggingFace
- # 2. Busca resultados no Cogfy
- # 3. Mapeia códigos para labels
- # 4. Calcula most_specific_theme
- # 5. Atualiza dataset
- ...
-```
-
-**Campos enriquecidos:**
-
-- `theme_1_level_1_code/label` - Tema nível 1
-- `theme_1_level_2_code/label` - Tema nível 2
-- `theme_1_level_3_code/label` - Tema nível 3
-- `most_specific_theme_code/label` - Tema mais específico disponível
-- `summary` - Resumo gerado por LLM
-
----
-
-## Diagrama de Classes
-
-```mermaid
-classDiagram
- class WebScraper {
- +base_url: str
- +agency: str
- +scrape(start_date, end_date) list
- +fetch_article_content(url) str
- -parse_listing_page(html) list
- -parse_article_page(html) dict
- }
-
- class EBCWebScraper {
- +parse_article(html) dict
- }
-
- class ScrapeManager {
- +scrapers: list
- +run_parallel() list
- +run_sequential() list
- }
-
- class DatasetManager {
- +repo_id: str
- +load_dataset() DataFrame
- +insert(articles) int
- +update(df) int
- +push() void
- }
-
- class CogfyManager {
- +api_key: str
- +collection_id: str
- +upload_records(records) list
- +get_record(unique_id) dict
- }
-
- class EnrichmentManager {
- +cogfy: CogfyManager
- +dataset: DatasetManager
- +enrich(start_date, end_date) int
- }
-
- EBCWebScraper --|> WebScraper
- ScrapeManager --> WebScraper
- EnrichmentManager --> CogfyManager
- EnrichmentManager --> DatasetManager
-```
-
----
-
-## CLI (main.py)
-
-O scraper usa Typer para CLI:
-
-```bash
-# Ver ajuda
-python src/main.py --help
-
-# Comandos disponíveis
-python src/main.py scrape # Raspar sites gov.br
-python src/main.py scrape-ebc # Raspar sites EBC
-python src/main.py upload-cogfy # Upload para Cogfy
-python src/main.py enrich # Buscar enriquecimento
-```
-
-### Exemplos de uso
-
-```bash
-# Raspar últimos 7 dias
-python src/main.py scrape \
- --start-date $(date -v-7d +%Y-%m-%d) \
- --end-date $(date +%Y-%m-%d)
-
-# Raspar período específico
-python src/main.py scrape \
- --start-date 2024-12-01 \
- --end-date 2024-12-03
-
-# Raspar EBC com atualização de existentes
-python src/main.py scrape-ebc \
- --start-date 2024-12-01 \
- --end-date 2024-12-03 \
- --allow-update
-
-# Upload para Cogfy
-python src/upload_to_cogfy_manager.py \
- --start-date 2024-12-01 \
- --end-date 2024-12-03
-
-# Enriquecimento (após ~20 min do upload)
-python src/enrichment_manager.py \
- --start-date 2024-12-01 \
- --end-date 2024-12-03
-```
-
----
-
-## Configuração
-
-### Variáveis de Ambiente
-
-```bash
-# .env
-HF_TOKEN=hf_xxxxxxxxxxxxxxxxxxxxx # Token HuggingFace (escrita)
-COGFY_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxx # API Key do Cogfy
-COGFY_COLLECTION_ID=xxxxxxxx-xxxx-xxxx # ID da collection Cogfy
-```
-
-### Arquivos de Configuração
-
-#### `site_urls.yaml` - URLs de raspagem
-
-```yaml
-# Lista de URLs a raspar
-urls:
- - url: https://www.gov.br/gestao/pt-br/assuntos/noticias
- agency: gestao
- - url: https://www.gov.br/fazenda/pt-br/assuntos/noticias
- agency: fazenda
- # ... ~160+ URLs
-```
-
-#### `agencies.yaml` - Mapeamento de órgãos
-
-```yaml
-# Mapeamento ID → Nome completo
-agencies:
- gestao: Ministério da Gestão e da Inovação em Serviços Públicos
- fazenda: Ministério da Fazenda
- saude: Ministério da Saúde
- # ... 156 órgãos
-```
-
-#### `themes_tree.yaml` - Árvore temática
-
-```yaml
-# Taxonomia de 25 temas × 3 níveis
-01 - Economia e Finanças:
- 01.01 - Política Econômica:
- - 01.01.01 - Política Fiscal
- - 01.01.02 - Autonomia Econômica
- 01.02 - Fiscalização e Tributação:
- - 01.02.01 - Fiscalização Econômica
-# ...
-```
-
----
-
-## Como Adicionar Nova Fonte
-
-### 1. Adicionar URL
-
-Editar `src/scraper/site_urls.yaml`:
-
-```yaml
-urls:
- # Adicionar nova entrada
- - url: https://www.gov.br/novo-orgao/pt-br/assuntos/noticias
- agency: novo-orgao
-```
-
-### 2. Adicionar Mapeamento
-
-Editar `src/scraper/agencies.yaml`:
-
-```yaml
-agencies:
- novo-orgao: Nome Completo do Novo Órgão
-```
-
-### 3. Testar Localmente
+## CLI Atual
```bash
-# Testar raspagem do novo órgão
-python src/main.py scrape \
- --start-date $(date +%Y-%m-%d) \
- --end-date $(date +%Y-%m-%d)
-```
+# Raspagem de sites gov.br
+data-platform scrape --start-date YYYY-MM-DD --end-date YYYY-MM-DD
-### 4. Verificar Compatibilidade
+# Raspagem de sites EBC
+data-platform scrape-ebc --start-date YYYY-MM-DD --end-date YYYY-MM-DD
-Se o site tiver estrutura HTML diferente:
+# Upload para Cogfy (enriquecimento)
+data-platform upload-cogfy --start-date YYYY-MM-DD --end-date YYYY-MM-DD
-1. Analisar estrutura no navegador
-2. Criar scraper especializado se necessário
-3. Adicionar testes
-
----
-
-## Testes
-
-```bash
-# Executar todos os testes
-poetry run pytest
-
-# Com coverage
-poetry run pytest --cov=src
-
-# Teste específico
-poetry run pytest tests/test_webscraper.py -v
-
-# Testes de integração (requer credenciais)
-poetry run pytest tests/integration/ -v
+# Buscar enriquecimento do Cogfy
+data-platform enrich --start-date YYYY-MM-DD --end-date YYYY-MM-DD
```
----
-
-## Docker
-
-```bash
-# Build
-docker build -t scraper .
-
-# Executar
-docker run --env-file .env scraper \
- python src/main.py scrape --start-date 2024-12-01 --end-date 2024-12-01
-```
-
----
-
-## Links Relacionados
+## Arquivos Principais (Novo Local)
-- [Fluxo de Dados](../arquitetura/fluxo-de-dados.md) - Pipeline completo
-- [Componentes Estruturantes](../arquitetura/componentes-estruturantes.md) - Árvore temática e órgãos
-- [Integração Cogfy](./cogfy-integracao.md) - Detalhes da classificação LLM
-- [Pipeline Scraper](../workflows/scraper-pipeline.md) - GitHub Actions
+| Componente | Localização |
+|------------|-------------|
+| WebScraper | `src/data_platform/scrapers/webscraper.py` |
+| EBCWebScraper | `src/data_platform/scrapers/ebc_webscraper.py` |
+| ScrapeManager | `src/data_platform/scrapers/scrape_manager.py` |
+| PostgresManager | `src/data_platform/managers/postgres_manager.py` |
+| StorageAdapter | `src/data_platform/managers/storage_adapter.py` |
+| site_urls.yaml | `src/data_platform/scrapers/site_urls.yaml` |
+| agencies.yaml | `src/data_platform/scrapers/agencies.yaml` |
diff --git a/docs/modulos/typesense-local.md b/docs/modulos/typesense-local.md
index 6b0b2f1..9eca8c5 100644
--- a/docs/modulos/typesense-local.md
+++ b/docs/modulos/typesense-local.md
@@ -1,84 +1,33 @@
-# Módulo: Typesense Local (typesense)
+# Módulo: Typesense Local
-> Ambiente de desenvolvimento local para busca full-text.
+> Ambiente de desenvolvimento local para busca full-text e vetorial.
-**Repositório**: [github.com/destaquesgovbr/typesense](https://github.com/destaquesgovbr/typesense)
+!!! info "Módulo Migrado"
+ Os scripts de sincronização com Typesense foram migrados para o repositório **data-platform**.
+ O repositório `typesense` foi arquivado.
-## Visão Geral
+ **Novo repositório**: [github.com/destaquesgovbr/data-platform](https://github.com/destaquesgovbr/data-platform)
-O repositório `typesense` fornece:
+## Visão Geral
-- **Docker Compose** para rodar Typesense localmente
-- **Scripts Python** para carregar dados do HuggingFace
-- **Configuração** da collection `news`
+O Typesense é usado para busca full-text e vetorial no portal. Em desenvolvimento local, você pode usar Docker Compose para rodar uma instância local.
```mermaid
flowchart LR
- HF[(HuggingFace)] -->|Download| SC[Scripts Python]
+ PG[(PostgreSQL)] -->|Sync| SC[data-platform CLI]
SC -->|Upsert| TS[(Typesense Local)]
TS -->|Busca| PO[Portal Dev]
```
---
-## Estrutura do Repositório
-
-```
-typesense/
-├── docker-compose.yml # Typesense container
-├── python/
-│ ├── scripts/
-│ │ └── load_data.py # Script de carga
-│ ├── requirements.txt # Dependências Python
-│ └── config.py # Configurações
-└── README.md
-```
-
----
-
## Quick Start
-### 1. Clonar repositório
+### 1. Subir Typesense com Docker
```bash
-git clone https://github.com/destaquesgovbr/typesense.git
-cd typesense
-```
-
-### 2. Subir Typesense
-
-```bash
-docker compose up -d
-```
-
-### 3. Verificar se está rodando
-
-```bash
-curl http://localhost:8108/health
-# Resposta: {"ok":true}
-```
-
-### 4. Carregar dados
-
-```bash
-cd python
-pip install -r requirements.txt
-python scripts/load_data.py --mode incremental --days 7
-```
-
-### 5. Testar busca
-
-```bash
-curl "http://localhost:8108/collections/news/documents/search?q=economia&query_by=title" \
- -H "X-TYPESENSE-API-KEY: xyz"
-```
-
----
-
-## Docker Compose
-
-```yaml
-# docker-compose.yml
+# Criar docker-compose.yml
+cat > docker-compose.yml << 'EOF'
version: '3.8'
services:
@@ -96,65 +45,59 @@ services:
volumes:
typesense-data:
+EOF
+
+docker compose up -d
```
-### Comandos Docker
+### 2. Verificar se está rodando
```bash
-# Subir
-docker compose up -d
+curl http://localhost:8108/health
+# Resposta: {"ok":true}
+```
-# Ver logs
-docker compose logs -f typesense
+### 3. Carregar dados (via data-platform)
-# Parar
-docker compose down
+```bash
+# No diretório data-platform
+data-platform sync-typesense --start-date $(date -v-7d +%Y-%m-%d)
+```
-# Remover dados (reset completo)
-docker compose down -v
+### 4. Testar busca
+
+```bash
+curl "http://localhost:8108/collections/news/documents/search?q=economia&query_by=title" \
+ -H "X-TYPESENSE-API-KEY: xyz"
```
---
-## Script de Carga (`load_data.py`)
-
-### Modos de operação
+## Sincronização com PostgreSQL
-| Modo | Descrição | Uso |
-|------|-----------|-----|
-| `incremental` | Carrega últimos N dias | Desenvolvimento diário |
-| `full` | Carrega dataset completo | Reset ou primeira carga |
+A sincronização agora lê do **PostgreSQL** (fonte de verdade) e escreve no Typesense.
-### Exemplos
+### CLI do data-platform
```bash
-# Carregar últimos 7 dias
-python scripts/load_data.py --mode incremental --days 7
+# Sync incremental (últimos N dias)
+data-platform sync-typesense --start-date YYYY-MM-DD
-# Carregar últimos 30 dias
-python scripts/load_data.py --mode incremental --days 30
-
-# Carga completa (demora mais)
-python scripts/load_data.py --mode full
+# Sync completo (todos os registros)
+data-platform sync-typesense --full-sync
```
-### Funcionamento interno
+### Fluxo Interno
```python
-def load_incremental(days: int):
- """Carrega dados dos últimos N dias."""
- # 1. Conectar ao Typesense
- # 2. Baixar dataset do HuggingFace
- # 3. Filtrar por data
- # 4. Upsert documentos
- pass
-
-def load_full():
- """Carga completa do dataset."""
- # 1. Deletar collection existente
- # 2. Criar collection com schema
- # 3. Baixar dataset completo
- # 4. Inserir todos os documentos
+# src/data_platform/jobs/typesense/sync_job.py
+def sync_typesense(start_date: date = None, full_sync: bool = False):
+ """Sincroniza PostgreSQL → Typesense."""
+ # 1. Conectar ao PostgreSQL
+ # 2. Conectar ao Typesense
+ # 3. Buscar notícias (com embeddings)
+ # 4. Converter para schema Typesense
+ # 5. Upsert em batches de 5000
pass
```
@@ -162,32 +105,48 @@ def load_full():
## Schema da Collection
+O schema inclui campos para busca vetorial:
+
```python
schema = {
"name": "news",
"fields": [
+ # Identificação
{"name": "unique_id", "type": "string"},
{"name": "agency", "type": "string", "facet": True},
+ {"name": "agency_name", "type": "string", "facet": True},
+
+ # Conteúdo
{"name": "title", "type": "string"},
{"name": "url", "type": "string"},
{"name": "image", "type": "string", "optional": True},
{"name": "content", "type": "string"},
+ {"name": "summary", "type": "string", "optional": True},
+
+ # Datas
{"name": "published_at", "type": "int64", "sort": True},
+
+ # Classificação original
{"name": "category", "type": "string", "optional": True, "facet": True},
{"name": "tags", "type": "string[]", "optional": True},
- # Campos de tema
- {"name": "theme_1_level_1_code", "type": "string", "optional": True, "facet": True},
- {"name": "theme_1_level_1_label", "type": "string", "optional": True, "facet": True},
- {"name": "theme_1_level_2_code", "type": "string", "optional": True, "facet": True},
- {"name": "theme_1_level_2_label", "type": "string", "optional": True},
- {"name": "theme_1_level_3_code", "type": "string", "optional": True, "facet": True},
- {"name": "theme_1_level_3_label", "type": "string", "optional": True},
+ # Temas (enriquecimento Cogfy)
+ {"name": "theme_l1_code", "type": "string", "optional": True, "facet": True},
+ {"name": "theme_l1_label", "type": "string", "optional": True, "facet": True},
+ {"name": "theme_l2_code", "type": "string", "optional": True, "facet": True},
+ {"name": "theme_l2_label", "type": "string", "optional": True},
+ {"name": "theme_l3_code", "type": "string", "optional": True, "facet": True},
+ {"name": "theme_l3_label", "type": "string", "optional": True},
{"name": "most_specific_theme_code", "type": "string", "optional": True, "facet": True},
{"name": "most_specific_theme_label", "type": "string", "optional": True},
- # Resumo AI
- {"name": "summary", "type": "string", "optional": True},
+ # Embedding para busca vetorial
+ {
+ "name": "content_embedding",
+ "type": "float[]",
+ "num_dim": 768,
+ "optional": True
+ },
],
"default_sorting_field": "published_at"
}
@@ -195,6 +154,25 @@ schema = {
---
+## Busca Vetorial
+
+O Typesense em produção suporta busca semântica via embeddings:
+
+```bash
+# Busca vetorial (requer embedding da query)
+curl -X POST "http://localhost:8108/multi_search" \
+ -H "X-TYPESENSE-API-KEY: xyz" \
+ -d '{
+ "searches": [{
+ "collection": "news",
+ "q": "*",
+ "vector_query": "content_embedding:([0.1, 0.2, ...], k:10)"
+ }]
+ }'
+```
+
+---
+
## API do Typesense
### Verificar saúde
@@ -236,14 +214,14 @@ per_page=10" \
-H "X-TYPESENSE-API-KEY: xyz"
```
-### Busca por múltiplos valores
+### Busca por tema
```bash
curl "http://localhost:8108/collections/news/documents/search?\
q=*&\
query_by=title&\
-filter_by=agency:[gestao,fazenda,saude]&\
-filter_by=theme_1_level_1_code:[01,03]" \
+filter_by=theme_l1_code:01&\
+sort_by=published_at:desc" \
-H "X-TYPESENSE-API-KEY: xyz"
```
@@ -317,14 +295,7 @@ curl http://localhost:8108/collections/news \
2. Recarregar dados:
```bash
-python scripts/load_data.py --mode incremental --days 7
-```
-
-### Dados desatualizados
-
-```bash
-# Recarregar últimos dias
-python scripts/load_data.py --mode incremental --days 30
+data-platform sync-typesense --start-date $(date -v-7d +%Y-%m-%d)
```
### Reset completo
@@ -337,7 +308,7 @@ docker compose down -v
docker compose up -d
# Recarregar dados
-python scripts/load_data.py --mode full
+data-platform sync-typesense --full-sync
```
---
@@ -350,7 +321,9 @@ python scripts/load_data.py --mode full
| Porta | 8108 | 8108 |
| API Key | xyz | Secret Manager |
| Dados | Últimos N dias | Dataset completo |
+| Embeddings | Opcional | Incluídos |
| Persistência | Volume Docker | Disco persistente |
+| Fonte de dados | PostgreSQL | PostgreSQL |
---
@@ -366,13 +339,24 @@ python scripts/load_data.py --mode full
```bash
# Carregar menos dados para desenvolvimento
-python scripts/load_data.py --mode incremental --days 3
+data-platform sync-typesense --start-date $(date -v-3d +%Y-%m-%d)
```
---
+## Arquivos Principais (data-platform)
+
+| Componente | Localização |
+|------------|-------------|
+| TypesenseClient | `src/data_platform/typesense/client.py` |
+| TypesenseIndexer | `src/data_platform/typesense/indexer.py` |
+| SyncJob | `src/data_platform/jobs/typesense/sync_job.py` |
+
+---
+
## Links Relacionados
+- [Data Platform](data-platform.md) - Repositório unificado
+- [PostgreSQL](../arquitetura/postgresql.md) - Fonte de verdade
- [Setup Frontend](../onboarding/setup-frontend.md) - Configuração do portal
-- [Workflow Typesense Data](../workflows/typesense-data.md) - Carga em produção
-- [Módulo Portal](./portal.md) - Uso do Typesense no portal
+- [Workflow Typesense Data](../workflows/typesense-data.md) - Sync em produção
diff --git a/docs/onboarding/roteiro-onboarding.md b/docs/onboarding/roteiro-onboarding.md
index f0cf9ff..81aab22 100644
--- a/docs/onboarding/roteiro-onboarding.md
+++ b/docs/onboarding/roteiro-onboarding.md
@@ -62,18 +62,18 @@ flowchart LR
3. [Componentes Estruturantes](../arquitetura/componentes-estruturantes.md) (20 min)
4. [Fluxo de Dados](../arquitetura/fluxo-de-dados.md) (20 min)
5. [Setup Backend](./setup-backend.md) (1-2 horas)
-6. [Módulo Scraper](../modulos/scraper.md) (quando disponível)
+6. [Data Platform](../modulos/data-platform.md) - Repositório unificado
7. [**Apache Airflow**](./airflow-tutorial.md) (3-5 horas)
8. [Primeiro PR](./primeiro-pr.md) (30 min)
**Exercícios práticos:**
1. Rodar o scraper localmente para um órgão específico
-2. Verificar os dados no HuggingFace após uma execução
+2. Verificar os dados no PostgreSQL após uma execução
3. Adicionar uma nova fonte de dados (site gov.br)
4. Executar o pipeline de enriquecimento manualmente
5. Configurar ambiente Airflow local com Astro CLI
-6. Criar primeira DAG de exemplo seguindo o tutorial
+6. Executar sync do Typesense localmente
---
@@ -238,11 +238,13 @@ Para uma formação completa com todos os exercícios e técnicas avançadas:
| Repositório | Descrição | Quem Usa |
|-------------|-----------|----------|
-| [scraper](https://github.com/destaquesgovbr/scraper) | Pipeline de dados | Backend |
-| [portal](https://github.com/destaquesgovbr/portal) | Portal web | Frontend |
-| [typesense](https://github.com/destaquesgovbr/typesense) | Typesense local | Ambos |
-| [infra](https://github.com/destaquesgovbr/infra) | Infraestrutura | DevOps |
+| [data-platform](https://github.com/destaquesgovbr/data-platform) | Pipeline de dados (scraper, enrichment, sync) | Backend |
+| [portal](https://github.com/destaquesgovbr/portal) | Portal web Next.js | Frontend |
+| [infra](https://github.com/destaquesgovbr/infra) | Infraestrutura Terraform | DevOps |
| [agencies](https://github.com/destaquesgovbr/agencies) | Dados de órgãos | Ambos |
+| [docs](https://github.com/destaquesgovbr/docs) | Documentação | Todos |
+
+> **Nota**: Os repositórios `scraper` e `typesense` foram arquivados. O código foi movido para `data-platform`.
---
@@ -272,13 +274,13 @@ Para uma formação completa com todos os exercícios e técnicas avançadas:
- [ ] Rodei o scraper para um órgão específico
- [ ] Entendi a integração com Cogfy
-- [ ] Entendi como funciona o DatasetManager
+- [ ] Entendi como funciona o PostgresManager
+- [ ] Conectei ao PostgreSQL via Cloud SQL Proxy
- [ ] Executei o workflow de testes
- [ ] Configurei ambiente Airflow local (Astro CLI)
- [ ] Criei minha primeira DAG
- [ ] Completei pelo menos 3 exercícios do tutorial Airflow
- [ ] Completei o [Tutorial Cloud Pub/Sub](./cloud-pubsub-tutorial.md)
-- [ ] Publiquei e consumi mensagens no Pub/Sub
### Frontend Específico
diff --git a/docs/onboarding/setup-backend.md b/docs/onboarding/setup-backend.md
index 80ef626..bf36295 100644
--- a/docs/onboarding/setup-backend.md
+++ b/docs/onboarding/setup-backend.md
@@ -1,6 +1,6 @@
# Setup Backend (Python)
-> Guia de configuração do ambiente para desenvolvedores Python trabalhando no scraper e pipeline de dados.
+> Guia de configuração do ambiente para desenvolvedores Python trabalhando no data-platform e pipeline de dados.
## Pré-requisitos
@@ -25,9 +25,9 @@ git --version # Git 2.40.x ou superior
## 1. Clonar Repositório
```bash
-# Clone o repositório do scraper
-git clone https://github.com/destaquesgovbr/scraper.git
-cd scraper
+# Clone o repositório data-platform
+git clone https://github.com/destaquesgovbr/data-platform.git
+cd data-platform
```
---
@@ -46,7 +46,7 @@ poetry shell
```bash
# Deve mostrar a ajuda do CLI
-python src/main.py --help
+data-platform --help
```
---
@@ -63,103 +63,174 @@ Ou crie manualmente:
```bash
# .env
-HF_TOKEN=hf_xxxxxxxxxxxxxxxxxxxxx
+
+# PostgreSQL (fonte de verdade)
+POSTGRES_HOST=localhost # ou IP do Cloud SQL via proxy
+POSTGRES_PORT=5432
+POSTGRES_DB=destaquesgovbr
+POSTGRES_USER=admin
+POSTGRES_PASSWORD=xxxxx
+
+# Cogfy (enriquecimento)
COGFY_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxx
COGFY_COLLECTION_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
+
+# Embeddings API
+EMBEDDINGS_API_URL=https://embeddings-xxx.run.app
+
+# Typesense
+TYPESENSE_HOST=localhost
+TYPESENSE_PORT=8108
+TYPESENSE_API_KEY=xyz
+
+# HuggingFace (opcional - para sync)
+HF_TOKEN=hf_xxxxxxxxxxxxxxxxxxxxx
```
-### Obter tokens
+### Obter credenciais
-| Token | Onde obter |
-|-------|------------|
-| `HF_TOKEN` | [huggingface.co/settings/tokens](https://huggingface.co/settings/tokens) - Criar token com permissão de escrita |
+| Credencial | Onde obter |
+|------------|------------|
+| PostgreSQL | Solicitar ao tech lead (acesso via Cloud SQL Proxy) |
| `COGFY_API_KEY` | Solicitar ao tech lead |
| `COGFY_COLLECTION_ID` | Solicitar ao tech lead |
-
-> **Nota**: Para desenvolvimento local, você pode usar o dataset de staging ou criar um fork no HuggingFace.
+| `EMBEDDINGS_API_URL` | Solicitar ao tech lead |
+| `HF_TOKEN` | [huggingface.co/settings/tokens](https://huggingface.co/settings/tokens) |
---
## 4. Estrutura do Projeto
```
-scraper/
-├── src/
-│ ├── main.py # CLI principal
-│ ├── dataset_manager.py # Gerenciador HuggingFace
-│ ├── cogfy_manager.py # Cliente Cogfy API
-│ ├── upload_to_cogfy_manager.py # Upload para inferência
-│ ├── enrichment_manager.py # Busca resultados Cogfy
+data-platform/
+├── src/data_platform/
+│ ├── cli.py # CLI principal (Typer)
+│ ├── managers/
+│ │ ├── postgres_manager.py # Acesso ao PostgreSQL
+│ │ └── storage_adapter.py # Abstração de storage
+│ ├── scrapers/
+│ │ ├── webscraper.py # Scraper genérico gov.br
+│ │ ├── ebc_webscraper.py # Scraper EBC
+│ │ ├── scrape_manager.py # Orquestração
+│ │ ├── agencies.yaml # Mapeamento de órgãos
+│ │ └── site_urls.yaml # URLs de raspagem
+│ ├── cogfy/
+│ │ ├── cogfy_manager.py # Cliente Cogfy API
+│ │ ├── upload_manager.py # Upload para inferência
+│ │ └── enrichment_manager.py # Busca resultados Cogfy
│ ├── enrichment/
-│ │ └── themes_tree.yaml # Árvore temática (25 temas)
-│ └── scraper/
-│ ├── webscraper.py # Scraper genérico gov.br
-│ ├── ebc_webscraper.py # Scraper EBC
-│ ├── scrape_manager.py # Orquestração
-│ ├── agencies.yaml # Mapeamento de órgãos
-│ └── site_urls.yaml # URLs de raspagem
-├── tests/ # Testes unitários
-├── .github/workflows/ # GitHub Actions
-├── pyproject.toml # Dependências Poetry
-└── Dockerfile # Build Docker
+│ │ └── themes_tree.yaml # Árvore temática (25 temas)
+│ ├── typesense/
+│ │ ├── client.py # Cliente Typesense
+│ │ └── indexer.py # Indexação de documentos
+│ ├── jobs/
+│ │ ├── embeddings/ # Geração de embeddings
+│ │ └── typesense/ # Sync com Typesense
+│ └── dags/ # DAGs do Airflow
+├── tests/ # Testes unitários
+├── .github/workflows/ # GitHub Actions
+├── pyproject.toml # Dependências Poetry
+└── Dockerfile # Build Docker
```
---
-## 5. Executar o Scraper Localmente
+## 5. Conectar ao PostgreSQL
-### Scraping de um período específico
+### Opção A: Cloud SQL Proxy (Desenvolvimento)
```bash
-# Raspar notícias dos últimos 7 dias
-python src/main.py scrape --start-date $(date -v-7d +%Y-%m-%d) --end-date $(date +%Y-%m-%d)
+# Instalar Cloud SQL Proxy
+# macOS
+brew install cloud-sql-proxy
-# Raspar período específico
-python src/main.py scrape --start-date 2024-12-01 --end-date 2024-12-03
+# Autenticar
+gcloud auth application-default login
+
+# Iniciar proxy
+cloud_sql_proxy -instances=PROJECT_ID:southamerica-east1:destaquesgovbr-postgres=tcp:5432
+```
+
+### Opção B: Túnel SSH (Via Dev VM)
+
+```bash
+# SSH com túnel
+gcloud compute ssh devvm --zone=southamerica-east1-a -- -L 5432:10.x.x.x:5432
```
-### Scraping de um órgão específico
+### Verificar conexão
```bash
-# Editar site_urls.yaml para ter apenas o órgão desejado
-# Ou usar filtros no código (ver webscraper.py)
+# Via psql
+psql -h localhost -U admin -d destaquesgovbr
+
+# Via Python
+python -c "from src.data_platform.managers.postgres_manager import PostgresManager; pm = PostgresManager(); print(pm.get_news_count())"
+```
+
+---
+
+## 6. Executar o Scraper Localmente
+
+### Scraping de um período específico
+
+```bash
+# Raspar notícias dos últimos 7 dias
+data-platform scrape --start-date $(date -v-7d +%Y-%m-%d) --end-date $(date +%Y-%m-%d)
+
+# Raspar período específico
+data-platform scrape --start-date 2024-12-01 --end-date 2024-12-03
```
### Scraping EBC
```bash
-python src/main.py scrape-ebc --start-date 2024-12-01 --end-date 2024-12-03
+data-platform scrape-ebc --start-date 2024-12-01 --end-date 2024-12-03 --allow-update
```
---
-## 6. Testar Integração Cogfy
+## 7. Testar Integração Cogfy
### Upload para Cogfy
```bash
-python src/upload_to_cogfy_manager.py --start-date 2024-12-01 --end-date 2024-12-03
+data-platform upload-cogfy --start-date 2024-12-01 --end-date 2024-12-03
```
### Buscar enriquecimento
```bash
# Aguardar ~20 minutos após upload
-python src/enrichment_manager.py --start-date 2024-12-01 --end-date 2024-12-03
+data-platform enrich --start-date 2024-12-01 --end-date 2024-12-03
```
---
-## 7. Setup Typesense Local (Opcional)
+## 8. Setup Typesense Local
-Para testar a indexação localmente:
+Para testar a busca localmente:
```bash
-# Clone o repositório do Typesense local
-git clone https://github.com/destaquesgovbr/typesense.git
-cd typesense
-
-# Subir Typesense com Docker
+# Criar docker-compose.yml para Typesense
+cat > docker-compose.yml << 'EOF'
+version: '3.8'
+services:
+ typesense:
+ image: typesense/typesense:0.25.2
+ container_name: typesense
+ ports:
+ - "8108:8108"
+ environment:
+ - TYPESENSE_API_KEY=xyz
+ - TYPESENSE_DATA_DIR=/data
+ volumes:
+ - typesense-data:/data
+volumes:
+ typesense-data:
+EOF
+
+# Subir Typesense
docker compose up -d
# Verificar se está rodando
@@ -169,13 +240,13 @@ curl http://localhost:8108/health
### Carregar dados no Typesense
```bash
-# No repositório typesense
-python python/scripts/load_data.py --mode incremental --days 7
+# Sync do PostgreSQL para Typesense
+data-platform sync-typesense --start-date $(date -v-7d +%Y-%m-%d)
```
---
-## 8. Executar Testes
+## 9. Executar Testes
```bash
# Executar todos os testes
@@ -190,7 +261,7 @@ poetry run pytest tests/test_webscraper.py -v
---
-## 9. Linting e Formatação
+## 10. Linting e Formatação
```bash
# Formatação com Black
@@ -205,52 +276,60 @@ poetry run mypy src/
---
-## 10. Build Docker
+## 11. Build Docker
```bash
# Build da imagem
-docker build -t scraper .
+docker build -t data-platform .
# Executar container
-docker run --env-file .env scraper python src/main.py --help
+docker run --env-file .env data-platform data-platform --help
```
---
## Comandos Úteis
-### CLI do Scraper
+### CLI do data-platform
```bash
# Ver ajuda
-python src/main.py --help
+data-platform --help
# Scraping gov.br
-python src/main.py scrape --start-date YYYY-MM-DD --end-date YYYY-MM-DD
+data-platform scrape --start-date YYYY-MM-DD --end-date YYYY-MM-DD
# Scraping EBC
-python src/main.py scrape-ebc --start-date YYYY-MM-DD --end-date YYYY-MM-DD --allow-update
+data-platform scrape-ebc --start-date YYYY-MM-DD --end-date YYYY-MM-DD --allow-update
# Upload para Cogfy
-python src/upload_to_cogfy_manager.py --start-date YYYY-MM-DD --end-date YYYY-MM-DD
+data-platform upload-cogfy --start-date YYYY-MM-DD --end-date YYYY-MM-DD
# Enriquecimento
-python src/enrichment_manager.py --start-date YYYY-MM-DD --end-date YYYY-MM-DD
+data-platform enrich --start-date YYYY-MM-DD --end-date YYYY-MM-DD
+
+# Gerar embeddings
+data-platform generate-embeddings --start-date YYYY-MM-DD
+
+# Sync Typesense
+data-platform sync-typesense --start-date YYYY-MM-DD
```
-### Interação com Dataset
+### Interação com PostgreSQL
```python
# Python interativo
-from src.dataset_manager import DatasetManager
+from src.data_platform.managers.postgres_manager import PostgresManager
-dm = DatasetManager()
-df = dm.load_dataset()
-print(f"Total de documentos: {len(df)}")
+pm = PostgresManager()
-# Filtrar por agência
-gestao_df = df[df['agency'] == 'gestao']
-print(f"Notícias do MGI: {len(gestao_df)}")
+# Contar notícias
+print(f"Total de notícias: {pm.get_news_count()}")
+
+# Buscar por agência
+gestao_news = pm.get_news_by_agency("gestao", limit=10)
+for news in gestao_news:
+ print(f"{news['published_at']}: {news['title']}")
```
---
@@ -259,9 +338,9 @@ print(f"Notícias do MGI: {len(gestao_df)}")
| Arquivo | Descrição | Quando Modificar |
|---------|-----------|------------------|
-| `src/scraper/site_urls.yaml` | URLs de raspagem | Adicionar novo órgão |
-| `src/scraper/agencies.yaml` | Mapeamento ID → Nome | Novo órgão |
-| `src/enrichment/themes_tree.yaml` | Árvore temática | Novo tema/subtema |
+| `src/data_platform/scrapers/site_urls.yaml` | URLs de raspagem | Adicionar novo órgão |
+| `src/data_platform/scrapers/agencies.yaml` | Mapeamento ID → Nome | Novo órgão |
+| `src/data_platform/enrichment/themes_tree.yaml` | Árvore temática | Novo tema/subtema |
| `pyproject.toml` | Dependências | Nova biblioteca |
| `.github/workflows/main-workflow.yaml` | Pipeline diário | Alterar pipeline |
@@ -286,11 +365,15 @@ flowchart TD
## Troubleshooting
-### Erro de autenticação HuggingFace
+### Erro de conexão com PostgreSQL
```bash
-# Fazer login manualmente
-huggingface-cli login
+# Verificar se proxy está rodando
+ps aux | grep cloud_sql_proxy
+
+# Reiniciar proxy
+pkill cloud_sql_proxy
+cloud_sql_proxy -instances=PROJECT_ID:southamerica-east1:destaquesgovbr-postgres=tcp:5432
```
### Erro de conexão com Cogfy
@@ -316,7 +399,7 @@ poetry install --sync
## Próximos Passos
1. Leia o código do `webscraper.py` para entender o scraping
-2. Explore o `dataset_manager.py` para entender a integração com HuggingFace
+2. Explore o `postgres_manager.py` para entender o acesso aos dados
3. Execute o pipeline completo localmente
4. Escolha uma issue para trabalhar
diff --git a/docs/onboarding/setup-datascience.md b/docs/onboarding/setup-datascience.md
index cc8d1bd..8d58e97 100644
--- a/docs/onboarding/setup-datascience.md
+++ b/docs/onboarding/setup-datascience.md
@@ -11,19 +11,26 @@ Este módulo prepara seu ambiente para análise de dados do projeto DestaquesGov
```mermaid
flowchart LR
subgraph "Pipeline DestaquesGovBr"
- A[Scraping] --> B[Enriquecimento LLM/Cogfy]
- B --> C[HuggingFace Dataset]
- C --> D[Typesense]
+ A[Scraping] --> B[(PostgreSQL)]
+ B --> C[Enriquecimento LLM/Cogfy]
+ C --> B
+ B --> D[Typesense]
D --> E[Portal Web]
+ B -.->|DAG Airflow| F[(HuggingFace)]
end
subgraph "Seu Ambiente"
- F[Python + Poetry] --> G[VSCode + Notebooks]
- G --> H[Análise de Dados]
- C -.->|datasets lib| H
+ G[Python + Poetry] --> H[VSCode + Notebooks]
+ H --> I[Análise de Dados]
+ F -.->|datasets lib| I
end
```
+!!! info "Fonte de Dados"
+ O **PostgreSQL** é a fonte de verdade central do sistema. O **HuggingFace** é sincronizado diariamente e serve como camada de distribuição de dados abertos.
+
+ Para análises exploratórias e Data Science, o dataset no HuggingFace é a forma mais prática de acessar os dados.
+
---
## Pré-requisitos
diff --git a/docs/workflows/airflow-dags.md b/docs/workflows/airflow-dags.md
new file mode 100644
index 0000000..64206c2
--- /dev/null
+++ b/docs/workflows/airflow-dags.md
@@ -0,0 +1,315 @@
+# Airflow DAGs (Cloud Composer)
+
+O projeto utiliza **Cloud Composer 3** (Apache Airflow gerenciado) para orquestração de pipelines de dados, especialmente a sincronização entre PostgreSQL e HuggingFace.
+
+!!! info "Cloud Composer"
+ **Ambiente**: `destaquesgovbr-composer`
+ **Região**: `us-central1`
+ **Versão**: Composer 3 / Airflow 3.x
+
+## Arquitetura
+
+```mermaid
+graph TB
+ subgraph "Cloud Composer"
+ SCHED[Scheduler]
+ WORKER[Workers
1-3 instâncias]
+ UI[Web UI]
+ TRIGGER[Triggerer]
+ end
+
+ subgraph "DAGs"
+ DAG1[sync_postgres_to_huggingface]
+ DAG2[test_postgres_connection]
+ end
+
+ subgraph "Recursos GCP"
+ PG[(PostgreSQL
Cloud SQL)]
+ SM[Secret Manager]
+ GCS[GCS Bucket
DAGs]
+ end
+
+ subgraph "Externos"
+ HF[(HuggingFace)]
+ end
+
+ GCS --> SCHED
+ SCHED --> WORKER
+ WORKER --> DAG1
+ WORKER --> DAG2
+ DAG1 --> PG
+ DAG1 --> HF
+ SM --> WORKER
+```
+
+## DAGs Disponíveis
+
+### `sync_postgres_to_huggingface`
+
+Sincroniza notícias do PostgreSQL para o HuggingFace diariamente.
+
+| Configuração | Valor |
+|--------------|-------|
+| **Schedule** | `0 6 * * *` (6 AM UTC) |
+| **Catchup** | Desabilitado |
+| **Retries** | 3 (com backoff exponencial) |
+| **Tags** | `sync`, `huggingface`, `postgres`, `daily` |
+
+#### Fluxo de Execução
+
+```mermaid
+sequenceDiagram
+ participant A as Airflow
+ participant PG as PostgreSQL
+ participant API as HuggingFace API
+ participant HF as HuggingFace Dataset
+
+ A->>PG: Query notícias do dia anterior
+ PG-->>A: Registros (ex: 500)
+
+ A->>API: Consulta IDs existentes via Dataset Viewer
+ API-->>A: IDs já sincronizados (ex: 480)
+
+ Note over A: Filtra apenas novos (ex: 20)
+
+ A->>A: Cria parquet shard
+ A->>HF: Upload do shard
+ HF-->>A: Commit confirmado
+
+ A->>HF: Upload para dataset reduzido
+```
+
+#### Abordagem Incremental
+
+A DAG utiliza uma abordagem de **append incremental via parquet shards** para evitar problemas de memória:
+
+1. **Consulta IDs existentes** via Dataset Viewer API (sem baixar o dataset completo)
+2. **Cria parquet shard** apenas com novos registros
+3. **Upload direto** via `huggingface_hub`
+
+**Vantagens**:
+- Memória: ~10MB (apenas novos registros) vs ~1-2GB (dataset completo)
+- Deduplicação automática
+- Commits atômicos por dia
+
+#### Estrutura do Shard
+
+```
+data/train-{YYYY-MM-DD}-{HHMMSS}.parquet
+```
+
+Exemplo: `data/train-2025-01-10-060532.parquet`
+
+#### Colunas Sincronizadas
+
+```python
+HF_COLUMNS = [
+ "unique_id", "agency", "published_at", "updated_datetime", "extracted_at",
+ "title", "subtitle", "editorial_lead", "url", "content",
+ "image", "video_url", "category", "tags",
+ "theme_1_level_1", "theme_1_level_1_code", "theme_1_level_1_label",
+ "theme_1_level_2_code", "theme_1_level_2_label",
+ "theme_1_level_3_code", "theme_1_level_3_label",
+ "most_specific_theme_code", "most_specific_theme_label",
+ "summary"
+]
+```
+
+#### Datasets Atualizados
+
+| Dataset | Colunas | Uso |
+|---------|---------|-----|
+| `nitaibezerra/govbrnews` | Todas (24) | Análise completa |
+| `nitaibezerra/govbrnews-reduced` | 4 (published_at, agency, title, url) | Listagens rápidas |
+
+### `test_postgres_connection`
+
+DAG de teste para verificar conectividade com o PostgreSQL.
+
+| Configuração | Valor |
+|--------------|-------|
+| **Schedule** | Manual (`None`) |
+| **Uso** | Validação pós-deploy |
+
+## Configuração do Composer
+
+### Workloads
+
+| Componente | CPU | Memória | Storage | Instâncias |
+|------------|-----|---------|---------|------------|
+| Scheduler | 0.5 | 2GB | 2GB | 1 |
+| Web Server | 1 | 2GB | 2GB | 1 |
+| Worker | 1 | 2GB | 2GB | 1-3 (auto) |
+| Triggerer | 0.5 | 2GB | - | 1 |
+| DAG Processor | 0.5 | 2GB | 1GB | 1 |
+
+### Airflow Config Overrides
+
+```python
+{
+ # Secret Manager Backend
+ "secrets-backend": "airflow.providers.google.cloud.secrets.secret_manager.CloudSecretManagerBackend",
+ "secrets-backend_kwargs": {
+ "connections_prefix": "airflow-connections",
+ "variables_prefix": "airflow-variables",
+ "project_id": "inspire-7-finep"
+ },
+
+ # Timezone
+ "core-default_timezone": "America/Sao_Paulo",
+
+ # Web UI
+ "webserver-rbac": "True",
+ "webserver-authenticate": "True",
+}
+```
+
+### PyPI Packages
+
+```
+psycopg2-binary>=2.9.9
+apache-airflow-providers-postgres>=5.10.2
+apache-airflow-providers-google>=10.14.0
+sqlalchemy>=1.4.52
+requests>=2.31.0
+pandas>=2.0.0
+```
+
+### Environment Variables
+
+```bash
+POSTGRES_HOST=10.x.x.x # IP privado Cloud SQL
+POSTGRES_PORT=5432
+POSTGRES_DB=govbrnews
+GCP_PROJECT_ID=inspire-7-finep
+GCP_REGION=southamerica-east1
+TYPESENSE_HOST=34.39.186.38
+```
+
+## Connections
+
+As connections são gerenciadas via **Secret Manager**:
+
+| Connection ID | Tipo | Secret |
+|--------------|------|--------|
+| `postgres_default` | Postgres | `airflow-connections-postgres_default` |
+| `huggingface_default` | Generic | `airflow-connections-huggingface_default` |
+
+### Formato das Connections
+
+```json
+// postgres_default
+{
+ "conn_type": "postgres",
+ "host": "10.x.x.x",
+ "port": 5432,
+ "schema": "govbrnews",
+ "login": "govbrnews_app",
+ "password": "xxx"
+}
+
+// huggingface_default
+{
+ "conn_type": "generic",
+ "password": "hf_xxx" // Token HuggingFace
+}
+```
+
+## Deploy de DAGs
+
+### Via GitHub Actions
+
+O workflow `composer-deploy-dags.yaml` faz deploy automático em push:
+
+```yaml
+# .github/workflows/composer-deploy-dags.yaml
+on:
+ push:
+ paths:
+ - 'src/data_platform/dags/**'
+
+jobs:
+ deploy:
+ steps:
+ - name: Upload DAGs to GCS
+ run: |
+ gsutil -m cp -r src/data_platform/dags/* \
+ gs://${{ env.COMPOSER_BUCKET }}/dags/
+```
+
+### Manual via gcloud
+
+```bash
+# Descobrir bucket do Composer
+BUCKET=$(gcloud composer environments describe destaquesgovbr-composer \
+ --location us-central1 \
+ --format="value(config.dagGcsPrefix)")
+
+# Upload das DAGs
+gsutil -m cp -r src/data_platform/dags/* $BUCKET/
+```
+
+## Monitoramento
+
+### Acessar Web UI
+
+```bash
+# Obter URL do Airflow
+gcloud composer environments describe destaquesgovbr-composer \
+ --location us-central1 \
+ --format="value(config.airflowUri)"
+```
+
+### Logs
+
+```bash
+# Ver logs de uma DAG run
+gcloud composer environments run destaquesgovbr-composer \
+ --location us-central1 \
+ dags list-runs -- -d sync_postgres_to_huggingface
+```
+
+### Métricas
+
+O Composer exporta métricas para Cloud Monitoring:
+
+- `composer.googleapis.com/environment/dag_processing/total_parse_time`
+- `composer.googleapis.com/environment/worker/task_success_count`
+- `composer.googleapis.com/environment/worker/task_failed_count`
+
+## Troubleshooting
+
+### DAG não aparece na UI
+
+1. Verificar se o arquivo foi copiado para o bucket:
+ ```bash
+ gsutil ls $BUCKET/dags/
+ ```
+
+2. Verificar logs do DAG Processor:
+ ```bash
+ gcloud composer environments run destaquesgovbr-composer \
+ --location us-central1 \
+ tasks log-read -- -d sync_postgres_to_huggingface -t sync_news_to_huggingface
+ ```
+
+### Erro de conexão com PostgreSQL
+
+1. Verificar se a connection existe no Secret Manager
+2. Verificar se o Composer tem acesso ao Cloud SQL via VPC
+
+### Erro de memória (OOM)
+
+- A abordagem incremental resolve isso
+- Se persistir, aumentar `memory_gb` dos workers no Terraform
+
+## Custos Estimados
+
+| Componente | Custo/mês |
+|------------|-----------|
+| Cloud Composer (SMALL) | ~$100-150 |
+| GCS (DAGs bucket) | ~$1 |
+| **Total** | **~$100-150** |
+
+!!! tip "Otimização de Custos"
+ O Composer está em `us-central1` (não `southamerica-east1`) para reduzir custos. A latência adicional é aceitável para jobs batch.
diff --git a/docs/workflows/scraper-pipeline.md b/docs/workflows/scraper-pipeline.md
index 29da0af..2deaea7 100644
--- a/docs/workflows/scraper-pipeline.md
+++ b/docs/workflows/scraper-pipeline.md
@@ -2,11 +2,11 @@
> Pipeline diário de coleta e enriquecimento de notícias.
-**Arquivo**: `scraper/.github/workflows/main-workflow.yaml`
+**Arquivo**: `data-platform/.github/workflows/main-workflow.yaml`
## Visão Geral
-O pipeline é executado diariamente às **4AM UTC** (1AM Brasília) e consiste em 5 jobs sequenciais:
+O pipeline é executado diariamente às **4AM UTC** (1AM Brasília) e consiste em 7 jobs sequenciais:
```mermaid
flowchart LR
@@ -14,6 +14,8 @@ flowchart LR
B --> C[upload-to-cogfy]
C --> D[wait-cogfy]
D --> E[enrich-themes]
+ E --> F[generate-embeddings]
+ F --> G[sync-typesense]
```
---
@@ -26,10 +28,10 @@ on:
- cron: '0 4 * * *' # 4AM UTC diário
workflow_dispatch: # Manual
inputs:
- min-date:
+ start-date:
description: 'Data inicial (YYYY-MM-DD)'
required: false
- max-date:
+ end-date:
description: 'Data final (YYYY-MM-DD)'
required: false
```
@@ -50,8 +52,8 @@ gh workflow run main-workflow.yaml
# Período específico
gh workflow run main-workflow.yaml \
- -f min-date=2024-12-01 \
- -f max-date=2024-12-03
+ -f start-date=2024-12-01 \
+ -f end-date=2024-12-03
```
---
@@ -60,21 +62,24 @@ gh workflow run main-workflow.yaml \
### Job 1: `scraper`
-Raspa notícias dos sites gov.br.
+Raspa notícias dos sites gov.br e insere no PostgreSQL.
```yaml
scraper:
runs-on: ubuntu-latest
container:
- image: ghcr.io/destaquesgovbr/scraper:latest
+ image: ghcr.io/destaquesgovbr/data-platform:latest
steps:
- name: Scrape gov.br sites
run: |
- python src/main.py scrape \
- --start-date ${{ inputs.min-date || steps.dates.outputs.min }} \
- --end-date ${{ inputs.max-date || steps.dates.outputs.max }}
+ data-platform scrape \
+ --start-date ${{ inputs.start-date || steps.dates.outputs.start }} \
+ --end-date ${{ inputs.end-date || steps.dates.outputs.end }}
env:
- HF_TOKEN: ${{ secrets.HF_TOKEN }}
+ POSTGRES_HOST: ${{ secrets.POSTGRES_HOST }}
+ POSTGRES_DB: ${{ secrets.POSTGRES_DB }}
+ POSTGRES_USER: ${{ secrets.POSTGRES_USER }}
+ POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }}
```
**Duração**: ~30-60 minutos (dependendo do período)
@@ -88,38 +93,44 @@ ebc-scraper:
needs: scraper
runs-on: ubuntu-latest
container:
- image: ghcr.io/destaquesgovbr/scraper:latest
+ image: ghcr.io/destaquesgovbr/data-platform:latest
steps:
- name: Scrape EBC sites
run: |
- python src/main.py scrape-ebc \
- --start-date ${{ inputs.min-date || steps.dates.outputs.min }} \
- --end-date ${{ inputs.max-date || steps.dates.outputs.max }} \
+ data-platform scrape-ebc \
+ --start-date ${{ inputs.start-date || steps.dates.outputs.start }} \
+ --end-date ${{ inputs.end-date || steps.dates.outputs.end }} \
--allow-update
env:
- HF_TOKEN: ${{ secrets.HF_TOKEN }}
+ POSTGRES_HOST: ${{ secrets.POSTGRES_HOST }}
+ POSTGRES_DB: ${{ secrets.POSTGRES_DB }}
+ POSTGRES_USER: ${{ secrets.POSTGRES_USER }}
+ POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }}
```
**Duração**: ~10-20 minutos
### Job 3: `upload-to-cogfy`
-Envia notícias para classificação no Cogfy.
+Envia notícias do PostgreSQL para classificação no Cogfy.
```yaml
upload-to-cogfy:
needs: ebc-scraper
runs-on: ubuntu-latest
container:
- image: ghcr.io/destaquesgovbr/scraper:latest
+ image: ghcr.io/destaquesgovbr/data-platform:latest
steps:
- name: Upload to Cogfy
run: |
- python src/upload_to_cogfy_manager.py \
- --start-date ${{ inputs.min-date || steps.dates.outputs.min }} \
- --end-date ${{ inputs.max-date || steps.dates.outputs.max }}
+ data-platform upload-cogfy \
+ --start-date ${{ inputs.start-date || steps.dates.outputs.start }} \
+ --end-date ${{ inputs.end-date || steps.dates.outputs.end }}
env:
- HF_TOKEN: ${{ secrets.HF_TOKEN }}
+ POSTGRES_HOST: ${{ secrets.POSTGRES_HOST }}
+ POSTGRES_DB: ${{ secrets.POSTGRES_DB }}
+ POSTGRES_USER: ${{ secrets.POSTGRES_USER }}
+ POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }}
COGFY_API_KEY: ${{ secrets.COGFY_API_KEY }}
COGFY_COLLECTION_ID: ${{ secrets.COGFY_COLLECTION_ID }}
```
@@ -143,28 +154,82 @@ wait-cogfy:
### Job 5: `enrich-themes`
-Busca resultados do Cogfy e atualiza dataset.
+Busca resultados do Cogfy e atualiza PostgreSQL.
```yaml
enrich-themes:
needs: wait-cogfy
runs-on: ubuntu-latest
container:
- image: ghcr.io/destaquesgovbr/scraper:latest
+ image: ghcr.io/destaquesgovbr/data-platform:latest
steps:
- name: Enrich with themes
run: |
- python src/enrichment_manager.py \
- --start-date ${{ inputs.min-date || steps.dates.outputs.min }} \
- --end-date ${{ inputs.max-date || steps.dates.outputs.max }}
+ data-platform enrich \
+ --start-date ${{ inputs.start-date || steps.dates.outputs.start }} \
+ --end-date ${{ inputs.end-date || steps.dates.outputs.end }}
env:
- HF_TOKEN: ${{ secrets.HF_TOKEN }}
+ POSTGRES_HOST: ${{ secrets.POSTGRES_HOST }}
+ POSTGRES_DB: ${{ secrets.POSTGRES_DB }}
+ POSTGRES_USER: ${{ secrets.POSTGRES_USER }}
+ POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }}
COGFY_API_KEY: ${{ secrets.COGFY_API_KEY }}
COGFY_COLLECTION_ID: ${{ secrets.COGFY_COLLECTION_ID }}
```
**Duração**: ~10-20 minutos
+### Job 6: `generate-embeddings`
+
+Gera embeddings para notícias sem vetores.
+
+```yaml
+generate-embeddings:
+ needs: enrich-themes
+ runs-on: ubuntu-latest
+ container:
+ image: ghcr.io/destaquesgovbr/data-platform:latest
+ steps:
+ - name: Generate embeddings
+ run: |
+ data-platform generate-embeddings \
+ --start-date ${{ inputs.start-date || steps.dates.outputs.start }}
+ env:
+ POSTGRES_HOST: ${{ secrets.POSTGRES_HOST }}
+ POSTGRES_DB: ${{ secrets.POSTGRES_DB }}
+ POSTGRES_USER: ${{ secrets.POSTGRES_USER }}
+ POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }}
+ EMBEDDINGS_API_URL: ${{ secrets.EMBEDDINGS_API_URL }}
+```
+
+**Duração**: ~10-15 minutos
+
+### Job 7: `sync-typesense`
+
+Sincroniza dados do PostgreSQL para o Typesense.
+
+```yaml
+sync-typesense:
+ needs: generate-embeddings
+ runs-on: ubuntu-latest
+ container:
+ image: ghcr.io/destaquesgovbr/data-platform:latest
+ steps:
+ - name: Sync to Typesense
+ run: |
+ data-platform sync-typesense \
+ --start-date ${{ inputs.start-date || steps.dates.outputs.start }}
+ env:
+ POSTGRES_HOST: ${{ secrets.POSTGRES_HOST }}
+ POSTGRES_DB: ${{ secrets.POSTGRES_DB }}
+ POSTGRES_USER: ${{ secrets.POSTGRES_USER }}
+ POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }}
+ TYPESENSE_HOST: ${{ secrets.TYPESENSE_HOST }}
+ TYPESENSE_API_KEY: ${{ secrets.TYPESENSE_API_KEY }}
+```
+
+**Duração**: ~5-10 minutos
+
---
## Diagrama de Sequência
@@ -172,35 +237,47 @@ enrich-themes:
```mermaid
sequenceDiagram
participant GH as GitHub Actions
- participant SC as Scraper Container
+ participant DP as Data Platform Container
participant GOV as Sites gov.br
participant EBC as Sites EBC
- participant HF as HuggingFace
+ participant PG as PostgreSQL
participant CF as Cogfy
+ participant EMB as Embeddings API
+ participant TS as Typesense
Note over GH: 4AM UTC - Trigger
- GH->>SC: Job: scraper
- SC->>GOV: Fetch ~160+ sites
- GOV-->>SC: HTML pages
- SC->>SC: Parse → Markdown
- SC->>HF: Insert articles
+ GH->>DP: Job: scraper
+ DP->>GOV: Fetch ~160+ sites
+ GOV-->>DP: HTML pages
+ DP->>DP: Parse → Markdown
+ DP->>PG: Insert articles
- GH->>SC: Job: ebc-scraper
- SC->>EBC: Fetch EBC sites
- EBC-->>SC: HTML pages
- SC->>HF: Insert/Update articles
+ GH->>DP: Job: ebc-scraper
+ DP->>EBC: Fetch EBC sites
+ EBC-->>DP: HTML pages
+ DP->>PG: Insert/Update articles
- GH->>SC: Job: upload-to-cogfy
- SC->>HF: Load articles
- SC->>CF: POST records (batch)
+ GH->>DP: Job: upload-to-cogfy
+ DP->>PG: Load articles
+ DP->>CF: POST records (batch)
Note over GH: Wait 20 min
- GH->>SC: Job: enrich-themes
- SC->>CF: GET processed records
- CF-->>SC: Themes + Summary
- SC->>HF: Update with enrichment
+ GH->>DP: Job: enrich-themes
+ DP->>CF: GET processed records
+ CF-->>DP: Themes + Summary
+ DP->>PG: Update with enrichment
+
+ GH->>DP: Job: generate-embeddings
+ DP->>PG: Get news without embeddings
+ DP->>EMB: POST texts (batch)
+ EMB-->>DP: Vectors 768-dim
+ DP->>PG: Update with embeddings
+
+ GH->>DP: Job: sync-typesense
+ DP->>PG: Get news for indexing
+ DP->>TS: Upsert documents
```
---
@@ -209,17 +286,29 @@ sequenceDiagram
| Secret | Descrição | Usado em |
|--------|-----------|----------|
-| `HF_TOKEN` | Token HuggingFace (write) | Todos os jobs |
+| `POSTGRES_HOST` | Host do Cloud SQL | Todos os jobs |
+| `POSTGRES_DB` | Nome do banco | Todos os jobs |
+| `POSTGRES_USER` | Usuário do banco | Todos os jobs |
+| `POSTGRES_PASSWORD` | Senha do banco | Todos os jobs |
| `COGFY_API_KEY` | API Key do Cogfy | upload, enrich |
| `COGFY_COLLECTION_ID` | ID da collection Cogfy | upload, enrich |
+| `EMBEDDINGS_API_URL` | URL da API de embeddings | embeddings |
+| `TYPESENSE_HOST` | Host do Typesense | sync-typesense |
+| `TYPESENSE_API_KEY` | API Key do Typesense | sync-typesense |
### Configurar secrets
```bash
# Via GitHub CLI
-gh secret set HF_TOKEN --body "hf_xxxxx"
+gh secret set POSTGRES_HOST --body "10.x.x.x"
+gh secret set POSTGRES_DB --body "destaquesgovbr"
+gh secret set POSTGRES_USER --body "admin"
+gh secret set POSTGRES_PASSWORD --body "xxxxx"
gh secret set COGFY_API_KEY --body "sk-xxxxx"
gh secret set COGFY_COLLECTION_ID --body "uuid-xxxxx"
+gh secret set EMBEDDINGS_API_URL --body "https://embeddings-xxx.run.app"
+gh secret set TYPESENSE_HOST --body "10.x.x.x"
+gh secret set TYPESENSE_API_KEY --body "xxxxx"
```
---
@@ -262,10 +351,15 @@ gh run view --log
- Enriquecimento não executa
- Dados ficam sem classificação até próxima execução
-### Falha em enriquecimento
+### Falha em embeddings
+
+- Notícias sem embeddings são marcadas
+- Próxima execução tenta novamente
-- Dataset mantém dados sem enriquecimento
-- Próxima execução pode recuperar
+### Falha em sync Typesense
+
+- Portal continua funcionando com dados antigos
+- Próxima execução tenta novamente
---
@@ -275,8 +369,8 @@ gh run view --log
```bash
gh workflow run main-workflow.yaml \
- -f min-date=2024-01-01 \
- -f max-date=2024-01-31
+ -f start-date=2024-01-01 \
+ -f end-date=2024-01-31
```
### Para reprocessar
@@ -284,42 +378,8 @@ gh workflow run main-workflow.yaml \
```bash
# Reprocessar últimos 7 dias
gh workflow run main-workflow.yaml \
- -f min-date=$(date -v-7d +%Y-%m-%d) \
- -f max-date=$(date +%Y-%m-%d)
-```
-
----
-
-## Workflow Separado: Dispatch
-
-Existe também `scraper-dispatch.yaml` para execuções manuais isoladas:
-
-```yaml
-# scraper-dispatch.yaml
-on:
- workflow_dispatch:
- inputs:
- min-date:
- required: true
- max-date:
- required: true
- job:
- type: choice
- options:
- - scrape
- - ebc-scrape
- - upload-cogfy
- - enrich
-```
-
-Uso:
-
-```bash
-# Apenas scraping
-gh workflow run scraper-dispatch.yaml \
- -f min-date=2024-12-01 \
- -f max-date=2024-12-03 \
- -f job=scrape
+ -f start-date=$(date -v-7d +%Y-%m-%d) \
+ -f end-date=$(date +%Y-%m-%d)
```
---
@@ -333,13 +393,24 @@ gh workflow run scraper-dispatch.yaml \
| upload-to-cogfy | 5-10 min |
| wait-cogfy | 20 min (fixo) |
| enrich-themes | 10-20 min |
-| **Total** | **~75-130 min** |
+| generate-embeddings | 10-15 min |
+| sync-typesense | 5-10 min |
+| **Total** | **~90-155 min** |
+
+---
+
+## Sync HuggingFace (Separado)
+
+O sync para o HuggingFace é feito via DAG no Cloud Composer, não faz parte deste workflow.
+
+→ Veja [Airflow DAGs](./airflow-dags.md) para detalhes.
---
## Links Relacionados
+- [Data Platform](../modulos/data-platform.md) - Repositório unificado
+- [PostgreSQL](../arquitetura/postgresql.md) - Fonte de verdade
- [Fluxo de Dados](../arquitetura/fluxo-de-dados.md) - Visão geral do pipeline
-- [Módulo Scraper](../modulos/scraper.md) - Detalhes do scraper
- [Integração Cogfy](../modulos/cogfy-integracao.md) - Classificação LLM
- [Docker Builds](./docker-builds.md) - Build da imagem
diff --git a/docs/workflows/typesense-data.md b/docs/workflows/typesense-data.md
index a342b5c..ececb1f 100644
--- a/docs/workflows/typesense-data.md
+++ b/docs/workflows/typesense-data.md
@@ -4,8 +4,8 @@
**Arquivos**:
-- `infra/.github/workflows/typesense-daily-load.yml`
-- `infra/.github/workflows/typesense-full-reload.yml`
+- `data-platform/.github/workflows/typesense-maintenance-sync.yaml`
+- `data-platform/.github/workflows/typesense-full-reload.yaml`
## Visão Geral
@@ -13,23 +13,23 @@ Existem dois workflows para gerenciar dados no Typesense:
| Workflow | Uso | Frequência |
|----------|-----|------------|
-| `typesense-daily-load` | Carga incremental | Diário (10AM UTC) |
+| `typesense-maintenance-sync` | Sync incremental | Diário (10AM UTC) |
| `typesense-full-reload` | Recarga completa | Manual (destrutivo) |
```mermaid
flowchart TB
- subgraph "Daily Load"
- A[HuggingFace] -->|Últimos 7 dias| B[Upsert Typesense]
+ subgraph "Maintenance Sync"
+ A[(PostgreSQL)] -->|Últimos N dias| B[Upsert Typesense]
end
subgraph "Full Reload"
- C[HuggingFace] -->|Dataset completo| D[Delete + Insert]
+ C[(PostgreSQL)] -->|Dataset completo| D[Delete + Insert]
end
```
---
-## Workflow: Daily Load
+## Workflow: Maintenance Sync
### Trigger
@@ -39,9 +39,9 @@ on:
- cron: '0 10 * * *' # 10AM UTC diário
workflow_dispatch:
inputs:
- days:
- description: 'Número de dias para carregar'
- default: '7'
+ start-date:
+ description: 'Data inicial (YYYY-MM-DD)'
+ required: false
```
### Execução
@@ -49,12 +49,12 @@ on:
```mermaid
sequenceDiagram
participant GH as GitHub Actions
- participant HF as HuggingFace
+ participant PG as PostgreSQL
participant TS as Typesense
Note over GH: 10AM UTC
- GH->>HF: Download últimos N dias
- HF-->>GH: Parquet data
+ GH->>PG: Query últimos N dias
+ PG-->>GH: News with embeddings
GH->>TS: Upsert documents
TS-->>GH: Success
```
@@ -62,39 +62,32 @@ sequenceDiagram
### Job
```yaml
-daily-load:
+sync:
runs-on: ubuntu-latest
+ container:
+ image: ghcr.io/destaquesgovbr/data-platform:latest
steps:
- - name: Checkout
- uses: actions/checkout@v4
-
- - name: Setup Python
- uses: actions/setup-python@v5
- with:
- python-version: '3.12'
-
- - name: Install dependencies
- run: pip install -r python/requirements.txt
-
- - name: Load data
+ - name: Sync to Typesense
run: |
- python python/scripts/load_data.py \
- --mode incremental \
- --days ${{ inputs.days || 7 }}
+ data-platform sync-typesense \
+ --start-date ${{ inputs.start-date || steps.dates.outputs.start }}
env:
+ POSTGRES_HOST: ${{ secrets.POSTGRES_HOST }}
+ POSTGRES_DB: ${{ secrets.POSTGRES_DB }}
+ POSTGRES_USER: ${{ secrets.POSTGRES_USER }}
+ POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }}
TYPESENSE_HOST: ${{ secrets.TYPESENSE_HOST }}
- TYPESENSE_PORT: ${{ secrets.TYPESENSE_PORT }}
TYPESENSE_API_KEY: ${{ secrets.TYPESENSE_API_KEY }}
```
### Execução manual
```bash
-# Com padrão de 7 dias
-gh workflow run typesense-daily-load.yml
+# Com padrão (últimos 7 dias)
+gh workflow run typesense-maintenance-sync.yaml
-# Com número específico de dias
-gh workflow run typesense-daily-load.yml -f days=30
+# Com data específica
+gh workflow run typesense-maintenance-sync.yaml -f start-date=2024-12-01
```
---
@@ -132,92 +125,112 @@ full-reload:
```yaml
full-reload:
runs-on: ubuntu-latest
+ container:
+ image: ghcr.io/destaquesgovbr/data-platform:latest
steps:
- name: Validate confirmation
if: inputs.confirm != 'DELETE'
run: exit 1
- - name: Checkout
- uses: actions/checkout@v4
-
- - name: Setup Python
- uses: actions/setup-python@v5
-
- - name: Install dependencies
- run: pip install -r python/requirements.txt
-
- name: Full reload
run: |
- python python/scripts/load_data.py --mode full
+ data-platform sync-typesense --full-sync
env:
+ POSTGRES_HOST: ${{ secrets.POSTGRES_HOST }}
+ POSTGRES_DB: ${{ secrets.POSTGRES_DB }}
+ POSTGRES_USER: ${{ secrets.POSTGRES_USER }}
+ POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }}
TYPESENSE_HOST: ${{ secrets.TYPESENSE_HOST }}
- TYPESENSE_PORT: ${{ secrets.TYPESENSE_PORT }}
TYPESENSE_API_KEY: ${{ secrets.TYPESENSE_API_KEY }}
```
### Execução
```bash
-gh workflow run typesense-full-reload.yml -f confirm=DELETE
+gh workflow run typesense-full-reload.yaml -f confirm=DELETE
```
---
-## Script de Carga (`load_data.py`)
+## CLI do data-platform
+
+### Sync Incremental
+
+```bash
+# Sync desde uma data
+data-platform sync-typesense --start-date 2024-12-01
+
+# Sync últimos 7 dias (padrão do workflow)
+data-platform sync-typesense --start-date $(date -v-7d +%Y-%m-%d)
+```
+
+### Sync Completo
+
+```bash
+# Full sync (destrutivo)
+data-platform sync-typesense --full-sync
+```
-### Modo Incremental
+### Funcionamento Interno
```python
-def load_incremental(days: int):
- """Carrega últimos N dias via upsert."""
- # 1. Conectar ao Typesense
- client = get_typesense_client()
-
- # 2. Baixar dataset do HuggingFace
- ds = load_dataset("nitaibezerra/govbrnews")
- df = ds["train"].to_pandas()
-
- # 3. Filtrar por data
- cutoff = datetime.now() - timedelta(days=days)
- df = df[df["published_at"] >= cutoff.timestamp()]
-
- # 4. Upsert documentos
- for batch in chunks(df.to_dict("records"), 1000):
- client.collections["news"].documents.import_(
- batch,
- {"action": "upsert"}
- )
-
- print(f"✅ Loaded {len(df)} documents")
+# src/data_platform/jobs/typesense/sync_job.py
+def sync_typesense(start_date: date = None, full_sync: bool = False):
+ """Sincroniza PostgreSQL → Typesense."""
+ # 1. Conectar ao PostgreSQL
+ pg = PostgresManager()
+
+ # 2. Conectar ao Typesense
+ ts = TypesenseClient()
+
+ if full_sync:
+ # 3a. Full: deletar e recriar collection
+ ts.delete_collection("news")
+ ts.create_collection("news", NEWS_SCHEMA)
+
+ # 4a. Buscar todos os registros
+ news = pg.iter_news_for_typesense()
+ else:
+ # 3b. Incremental: apenas upsert
+ # 4b. Buscar registros desde start_date
+ news = pg.iter_news_for_typesense(since=start_date)
+
+ # 5. Upsert em batches de 5000
+ for batch in chunks(news, 5000):
+ ts.upsert_documents("news", batch)
```
-### Modo Full
+---
+
+## Dados Sincronizados
+
+O sync inclui embeddings para busca vetorial:
```python
-def load_full():
- """Recarga completa (destrutivo)."""
- client = get_typesense_client()
-
- # 1. Deletar collection existente
- try:
- client.collections["news"].delete()
- print("🗑️ Collection deleted")
- except Exception:
- pass
-
- # 2. Criar collection com schema
- client.collections.create(NEWS_SCHEMA)
- print("📦 Collection created")
-
- # 3. Baixar dataset completo
- ds = load_dataset("nitaibezerra/govbrnews")
- df = ds["train"].to_pandas()
-
- # 4. Inserir todos os documentos
- for batch in chunks(df.to_dict("records"), 1000):
- client.collections["news"].documents.import_(batch)
-
- print(f"✅ Loaded {len(df)} documents")
+document = {
+ "unique_id": row["unique_id"],
+ "agency": row["agency_key"],
+ "agency_name": row["agency_name"],
+ "title": row["title"],
+ "url": row["url"],
+ "image": row["image_url"],
+ "content": row["content"],
+ "summary": row["summary"],
+ "published_at": int(row["published_at"].timestamp()),
+
+ # Temas
+ "theme_l1_code": theme_l1.code if theme_l1 else None,
+ "theme_l1_label": theme_l1.label if theme_l1 else None,
+ "theme_l2_code": theme_l2.code if theme_l2 else None,
+ "theme_l2_label": theme_l2.label if theme_l2 else None,
+ "theme_l3_code": theme_l3.code if theme_l3 else None,
+ "theme_l3_label": theme_l3.label if theme_l3 else None,
+ "most_specific_theme_code": most_specific.code if most_specific else None,
+ "most_specific_theme_label": most_specific.label if most_specific else None,
+
+ # Embedding para busca vetorial
+ "content_embedding": row["content_embedding"], # 768 dims
+}
```
---
@@ -226,19 +239,22 @@ def load_full():
| Secret | Descrição |
|--------|-----------|
+| `POSTGRES_HOST` | Host do Cloud SQL |
+| `POSTGRES_DB` | Nome do banco |
+| `POSTGRES_USER` | Usuário do banco |
+| `POSTGRES_PASSWORD` | Senha do banco |
| `TYPESENSE_HOST` | IP/hostname do Typesense |
-| `TYPESENSE_PORT` | Porta (8108) |
| `TYPESENSE_API_KEY` | API Key de admin |
---
## Quando Usar Cada Workflow
-### Daily Load (Incremental)
+### Maintenance Sync (Incremental)
- ✅ Atualização diária normal
- ✅ Correção de dados recentes
-- ✅ Teste com período maior
+- ✅ Após pipeline diário do scraper
### Full Reload
@@ -246,6 +262,7 @@ def load_full():
- ⚠️ Dados corrompidos
- ⚠️ Limpeza completa necessária
- ⚠️ Primeira carga em novo ambiente
+- ⚠️ Após adição de embeddings em massa
---
@@ -254,22 +271,22 @@ def load_full():
### Ver execuções
```bash
-# Daily load
-gh run list --workflow=typesense-daily-load.yml
+# Maintenance sync
+gh run list --workflow=typesense-maintenance-sync.yaml
# Full reload
-gh run list --workflow=typesense-full-reload.yml
+gh run list --workflow=typesense-full-reload.yaml
```
### Verificar dados no Typesense
```bash
-# Via SSH no servidor
-curl "http://localhost:8108/collections/news" \
+# Via SSH no servidor ou API
+curl "http://:8108/collections/news" \
-H "X-TYPESENSE-API-KEY: $API_KEY"
# Contagem de documentos
-curl "http://localhost:8108/collections/news/documents/search?q=*&per_page=0" \
+curl "http://:8108/collections/news/documents/search?q=*&per_page=0" \
-H "X-TYPESENSE-API-KEY: $API_KEY"
```
@@ -277,23 +294,30 @@ curl "http://localhost:8108/collections/news/documents/search?q=*&per_page=0" \
## Troubleshooting
-### Daily load não atualiza
+### Maintenance sync não atualiza
-1. Verificar se HuggingFace tem dados novos
+1. Verificar se PostgreSQL tem dados novos
2. Verificar logs do workflow
-3. Executar manualmente com mais dias
+3. Executar manualmente com data específica
### Full reload falha
-1. Verificar conexão com Typesense
-2. Verificar espaço em disco
-3. Verificar memória disponível
+1. Verificar conexão com PostgreSQL
+2. Verificar conexão com Typesense
+3. Verificar espaço em disco no servidor Typesense
+4. Verificar memória disponível
### Dados inconsistentes
1. Executar full reload
2. Verificar schema da collection
-3. Verificar dados no HuggingFace
+3. Verificar dados no PostgreSQL
+
+### Embeddings ausentes
+
+1. Verificar se job `generate-embeddings` executou com sucesso
+2. Verificar se registros têm `content_embedding` no PostgreSQL
+3. Re-executar pipeline de embeddings se necessário
---
@@ -301,14 +325,16 @@ curl "http://localhost:8108/collections/news/documents/search?q=*&per_page=0" \
| Workflow | Duração Típica |
|----------|----------------|
-| Daily Load (7 dias) | 5-10 min |
-| Daily Load (30 dias) | 15-20 min |
-| Full Reload | 30-60 min |
+| Maintenance Sync (7 dias) | 5-10 min |
+| Maintenance Sync (30 dias) | 15-20 min |
+| Full Reload | 45-90 min |
---
## Links Relacionados
+- [Data Platform](../modulos/data-platform.md) - Repositório unificado
+- [PostgreSQL](../arquitetura/postgresql.md) - Fonte de verdade
- [Typesense Local](../modulos/typesense-local.md) - Ambiente de desenvolvimento
- [Fluxo de Dados](../arquitetura/fluxo-de-dados.md) - Pipeline completo
- [Arquitetura GCP](../infraestrutura/arquitetura-gcp.md) - Infraestrutura
diff --git a/mkdocs.yml b/mkdocs.yml
index 33f35f8..f7456d9 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -44,9 +44,11 @@ nav:
- Home: index.md
- Arquitetura:
- Visão Geral: arquitetura/visao-geral.md
+ - PostgreSQL: arquitetura/postgresql.md
- Fluxo de Dados: arquitetura/fluxo-de-dados.md
- Componentes: arquitetura/componentes-estruturantes.md
- Módulos:
+ - Data Platform: modulos/data-platform.md
- Scraper: modulos/scraper.md
- Portal: modulos/portal.md
- Cogfy/LLM: modulos/cogfy-integracao.md
@@ -59,6 +61,7 @@ nav:
- Deploy Portal: workflows/portal-deploy.md
- Docker Builds: workflows/docker-builds.md
- Typesense Data: workflows/typesense-data.md
+ - Airflow DAGs: workflows/airflow-dags.md
- Infraestrutura:
- Arquitetura GCP: infraestrutura/arquitetura-gcp.md
- Terraform: infraestrutura/terraform-guide.md