O Searchbase Cloud ainda não está no ar, mas já estou muito perto do ponto em que pode estar. Como parte da preparação da infraestrutura para o deployment, comecei finalmente a dimensionar correctamente as workloads individuais em vez de trabalhar com suposições vagas.

Um componente logo se destacou: o fetching de páginas.

O Searchbase consegue ir buscar um URL, renderizá-lo quando há JavaScript envolvido, e devolver o conteúdo útil como Markdown tanto através da REST API como das ferramentas MCP. Até agora, esse trabalho tem sido feito por um worker do Crawl4AI.

O Crawl4AI funciona bem. Para o ambiente em que o Searchbase cresceu originalmente, sinceramente não tenho grande coisa a reclamar. Se o têm a correr num homelab, numa máquina que já é vossa, ou para um único utilizador, mais alguns núcleos de CPU e um bocado de RAM não são necessariamente uma grande deal. Os recursos já lá estão, e se um contentor gasta mais dois gigabytes de memória, provavelmente isso não muda a vossa fatura mensal.

Um serviço alojado com exactamente zero clientes é um cálculo completamente diferente.

Cada CPU e cada gigabyte de memória que preciso de reservar tem um custo mensal real associado. De repente estava olhando para um crawler que podia representar uma parte surpreendentemente grande dos recursos necessários para lançar o Searchbase Cloud.

Podia atirar mais infraestrutura para cima, partir para outro pool de workers, usar serverless containers, meter autoscaling, e fazer com que a arquitectura fosse esperta o suficiente para esconder o problema.

Mas isso parecia-me muita complexidade para resolver algo que não devia ser tão caro assim desde o início.

Por isso, antes de desenhar em cima do problema, decidi medi-lo.

Primeiro, perceber se o problema é mesmo tão mau

Montei uma matriz de capacidade para o Searchbase e comecei a correr o fetch worker com diferentes limites de CPU e memória. Noventa configurações, de 1 a 8 CPUs e de 2 a 16 GiB, em dois cenários. O que interessa aqui é o caminho de fetch com JavaScript, medido directamente contra o worker.

Isto nunca se destinou a ser um benchmark académico de browsers. Não me interessava particularmente descobrir o throughput máximo absoluto de todas as configurações possíveis.

Duas coisas sobre a forma como esses números saem, porque mais tarde isso importa. Cada célula é um teste de capacidade curto, e o tooling reporta 70% da taxa estável mais alta alcançada como o valor conservador. E uma célula fica com um limite inferior, escrito ≥, quando a fase de discovery atingiu a taxa máxima do cenário sem nunca saturar. Ou seja, ≥ quer dizer que o teste acabou antes do software.

Eu tinha uma pergunta muito mais simples do que tudo isso:

O que é que eu preciso mesmo de provisionar para conseguir lançar o Searchbase Cloud sem ter que ir viver para de baixo da ponte antes de sequer ter utilizadores?

Os resultados do Crawl4AI confirmaram que a preocupação era real.

O Crawl4AI saturou em todas as 90 células. Nem uma única com limite inferior. O seu melhor resultado em toda a matriz foi 8,07 pedidos seguros por segundo, e isso exigia 8 CPUs e 16 GiB. Com 4 CPUs e 2 GiB, a configuração que eu estava realmente a olhar, conseguiu 5,24.

Isso não é mau desempenho. O problema era o que eu tinha de alocar para o conseguir.

O working set do worker, amostrado ao longo da matriz do Crawl4AI, foi desde 283 MiB até 16 GiB, conforme a célula. Na célula de 4 CPUs e 2 GiB chegou a cerca de 2 GiB, que é exactamente o tecto que eu tinha definido. Estava a viver mesmo no limite.

Por baixo do Crawl4AI está o Chromium, o Playwright, Python, e tudo o que é preciso para um browser completo se comportar de forma fiável. Há muitas boas razões para essa stack, especialmente quando a compatibilidade importa, mas não é nada de leve.

E a forma como escalou dizia a mesma coisa. Mantendo a memória em 2 GiB e a subir o número de CPUs, o valor conservador foi 0,98, 2,30, 4,04, 5,24, 5,25, 6,56. Mesmo com 8 CPUs ficava abaixo do melhor número de toda a matriz, porque o melhor número exigia 16 GiB. A stack não está partida, é apenas pesada, e comprar oito vezes mais CPU não era a forma como eu queria resolver um problema de custo.

Foi nesse ponto que comecei à procura de alternativas. Queria algo que ainda pudesse executar JavaScript e dar-me um DOM correctamente renderizado, mas sem arrastar a stack completa do Chromium para a história.

E foi assim que encontrei o Lightpanda.

Bem… isto é interessante

