Feat/bge m3 migration - #184
Conversation
Add support for migrating from mpnet-768d to BGE-M3-1024d embeddings with zero-downtime dual-index strategy. Database (PostgreSQL): - Migration 004: Add content_embedding (1024d) + embedding_model_version - Rename existing content_embedding → content_embedding_legacy (768d) - Create HNSW index for new BGE-M3 embeddings - Add migration tracking index for batch processing Typesense: - Update collection schema to support dual embeddings (768d + 1024d) - Add embedding_model_version field for tracking - Update indexer to sync both embedding fields Models: - Add content_embedding_legacy, content_embedding, embedding_model_version - Update News and NewsInsert Pydantic models Migration Strategy: - New articles: use BGE-M3 (1024d) immediately - Existing articles: gradual migration via DAG (10k/day) - Collection will be recreated with new schema (requires manual step) Rollback: - scripts/migrations/004_rollback.sql to revert if needed Related: - destaquesgovbr/embeddings#1 (API changes) - destaquesgovbr/data-science#1 (model validation) - #175
Add complete offline migration pipeline for mpnet → BGE-M3 embeddings
using GPU (EC2 L4).
Scripts:
- migrate_to_bge_m3.py: Main migration script with GPU support
- generate: Create embeddings from dump
- upload: Bulk upload to PostgreSQL
- full: Complete pipeline
Features: checkpoints, resume, progress bars, error handling
- dump_articles_for_migration.sql: Export articles from PostgreSQL
- csv_to_parquet.py: Convert CSV → Parquet (compression + speed)
- test_local.sh: Local validation script with sample data
- requirements.txt: Python dependencies
- README.md: Complete documentation (30+ pages)
Architecture:
- Offline processing (zero impact on production API)
- GPU L4: ~200-300 articles/s (vs ~1-2 on CPU)
- Total time: 15-25h for 300k articles (vs 30 days with DAG)
- Cost: $0 (EC2 already exists)
Usage:
# Quick test
./test_local.sh
# Full pipeline
python migrate_to_bge_m3.py full \
--input artigos_para_migrar.parquet \
--database-url $DATABASE_URL
Related: #175
Add scripts to facilitate local testing of embeddings migration: - setup_local_test.sh: Automated setup script - Creates test database (govbrnews_test) - Restores SQL dump - Applies migration 004 - Shows statistics - QUICKSTART.md: Step-by-step guide - Option 1: Automated script - Option 2: Manual steps - End-to-end test - Troubleshooting Workflow: 1. ./setup_local_test.sh (restore dump + apply migration) 2. Export articles for migration 3. Test embedding generation with GPU/CPU 4. Upload back to local DB 5. Validate results This allows testing the complete pipeline locally before running on EC2 L4 with production data. Related: #175
Fix paths to: - Dump file: ../data_dump → ../../data_dump - Migration: ../migrations → ../../scripts/migrations
Detect available PostgreSQL user (current user or postgres) and use it for all psql/createdb commands. Fixes: 'role lpmoraes does not exist' error
- Remove hard dependency on pv (progress viewer) - Fallback to direct psql < dump.sql when pv not available - Add exit code checking and error handling - Provide manual command if restore fails Fixes: dump silently failing when pv command not found
Fix TypeError when uploading embeddings: convert numpy.ndarray to list before psql insert. Tested with 100 articles: all uploaded successfully.
Increase content preview from 500 chars to 24,000 chars to better utilize BGE-M3's 8192 token capacity. Changes: - Add MAX_CHARS = 24000 constant - Use available_chars calculation for content - Add safety truncation at the end - Update docstring with BGE-M3 limits Rationale: - BGE-M3 supports 8192 tokens (~32k chars) - Database has articles with content up to 7MB - Previous 500 char limit was too conservative - 24k chars ≈ 8k tokens (conservative estimate) Impact: - Better embeddings for long articles without summary - No change for articles with summary (already good) - Stays within model limits (safety truncation)
REVISÃO DO PR #184 — data-platformAutor: Luis F Moraes (ODenteAzul) RESUMO EXECUTIVOEste PR implementa a migração completa de embeddings mpnet-768d → BGE-M3-1024d com estratégia dual-index (zero downtime) e pipeline offline otimizado para GPU. A implementação é sólida com documentação excepcional, mas requer correções críticas antes do merge. DECISÃO: REQUER MUDANÇAS PROBLEMAS CRÍTICOS (bloqueiam merge)🔴 [CRITICO] Migration SQL rename sem coordenação com códigoArquivo: Problema: Migration renomeia Impacto: Scrapers/DAGs que escrevem Sugestão: Adicionar step intermediário: -- Step 1.5: Criar coluna legacy primeiro, copiar dados
ALTER TABLE news ADD COLUMN IF NOT EXISTS content_embedding_legacy vector(768);
UPDATE news SET content_embedding_legacy = content_embedding WHERE content_embedding IS NOT NULL;
-- Step 1.6: Dropar coluna antiga e recriar como 1024d
ALTER TABLE news DROP COLUMN content_embedding;
ALTER TABLE news ADD COLUMN content_embedding vector(1024);Ou documentar explicitamente no README que a migration requer parada temporária de writes na tabela Ordem crítica de deploy:
🔴 [CRITICO] UPDATE sem verificação de affected rowsArquivo: Problema: Upload usa Impacto: Se dump contiver IDs de artigos deletados após export, embeddings serão "perdidos" sem erro visível. Script reportará "uploaded: N embeddings" mas banco não terá essas linhas atualizadas. Sugestão: Verificar affected rows: cursor.execute("""UPDATE news SET ... WHERE id = %s""", (..., row["id"]))
if cursor.rowcount == 0:
logger.warning(f"Article id={row['id']} not found in DB (possibly deleted)")
stats["skipped"] += 1Adicionar campo 🔴 [CRITICO] Portal não coordenadoProblema: Typesense schema muda (adiciona Impacto: Se portal fizer queries de busca semântica durante migração, pode:
Sugestão: Criar issue no repo
export interface ArticleRow {
content_embedding?: number[]; // 1024-dim (BGE-M3)
content_embedding_legacy?: number[]; // 768-dim (mpnet)
embedding_model_version?: string;
...
}
const embeddingField = article.content_embedding ? 'content_embedding' : 'content_embedding_legacy';PROBLEMAS ALTOS (devem ser corrigidos)🟠 [ALTO] Upload row-by-row ineficienteArquivo: Problema: Upload executa Impacto: Upload estimado em ~15min pode levar horas. Throughput reportado de "323 updates/s" é irreal para single-row UPDATEs. Sugestão: Usar bulk update com from psycopg2.extras import execute_batch
values = [(row["embedding"], 'bge-m3', row["id"]) for _, row in batch.iterrows()]
execute_batch(cursor, """
UPDATE news SET
content_embedding = %s::vector,
embedding_model_version = %s,
embedding_generated_at = NOW()
WHERE id = %s
""", values, page_size=1000)
conn.commit()Ganho esperado: 10-50x mais rápido (~30s para 300k em vez de 15min). 🟠 [ALTO] Migration não idempotenteArquivo: Problema: Query Impacto: Registros com Sugestão: Adicionar guard: UPDATE news
SET embedding_model_version = 'mpnet'
WHERE content_embedding_legacy IS NOT NULL
AND (embedding_model_version IS NULL OR embedding_model_version = '');
-- não sobrescreve 'bge-m3'🟠 [ALTO] Typesense dimension mismatch durante transiçãoArquivo: Problema: Schema Typesense define Impacto: Artigos com embedding legacy (768-dim) serão rejeitados pelo Typesense (dimension mismatch). Verificação: Código do indexer (linhas 2135-2152) JÁ TRATA CORRETAMENTE dual embeddings — OK. Mas documentar no README o comportamento durante transição:
PROBLEMAS MÉDIOS (não bloqueiam merge)🟡 [MEDIO] Normalização de embeddings não documentadaArquivo: Problema: Sugestão: Adicionar comment explicitando: normalize_embeddings=False, # OK: Typesense usa vector_cosine_ops (normaliza server-side)🟡 [MEDIO] Error threshold absoluto em vez de rateArquivo: Problema: Sugestão: Usar error rate: error_rate = errors / max(processed, 1)
if error_rate > 0.01: # 1% error rate
logger.error(f"Error rate too high ({error_rate:.2%}), aborting...")
raise🟡 [MEDIO] BigQuery sync não atualizadoProblema: PR não atualiza Impacto: BigQuery ficará desatualizado. Analytics/dashboards que dependem de tracking não funcionarão. Sugestão: Adicionar em PR subsequente ou marcar TODO no código. 🟡 [MEDIO] Testabilidade da classe MigratorArquivo: Problema: Classe Sugestão: Adicionar injeção de dependência: def __init__(self, model_name: str = "BAAI/bge-m3", model=None, ...):
self.model = model or SentenceTransformer(model_name, device=device)Criar testes com modelo mockado. PROBLEMAS BAIXOS (podem ir como follow-up)⚪ [BAIXO] Logging de truncationArquivo: ⚪ [BAIXO] Progress log frequencyArquivo: ✅ PONTOS POSITIVOS
📋 CHECKLIST PRÉ-MERGE
Revisão feita via /revisar-pr skill | DGB Project |
Correções baseadas na revisão de Miguel (@miguellsfilho): Fix #2 (CRÍTICO): Verificar rowcount no upload - Adiciona verificação de cursor.rowcount após UPDATE - Detecta artigos deletados após dump (IDs órfãos) - Log warning + contador "skipped" no resultado final - Previne perda silenciosa de embeddings Fix #4 (ALTO): Usar execute_batch para bulk upload - Substitui loop row-by-row por psycopg2.extras.execute_batch - Ganho estimado: 10-50x mais rápido - Upload de 334k artigos: ~1.5h → ~5-15 minutos - Mantém contagem de skipped via cursor.rowcount Fix #5 (ALTO): Migration SQL idempotente - Adiciona guard: (embedding_model_version IS NULL OR = '') - Previne sobrescrever 'bge-m3' → 'mpnet' se migration rodar 2x - Segurança operacional Fix #6 (MÉDIO): Documentar normalização no código - Adiciona comment explicando por que normalize_embeddings=False - Contexto: pgvector vector_cosine_ops + Typesense normalizam server-side Decisões (acordo com @LPMoraes): - Fix #1 (rename): NÃO corrigido - DAGs têm retry, window ~2-5s OK - Fix #3 (portal): Coordenar com Miguel antes de implementar Issue: #184 Review: #184 (comment) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Code Review Feedback - Correções ImplementadasFixes Aplicados (Commit 302a043)Fix #2 (CRÍTICO) - Verificar rowcount no upload
Fix #4 (ALTO) - execute_batch para bulk upload
Fix #5 (ALTO) - Migration SQL idempotente
Fix #6 (MÉDIO) - Documentar normalização
Não Corrigido (com justificativa)Fix #1 (CRÍTICO) - Migration rename
Fix #3 (CRÍTICO) - Portal não coordenado
Status: Correções CRÍTICAS e ALTAS implementadas |
Migration integration tests verify SQL sequence correctness, not Python code coverage. The addopts in pyproject.toml applies --cov globally and the fail_under=70 threshold on main causes this job to fail with 5.41% coverage (only models/news.py is exercised). Using --no-cov overrides addopts for this job. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
🔍 SEGUNDA REVISÃO COMPLETA — PR #184Revisor: Claude Code (via @miguellsfilho) 📊 RESUMO EXECUTIVODECISÃO: APROVAR COM CONDIÇÕES O PR implementa migração de embeddings Estimativa de esforço para correções: ~1.5 horas ✅ VALIDAÇÃO DAS CORREÇÕES APLICADAS (Commit 302a043)Fix #2 — Verificação de
|
| Correção | Complexidade | Tempo | Risco |
|---|---|---|---|
| NOVO-4 (RENAME idempotente) | Baixa | 15 min | Baixo |
| NOVO-6 (Typesense conflict check) | Média | 30 min | Médio |
| NOVO-1 (validar dimensão) | Baixa | 10 min | Baixo |
| EC-3 (error handling modelo) | Baixa | 20 min | Baixo |
| TOTAL | - | ~1.5h | - |
🎯 RECOMENDAÇÃO FINAL
APROVAR COM CONDIÇÕES:
- ✅ Commit
302a043foi muito efetivo — especialmenteexecute_batch ⚠️ 4 correções necessárias (~1.5h de trabalho total)⚠️ Coordenar deploy portal antes de produção (Fix feat: Phase 3 Complete - Migration 309k records to Cloud SQL #3)
Após correções: PR estará PRONTO PARA MERGE.
⭐ PONTOS POSITIVOS MANTIDOS
- Documentação excepcional (README 481 linhas + QUICKSTART 288 linhas)
- Checkpoint system robusto (resume após interrupção)
- Estratégia dual-index segura (rollback completo possível)
- Performance excelente (execute_batch: ganho 25-100x confirmado)
📈 QUALIDADE GERAL
⭐⭐⭐⭐ (4/5) — Muito bom, requer apenas polish final.
🛠️ GUIA RÁPIDO DE IMPLEMENTAÇÃO
Para implementar as 4 correções usando Claude Code:
1️⃣ NOVO-4 — Migration SQL idempotente (~5 min)
# No terminal do projeto:
cd /caminho/para/data-platform
# Prompt para Claude:
"Edit scripts/migrations/004_add_bge_m3_columns.sql:
Substituir as linhas 7-8 (Step 1 - ALTER TABLE news RENAME COLUMN...)
pelo bloco DO $$ idempotente que está no comentário NOVO-4 da segunda review
do PR #184.
O novo código deve verificar se a coluna content_embedding existe e se
content_embedding_legacy não existe antes de fazer o RENAME."2️⃣ NOVO-6 — Typesense schema conflict (~15 min)
# Prompt para Claude (em 2 passos):
# Passo 1:
"Edit src/data_platform/typesense/collection.py:
Adicionar a função _check_embedding_dimension_conflict() logo após os imports
(por volta da linha 25). Usar o código completo da CORREÇÃO 1 do comentário
NOVO-6 da segunda review do PR #184."
# Passo 2:
"Edit src/data_platform/typesense/collection.py:
Localizar a função update_schema() e adicionar no INÍCIO do corpo da função
a chamada: _check_embedding_dimension_conflict(self.client, collection_name='news')
Ver CORREÇÃO 2 do comentário NOVO-6."3️⃣ NOVO-1 — Validar dimensão do embedding (~5 min)
# Prompt para Claude:
"Edit scripts/embeddings-migration/migrate_to_bge_m3.py:
Na função process_batch(), adicionar validação de dimensão do embedding
ANTES do return (logo após o model.encode(), linha ~163).
Usar o código da CORREÇÃO do comentário NOVO-1 da segunda review.
Deve validar se embeddings.shape[1] == 1024 e raise ValueError se diferente."4️⃣ EC-3 — Error handling do modelo (~10 min)
# Prompt para Claude:
"Edit scripts/embeddings-migration/migrate_to_bge_m3.py:
Na função __init__() da classe BGE_M3_Migrator (linhas ~1037-1042),
substituir o bloco de load do modelo (desde 'logger.info(Loading model...)'
até 'logger.info(Embedding dimension...)') pelo código com try/except
e troubleshooting steps do comentário EC-3 da segunda review.
O novo código deve:
1. Carregar modelo dentro de try/except
2. Validar com test embedding
3. Logar troubleshooting steps se falhar"Testar após implementação:
# Teste 1: Migration SQL idempotente
cd scripts/embeddings-migration
./setup_local_test.sh
psql govbrnews_test < ../migrations/004_add_bge_m3_columns.sql
psql govbrnews_test < ../migrations/004_add_bge_m3_columns.sql # 2ª vez deve funcionar
# Teste 2: Script de migração com sample
python csv_to_parquet.py /tmp/artigos_para_migrar.csv --sample 100
python migrate_to_bge_m3.py generate \
--input artigos_para_migrar.parquet \
--output test_embeddings.parquet \
--batch-size 32
# Deve ver logs:
# "✅ Model loaded and validated in X.Xs"
# "Embedding dimension: 1024"Commitar:
git add scripts/migrations/004_add_bge_m3_columns.sql
git add scripts/embeddings-migration/migrate_to_bge_m3.py
git add src/data_platform/typesense/collection.py
git commit -m "fix: segunda review PR #184 - idempotência e validações
- Migration SQL idempotente (NOVO-4)
- Verificação de schema conflict Typesense (NOVO-6)
- Validação de dimensionalidade do embedding (NOVO-1)
- Error handling melhorado no load do modelo (EC-3)
Review: https://github.com/destaquesgovbr/data-platform/pull/184#issuecomment-XXX
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>"Revisão feita via /revisar-pr skill | Projeto DGB
Título:
Descrição:
2. Rodar Migração na EC2 L4
Tempo estimado: ~20-30 minutos para 334k artigos
3. Recriar Typesense Collection
Seguir passos em
PLANO_MIGRACAO_BGE_M3.md(repo infra)Breaking Changes
Nenhum. Estratégia dual-index:
Arquivos Principais
Performance Esperada
Documentação
Ver documentação completa em:
scripts/embeddings-migration/README.mdscripts/embeddings-migration/QUICKSTART.mdPLANO_MIGRACAO_BGE_M3.md(repo infra)