51 milhões de CNPJs num único arquivo SQLite de 68 GB, com Rust e busca inteligente usando Jev
Quem vende para empresa no Brasil conhece o ritual: abrir planilha, filtrar
CNAE, cruzar com cidade, caçar telefone. As ferramentas gringas de prospecção
cobrem pouco as empresas daqui, e as brasileiras quase sempre pedem que você
monte o filtro campo por campo.
Eu queria escrever "clínicas odontológicas abertas nos últimos 12 meses em
Campinas" e receber a lista. Então construí isso: o
Prospecta.
O que ele faz
- Busca em linguagem natural sobre a base pública de CNPJ da Receita Federal:
51 milhões de empresas, atualizada todo mês. - A frase vira filtro: atividade, cidade, porte, data de abertura. A tela
mostra o que foi entendido ("Entendi: Odontologia · Campinas, SP"), e dá
para corrigir. - Cada ficha mostra o que vender para aquela empresa conforme o seu nicho:
sem site, sem Google Meu Negócio, sem pixel de anúncio, sócio novo, filial
nova. Cada item vem com a prova de onde saiu. - Obras em andamento por cidade, a partir do Cadastro Nacional de Obras: quem
começou a construir e o contato da construtora. - Exportação em Excel e CSV, e uma API REST para quem quer levar a base para o
próprio sistema.
Como está montado (e por que SQLite)
Quando conto que a base inteira roda em SQLite, a primeira reação costuma ser
"mas SQLite não é para protótipo?". Não é. Aqui ele aguenta 51 milhões de
empresas, os estabelecimentos e o quadro societário, num arquivo só de uns
68 GB.
- SQLite em modo WAL. Leitores não travam o escritor, então a API lê
enquanto a importação mensal grava. - Sem servidor de banco. Não há Postgres para administrar, nem rede entre a
API e os dados. A consulta é uma chamada de função no mesmo processo. - Pools separados por tipo de trabalho: 15 conexões só de leitura
(query_only) para o site, duas para exportação e duas de escrita. Uma
exportação pesada não rouba conexão de quem está navegando. - Busca textual em FTS5, a do próprio SQLite, para razão social e nome
fantasia. As tabelas são external content: o índice guarda só o rowid e
busca o texto na tabela original, sem duplicar milhões de nomes no disco. - Duas dezenas de índices escolhidos a partir das consultas que o produto
de fato faz. - Rust (actix-web) na frente. A API sobe com pouca memória, e o tipo do
Rust pega na compilação muito erro que, em outra linguagem, só apareceria em
produção. - Jev, da TypeSafe AI, para a linguagem natural. Um parser próprio resolve
a maior parte das frases. O que ele não entende vai para o Jev, e cada
decisão fica registrada para virar regra do parser depois. Com o tempo, menos
frases precisam de IA. - Site em Next.js na Vercel, com as páginas de cidade, estado e setor
geradas e guardadas em cache (ISR).
O SQLite tem limites, claro: um escritor por vez e tudo numa máquina só. Para
uma base que muda uma vez por mês e é lida o resto do tempo, isso é
exatamente o perfil em que ele brilha.
Coisas que aprendi no caminho
1. A grafia do código do município muda de tabela para tabela. Na mesma
base, uma coluna guarda 0255 e a outra guarda 255. Resultado: Manaus,
Belém e São Luís respondiam zero empresas, com HTTP 200 e nenhum erro no log.
Eram 998 municípios, todos com código abaixo de mil.
2. IN com as duas grafias parecia a solução e piorou tudo. Com IN, o
SQLite perdia o índice que servia ao ORDER BY data_abertura. Em cidades
pequenas, a consulta foi de 0,24 s para 6,7 s. Hoje a API descobre qual
grafia existe naquela coluna e filtra por igualdade.
3. Contar na hora não escala, e o NOT INDEXED salvou. Cada página de
cidade disparava 25 contagens (cidade × setor, por porte...). Juntas, elas
ocupavam o pool de conexões, e a listagem da mesma página ficava na fila.
Hoje esses números saem de um "retrato" calculado uma vez na importação.
O detalhe bonito é como ele é calculado: uma varredura sequencial com
FROM empresa NOT INDEXED. Sem essa dica, o SQLite ia pelo índice de situação
e fazia uma busca na tabela para cada empresa ativa, um plano que chegou a
17 minutos num agregado parecido. Forçando a leitura em ordem, numa base de
teste com 5 milhões de linhas, levou 4,5 s. Às vezes o índice é o caminho
mais lento.
4. O ETL que derrubou o SEO. Em agosto, a importação mensal estourou a
memória e deixou o banco sem índice por alguns dias. As páginas passaram a
demorar e a sair vazias, justamente na semana em que o Google estava
conhecendo o site. Ele viu até 25% de falhas de conexão, cortou o rastreio
para quase zero e tirou o site do índice. Ficaram 22 páginas.
O conserto teve três partes:
- página sem dado não entra mais no cache; o site passa a servir a última
versão boa; - o sitemap encolheu de 3.772 para 163 URLs fortes;
- a importação vai sair do servidor que atende o site.
Se você tem um site novo, essa é a lição: nas primeiras semanas o Google
está decidindo se confia em você, e uma queda nessa janela custa meses.
Para quem é
- Quem vende B2B no Brasil e perde tempo montando lista.
- Agências e consultorias que prospectam por nicho (marketing, contabilidade,
sites, engenharia). - Devs que precisam de dados de CNPJ: a API tem sandbox com o mesmo JSON da
produção e 500 chamadas grátis por mês, sem cartão.
Documentação.
Tem plano grátis com busca ilimitada e 10 contatos por mês, também sem
cartão.
O que eu queria de vocês
- Testem a busca com a frase de vocês e me digam onde ela errou. É o que mais
me ajuda a melhorar o parser. - Quem integra dados de CNPJ: o que falta na API?
- Alguém já passou por queda de indexação parecida e conseguiu voltar mais
rápido? Aceito dicas.
Obrigado por lerem até aqui!