O Lightpanda é um browser headless escrito em Zig, construído de raiz para automação de browsers e cargas para AI, em vez de ser mais um wrapper de Chromium ou WebKit. Fala CDP, por isso as ferramentas de browser habituais encaixam-se à volta dele.

No papel, parecia quase suspeitamente adequado ao problema. Execução de JavaScript, APIs de DOM, compatibilidade com as ferramentas de browser, mas com uma pegada de recursos muito menor.

Para o que vale, o Lightpanda publica os seus próprios números, e apontam na mesma direcção: 9x mais rápido na execução e 16x menos memória que o Chrome, medidos ao ir buscar 933 páginas reais com o chromedp num AWS EC2 m5.large. Esses são números de fornecedor, no hardware deles, e não os reproduzo aqui. Mas pelo menos a ordem de grandeza não foi uma surpresa quando os meus próprios testes voltaram.

Claro que “no papel” quer dizer muito pouco, por isso adicionei um worker Lightpanda por trás da mesma interface de fetch do Searchbase e voltei a correr a matriz toda. Mais noventa configurações, os mesmos cenários, a mesma regra conservadora.

A diferença não foi nada subtil!

O Lightpanda nunca saturou. Nem uma única vez. As 90 células voltaram todas com limite inferior.

Com 2 CPUs e 2 GiB aguentou pelo menos 21,66 pedidos seguros por segundo. A taxa estável mais alta que realmente alcançou foi 30,94, e eu tinha configurado o teste para parar de carregar aos 32/s.

Esse era o tecto do teste. Eu acabei o teste antes de o browser acabar.

E a parte que genuinamente não esperava: já lá estava com 1 CPU e 2 GiB, nos 20,99 pedidos seguros por segundo. Uma CPU, dois gigabytes, e o tecto que existia era o meu, não o do browser. Nunca subiu muito acima dos 21,66 em lado nenhum da matriz, precisamente porque o tecto estava no caminho.

Ao lado, as duas matrizes:

WorkerCPUMemóriaPedidos seguros/sSaturou?
Crawl4AI, melhor célula da matriz8 CPUs16 GiB8,07sim, nas 90 células
Crawl4AI4 CPUs2 GiB5,24sim
Lightpanda2 CPUs2 GiB≥21,66nunca
Lightpanda1 CPU2 GiB≥20,99nunca

Por isso a ideia é simples. O Lightpanda com uma CPU aguentou mais do que aquilo que o Crawl4AI precisou de oito CPUs para alcançar. Na comparação a 4 CPUs, o Lightpanda deu-me mais do que quatro vezes a capacidade conservadora usando metade da alocação de CPU. E, ao contrário do Crawl4AI, nunca encontrei o tecto dele.

A imagem de memória é ainda mais absurda. O pico amostrado do worker em toda a matriz do Lightpanda ficou entre 15 MiB e 29 MiB, contra um intervalo de 283 MiB a 16 GiB no Crawl4AI. Uma ressalva, e o tooling é honesto sobre isso: o sampling de cinco segundos pode perder picos curtos de processos filho do Lightpanda, por isso leiam isso como um piso e não como um tecto.

Conseguia ele fazer 40 pedidos por segundo? 60? 100?

Talvez. Não sei, e nunca descobri, porque foi o teste que parou primeiro.

Encontrar o máximo nunca foi o objectivo. Eu precisava de saber se o fetching de páginas ainda tinha de dominar o custo e a arquitectura de um deployment do Searchbase Cloud.

Aparentemente não.

Bónus: a imagem do contentor também ficou pequenina

Houve outro efeito secundário simpático em que eu não tinha pensado a sério até olhar para as imagens Docker locais.

A imagem publicada do worker do Crawl4AI aparecia com cerca de 3,66 GB na minha máquina.

A imagem publicada do worker do Lightpanda?

172 MB.

Isto é aproximadamente uma redução de 21x no tamanho de imagem que o Docker estava a reportar.

Isto não torna magicamente mais rápidas as execuções individuais, mas operacionalmente é uma diferença bastante simpática. Imagens mais pequenas significam pulls mais rápidos, deployments mais céleres, menos espaço no registry, e menos dados para mover sempre que um worker arranca num sítio novo.

Fica ainda mais interessante se eu decidir correr estes workers em serverless containers, onde puxar e arrancar uma imagem é algo com que me importo muito mais do que me importaria numa máquina de homelab que fica sempre ligada.

A coisa toda estava a começar a ficar muito promissora.

Mas ser rápido é inútil se o conteúdo estiver errado.

Um browser ridiculamente rápido que perde conteúdo continua a ser inútil

O Searchbase não vai buscar páginas porque ir buscar HTML é difícil. A parte útil é pegar numa página moderna, incluindo conteúdo gerado por JavaScript, e dar a um agent qualquer coisa com sentido para trabalhar.

