Resumo executivo
git fetch origin main, os dois repositórios estão alinhados com origin/main (0 à frente/atrás). Os não rastreados existentes imovel-central-docker/.ai/ e crawler-imoveis/.envrc foram preservados.withoutOverlapping(180); a duração depende de páginas, volume, Selenium, site externo e rede. Os 180 minutos são a janela de bloqueio contra sobreposição, não uma duração garantida.Fluxo completo atual
O scheduler Laravel dispara o comando; por padrão chama o runner HTTP do container crawler. O runner inicia o CLI em uma thread quando assíncrono e devolve 202 Accepted. Cada scraper coleta e envia cada imóvel individualmente; o backend grava properties, imagens e endereço.
Quando roda
| Imobiliária / chave | Discover | Update |
|---|---|---|
Carol Gianazi — carol-gianazi | 01:00 | 03:30 |
Fadel — fadel | 01:30 | 04:00 |
Lúzia Imóveis — luzia | 02:00 | 04:30 |
Village Imobiliária — village | 02:30 | 05:00 |
Silvana Lopes — silvana-lopes | 03:00 | 05:30 |
| crawler:prune-stale | 06:00 | |
- Fuso:
America/Sao_Paulo. withoutOverlapping(180)impede execução conflitante;onOneServer()coordena uma instância.- O serviço Docker
schedulerprecisa estar ativo comphp artisan schedule:work. - O prune usa
CRAWLER_STALE_DAYS, padrão7, e não apaga registros.
Fonte: routes/console.php:L25-L65 · config/crawler.php:L15-L43
O que cada scraper faz
| Chave | Fonte | Discover | Update | Detalhes |
|---|---|---|---|---|
carol-gianazi | carolgianazi.com.br | Selenium, paginação start | URLs ativos do backend | Espera de 1–3 s. |
fadel | imobiliariafadel.com.br | Selenium, páginas numeradas | URLs ativos do backend | Espera de ~2 s. |
luzia | luziaimoveis.com.br | Selenium, páginas numeradas | URLs ativos do backend | Espera de 1–3 s. |
village | villageimobiliaria.imb.br | API /api/listings?pagina=N | Varre API até IDs registrados | Timeout 20 s/página; filtra imob_property. |
silvana-lopes | silvanalopesimobiliaria.com.br | POST para API externa | Compara IDs registrados | Timeout externo 60 s; paginação própria. |
discover coleta o catálogo e chama sync_property() por imóvel. update consulta GET /api/v1/crawler/properties paginado (até 100 por página), filtrando ativos e imobiliária, e só então re-coleta os registrados.
Chaves, autenticação e payload
Segredo
CRAWLER_API_TOKEN é igual no backend e Python. Envio: Authorization: Bearer <token>; também aceita X-Crawler-Token.
Endereços
Docker: http://backend:9000/api/v1/crawler/properties; runner: http://crawler:8080; standalone: http://localhost:9000/api/v1/crawler/properties.
Modos
discover/full varrem o site; update reprocessa cadastrados.
Formato enviado por imóvel
{
"external_id": "ID da imobiliária", "external_url": "https://...",
"real_estate_company_name": "Nome", "description": "...", "price": 0,
"property_type": "apartment", "business_type": "sell",
"latitude": -23.0, "longitude": -46.0,
"count_suites": 1, "count_rooms": 2, "count_bedrooms": 2,
"count_bathrooms": 2, "count_kitchens": 1, "count_parking_spaces": 1,
"images": ["https://..."],
"address": {"state":"SP","city":"...","neighborhood":"...","street":"...",
"number":10,"address":"...","complement":"..."}
}
A validação exige ID, URL, imobiliária, tipo de propriedade, tipo de negócio e estado/cidade/bairro. Preço, coordenadas, contagens, imagens e detalhes de endereço podem ser nulos; imagens precisam ser URLs.
O que acontece com cada imóvel
- Resolve estado por nome/sigla; cidade por nome ou correspondência textual próxima; bairro com a mesma lógica.
- Resolve a imobiliária por mapas de nomes/chaves, por exemplo
carol-gianazi → Gianazi,luzia → Lúzia Imóveis,village → Village. - Identifica por
real_estate_company_id + external_idviaupdateOrCreate; novo recebe ID, existente é atualizado. - Monta slug com tipo, bairro, cidade, imobiliária e ID externo; garante unicidade com
-1,-2etc. - Cada sync força
active=trueelast_synced_at=now(); preço, descrição, tipo, negócio, coordenadas e contagens vêm do payload. - Imagens externas entram com
firstOrCreate; endereço é atualizado na relação. - Às 06:00, externo ativo com
last_synced_atnulo ou anterior ao limite viraactive=false; não é deletado.
Como a execução é disparada
| Configuração | Padrão | Efeito |
|---|---|---|
CRAWLER_RUNNER_DRIVER | http | Laravel faz POST no runner Python. |
CRAWLER_RUNNER_URL | http://crawler:8080 | Container/porta do runner. |
CRAWLER_RUNNER_TIMEOUT | 3600 | Timeout da chamada síncrona; agenda normal é async. |
--sync | desligado | Sem flag, Laravel recebe aceite e logs ficam no crawler; com flag, aguarda. |
POST /run | async=true | Responde 202 e roda CLI em thread; false espera subprocesso e retorna stdout/stderr. |
O container crawler usa restart: unless-stopped, monta backend/crawler:/app e Chromium em /usr/bin/chromium. O scheduler usa php artisan schedule:work. Em produção, limites declarados: crawler 1 CPU/1536 MB; scheduler 0,20 CPU/256 MB.
Double check: Meilisearch e indexação
scout_driver=meilisearch, scout_queue=false, queue_default=redis e host http://meilisearch:7700. O endpoint de saúde respondeu disponível, o índice properties existe e contém 8.966 documentos, exatamente a mesma quantidade de registros em properties no MySQL.| Parte | Como está | Impacto |
|---|---|---|
| Modelo | Property usa Laravel\Scout\Searchable e indexa id, price, endereço textual e description. | O texto da busca pode vir do Meilisearch. |
| Consulta | Property::search($term)->keys() produz IDs; depois o MySQL aplica active=true e filtros de negócio, tipo, preço, quartos etc. | Filtros continuam no banco; a ordenação final também é do MySQL. |
| Índice atual | properties, primary key id, 8.966 documentos, sem atributos filterable/sortable configurados. | Está adequado ao padrão atual de buscar IDs e filtrar no MySQL, mas não para filtros nativos no Meili. |
| Ambiente local | SCOUT_QUEUE=false. | A sincronização acontece na própria operação de criação/alteração do imóvel. |
| Produção | docker-compose.prod.yml força SCOUT_QUEUE=true e usa fallback SCOUT_DRIVER=collection se a variável não vier do ambiente. | Sem SCOUT_DRIVER=meilisearch efetivo, produção não usa Meili; com fila, o Horizon precisa estar saudável. |
Pontos de atenção encontrados
- Endereço/imagens não reindexam o imóvel sozinhos: no crawler,
Property::updateOrCreate()acontece antes dePropertyAddress::updateOrCreate()e das imagens. ComoPropertyAddressePropertyImagenão usamSearchable, uma mudança apenas no endereço pode não atualizar o documento do Meili. - Prune em massa não dispara evento do modelo:
->update(['active' => false])é update de query. O campoactivenem está no documento indexado, então a busca continua correta porque o MySQL filtra ativos, mas ficam documentos inativos no índice. - Configuração do índice é genérica:
searchableAttributes=['*'], semfilterableAttributesesortableAttributes. Isso é coerente com a implementação atual, mas impede aproveitar filtros/sort diretamente no Meili. - Dados atuais não estão recentes no ambiente local: o índice e o banco têm atualização máxima em
2026-07-09. Isso prova consistência de contagem, não atualização contínua; os containerscrawlereschedulernão estavam em execução neste compose local.
SCOUT_DRIVER=meilisearch e reindexar o imóvel depois de atualizar endereço/imagens (idealmente em uma transação/serviço único ou por observers/jobs específicos).Property/Searchable · config Scout · config produção · ordem do upsert do crawler
Standalone e fluxo legado
crawler-imoveis/ contém notebooks Jupyter, modelos e cliente de API. O código integrado em imovel-central-docker/backend/crawler/ é a versão operacional do Docker; o standalone é execução manual/experimental.
- Notebooks:
village.ipynb,fadel.ipynb,carolgianazi.ipynb,luziaimoveis.ipynb,silvanalopesimobiliaria.ipynb. - Recomendado no README: cada imóvel é enviado imediatamente por
api_client.sync_property(). - Legado:
storage.ipynbcriaproperties.jsone envia arquivo paraPOST /api/v1/crawler/sync/properties. - O lote salva arquivo, dispara
ProcessCrawlerDataJob, divide em chunks de 200, despachaProcessCrawlerChunkJobe apaga o arquivo; erros são registrados. - O compose standalone expõe Jupyter em
8888com tokencrawler123, diferente do runner operacional em8080.
scraped_properties, mas o serviço atual grava em properties. As tabelas scraped_* não são o destino do fluxo atual mostrado.README · compose · job de lote
Operação e diagnóstico
# listar chaves
docker compose exec backend php artisan crawler:run --list
# descoberta manual
php artisan crawler:run --company=village --mode=discover
# atualizar cadastrados
php artisan crawler:run --company=fadel --mode=update
# aguardar término
php artisan crawler:run --company=luzia --mode=discover --sync
# simular prune
php artisan crawler:prune-stale --dry-run
# logs
docker compose logs -f crawler scheduler
Token vazio → 503; token incorreto → 401. Imobiliária/endereço não resolvido → falha de validação. Item sem ID/URL → skipped.
Fontes consultadas
Orquestração
routes/console.php · RunCrawlerCommand.php · config/crawler.php · runner_server.py
Scrapers
API e persistência
api.php · token · validação · sync service · prune
Standalone / lote
README · api_client.py · storage.py · ProcessCrawlerDataJob.php · ProcessCrawlerChunkJob.php