1

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!

Carregando publicação patrocinada...