harness-kit wiki

Search Engine

Série System Design #3 (EP24). Topic skill: skills/search-engine/. Projete um search engine web com crawler, indexing, ranking e serving em escala.

1. O problema e por que ele engana

A maioria das pessoas imagina uma caixinha simples: o usuário digita a query, recebe dez links, fim. Por baixo existe um pipeline enorme. Você descobre páginas, decide o que vale a pena buscar, baixa conteúdo sem prejudicar sites de terceiros, faz parse e extrai texto/links/metadados, normaliza, deduplica, constrói um inverted index, calcula features offline, opcionalmente gera embeddings e, no fim, responde queries em poucos milissegundos com ranking relevante.

A arquitetura muda enormemente conforme o objetivo. Busca em documentação interna é um problema. Busca pública em escala web é outro mundo. Uma frase resume o sistema:

Um search engine é uma fábrica distribuída que transforma URLs em documentos rankeáveis.

É uma cadeia de decisões: o que descobrir, buscar, armazenar, indexar, recuperar e promover. Cada stage mata custo ruim e preserva sinal útil.

2. Quatro subsistemas + dois planos de apoio

3. Requisitos

Funcionais: aceitar seeds; descobrir links recursivamente; respeitar a política de crawl (robots.txt, delays, limites por host); buscar HTML (opcionalmente PDFs, feeds, imagens); fazer parse e extrair texto, links, título, headings, anchor text, canonical, idioma, timestamp, metadados estruturados; detectar duplicatas e quase-duplicatas; construir um inverted index para busca lexical; opcionalmente um index vetorial para recall semântico; responder queries com paginação, snippets, filtros e ranking de relevância; recrawlear periodicamente para manter freshness.

Não funcionais: escalabilidade horizontal em todo stage; alto throughput offline; baixa latency de serving (tipicamente < 200 ms ponta a ponta, de preferência bem menos); alta disponibilidade no caminho da query; consistência eventual entre crawl e busca é aceitável desde que convirja; custo previsível; comportamento seguro e não abusivo com sites de terceiros; observabilidade suficiente para explicar por que uma página não foi indexada ou por que uma query retornou o que retornou.

4. Modelo mental ponta a ponta

flowchart LR
  seeds --> frontier --> fetcher --> parser --> extractor --> dedup
  dedup --> docstore --> index
  index --> serving
  serving --> analyzer --> retriever --> ranker --> result

Seeds → o frontier escolhe uma URL elegível → o fetcher baixa → o parser transforma bytes em documento estruturado → o extractor produz texto limpo, outlinks e metadados → o deduplicador decide novo/duplicata exata/quase-duplicata → o document store persiste a versão canonical → o indexing tokeniza e constrói as postings lists → jobs offline calculam sinais globais (por exemplo, popularidade no link graph) → na hora da query o analyzer normaliza, o retriever acha candidatos, o ranker pontua e o result builder monta os snippets.

5. O crawler (o coração)

URL frontier

A estrutura central. Ela não é uma fila só. Uma implementação séria separa três conceitos:

Por que um Bloom filter? (Burton Bloom, 1970.) Uma estrutura probabilística de pertinência a conjunto: responde "já vi essa URL?" em O(1) e poucos bits por elemento, sem falsos negativos e com taxa de falso positivo ajustável. Em escala web você não consegue manter cada URL vista em memória de forma exata; o Bloom filter entrega "definitivamente nova" vs "provavelmente vista, vá conferir no store" de forma barata.

Multi-fila por host

Uma única fila global cria dois problemas: domínios quentes (um domínio muito linkado enche a fila) e perda de politeness (você dispara muitas requisições concorrentes contra um host, parecendo um DDoS). A correção: uma fila pendente por host, cada uma com um next_eligible_timestamp; um heap global ordena os hosts pelo menor tempo elegível e maior prioridade. Quando um host fica elegível, você tira uma URL, busca, atualiza o backoff e reinsere o host. Isso dá fairness e politeness juntos.

Canonicalização

