Search and Scrape
Endpoint único que combina busca web + extração de conteúdo em uma requisição.
GET /api/search-and-scrapeSem autenticação. Sem rate limit aplicado por padrão.
Busca direta de imagens
Use images=true para buscar imagens diretamente pela NetFind:
curl -G 'https://netfind.io/api/search-and-scrape' \
--data-urlencode 'q=Breaking Bad elenco' \
--data-urlencode 'images=true' \
--data-urlencode 'limit=20'Esse modo retorna imagens sem abrir as páginas de origem, extrair artigos ou baixar os arquivos das fotos. Mantém a ordem de relevância e remove URLs de imagem duplicadas. limit aceita 1–50 e tem padrão 5; pode retornar menos imagens que o limite solicitado. Ausente ou images=false preserva a busca web existente.
O contrato de imagens é separado do contrato de páginas descrito abaixo:
{
"schemaVersion": "images-1",
"mode": "images",
"query": "Breaking Bad elenco",
"fromCache": false,
"results": [{
"title": "Título do resultado",
"url": "https://example.com/pagina-de-origem",
"sourceUrl": "https://example.com/pagina-de-origem",
"image": "https://example.com/foto.jpg",
"thumbnail": "https://example.com/miniatura.jpg",
"width": 1200,
"height": 800,
"alt": ""
}],
"metadata": {
"totalResults": 100,
"returned": 1,
"scrapedResults": 0,
"timing": { "total": 900, "search": 900, "scrape": 0 }
}
}Valores ilustrativos. image é a URL original; thumbnail é a URL da miniatura. Dimensões são da imagem original, ou null quando desconhecidas. title é o título do resultado, não uma legenda verificada. alt fica vazio quando indisponível. Os resultados não comprovam identidade ou pertencimento a grupos e os arquivos originais ainda podem bloquear o download. Para usar o proxy da NetFind, envie image ao endpoint de proxy autenticado existente.
Exibir a imagem original sem proxy
Use results[i].image como src do elemento <img>. O navegador baixa a imagem diretamente do site de origem/CDN; a NetFind não baixa nem converte o arquivo. Isso é independente do transporte usado pelo servidor para consultar o buscador.
const params = new URLSearchParams({
q: 'Breaking Bad elenco',
images: 'true',
limit: '20'
});
const response = await fetch(`/api/search-and-scrape?${params}`);
if (!response.ok) throw new Error(`Busca de imagens: HTTP ${response.status}`);
const { results } = await response.json();
for (const result of results) {
const img = document.createElement('img');
img.alt = result.alt || result.title || '';
img.loading = 'lazy';
// Opcional: usar a miniatura quando a origem bloquear ou falhar.
img.onerror = () => {
img.onerror = null;
if (result.thumbnail) img.src = result.thumbnail;
};
img.src = result.image;
document.querySelector('#galeria').append(img);
}O exemplo pressupõe um contêiner <div id="galeria"></div> e execução no mesmo domínio da API. O fallback é feito pelo cliente, não automaticamente pela API. Alguns sites bloqueiam hotlink; nesse caso a miniatura pode ter resolução menor. As dimensões são informadas nos resultados, sem verificação do arquivo. A busca mantém a ordem de relevância e não garante alta resolução nem ordena por tamanho.
Segurança e privacidade: URLs retornadas pelo buscador não são verificadas por antivírus. Exibir uma imagem com <img> normalmente não executa scripts da página de origem, mas arquivos maliciosos podem explorar falhas de decodificação do navegador. Mantenha os navegadores atualizados. No carregamento direto, o servidor da imagem recebe o IP do visitante e pode receber o referenciador conforme a política do navegador/site. O proxy de imagens existente não oferece análise antivírus.
Esse modo mantém um cache próprio de links e metadados por 24 horas. Não armazena os arquivos das fotos. Consultas idênticas reutilizam o lote de resultados, inclusive com valores diferentes de limit; fromCache: true indica essa reutilização. nocache=true força uma nova busca e atualiza o cache quando há resultados. Respostas vazias e erros não são armazenados. Se o cache estiver indisponível, a busca continua normalmente. local=true, html=true e filtros de publicação são incompatíveis e retornam HTTP 400. smart_chunks, links, preferred, no_url_boost e debug_images não se aplicam. stream=true retorna um único evento SSE results no sucesso; erros retornam JSON com status HTTP. Não há evento sites, pois não ocorre extração de páginas.
Uma resposta válida sem imagens retorna HTTP 200 e results: []. Bloqueio, formato inesperado ou falha na busca retornam HTTP 502; o prazo total é de 12 segundos (HTTP 504). Saturação temporária pode retornar HTTP 503 com Retry-After; aguarde esse intervalo antes de tentar novamente. Não há fallback para fotos de artigos. O parâmetro images também aparece no rastreamento por X-Request-ID.
Rastreamento de requisições
Todas as respostas desta rota incluem X-Request-ID. Envie esse cabeçalho com um identificador único por tentativa para conseguir rastrear inclusive requisições abortadas antes da resposta. São aceitos de 1 a 128 caracteres: letras, números, ., _, :, -. Se estiver ausente ou inválido, a API gera um UUID.
curl -H "X-Request-ID: suporte-20260914-tentativa-1" \
--max-time 25 \
"https://netfind.io/api/search-and-scrape?q=marombeiro&nocache=true"Para suporte, envie o X-Request-ID, horário UTC e, se disponível, CF-Ray. O erro de timeout HTTP 504 também inclui requestId no JSON. O timeout recomendado no cliente é 25 segundos para acomodar o teto interno de 15 segundos e o transporte da resposta.
O servidor registra início, parâmetros da busca, resposta pronta, status HTTP, duração, timeout e desconexão quando observada pelo runtime. Os eventos ficam em api_request_events, correlacionados por requestId; cada tentativa possui um instanceId distinto. Os registros de processamento em request_logs também incluem requestId quando gerados. Chaves de API, cookies e cabeçalhos de autorização não são registrados por esse rastreamento.
body_consumed indica que o runtime leu o corpo da resposta para envio, não confirma recebimento pelo cliente. Proxies podem ocultar ou atrasar a notificação de desconexão; quando isso ocorrer, o servidor não consegue determinar o instante exato do aborto no cliente.
Query parameters
| Parâmetro | Tipo | Default | Descrição |
|---|---|---|---|
q | string | obrigatório | Termo de busca ou uma URL http(s)://... pra scrape direto (pula a busca) |
limit | int | 5 | Máximo de resultados (1–50) |
links | bool | true | false remove links markdown [texto](url) do content |
smart_chunks | bool | false | Adiciona o campo relevant_chunk (trecho mais relevante por resultado) |
nocache | bool | false | Ignora o cache de busca, força refazer |
local | bool | false | Só retorna o que já está em cache, sem nova busca |
stream | bool | false | Resposta como SSE (eventos incrementais) |
html | bool | false | Retorna o HTML bruto completo quando q é uma URL direta |
published_after | ISO 8601 | — | Inclui resultados publicados nesse instante ou depois |
published_before | ISO 8601 | — | Inclui resultados publicados estritamente antes desse instante |
undated | exclude | include | exclude | Política para páginas sem data quando há filtro temporal |
Os filtros temporais são aplicados antes do limit. Eles usam a data de publicação declarada pela página e, portanto, representam um proxy retrospectivo (PUBLICATION_DATE_PROXY), não um snapshot histórico imutável.
O modo síncrono tem teto de 15 segundos, tanto para buscas por termos quanto para URL direta. Quando q é uma URL direta, a rota faz no máximo duas tentativas de acesso, com até 6,5 segundos por tentativa. Buscas por termos têm prazo interno de 13 segundos e até 10 segundos por página, respeitando o tempo restante. Esses valores são limites máximos: a resposta é enviada assim que o processamento termina.
Para vídeos do YouTube com legendas disponíveis, content inclui canal, descrição e legendas com timestamps (até 30.000 caracteres no total). O cliente Android consulta a versão a cada 24 horas pela Play Store, com fallback para a configuração mantida pelo yt-dlp. Se ambas as fontes falharem, mantém a última versão e tenta atualizar novamente após 5 minutos. Não há transcrição própria do áudio.
Correspondência e ordenação
Consultas com trechos entre aspas retas ("...") ou curvas (“...”) usam o modo exact. Cada trecho deve aparecer como sequência de palavras em pelo menos um campo de identidade da página: title, scrapedTitle, snippet ou metaDescription. Maiúsculas, acentos e separadores são normalizados: Histórias — Remix — DJ PANDISK corresponde a "Historias - Remix DJ PANDISK". Não há correção ortográfica, stemming ou equivalência fuzzy no aceite; Historia não corresponde a Historias. Todos os termos da consulta são exigidos, inclusive os que ficam fora das aspas. Menções apenas no corpo, rodapé ou resumo de IA não aprovam uma consulta exata.
O cache de busca não usa equivalência fuzzy para consultas com aspas. Resultados vindos de cache, índice local, busca externa e fallback passam pelo mesmo filtro. A API pode retornar results: [] com HTTP 200 quando não encontrar correspondência suficiente.
relevanceScore é uma pontuação lexical entre 0 e 1, não uma probabilidade de veracidade: 80% cobertura dos termos + 20% cobertura nos títulos. matchedTerms e missingTerms contêm tokens normalizados (por exemplo, dj e pandisk separados). exactMatch indica cobertura de todos os termos e de todas as frases citadas nos campos de identidade. Em buscas sem aspas, o corpo também participa da cobertura.
O ranking ocorre antes de limit, sobre os candidatos disponíveis dentro do prazo. Páginas de plataformas musicais recebem preferência apenas em empate de relevância quando a consulta contém termos musicais. A API não garante ter encontrado as melhores páginas de toda a web: indisponibilidade dos sites e o prazo limitam a cobertura. A ordem de conclusão do scrape não define a relevância.
Contrato JSON — versão 2
200 OK · application/json. O mesmo contrato se aplica a respostas frescas, cache, índice local, URL direta e ao evento SSE results.
interface SearchResponse {
schemaVersion: '2';
query: string;
results: SearchResult[];
fromCache: boolean | 'partial';
localOnly: boolean;
fromLocalIndex: boolean;
// Opcional: diagnóstico quando a busca usa o índice local.
confidence?: {
lowConfidence: boolean; reason: string;
topScore: number; topMatchRatio: number; distinctiveStemCount: number;
};
metadata: {
totalResults: number; // candidatos reportados pelo caminho de recuperação
scrapedResults: number; // resultados efetivamente retornados após os filtros
returned: number; // igual a results.length
matchMode: 'exact' | 'ranked';
timing: { total: number; search: number; scrape: number }; // milissegundos
// Opcionais: presentes quando há janela de publicação.
totalCandidates?: number;
withinPublicationWindow?: number;
excludedAfter?: number;
excludedBefore?: number;
excludedUndated?: number;
temporalMode?: 'PUBLICATION_DATE_PROXY';
coverageComplete?: boolean;
};
}
interface SearchResult {
title: string;
url: string;
displayUrl: string;
snippet: string;
favicon: string;
thumbnail: string | null;
content: string;
scrapedTitle: string;
image: string; // URL da imagem principal; vazio se não encontrada
images: RelevantImage[];
datePublished: string; // data declarada pela página, ou vazio
datePublishedSource: string;
publishedAt: number | null; // Unix timestamp em MILISSEGUNDOS; nunca string
datePrecision: 'SECOND' | 'MINUTE' | 'DAY' | 'UNKNOWN';
dateConfidence: number;
category: string;
aiSummary: string | null;
aiTopics: string[] | null;
aiContentType: string | null;
qualityScore: number | null;
metaDescription: string;
scrapeTime: number | null; // milissegundos; null se não disponível
htmlLength: number | null;
hasJsonLd: boolean | null;
internalLinks: string[];
externalLinks: string[];
fromCache: boolean;
fromLocalIndex: boolean;
fromFallback: boolean;
relevanceScore: number;
matchedTerms: string[];
missingTerms: string[];
exactMatch: boolean;
relevant_chunk?: string | null; // somente com smart_chunks=true
html?: string | null; // somente com html=true e q como URL direta
}
interface RelevantImage {
url: string;
alt: string;
caption: string;
sourceUrl: string;
sourceText: string;
credit: string;
license: string;
licenseUrl: string;
usageStatus: 'review_required' | 'attribution_required' | 'permitted';
width: number | null;
height: number | null;
imageRole: 'primary' | 'content';
}Todos os campos sem ? são presentes. Strings desconhecidas são "", listas são [] e números/booleanos desconhecidos nos campos anuláveis são null. Enriquecimento de IA pode chegar depois da primeira consulta, mas mantém o mesmo tipo. Campos internos como finishOrder, _localScore e identificadores de banco não fazem parte da versão 2 e não são expostos.
fromCache no envelope descreve o cache da consulta; em cada resultado indica o cache da URL, quando identificado pelo caminho de recuperação. nocache=true ignora o cache de consulta na busca externa, mas ainda pode reutilizar páginas já extraídas. Em local=true, a busca permanece local.
Imagens
A API guarda links e metadados, sem hospedar cópias. O extrator prioriza article, main ou a área editorial identificada; remove avatares, interface, rodapé, comentários, apoiadores e recomendações identificáveis no DOM. Imagens com dimensões conhecidas abaixo de 200 × 120 são descartadas. A imagem principal indicada pela página pode ser mantida sem dimensões.
O filtro é estrutural, não uma classificação visual por IA. Imagens antigas sem classificação de origem são conservadoramente limitadas à principal até uma nova extração. imageRole identifica apenas as classes que a API entrega: primary e content.
Exemplo: consulta exata sem correspondência
{
"schemaVersion": "2",
"query": "\"Historias - Remix DJ PANDISK\"",
"results": [],
"fromCache": false,
"localOnly": false,
"fromLocalIndex": false,
"metadata": {
"totalResults": 14,
"scrapedResults": 0,
"returned": 0,
"matchMode": "exact",
"timing": { "total": 5890, "search": 4000, "scrape": 1890 }
}
}Tempos e contagem acima são ilustrativos. Ausência de correspondência não é erro HTTP. Erros de parâmetro retornam HTTP 400 com { error: string }; timeout pode retornar HTTP 504 com { error: string, query?: string }; falhas de extração ou internas podem retornar { error: string, detail?: string } com status não 2xx. Se a capacidade temporária estiver esgotada e não houver resultados utilizáveis, a API pode responder HTTP 503 com Retry-After. Consultas compartilhadas preservam o cancelamento individual: encerrar uma requisição ou stream não interrompe outras requisições da mesma consulta. O envelope de sucesso não se aplica a erros.
Streaming (SSE)
Com stream=true, a resposta vira text/event-stream com três eventos:
event: sites
data: {"query":"...","provisional":true,"sites":[{"url","title","snippet","favicon","thumbnail"}, ...]}
event: results
data: { ... mesmo payload completo do modo JSON ... }
event: error
data: {"error":"..."}siteschega assim que a busca retorna as URLs — antes do scrape concluir. Útil pra mostrar "buscando em N sites…" na UI.resultschega ao final, com o payload completo.errorse a request falhar a meio caminho.
Exemplos
curl
# Busca normal
curl "https://netfind.io/api/search-and-scrape?q=python+tutorial&limit=5"
# Scrape direto de uma URL (q começa com http/https → pula a busca)
curl "https://netfind.io/api/search-and-scrape?q=https%3A%2F%2Fexample.com"
# Scrape direto incluindo o HTML bruto (refaz o scrape e não persiste o HTML no cache)
curl "https://netfind.io/api/search-and-scrape?q=https%3A%2F%2Fexample.com&html=true"
# Streaming SSE
curl -N "https://netfind.io/api/search-and-scrape?q=ia+generativa&limit=8&stream=true"
# Forçar resposta fresca (ignora cache)
curl "https://netfind.io/api/search-and-scrape?q=cotacao+dolar&nocache=true"
# Busca por janela de publicação
curl "https://netfind.io/api/search-and-scrape?q=bitcoin+ETF&published_after=2026-08-15T06%3A00%3A00Z&published_before=2026-08-16T18%3A00%3A00Z&undated=exclude&limit=8"Node / TypeScript
const params = new URLSearchParams({ q: 'python tutorial', limit: '5' });
const res = await fetch(`https://netfind.io/api/search-and-scrape?${params}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const { results, metadata } = await res.json();
for (const r of results) {
console.log(`${r.title}\n ${r.url}\n ${r.content.slice(0, 200)}\n`);
}Streaming em Node (SSE manual)
const res = await fetch(
`https://netfind.io/api/search-and-scrape?q=${encodeURIComponent(query)}&stream=true&limit=8`
);
const reader = res.body!.getReader();
const decoder = new TextDecoder();
let buf = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buf += decoder.decode(value, { stream: true });
let i;
while ((i = buf.indexOf('\n\n')) !== -1) {
const block = buf.slice(0, i);
buf = buf.slice(i + 2);
const event = block.match(/^event: (.+)$/m)?.[1];
const data = block.match(/^data: (.+)$/m)?.[1];
if (!event || !data) continue;
const payload = JSON.parse(data);
if (event === 'sites') console.log(`🔍 ${payload.sites.length} URLs encontradas`);
if (event === 'results') console.log(`✅ ${payload.results.length} resultados`);
if (event === 'error') console.error(`❌ ${payload.error}`);
}
}Notas práticas
- URL como
q— seqcomeça comhttp(s)://, o endpoint pula a busca e faz scrape direto da URL informada. contentem markdown leve — preserva links, listas e cabeçalhos. Uselinks=falsese quiser texto puro. Truncado em 25.000 caracteres.- Cache de buscas — queries idênticas reutilizam a resposta. TTL é variável:
- Time-sensitive (queries com termos como "hoje", "agora", "2026", "cotação"…): 6h
- Estáveis ("como fazer X", "o que é X"…): permanente
- Default: 60 dias
- Use
nocache=truequando precisar de resultado fresco.
- Sites client-rendered — páginas cujo HTML inicial é shell vazio (React/Vue puros) podem chegar na primeira busca apenas com metadados. Buscas seguintes já trazem o conteúdo extraído.
aiSummary/aiTopics— o enriquecimento por IA roda após o scrape. URLs novas chegam com esses campos emnull; em buscas posteriores eles aparecem preenchidos.- Deadline da resposta padrão — o modo síncrono tem deadline interno. Para buscas longas (limit alto, muitos sites lentos), prefira
stream=truee processe o eventositesenquanto o scrape conclui. qaceita aspas — passar o termo entre aspas (q="nome exato") ativa o filtro lexical exato descrito acima, sem aceite fuzzy. Pode retornar zero resultados.
