Scraping de Notícias, Embeddings de Artigos e Agrupamento de Histórias



#machine-learning #nlp

O objetivo do produto é prático: agrupar artigos sobre a mesma história, depois comparar quais fontes cobriram o evento, como cada fonte enquadrou o evento e quais afiliações políticas deram atenção para a cobertura.

O sistema agrupa artigos de notícias por evento para comparar como diferentes fontes cobrem a mesma história.

O pipeline atual tem cerca de 1,3 mil artigos extraídos e 855 artigos agrupados, com os dois números crescendo conforme a ingestão continua.

A heurística contra fake news é útil, mas limitada. Quando várias fontes de diferentes afiliações políticas cobrem a mesma história, o risco de fake news diminui. Isso não prova a veracidade do fato. Qualidade da fonte, evidência primária e contexto ainda fazem parte da análise.

Pipeline Atual

Para cada artigo extraído, o sistema:

  • Limpa o texto do corpo
  • Normaliza o título
  • Salva o registro do artigo no Postgres
  • Salva o JSON bruto do scrape no R2
  • Calcula um content_hash a partir dos textos exatos enviados para embedding
  • Enfileira um job de embedding no BullMQ
  • Gera embeddings em um worker Python
  • Compara o artigo com clusters recentes
  • Entra em um cluster existente ou cria um novo cluster

O worker usa intfloat/multilingual-e5-base para embeddings e salva os vetores no Postgres com pgvector.

O Primeiro Problema de Clustering

A primeira versão do clustering usava similaridade pelo corpo completo do artigo.

Isso funcionou para agrupamento amplo por assunto, mas gerou correspondências ruins em notícias políticas.

Um erro comum tinha este formato:

  • Mesmo político
  • Mesmo partido
  • Mesmo contexto eleitoral
  • Evento diferente

Artigos completos repetem contexto de fundo com frequência. Eles citam os mesmos nomes, cargos, partidos, locais e histórico. Um embedding do corpo completo enxerga esses artigos como parecidos, mesmo quando eles descrevem histórias diferentes.

Aumentar o threshold não resolveu bem o problema. A mudança reduziu algumas correspondências falsas, mas também criou risco de separar artigos sobre o mesmo evento.

A Estratégia Atual de Embeddings

O pipeline atual salva dois textos de embedding por artigo:

  • embedding_text: título normalizado + corpo completo limpo
  • lead_text: título normalizado + as 3 primeiras frases limpas do corpo

O worker salva os dois vetores:

  • embedding_kind = full
  • embedding_kind = lead

O embedding completo mede similaridade semântica ampla. O embedding do lead verifica o enquadramento do evento.

Isso importa porque o lead costuma carregar o evento atual. O corpo costuma carregar contexto reutilizável.

Correspondência de Clusters

Um artigo novo não entra em um cluster depois de uma única checagem de similaridade.

O worker primeiro compara o artigo com centróides de clusters recentes. Depois o worker verifica o artigo representante do cluster candidato.

A regra atual de entrada no cluster usa:

  • Similaridade com centróide >= article_clustering_runs.similarity_threshold
  • Similaridade com o representante completo >= article_clustering_runs.similarity_threshold
  • Similaridade com o lead do representante >= 0.91

O threshold inicial atual para article_clustering_runs.similarity_threshold é 0.93.

O worker verifica até 5 candidatos mais próximos por centróide entre clusters recentes. A janela atual de cluster é de 7 dias.

Isso mantém os clusters focados em histórias atuais. Histórias antigas relacionadas pertencem a links de histórico, não ao cluster ativo da história.

Recência de Notícias

Recência importa porque notícias mudam depois da publicação.

Um veículo pode atualizar uma manchete, adicionar contexto, corrigir um parágrafo ou alterar o corpo. O código de scraping também pode mudar a forma de limpar o texto.

O sistema calcula content_hash a partir dos dois textos exatos enviados para embedding:

  • embedding_text
  • lead_text

Quando um artigo já existe, o scraper compara o novo hash com articles.content_hash.

Se os hashes forem iguais, o conteúdo usado para embedding não mudou. Nesse caso, o sistema não precisa enfileirar um novo job de embedding.

Se os hashes forem diferentes, o artigo foi atualizado ou o pipeline produziu um texto de embedding diferente. O sistema salva a nova versão do artigo, atualiza articles.content_hash e enfileira um novo job de embedding com esse hash.

Antes de gerar embeddings, o worker compara o contentHash recebido na fila com o articles.content_hash atual.

Se os hashes forem diferentes, o worker ignora o job. Isso significa que aquele job ficou obsoleto: uma versão mais nova do artigo já foi salva depois que ele foi enfileirado.

Isso evita que retries antigos ou jobs atrasados sobrescrevam embeddings e decisões de clustering baseadas em uma versão mais recente do artigo.

Por Que Isso Ajuda na Comparação de Viés

Comparação de viés precisa primeiro de agrupamento por história.

Você não deveria comparar manchetes aleatórias entre veículos. Você deveria comparar a cobertura do mesmo evento.

Quando o sistema agrupa artigos por história, você tem uma visão mais limpa de:

  • Quais fontes cobriram o evento
  • Quais fontes ignoraram o evento
  • Quais afiliações políticas notaram o evento
  • Como títulos e leads enquadram o mesmo evento
  • Se a cobertura aparece em múltiplas afiliações

Em Desenvolvimento

O sistema ainda está em desenvolvimento ativo.

O pipeline de ingestão, os embeddings e o clustering já funcionam, mas análise de viés, comparação de enquadramento entre fontes e visualização dos dados ainda estão sendo construídos.

A base está crescendo a cada ciclo de scraping, e os próximos passos vão transformar esses clusters em insights reais sobre como diferentes veículos cobrem as mesmas histórias. O potencial aqui é grande: uma ferramenta que torna a análise de mídia mais acessível, sistemática e fundamentada em dados.