Normalize antes de enfileirar: host em minúsculas, remover fragmentos, portas default, limpar query params irrelevantes, resolver caminhos relativos, remover session ids conhecidos, normalizar barra final. Pular isso explode duplicatas e custo de crawl.

robots.txt e politeness

Faça cache do robots.txt por host com TTL; respeite allow/disallow e crawl-delay (o Robots Exclusion Protocol, hoje RFC 9309, 2022). Politeness não é um sleep fixo:

next_request_allowed = max(min_delay, k * observed_latency, robots_crawl_delay)

mais concorrência máxima por host e backoff exponencial em erros. Isso evita martelar sites lentos.

Fetcher

Stateless, I/O pesado: rede assíncrona, connection pooling, cache de DNS, reuso de TLS, gzip/brotli, limites de redirect, tamanho máximo de download, sniffing de content-type (não confie só no header) e GET condicional (ETag / If-Modified-Since) para recrawl barato. Persista metadados: código de status, headers, URL final após redirects, tempo de resposta, checksum do corpo.

Armadilhas de crawler

A web tem infinitas páginas falsas: calendários gerando datas sem fim, combinações de facetas de e-commerce, URLs com parâmetros arbitrários, a busca interna do próprio site, loops de paginação. Guardrails: crawl budget por host, limite de fan-out por página, blocklists de regex de parâmetros, um score de repetição de template, limites de profundidade. Sem isso, 80% do custo vai para os piores 5% da web.

6. Agendamento de crawl (onde dinheiro vira estratégia)

Crawlear a web inteira todo dia é impossível para quase todo mundo. Com um budget finito de requisições, banda e CPU, agendar é uma decisão de negócio. Um crawl score aproximado:

crawl_score ~ quality * freshness_need * business_priority / fetch_cost

(Intuição, não uma fórmula universal literal.) Recrawl adaptativo: mudou duas vezes num intervalo curto → encolha a janela; não mudou muitas vezes → aumente; erros → recue. Muito melhor que um cron fixo. Tiers de freshness A/B/C/D (minutos → raramente) por domínio, padrão de URL ou score dinâmico.

7. Teoria, deduplicação

Busca web sem dedup é caos: o mesmo conteúdo aparece com/sem www, http/https, parâmetros diferentes, páginas de impressão, sindicação, espelhos, republicações e duplicatas suaves.

Três níveis:

  1. Duplicata de URL: mesma URL normalizada.
  2. Duplicata exata de conteúdo: mesmo hash do texto limpo.
  3. Quase-duplicata: conteúdo quase igual.

Para quase-duplicatas você gera fingerprint com shingles + MinHash (Broder, 1997) ou SimHash (Charikar, 2002). O MinHash estima a similaridade de Jaccard entre conjuntos de shingles a partir de alguns mínimos de hash; o SimHash mapeia um documento para um vetor de bits onde a distância de Hamming acompanha a similaridade, o que permite clusterizar por fingerprint de forma barata. Guarde um document_fingerprint e um canonical_document_id; muitas URLs mapeiam para um documento canonical. Deduplique cedo e em camadas, senão você paga para processar duplicatas caras, e consolide os sinais de ranking (links de entrada, cliques) no canonical.

8. Storage por função

9. Teoria, o inverted index

A estrutura lexical clássica. Em vez de guardar os termos por documento, guarde, para cada termo, a lista de documentos em que ele aparece. Essa lista é uma postings list:

term: crawler
postings: [(doc1, tf=3, positions=[4,18,22]), (doc7, tf=1, positions=[9])]

Por posting: doc_id, frequência do termo, posições (para queries de frase), info de campo (título vs corpo), payloads opcionais.

Pipeline de build

Tokenizar → normalizar (minúsculas, remover acentos, stem/lematizar) → descartar stopwords quando fizer sentido → emitir pares term → postingsort-merge por termo → comprimir postings → persistir segmentos imutáveis → publicar uma nova versão do index. Isso é naturalmente distribuível, modele como MapReduce (Dean & Ghemawat, 2004) ou streaming com compactação em batch.

Sharding