Por isso, depois dos resultados de desempenho, a qualidade do conteúdo passou a ser a pergunta seguinte.

Comecei pelo meu próprio site de CV porque sei exactamente o que é que lá deve estar. Os resultados do Crawl4AI e do Lightpanda foram muito próximos. Depois de ignorar diferenças de formatação Markdown inofensivas, essencialmente todo o conteúdo com significado lá estava.

O Lightpanda até produziu menos ruído decorativo em alguns sítios, o que foi animador.

Depois experimentei o quotes.toscrape.com/js/, especificamente porque as citações são renderizadas por JavaScript, e as coisas começaram a ficar esquisitas. O Crawl4AI devolvia as dez citações, o Lightpanda devolvia oito. Isso não era algo que eu estivesse disposto a despachar com um sorriso. Um Markdown feio não faz mal. Perder conteúdo com significado em silêncio, faz!

O meu primeiro palpite foi que a conversão de Markdown do Lightpanda estava lixada, especialmente porque despejar o HTML renderizado mostrava algo interessante: as dez citações estavam lá todas.

O JavaScript tinha corrido bem. O DOM continha as citações que faltavam. A paginação estava lá. O rodapé estava lá. O Lightpanda tinha renderizado a página com sucesso.

Em algum sítio entre o DOM renderizado e o Markdown final, o conteúdo estava a desaparecer. Por isso abri a issue #3661, o que levou a investigação a mais uma volta.

Eu estava a pedir ao Lightpanda para atirar conteúdo ao lixo

Um dos maintainers do Lightpanda experimentou o mesmo URL tanto na release estável como no main actual, e não conseguiu reproduzir de todo a falta das citações.

Isso obrigou-me a voltar e a olhar com mais cuidado para o comando exacto que o Searchbase estava a correr:

lightpanda fetch \
  --dump markdown \
  --strip-mode clutter \
  --fail-on-http-error \
  --wait-ms 500 \
  --log-level error \
  https://quotes.toscrape.com/js/

A parte interessante era:

--strip-mode clutter

Tirei essa opção e as dez citações voltaram imediatamente.

Como o resto do conteúdo da página.

Isso explicava onde é que o conteúdo estava a desaparecer, mas o motivo acabou por ser ainda mais específico.

As tags nessa página estão representadas com elementos <a> sem href. Mais ou menos assim:

<a class="tag">change</a>

Não são links, mas a detecção de clutter do Lightpanda estava a contar o texto deles como texto de link na mesma.

Para citações curtas com várias tags, isso empurrava a link density calculada para cima da margem de lixo. A correcção que foi para upstream escreve os números de forma formosa: a margem é 0,2, e as duas citações que desapareceram mediram aproximadamente 0,28 e 0,22. A citação do Steve Martin que sobreviveu ficou nos cerca de 0,196. Não “logo abaixo”. 0,196 contra um limite de 0,2.

Isso é quase maravilhosamente específico.

O mesmo comportamento explicava também outra pequena chatice no Markdown que eu tinha notado. Esses elementos de tag estavam a ser renderizados assim:

[change]()

mesmo não havendo URL e portanto nenhum link real.

Os maintainers do Lightpanda já fizeram merge da PR #3663, que muda os dois comportamentos. Âncoras sem href são tratadas como texto normal no Markdown, e já não são contadas como texto de link pelo filtro de clutter.

Melhor ainda, a correcção inclui um teste de regressão baseado exactamente no tipo de citação curta com várias tags que despoletou esta investigação. O teste cita literalmente o Edison e garante que a frase dele sobrevive à extracção.

Há aqui uma distinção importante. As duas citações que faltavam eram um bug. A paginação e o rodapé serem removidos são, de acordo com a correcção, o filtro de clutter a fazer o que lhe compete fazer.

E isso faz todo o sentido.

Se quero mesmo esse comportamento no Searchbase… é outra questão.

Por agora não tenho pressa de empacotar a correcção nem de declarar o problema resolvido. A alteração já está upstream, mas quero que entre numa release estável do Lightpanda, e quero atirar-lhe uma colecção muito mais vasta de páginas reais antes de começar a confiar no clutter stripping. O último commit no repositório do Searchbase é bastante revelador a esse respeito: fix(lightpanda): remove clutter strip mode.

Uma página sintética a encontrar um bug não prova que toda a pipeline de extracção é insegura, da mesma forma que uma correcção bem sucedida não marca magicamente como madura.

Por isso… mais testes.

O que mudou no Searchbase

O Searchbase suporta agora o Lightpanda como fetch worker opcional, a par do Crawl4AI.

Ambos estão por trás da mesma interface de fetch do Searchbase, por isso os clientes não precisam de se importar com qual das implementações está a fazer o trabalho.

A REST API fica igual.

As ferramentas MCP ficam iguais.

