Skip to content

Latest commit

 

History

History
378 lines (291 loc) · 18.1 KB

File metadata and controls

378 lines (291 loc) · 18.1 KB

Scraper Go - Documentação

Este documento descreve o scraper implementado em Go localizado em scraper-go/.

Visão geral

O scraper Go concentra a coleta e normalização de vagas. Ele recebe configurações de busca do backend, executa fontes externas habilitadas, aplica deduplicação e classificação local, persiste vagas no Valkey e publica índices para a API consultar com baixa latência.

Exemplos por adaptador

Abaixo há exemplos simplificados do payload esperado internamente e de como cada adaptador normalmente formata/retorna vagas.

  • LinkedIn
    • Comportamento: consulta o endpoint público jobs-guest e faz parsing HTML com goquery.
    • Exemplo (Job individual retornado pelo adaptador):
{
  "id": "24a1b2c3d4e5f6a7b8c9d0e1",
  "title": "Frontend Engineer",
  "company": "Empresa X",
  "location": "São Paulo, SP",
  "url": "https://www.linkedin.com/jobs/view/123456789/",
  "source": "LinkedIn",
  "sources": ["LinkedIn"],
  "keyword": "react",
  "keywords": ["react"]
}
  • Adzuna
    • Comportamento: usa API oficial (quando ADZUNA_APP_ID/ADZUNA_APP_KEY configurados) e retorna JSON com campos estruturados.
    • Operação: suporta SearchBatch com slot rotativo de keywords e limite padrão de 5 páginas por keyword.
    • Exemplo (Job adaptado):
{
  "id": "adzuna-987654",
  "title": "Desenvolvedor Backend",
  "company": "ACME",
  "location": "Remoto",
  "url": "https://www.adzuna.com/descricao/987654",
  "source": "Adzuna",
  "sources": ["Adzuna"],
  "keyword": "node",
  "keywords": ["node"]
}
  • Jooble
    • Comportamento: integra com Jooble API quando JOOBLE_API_KEY presente; pode usar Redis para controle de cota.
    • Operação: usa slot rotativo, cota diária e cadência de 12h para proteger a integração.
    • Exemplo (Job adaptado):
{
  "id": "jooble-13579",
  "title": "Data Engineer",
  "company": "Empresa Y",
  "location": "Curitiba, PR",
  "url": "https://jooble.org/vaga/13579",
  "source": "Jooble",
  "sources": ["Jooble"],
  "keyword": "data",
  "keywords": ["data"]
}
  • Greenhouse / Lever / TheMuse
    • Comportamento: adaptadores per-company que fazem scraping/parsing da vaga ou consomem endpoints públicos por empresa.
    • Exemplo (job adaptado genérico):
{
  "id": "greenhouse-abc123",
  "title": "Product Manager",
  "company": "Startup Z",
  "location": "Los Angeles, CA",
  "url": "https://boards.greenhouse.io/companyz/jobs/321",
  "source": "Greenhouse",
  "sources": ["Greenhouse"],
  "keyword": "product",
  "keywords": ["product"]
}

Descobrir tokens do Greenhouse

O board_token é o trecho final da URL pública da Greenhouse. Por exemplo:

  • https://job-boards.greenhouse.io/reddit → token reddit
  • https://job-boards.greenhouse.io/gitlab → token gitlab

Para validar tokens ou testar nomes de empresas, use:

cd scraper-go
go run ./cmd/greenhouse-discover -names Reddit,GitLab

Saída esperada:

OK   gitlab                             186 vagas  https://job-boards.greenhouse.io/gitlab
OK   reddit                             195 vagas  https://job-boards.greenhouse.io/reddit

Também é possível validar o JSON atual:

cd scraper-go
go run ./cmd/greenhouse-discover -file internal/interfaces/greenhouseCompanies.json

Tokens com OK podem entrar em internal/interfaces/greenhouseCompanies.json. Tokens com MISS status=404 devem ser removidos ou substituídos.