Duas estratégias: document sharding (cada shard guarda um subconjunto de documentos e seus termos) vs term sharding (cada shard guarda um subconjunto de termos). Document sharding costuma simplificar serving, replicação e rebalanceamento: a query faz fan-out para todos os document shards, cada um retorna um top-K local e um agregador faz o merge.

Segmentos e merge

Atualizar um index grande no lugar é caro. O modelo Lucene (Doug Cutting) usa segmentos imutáveis mais merges periódicos em background: escritas sequenciais baratas, snapshots simples, rollback fácil, serving concorrente durante o reindex. Custos: trabalho de merge em background, docs deletados ficam como tombstones até o merge e mais segmentos elevam o custo da query.

Compressão

Postings precisam ser comprimidas ou o index explode: delta encoding de doc ids, variable-byte encoding, frame-of-reference, bit packing e skip lists para pular blocos. Objetivo: ler rápido sem queimar CPU na descompressão.

10. Query serving (o caminho online)

sequenceDiagram
  Client->>Gateway: query
  Gateway->>Analyzer: normalize, parse operators, classify intent
  Analyzer->>Shards: fan-out
  Shards-->>Ranker: top-K + partial scores
  Ranker->>Assembler: full features -> final score
  Assembler-->>Client: snippets + results

Analyzer

Normalizar caixa, tokenizar, correção ortográfica opcional, expansão de sinônimos, detecção de idioma, parsing de operadores (aspas, -, site:, filetype:) e classificação de intenção (navegacional / informacional / transacional / fresh). A intenção muda o ranking: as mesmas palavras podem querer coisas diferentes.

Recuperação de candidatos

Você não faz ranking da web inteira. Primeiro recupere um conjunto de candidatos via BM25 no index lexical, filtros de campo, boosts de título/anchor, opcionalmente busca ANN sobre embeddings, ou uma união híbrida. Tipicamente top-N por shard (~500-1000), depois merge.

BM25 (Robertson & Zaragoza, The Probabilistic Relevance Framework: BM25 and Beyond) é a função padrão de relevância lexical: recompensa frequência de termo com saturação e penaliza documentos longos, com parâmetros ajustáveis k1 e b. É o baseline burro de carga sobre o qual sistemas fortes ainda constroem.

Ranking

O score final combina famílias de features:

Um começo simples é um score linear ponderado, por exemplo 0.45*bm25 + 0.20*title + 0.15*authority + 0.10*freshness + 0.10*anchor. Sistemas maduros migram para learning-to-rank: mas só com boas features e dados de clique, senão você compra complexidade cara.

Montagem do resultado

Snippet com destaque, URL canonical, título limpo, breadcrumbs, data quando relevante, sitelinks e dedup de resultados quase idênticos. Um bom snippet dirige a qualidade percebida; não é cosmética.

Um search engine web de verdade não vive só do texto da página. O link graph é um sinal global forte: se muitas páginas relevantes apontam para um documento, isso sugere autoridade. Construa um grafo direcionado (vértices = documentos ou domínios, arestas = hyperlinks, pesos consideram contexto, posição do link, anchor text) e rode jobs batch offline (diários/horários, nunca no caminho da query) para calcular autoridade, hub, centralidade e reputação de domínio.

PageRank (Brin & Page, The Anatomy of a Large-Scale Hypertextual Web Search Engine, 1998) é a aproximação famosa: a importância de uma página é a distribuição estacionária de um navegante aleatório seguindo links com um fator de amortecimento. A prática moderna combina isso com muitas checagens anti-spam, senão link farms e redes de spam manipulam o resultado. HITS (Kleinberg, 1999) é a formulação irmã de hub/autoridade.

12. Spam, abuso e qualidade

Busca aberta atrai adversários. Abusos: keyword stuffing, texto escondido, doorway pages, link farms, cloaking, conteúdo de baixo valor em massa, duplicação agressiva, redirects enganosos. Defesas: um classificador de spam sobre features de conteúdo + link graph, reputação de domínio, limites por template/cluster, detecção de boilerplate, checagens de similaridade em massa, review manual para casos estratégicos e um loop de feedback de clique/bounce. Sem uma camada de qualidade, o melhor index do mundo serve lixo rápido.