Escolhem simplesmente qual worker querem por trás delas.

O Searchbase Cloud e o default da versão open source são duas decisões diferentes, e quero ser claro quanto às duas.

O Searchbase Cloud vai usar o Lightpanda. Essa está decidida. Sou eu que vou pagar a factura mensal, e a diferença de recursos é demasiado grande para simplesmente aceitar o custo do worker mais pesado. Nenhum calendário de releases trava esta.

O default do open source é outra questão, porque quem o corre são outras pessoas. Mudá-lo muda o comportamento de todas as instalações self-hosted que já existem, por isso vai mudar numa release maior futura, assim que duas coisas sejam verdade: as correcções upstream já terem entrado numa release estável do Lightpanda, e uma colecção muito mais vasta de páginas reais ter passado pelo pipeline sem nada de estranho a aparecer.

Até lá continua a ser uma escolha explícita, e o Crawl4AI continua disponível durante esse tempo. Se nunca quiserem que o default mude no vosso deployment, nada disto vos obriga.

E a opção já vale a pena muito antes de nada disso. O mesmo perfil de recursos que a torna atraente para manter o Searchbase Cloud barato também faz dela uma opção muito mais atrativa para máquinas pequenas em self hosting.

Sobre esses números de benchmark

Uma última ressalva, porque os números de benchmark têm uma tendência maravilhosa para escaparem do seu contexto e se transformarem em factos na Internet.

Estes foram testes de capacidade num único host, co-localizados, contra uma única página JavaScript local e determinística. Um host partilhado, uma página, uma corrida por definição, e fases de descoberta curtas. Foram testes curtos, não testes de resistência em produção, e não estabelecem capacidade de produção nem equivalência de conteúdo.

Todos os valores nas tabelas acima são os 70% conservadores da taxa estável mais alta alcançada, não a taxa bruta. E todos os ≥ são limites inferiores, porque foi o teste que parou, não o software. Nunca encontrei o máximo real do Lightpanda, e não vou fingir o contrário.

Os números de memória são working sets amostrados num intervalo de cinco segundos, por isso picos curtos de “processos filho” do lado do Lightpanda podem passar completamente despercebidos.

E há mais uma coisa que vale a pena dizer. Estes resultados vêm também de um nightly anterior do Lightpanda. A imagem da release 0.4.1 actual ainda não foi testada, no entanto não espero grandes diferenças.

Por isso a conclusão não é que o Lightpanda lida universalmente com 32 páginas por segundo, ou que é sempre quatro vezes mais rápido que o Crawl4AI.

A conclusão é muito mais útil para mim:

Para a workload que eu estava a testar, o Lightpanda forneceu muito mais capacidade do que preciso para lançar o Searchbase Cloud, exigindo muito menos recursos.

Era essa a resposta que eu precisava.

As matrizes guardadas e o gráfico estão na documentação do Searchbase, e o tooling de testes de carga está no repositório. Por isso, se têm curiosidade com o vosso hardware ou a vossa workload, não tomem os meus números como sagrados.

Corram-no vocês.

Tudo isto porque não queria um serviço caro com zero clientes

A parte engraçada é que comecei tudo isto porque não queria gastar muito dinheiro a correr um crawler para clientes que ainda não tenho.

Em vez disso, acabei por questionar parte da arquitectura do Searchbase, montar testes de capacidade a sério, encontrar o Lightpanda, descobrir que o seu perfil de desempenho é completamente diferente do worker baseado em Chromium, achar que tinha encontrado um bug de serialização de Markdown, descobrir que o browser estava na verdade a renderizar tudo correctamente, reduzir o problema a uma heurística de limpeza de conteúdo, e conseguir que esse issue fosse corrigido upstream.

Não é mau, para quem só queria poupar uns euros.

E é exactamente por isto que gosto de fazer este tipo de trabalho antes do lançamento.

O crawler já funcionava. Podia facilmente tê-lo utilizado, pagar a infraestrutura, e seguir em frente. Mas uma vez que existem clientes, esse custo torna-se normal. Uma vez que a infraestrutura existe, torna-se arquitectura. Uma vez que é arquitectura, mudá-la por alguma razão transforma-se num projecto.

Em vez disso, ter zero clientes tornou a ineficiência impossível de justificar e como tal fui à procura de algo melhor.

O resultado não é apenas um caminho mais barato para o Searchbase Cloud. A versão open source tem agora um fetch worker que pode correr com recursos muito menores, deploy a partir de uma imagem muito mais pequena, e continuar a dar mais capacidade do que eu actualmente sei o que fazer com ela.

É um resultado bastante bom para um problema que eu descobri originalmente a olhar para uma factura de cloud que ainda nem sequer existia.

O Searchbase é open source:

Escrito com a ajuda do space-bunny-free (OpenCode Zen).