CormorantDocs
Menu

Search in depth

POST /v1/search answers a query with merged, deduplicated results from several search engines, falling back to a paid API only when it has to.

Request#

JSON
{
  "query": "Nestlé sustainability report 2025",
  "language": "en",
  "country": "CH",
  "max_results": 20,
  "freshness": "month",
  "cache_ttl_s": 86400,
  "project_ref": "company:4711"
}

Only query is required. language is ISO 639-1 (optionally with a region, de-CH), country ISO 3166-1 alpha-2, max_results 1–50 (default 20), freshness one of day, week, month, year.

Providers and fallback#

  1. Shared cache — keyed by the normalised query (whitespace collapsed, lower case) plus language, country, max_results and freshness; entries live 7 days. cache_ttl_s caps the age you accept; 0 bypasses the cache.

  2. SearXNG — Cormorant's own metasearch instances (Bing, Yahoo, Yep, Yandex, Seznam, DuckDuckGo, Brave, Wikipedia, Bing News). Free.

  3. Brave Search API — the paid fallback, used when SearXNG

    • fails or times out (fallback_reason: searxng_error, searxng_timeout, no_searxng_instance),
    • reports unresponsive engines and fewer than two engines answered (unresponsive_engines, or captcha_engines when an engine was CAPTCHA-blocked), or
    • returns fewer than 3 usable results (too_few_results).

provider says who answered (searxng or brave), fallback_reason why the fallback was considered and unresponsive_engines which SearXNG engines failed for this query. When Brave is used, its results come first, followed by results only SearXNG found.

If Brave is unavailable or your project's budget is exhausted, whatever SearXNG returned is served (and not cached) with fallback_reason set. A search that the engines answered but that found nothing returns results: [] with error: null. Only when the providers could not answer is error set — for example budget_exhausted, brave_http_429 or searxng_timeout.

Results: nothing is lost#

Results are deduplicated by normalised URL (scheme, www., trailing slash and tracking parameters ignored). For each URL, every distinct snippet and every engine that returned it is kept, in order of first appearance:

JSON
{
  "url": "https://www.nestle.com/sustainability/reporting",
  "title": "Creating Shared Value and Sustainability Report 2025",
  "snippets": ["Our 2025 report covers ...", "Download the full report ..."],
  "engines": ["bing", "duckduckgo", "brave_api"],
  "published_at": "2026-03-02T00:00:00",
  "rank": 1
}

More engines agreeing on a URL is a useful relevance signal; more snippets give an LLM more context before it decides to fetch the page.

Costs#

SearXNG answers and cache hits are free. A Brave call costs a small fixed amount per request (operator-configured, on the order of half a cent), shown in cost_usd and charged to your project's budget. Brave's own rate limits are retried by Cormorant and never billed.

Examples#

curl
curl -sS https://api.cormorant.ch/v1/search \
  -H "Authorization: Bearer $CORMORANT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"query": "Holcim annual report 2025", "language": "en", "freshness": "year", "max_results": 10}'
Python
answer = client.post(
    "/v1/search",
    json={
        "query": "Holcim annual report 2025",
        "language": "en",
        "freshness": "year",
        "max_results": 10,
    },
).json()
if answer["error"]:
    print("search failed:", answer["error"])
for hit in answer["results"]:
    print(f"{hit['rank']:>2} {hit['title']} ({', '.join(hit['engines'])})\n   {hit['url']}")
TypeScript
const answer = await (
  await fetch(`${BASE}/v1/search`, {
    method: "POST",
    headers,
    body: JSON.stringify({ query: "Holcim annual report 2025", language: "en", freshness: "year", max_results: 10 }),
  })
).json();
for (const hit of answer.results) console.log(hit.rank, hit.title, hit.url, hit.engines.join(","));

Tips#

  • Searching is cheap; fetching is where the time goes. Use the snippets to decide which results deserve a fetch.
  • Keep max_results as small as you need: it is part of the cache key, so consistent values also mean more cache hits.
  • For many queries use POST /v1/batch/search — see Sync vs batch.

View as Markdown