Esses exemplos ilustram o contrato interno entre adaptadores e pipeline: o pipeline espera domain.Job com campos normalizados (URL limpa, título/empresa/local, Source e StableID calculável). O jobstore.StableID deriva o ID a partir de título+empresa+local ou URL.

Especificação OpenAPI (local)

Criei uma especificação OpenAPI mínima em openapi.yaml descrevendo os endpoints HTTP públicos do serviço Go (/scrape, /api/keywords). Ela pode ser usada para gerar clientes ou documentação interativa.

Arquivo: scraper-go/openapi.yaml (no repositório)


O scraper é um serviço HTTP em Go que consulta múltiplas fontes de vagas (LinkedIn, Adzuna, Greenhouse, TheMuse, Lever, Jooble, etc.), agrega os resultados, remove duplicatas e persiste/retorna as vagas via cache e índice Redis/Valkey. Ele foi projetado para ser usado internamente pelo backend Node.js, que delega buscas ao serviço Go.

Componentes principais:

  • cmd/server — inicializador e ponto de entrada.
  • internal/domain — modelos centrais do scraper (Job, ScrapeRequest, ScrapeResponse, classificação).
  • internal/ports — contratos internos da aplicação, como fontes de vagas, repositórios, cache e métricas.
  • internal/adapters — composição de adapters concretos e registry das fontes habilitadas.
  • internal/adapters/<fonte> — implementação isolada de cada fonte externa (gupy, inhire, greenhouse, lever, jooble, linkedin, themuse, adzuna).
  • internal/adapters/adapterutil — helpers compartilhados entre adapters concretos.
  • internal/pipeline — camada de aplicação do pipeline de scraping, orquestra execução concorrente e indexação.
  • internal/jobstore — persistência em Redis (valkey) para jobs, índices e IDs estáveis.
  • internal/cache — abstração de cache com implementação Redis e memória (fallback).
  • internal/dedup — regras para deduplicação/merge de vagas.
  • internal/keywords — carregamento e persistência de keywords (configuração).
  • internal/inflight — deduplicador de requisições concorrentes (singleflight).
  • internal/cronjob — scheduler de scraping em background e execução manual.
  • internal/metrics — métricas Prometheus por fonte/execução.
  • cmd/greenhouse-discover — utilitário Go para validar tokens de empresas Greenhouse antes de atualizar internal/interfaces/greenhouseCompanies.json.

Arquitetura atual

O scraper evolui para uma arquitetura hexagonal de forma incremental. O domínio fica em internal/domain, as portas em internal/ports e os adapters concretos ficam em subpastas de internal/adapters.

A fronteira principal é a porta ports.JobSource: cada fonte externa implementa SourceName, Search e, opcionalmente, SearchBatch. O pipeline recebe uma lista de ports.JobSource já montada pelo servidor/registry, evitando que a camada de aplicação conheça diretamente as implementações concretas.

Desenho atual:

cmd/server
  monta cache, Valkey, stores, scheduler e adapters

internal/domain
  modelos centrais do scraper

internal/ports
  contratos que a aplicação consome

internal/pipeline
  orquestra scraping, dedupe, classificação e indexação

internal/adapters
  registry e implementações concretas por fonte

Ainda há pontos a evoluir: pipeline e cronjob continuam recebendo *redis.Client em fluxos de indexação/cadência, e métricas Prometheus ainda são chamadas diretamente. Os próximos passos naturais são criar adapters outbound para Valkey e Prometheus por trás de portas específicas, reduzindo ainda mais o acoplamento de infraestrutura.

Como executar

Requisitos: Go >= 1.26, Redis (opcional, mas recomendado)

Construir e executar:

cd scraper-go
go build ./cmd/server
./server

Rodar em desenvolvimento (carregando .env):

cd scraper-go
go run ./cmd/server

Docker: há um Dockerfile em scraper-go/. No Docker Compose, configure VALKEY_URL=redis://valkey:6379/0 no .env da raiz para que o scraper acesse o Valkey pelo nome do serviço na rede Docker.

