Fetching latest headlines…
🔍 Como tornei milhões de conversas históricas "invisíveis" buscáveis em tempo real
NORTH AMERICA
🇺🇸 United StatesMay 11, 2026

🔍 Como tornei milhões de conversas históricas "invisíveis" buscáveis em tempo real

1 views0 likes0 comments
Originally published byDev.to

O problema

Trabalhei recentemente em uma solução para um cenário que muita gente que
mexe com sistemas em escala já enfrentou: um sistema gera diariamente
arquivos JSON que vão para um storage ou qualquer outro banco como histórico, seguindo um padrão de nomenclatura previsível. Com o tempo, esses arquivos se acumulam aos milhões.

A dor apareceu quando surgiu a necessidade de consultar esse histórico.

Três problemas surgiram ao mesmo tempo

Parte da informação que identifica cada arquivo nem sempre estava preenchida no nome, porque é um dado mutável e nem sempre estava disponível no momento da geração.

As pessoas precisavam buscar por critérios variados, alguns estruturados, outros por palavra-chave dentro do conteúdo, e nada disso estava exposto no nome do arquivo

Eram milhões de arquivos espalhados em containers diferentes, varrer linearmente era inviável.

Em resumo: tínhamos os dados, mas eram efetivamente invisíveis.

A solução

Construí um pipeline em .NET 9 que varre o storage e indexa todo o conteúdo em um cluster Elasticsearch (rodando em Kubernetes via ECK
Operator), com mapping otimizado para texto em português (analyzer com
stemmer, asciifolding e stopwords em PT-BR). O resultado é busca textual
em milhões de documentos em tempo real, mesmo quando o termo aparece
apenas no meio de uma seção específica do conteúdo.

Os pontos de design que fizeram diferença

Pipeline producer/consumer com System.Threading.Channels
Producer lista arquivos paginado, workers em paralelo baixam e parseiam em streaming, e bulk indexer envia batches pro Elastic. Channels bounded controlam pressão de memória.

Parser tolerante a dados sujos
Os arquivos históricos vinham com toda sorte de inconsistência: HTML entities, campos ausentes, datas inválidas, números como string, ordem de seções variável. Implementei discriminated union ParseResult (Success/Skipped/Failed) com helpers TryGet defensivos.
Política: indexa o que conseguiu, descarta apenas o irrecuperável, nunca derruba o pipeline por um arquivo ruim.

Busca híbrida em duas camadas
Quando o termo digitado tem um padrão estruturado, a API consulta o próprio storage (prefix lookup nativo, extremamente rápido) e o Elastic em paralelo. Resultados unidos: hits do storage no topo, hits textuais abaixo. O melhor de dois mundos sem latência adicional.

Boosts diferenciados por campo no Elastic
Match em campos estruturados tem boost maior. Match em texto solto tem boost menor. Resultado: busca por uma palavra específica sobe primeiro quem tem essa palavra em campo identificador, e só depois quem mencionou em texto livre.

Estratégia de alias para reindexação sem downtime
Cada carga gera um índice físico novo (com timestamp no nome) e o alias é trocado atomicamente no final. Permite reprocessar o histórico inteiro sem afetar produção.

Checkpointing stateless
Cada partição tem seu progresso salvo em storage externo. Se o pod morrer no meio da carga, retoma exatamente de onde parou. Sem volumes persistentes, sem estado local.

Highlights com fragmentos contextuais
Quando a busca casa em texto livre, o Elastic retorna trechos do conteúdo com a palavra encontrada destacada. A interface mostra o contexto direto na lista de resultados, sem precisar abrir cada item para descobrir por que ele apareceu.

🤖 O papel da IA nesse processo

Não vou fingir que cheguei nessa arquitetura sozinho em uma tarde. Usei
LLM como par técnico ao longo de todo o caminho, e a forma como usei
fez toda a diferença no resultado.

Onde a IA ajudou de verdade

Brainstorming de trade-offs arquiteturais
"Mensageria entre processos faz sentido aqui ou é overhead?", "Cursor pagination ou from/size pra infinite scroll?", "Alias com swap ou reindex in-place?".
Em vez de só pedir resposta, debati cada decisão e pesei prós e contras. A IA é excelente como sparring de design.

Análise crítica do código existente
O projeto inicial tinha mais peças do que precisava. Pedi uma análise técnica completa, code smells, dead code, bugs latentes, aderência a boas práticas. Saiu um documento de diagnóstico priorizado por severidade. Decidi simplificar a arquitetura com base nesse diagnóstico.

Modelagem do parser tolerante
Os cenários de dados sujos foram enumerados de forma exaustiva e cada um virou um caso de teste antes da implementação. Test-driven, mas com IA gerando o checklist.

Mapping do Elasticsearch
Definir analyzer correto pra português, decidir entre tipos de campo, projetar nested vs flat, é o tipo de decisão onde experiência prévia conta muito. Conversar com a IA acelerou em horas o que levaria dias de leitura de docs.

Prompts estruturados para agente de codificação
Quebrei a implementação em fases, cada uma com prompt detalhado contendo arquitetura alvo, decisões já tomadas, restrições, e ordem de execução. Resultado: o agente executou sem perder contexto e sem inventar.

Onde a IA NÃO substituiu o engenheiro

  • Decisões de negócio (volumes reais, padrões de acesso, regras de tratamento de exceções específicas do domínio)
  • Validar suposições contra a realidade do projeto (rodar build, ler logs, executar queries pra verificar comportamento)
  • Conhecer a infra real (capacidade dos volumes, classes de storage disponíveis, limites do cluster)
  • Priorizar segurança (a IA aponta riscos, mas a decisão de parar tudo pra resolver é humana)

A lição mais importante

IA é multiplicador, não substituto. Quem souber fazer perguntas certas, validar respostas e contextualizar com a realidade do projeto extrai muito valor. Quem só pede "me dá a solução" recebe código genérico que não resolve o problema real.

Stack utilizada

  • .NET 9 (Worker Service + ASP.NET Core)
  • Azure Blob Storage (origem dos dados)
  • Elasticsearch 8.x em Kubernetes via ECK Operator
  • Azure Table Storage (checkpoint stateless)
  • SDK oficial do Elastic (tipado)
  • System.Threading.Channels (pipeline assíncrono in-process)
  • Claude (sparring técnico e análise de código)
  • Claude Code (execução guiada por prompts estruturados)

Lições que valeram o post

Dados reais são sujos. Investi mais tempo projetando tolerância a inconsistências do que no pipeline em si. E foi o tempo mais bem gasto, em produção, um único arquivo malformado não pode derrubar uma carga em larga escala.

Mensageria nem sempre é a resposta. Para uma carga histórica única + ingestão futura via eventos do próprio storage, removi a camada de filas intermediária e simplifiquei para um indexer mais direto. Menos infra, menos custo, mais simples de testar.

Storage tem capacidades subutilizadas. O prefix lookup nativo do storage é absurdamente rápido e barato. Combinar isso com Elastic para o resto deu uma estratégia híbrida que nenhum dos dois sozinho daria.

Reindexar sem downtime é decisão arquitetural, não otimização. Resolver isso com alias desde o dia 1 me poupou de um problema futuro que apareceria do nada.

IA não pensa por você, mas amplifica quem pensa. O diferencial está em fazer as perguntas certas, contextualizar com seu projeto e validar tudo na prática.

Comments (0)

Sign in to join the discussion

Be the first to comment!