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
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
| Provider | Role | Endpoints | Auth | Pagination |
|---|---|---|---|---|
| Serper.dev | Primary | POST google.serper.dev/search (web), POST google.serper.dev/news (news) | X-API-KEY: SERPER_API_KEY | page 1-N, 10 results/page, capped at 10 pages (~100 results) |
| Brave Search | Fallback | GET /res/v1/web/search, GET /res/v1/news/search | X-Subscription-Token: BRAVE_API_KEY | offset 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] = NoneFreshness
A single UnifiedFreshness object maps one freshness bucket to each provider's own
parameter, so callers pick a window once:
| Bucket | Serper (tbs) | Brave (freshness) |
|---|---|---|
past_day | qdr:d | pd |
past_week | qdr:w | pw |
past_month | qdr:m | pm |
past_year | qdr:y | py |
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
| Variable | Description | Default |
|---|---|---|
SERPER_API_KEY | Serper.dev API key (primary provider) | "" |
BRAVE_API_KEY | Brave 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.
Event Processor
Go-based deterministic event ingestion service with pluggable source adapters, PostGIS spatial correlation, hash-based deduplication, and atomic upserts.
Web Crawler
Go API server with Python crawl4ai worker featuring a 4-level strategy fallback chain for reliable web content extraction from news sites and official reports.