No Compose da raiz, o serviço escuta em http://localhost:8081.

Limites globais de execução

O scraper possui um orçamento global de concorrência por execução controlado por SCRAPER_MAX_CONCURRENCY.

  • Padrão interno: 12, usado quando a variável não está definida.
  • Valor válido: inteiro positivo.
  • Valores inválidos explícitos ("", 0, negativo ou não numérico) fazem a aplicação falhar no startup, sem fallback silencioso.
  • Cron e POST /admin/scrape usam o limite global configurado.
  • POST /scrape preserva o contrato atual: quando maxConcurrency não é informado, ou vem como 0/negativo, usa o limite global; quando vem positivo abaixo do teto, usa o valor solicitado; quando vem acima do teto, usa o teto global.
  • A concorrência efetiva é calculada antes da chave de cache e é o mesmo valor usado pelo pipeline, logs e semáforo.

O semáforo global atual é criado uma vez por chamada do pipeline. Portanto, o limite é por execução: duas execuções simultâneas ainda podem possuir dois semáforos independentes com a mesma capacidade. Lock entre cron/manual, prevenção de simultaneidade e limites por provider pertencem às próximas sub-issues.

No Docker Compose de produção, o serviço scraper-go também define:

  • GOMAXPROCS=2: limita a quantidade de threads do Go executando código simultaneamente.
  • GOMEMLIMIT=1500MiB: define uma meta de memória para o runtime e influencia o garbage collector.
  • mem_limit: 2g: limite externo do container. GOMEMLIMIT não substitui esse limite; ele fica abaixo de 2g para preservar margem operacional.
  • cpus: 1.5: limita o container a 1,5 CPU.

Endpoints HTTP

O serviço expõe endpoints HTTP (implementação em cmd/server e arquivos associados). Principais rotas:

  • POST /scrape — body JSON com ScrapeRequest para disparar uma busca em todas as fontes configuradas. Retorna ScrapeResponse com jobs, total, cachedAt e fromCache.
  • GET /health — verifica se o scraper está online e qual cache está em uso.
  • GET /metrics — métricas Prometheus.
  • GET /api/keywords — retorna as keywords atualmente carregadas.
  • POST /api/keywords — atualiza/persiste as keywords (aceita keywords: string[]).
  • POST /admin/scrape — dispara uma execução manual em background; retorna 409 se já houver execução em andamento.
  • GET /admin/scrape/status — informa se existe uma execução em andamento.
  • GET /admin/jobs/count — retorna a quantidade de vagas persistidas no Valkey.
  • GET /admin/jobs — lista uma amostra das vagas persistidas no Valkey; aceita limit.

Exemplo de ScrapeRequest (JSON):

{
  "keywords": ["react", "node"],
  "searchLocation": "Brasil",
  "searchGeoId": "106057199",
  "searchLanguage": "pt",
  "jobTypes": "C,F",
  "timeFilter": "r604800",
  "remoteOnly": true,
  "resultsPerPage": 25,
  "maxPagesPerKeyword": 5,
  "waitBetweenSearchesMs": 3000,
  "pageTimeoutMs": 15000,
  "maxConcurrency": 50
}

Exemplo de ScrapeResponse (JSON):

{
  "jobs": [
    {
      "id": "...",
      "title": "Frontend Engineer",
      "company": "Empresa X",
      "location": "São Paulo, SP",
      "url": "https://...",
      "source": "LinkedIn",
      "sources": ["LinkedIn"],
      "keyword": "react",
      "keywords": ["react"]
    }
  ],
  "total": 1,
  "cachedAt": "2026-05-26T10:00:00Z",
  "fromCache": false
}

Observação: os endpoints acima refletem a implementação atual em scraper-go/cmd/server.

Pipeline de scraping

