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#
{
"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#
-
Shared cache — keyed by the normalised query (whitespace collapsed, lower case) plus
language,country,max_resultsandfreshness; entries live 7 days.cache_ttl_scaps the age you accept;0bypasses the cache. -
SearXNG — Cormorant's own metasearch instances (Bing, Yahoo, Yep, Yandex, Seznam, DuckDuckGo, Brave, Wikipedia, Bing News). Free.
-
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, orcaptcha_engineswhen an engine was CAPTCHA-blocked), or - returns fewer than 3 usable results (
too_few_results).
- fails or times out (
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:
{
"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 -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}'
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']}")
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_resultsas 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.