13. Freshness vs custo

Mais freshness significa mais crawl, mais processamento e mais custo. Menos significa resultados velhos, inconsistentes e confiança perdida. A resposta raramente é uniforme, use tiers: Tier A (muito dinâmico, alto valor) recrawleado em minutos/horas; Tier B diário; Tier C semanal/mensal; Tier D raramente. Defina o tier por domínio, padrão de URL ou score dinâmico.

14. Publicação do index e consistência

Index e documentos raramente estão sincronizados em tempo real. Opere com versões: workers constroem novos segmentos, um manifest descreve a versão completa do index, o publisher faz o commit dela de forma atômica, os query servers fazem warm up da nova versão e então ocorre o swap. Benefícios: rollback simples, serving sem downtime, consistência de leitura por versão. Para updates frequentes, combine um snapshot base com indexes delta menores.

15. Busca híbrida (lexical + semântica)

Muitos times pulam direto para embeddings. Para busca geral, o lexical continua sendo a fundação; os embeddings complementam, não substituem automaticamente. O lexical é preciso para termos raros, nomes, códigos e queries específicas; o semântico ajuda o recall para linguagem natural, sinônimos e fraseados variados; o ranking híbrido normalmente ganha de qualquer um dos dois sozinho. Implementação: inverted index para recall lexical, um index ANN (por exemplo HNSW) para embeddings de documentos, embedding da query gerado online ou cacheado, união dos dois conjuntos de candidatos e o ranker final decide. Mas gerar embeddings, guardar vetores e rodar ANN em escala não é barato, faça isso só quando o problema exigir.

16. Escala e particionamento

Frontier particionado por hash do host (um dono por host, para a coordenação de politeness ficar num lugar só). Fetchers stateless, com autoscaling. Index: por exemplo 64 shards lógicos × 2-3 réplicas, o leader publica os segmentos, os followers servem, rebalanceamento gradual. Jobs de link graph como batch distribuído pesado em janelas, nunca no caminho da query.

17. Observabilidade e SRE

Ferramentas de debugging obrigatórias: inspeção de URL (status de crawl + index de uma URL), query explain (quais shards responderam, candidatos, features, score final), dashboard de host (erros, politeness, backlog, bloqueios por host) e replay de documento pelo pipeline. Sem isso, o time passa a vida adivinhando.

18. Modos de falha

Falha Causa Solução
Gargalo no scheduler central processo único particionar o frontier, distribuir a posse
Custo enorme por dedup tardio dedup só no fim dedup em camadas: URL, exata, quase
p99 estoura fan-out excessivo sharding balanceado, caches, poda de candidatos
Relevância estagna sem loop de feedback instrumentar cliques, abandono, zero-resultados; análise offline contínua
Crawler preso em armadilhas sem budget/heurística por host crawl budget por domínio, bloqueios dinâmicos, filtros de parâmetros
Publicação do index quebra o serving publicação sem versão snapshots atômicos + warmup antes do swap

19. Plano incremental

  1. Fatia vertical: conjunto pequeno de seeds, só HTML, frontier por host com politeness básico, parser simples, inverted index básico (Lucene/OpenSearch serve), query com BM25, ferramenta de inspeção de URL.
  2. Eficiência / qualidade: dedup exata + quase, canonicalização melhor, recrawl adaptativo, scoring inicial de qualidade, snippets melhores, mais observabilidade.
  3. Escala real: frontier particionado, fetchers distribuídos, index shardeado e versionado, jobs de link graph, ranking multi-fator, disaster recovery + replay.
  4. Relevância avançada: learning-to-rank, sinais comportamentais, embeddings híbridos, personalização, anti-spam sofisticado.

Relevância avançada em cima de ingestão ruim é maquiagem cara. A ordem importa.

20. Resumo operacional (a estrela-guia)

Referências

Tradução manual de Search Engine, a página original em inglês na wiki.