Fluxo principal:

  1. Recebe ScrapeRequest com keywords e configuração.
  2. Verifica cache (internal/cache). Se encontrado, retorna resultado cacheado.
  3. Caso contrário, executa pipeline.ScrapeAllSources que:
    • Recebe a lista de fontes já montada pelo servidor/registry.
    • Cria uma tarefa por fonte batch (SearchBatch) ou uma tarefa por keyword para fontes sem batch, sempre respeitando MaxConcurrency.
    • Cada adaptador realiza requisições HTTP específicas, parseia HTML/JSON quando necessário e retorna domain.Job.
    • Agrega resultados e aplica deduplicação (dedup.DedupeJobs).
    • Classifica vagas por família, tecnologias e senioridade antes da indexação.
    • Persiste vagas novas no jobstore (Redis) e atualiza índices invertidos (função IndexJobsInValkey).
    • Escreve resultado no cache para próximas requisições.

Concorrência e resiliência:

  • Semáforos por adaptador (ex.: LinkedIn usa um semáforo de 5 simultâneos para proteção).
  • Orçamento global por execução via SCRAPER_MAX_CONCURRENCY, aplicado antes do cache e do pipeline.
  • Tratamento de status 429 com backoff; aborta apenas a keyword afetada em caso de falhas persistentes.
  • Uso de inflight para evitar que múltiplas requisições idênticas disparem scrapes simultâneos.
  • Slots rotativos reduzem o número de keywords/queries por rodada em fontes caras, preservando cobertura progressiva em execuções futuras.

Adaptadores

Cada adaptador em internal/adapters/<fonte> implementa a porta ports.JobSource com SourceName() e Search(ctx, keyword, req). Quando a fonte consegue buscar várias keywords em uma chamada/lote, ela também pode implementar ports.BatchJobSource. Implementações incluem:

  • internal/adapters/linkedin — busca via endpoint público jobs-guest do LinkedIn; parsing com goquery.
  • internal/adapters/adzuna — integra via API Adzuna (se configurada com ADZUNA_APP_ID/ADZUNA_APP_KEY).
  • internal/adapters/jooble — integra com Jooble API (se JOOBLE_API_KEY configurada); pode usar Redis para quota.
  • internal/adapters/greenhouse, internal/adapters/lever, internal/adapters/themuse — adaptadores por empresa/plataforma com parsing/integração próprios.
  • internal/adapters/gupy, internal/adapters/inhire — adaptadores para fontes usadas no fluxo atual de vagas.

Estratégia por fonte

  • LinkedIn: sempre habilitado; SearchBatch seleciona um slot rotativo de keywords por execução. Defaults atuais: 5 páginas por keyword e 30 keywords por rodada.
  • Adzuna: habilitado quando ADZUNA_APP_ID e ADZUNA_APP_KEY existem; SearchBatch usa slot rotativo. Defaults atuais: 5 páginas por keyword e 30 keywords por rodada.
  • Gupy: habilitado com GUPY_ENABLED=true; usa queries expandidas, descoberta por termos amplos e sweep opcional. O default atual limita o sweep a offset 10000 e processa 60 queries por rodada.
  • Jooble: habilitado com JOOBLE_API_KEY; usa cota diária, slot rotativo e cadência de 12h.
  • Greenhouse: habilitado com GREENHOUSE_ENABLED=true; cria um adapter por empresa listada em internal/interfaces/greenhouseCompanies.json.
  • Lever: habilitado com LEVER_ENABLED=true; cria adapters a partir de internal/interfaces/leverCompanies.json.
  • InHire: habilitado com INHIRE_ENABLED=true; consulta tenants de internal/interfaces/inhireTenants.json e só enriquece detalhes quando INHIRE_ENRICH_DETAILS=true.

Boas práticas nos adaptadores:

  • Normalização de campos (URL, título, empresa, localização).
  • Derivação de ID estável via jobstore.StableID (título+empresa+local ou URL normalizada).
  • Dedupe local antes de retornar ao pipeline.

