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.
Top comments (0)