TerraGuard

Search Layer

API-based web + news search using Serper.dev as the primary provider and Brave Search as the error fallback, called directly from the Backend API.

Overview

TerraGuard's search layer discovers news articles, situation reports, and official advisories about disaster events. It is an API-based layer: the Backend API calls commercial search APIs directly. There is no self-hosted search engine and no Tor proxy in the request path.

The layer lives inside the Backend API as a shared module (app/common/search_providers.py) and exposes two entry points — search_web() and search_news() — used by event knowledge discovery, the news crawler, situation-report generation, system-health checks, and the agentic web-search tool.

Architecture change. The earlier design routed search through a standalone Go microservice (tg-search-api) backed by SearXNG over Tor with Brave as a fallback. SearXNG and the standalone search service have been removed. The backend now calls Serper.dev (primary) and Brave Search (fallback) directly. See What was removed.

Architecture

Loading diagram...

The fallback is error-triggered only: Brave is called when Serper raises an error (missing key, network failure, upstream 5xx). A successful-but-empty Serper response does not trigger fallback.

Providers

ProviderRoleEndpointsAuthPagination
Serper.devPrimaryPOST google.serper.dev/search (web), POST google.serper.dev/news (news)X-API-KEY: SERPER_API_KEYpage 1-N, 10 results/page, capped at 10 pages (~100 results)
Brave SearchFallbackGET /res/v1/web/search, GET /res/v1/news/searchX-Subscription-Token: BRAVE_API_KEYoffset 0–9; web 20/page, news 50/page

Serper.dev proxies Google's web and news indexes through a single API call, which removed the need for the SearXNG meta-search engine and the Tor anonymity layer that previously guarded against IP rate-limiting.

Unified result model

Both providers normalize into one SearchResult shape so consumers never branch on provider:

@dataclass
class SearchResult:
    url: str
    title: str
    snippet: str
    source: str        # "serper" | "brave"
    search_type: str   # "web" | "news" | "siterep"
    published_date: Optional[str] = None
    position: Optional[int] = None

Freshness

A single UnifiedFreshness object maps one freshness bucket to each provider's own parameter, so callers pick a window once:

BucketSerper (tbs)Brave (freshness)
past_dayqdr:dpd
past_weekqdr:wpw
past_monthqdr:mpm
past_yearqdr:ypy
no_filter

age_to_freshness(anchor) picks the smallest bucket containing now − anchor, so a freshly detected event searches past_day while an older one widens automatically.

Entry points

from app.common.search_providers import search_web, search_news, age_to_freshness

# Web search — Serper primary, Brave fallback on error
results = await search_web("earthquake Turkey 2026", freshness=age_to_freshness(event.detected_at), limit=100)

# News search
news = await search_news("Cyclone Mocha landfall", limit=50)

Both return a flat list[SearchResult]. When Serper errors, the orchestrator appends Brave results to whatever Serper returned and logs the fallback.

Configuration

VariableDescriptionDefault
SERPER_API_KEYSerper.dev API key (primary provider)""
BRAVE_API_KEYBrave Search subscription token (fallback)""

Both live in the Backend API's environment (app/config.py). If SERPER_API_KEY is unset, every web/news call falls straight through to Brave; if both are unset, search returns an empty result set with a logged error rather than raising.

What was removed

The previous search stack — a standalone tg-search-api Go service backed by SearXNG (meta-searching Google/Bing/DuckDuckGo via a Tor SOCKS proxy) with Brave as a fallback — has been decommissioned and removed from the system: the SearXNG and Tor containers are gone from the dev and production topology, and the backend no longer makes any call to a search service.

It was removed because:

  • Serper.dev returns Google-quality web + news results through a single authenticated API, removing the operational burden of self-hosting SearXNG.
  • The Tor anonymity layer (needed to dodge IP rate-limiting on scraped engines) is unnecessary against a paid API.
  • Collapsing search into a backend module removed a network hop, two containers (SearXNG + Tor), and the SearXNG/Tor/residential-proxy failure modes.

The result is a two-provider layer — Serper primary, Brave fallback — with no self-hosted search infrastructure.

On this page