Skip to content

Search and Scrape

Endpoint único que combina busca web + extração de conteúdo em uma requisição.

GET /api/search-and-scrape

Sem autenticação. Sem rate limit aplicado por padrão.

Busca direta de imagens

Use images=true para buscar imagens diretamente pela NetFind:

bash
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:

json
{
  "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.

js
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.

bash
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âmetroTipoDefaultDescrição
qstringobrigatórioTermo de busca ou uma URL http(s)://... pra scrape direto (pula a busca)
limitint5Máximo de resultados (1–50)
linksbooltruefalse remove links markdown [texto](url) do content
smart_chunksboolfalseAdiciona o campo relevant_chunk (trecho mais relevante por resultado)
nocacheboolfalseIgnora o cache de busca, força refazer
localboolfalseSó retorna o que já está em cache, sem nova busca
streamboolfalseResposta como SSE (eventos incrementais)
htmlboolfalseRetorna o HTML bruto completo quando q é uma URL direta
published_afterISO 8601Inclui resultados publicados nesse instante ou depois
published_beforeISO 8601Inclui resultados publicados estritamente antes desse instante
undatedexclude | includeexcludePolí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.

ts
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

json
{
  "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":"..."}
  • sites chega assim que a busca retorna as URLs — antes do scrape concluir. Útil pra mostrar "buscando em N sites…" na UI.
  • results chega ao final, com o payload completo.
  • error se a request falhar a meio caminho.

Exemplos

curl

bash
# 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

ts
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)

ts
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

  1. URL como q — se q começa com http(s)://, o endpoint pula a busca e faz scrape direto da URL informada.
  2. content em markdown leve — preserva links, listas e cabeçalhos. Use links=false se quiser texto puro. Truncado em 25.000 caracteres.
  3. 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=true quando precisar de resultado fresco.
  4. 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.
  5. aiSummary / aiTopics — o enriquecimento por IA roda após o scrape. URLs novas chegam com esses campos em null; em buscas posteriores eles aparecem preenchidos.
  6. Deadline da resposta padrão — o modo síncrono tem deadline interno. Para buscas longas (limit alto, muitos sites lentos), prefira stream=true e processe o evento sites enquanto o scrape conclui.
  7. q aceita aspas — passar o termo entre aspas (q="nome exato") ativa o filtro lexical exato descrito acima, sem aceite fuzzy. Pode retornar zero resultados.

Feito com VitePress