Architecture

seia is a single crate that ships both a library (src/lib.rs) and a CLI (src/main.rs). The design goal is one query surface, many backends: a caller picks an Engine and gets back the same SearchResult regardless of which backend produced it.

Module map

src/
├── lib.rs          public API surface
├── main.rs         clap CLI (search / engines)
├── engines.rs      Engine enum: as_str, api_key_env, needs_key
├── engines_impl/   one module per backend
│   ├── duckduckgo.rs   scrape (HTML)
│   ├── wikipedia.rs    API (JSON)
│   ├── tavily.rs       API (JSON, key)
│   ├── searxng.rs      API (JSON, self-hosted)
│   ├── bing.rs         API (JSON, key)
│   ├── brave.rs        API (JSON, key)
│   ├── zhipu.rs        API (JSON, key — 智谱 Web Search)
│   ├── bocha.rs        API (JSON, key — 博查 Web Search)
│   └── metaso.rs       API (JSON, key — 秘塔 Web Search)
├── client.rs       SearchClient + SearchOptions
├── extractor.rs    full-page content fetcher (for --fetch)
└── result.rs       SearchResult / SearchItem / SearchMode

Two execution paths, one result type

All paths converge on SearchResult:

query + Engine ─► SearchClient ─► engines_impl/* ─► unify ─► SearchResult

SearchMode (Api / Scrape) records which path produced a result, so callers can distinguish a structured API answer from a scraped page.

Dispatch

SearchClient::search_with_options is a flat match on Engine. Adding a backend means: implement one function in engines_impl/, add an Engine variant, add a match arm. There is no trait object or dynamic dispatch — the set of engines is closed and known at compile time, which keeps the API predictable and the binary small.

No headless browser

seia deliberately ships no browser automation. Every backend is a plain HTTP client. Engines that aggressively block non-browser traffic (Google, Baidu, Yandex web search) are out of scope — reach them through their official APIs or a dedicated browser tool such as shirabe when one becomes available as a standalone MCP.

Content enrichment

SearchOptions::fetch_content is an orthogonal concern: after the engine returns SearchItems, extractor::fetch_content downloads and cleans each page. This is engine-agnostic.