Persistência e índice (Valkey)

  • jobstore.SaveBatch persiste vagas no Redis com TTL e mantém um índice global (scraper:jobs:index).
  • pipeline.IndexJobsInValkey cria índices invertidos por keyword e sub-termos (scraper:jobs:keyword:<term>), além de manter TTL para índices.
  • A classificação local também gera índices estruturados: scraper:jobs:family:<family>, scraper:jobs:technology:<technology> e scraper:jobs:seniority:<seniority>.
  • Filtros estruturados de localização, modelo, contrato e senioridade continuam em chaves como scraper:jobs:country:<value>, scraper:jobs:model:<value> e scraper:jobs:contract:<value>.
  • jobstore.StableID garante IDs determinísticos para permitir identificação e deduplicação entre execuções.

Cache e configuração

  • Cache é abstraído por internal/cache com implementações Redis (NewRedisCache) e memória (fallback para testes).
  • Chave de cache do scraper é construída por pipeline.BuildCacheKey (consistente com os parâmetros de busca).

Testes

  • Há testes e fixtures (ex.: internal/keywords/keywords.test.json) para validar normalização.
  • Recomenda-se executar go test ./... dentro de scraper-go.
  • Para validar a configuração final do Compose, execute na raiz:
docker compose \
  -f docker-compose.infra.yml \
  -f docker-compose.yml \
  -f docker-compose.migrate.yml \
  config

Confirme no serviço scraper-go os equivalentes de SCRAPER_MAX_CONCURRENCY=12, GOMAXPROCS=2, GOMEMLIMIT=1500MiB, cpus: 1.5 e mem_limit: 2g.

Variáveis de ambiente importantes

  • VALKEY_URL — conexão Redis/Valkey. Em Docker Compose, use redis://valkey:6379/0; em execução local fora do Docker, use uma URL acessível pelo host, por exemplo redis://localhost:6379/0.
  • SCRAPER_MAX_CONCURRENCY — teto global de concorrência por execução. Padrão: 12. Configuração explícita inválida impede a inicialização.
  • GOMAXPROCS — limite efetivo de threads executando código Go simultaneamente. Valor inicial no Compose: 2.
  • GOMEMLIMIT — meta de memória do runtime/GC. Valor inicial no Compose: 1500MiB; não substitui mem_limit do container.
  • JOOBLE_API_KEY — Jooble integration.
  • ADZUNA_APP_ID / ADZUNA_APP_KEY — Adzuna API.
  • LINKEDIN_KEYWORD_SLOT_SIZE — quantidade máxima de keywords do LinkedIn por execução quando a busca vier com uma lista grande. Padrão: 30.
  • ADZUNA_KEYWORD_SLOT_SIZE — quantidade máxima de keywords do Adzuna por execução. Padrão: 30.
  • GUPY_ENABLED — habilita o adapter Gupy.
  • GUPY_RAW_DISCOVERY_ENABLED — adiciona queries amplas de tecnologia na Gupy.
  • GUPY_FULL_SWEEP_ENABLED / GUPY_FULL_REMOTE_SWEEP_ENABLED — controla sweeps amplos na Gupy.
  • GUPY_QUERY_LIMIT — limita quantas queries expandidas da Gupy rodam por execução. Padrão: 60.
  • INHIRE_ENABLED, INHIRE_TENANTS_FILE, INHIRE_ENRICH_DETAILS, INHIRE_DETAILS_MODE, INHIRE_DETAILS_CONCURRENCY, INHIRE_DETAILS_TIMEOUT_MS — controlam fonte e enriquecimento InHire.
  • GREENHOUSE_ENABLED, GREENHOUSE_COMPANIES_FILE — controlam fonte Greenhouse.
  • LEVER_ENABLED, LEVER_COMPANIES_FILE, LEVER_INCLUDE_ALL_JOBS — controlam fonte Lever.
  • Configurações de logging, quota e performance podem ser definidas via .env.

Observações operacionais

  • Projetado para rodar frequentemente; use caching e indexação para reduzir chamadas repetidas.
  • Monitorar erros 429 e ajustar WaitBetweenSearchesMs / semáforos por adaptador.
  • Verifique logs estruturados (slog JSON) e /metrics para métricas de sucesso/falhas por adaptador.