# OpenSERP Architecture ## 1. Overview OpenSERP is a Go API + CLI for search results extraction from Google, Yandex, Baidu, Bing, and DuckDuckGo. It supports two execution modes: - Browser mode (default): headless Chromium via `go-rod`, with engine-specific DOM parsing. - Raw HTTP mode: direct requests + HTML parsing (`goquery`) for engines that implement raw parsing. Browser mode is the primary path and supports all engines. Raw mode currently supports Google, Yandex, and Baidu only. ## 2. Directory Structure ```text openserp/ ├── main.go # Entry point, executes cmd.RootCmd ├── AGENTS.md # Contributor + agent project guidance ├── README.md # User-facing quickstart and API overview ├── config.yaml # Runtime configuration (loaded by Viper) ├── docs/ │ ├── ARCHITECTURE.md # This architecture reference │ ├── openapi.yaml # OpenAPI 3.0 specification │ └── embed.go # Embeds openapi.yaml for /openapi.yaml endpoint ├── cmd/ │ ├── root.go # Cobra root command + Viper config binding/defaults │ ├── serve.go # HTTP server bootstrap, engine wiring, browser pooling │ ├── search.go # CLI one-shot search command │ └── proxy_policy.go # Proxy policy mapping from config to runtime ├── core/ │ ├── common.go # Shared domain types: Query, SearchResult, SearchEngine │ ├── server.go # Fiber routes, request handlers, cache/proxy headers │ ├── middleware.go # CORS, request logging, JSON error envelope │ ├── browser.go # Chromium navigation lifecycle and page orchestration │ ├── http_client.go # Raw HTTP client (uTLS fingerprinting) │ ├── resilient.go # Retry + CB + rate limiting + proxy orchestration │ ├── retry.go # Backoff retry runner and retry conditions │ ├── circuit_breaker.go # Per-engine circuit breaker state machine │ ├── cache.go # In-memory TTL cache for API responses │ ├── proxy.go # Proxy normalization, pools, health/rotation, stats │ ├── logger.go # Logging setup helpers │ └── captcha.go # Captcha-related helpers/errors ├── google/ # Google engine implementation │ ├── url.go # URL builders │ ├── search.go # Browser mode parser │ └── search_raw.go # Raw HTTP parser ├── yandex/ # Yandex engine implementation │ ├── url.go │ ├── search.go │ └── search_raw.go ├── baidu/ # Baidu engine implementation │ ├── url.go │ ├── search.go │ └── search_raw.go ├── bing/ # Bing engine implementation (browser-only) │ ├── url.go │ └── search.go ├── duckduckgo/ # DuckDuckGo engine implementation (browser-only) │ ├── url.go │ └── search.go ├── testutil/ # Integration gating and shared test fixtures/helpers └── .github/workflows/ci.yml # CI checks (test/vet/build/lint/openapi lint) ``` ## 3. Key Interfaces and Types ### `core.SearchEngine` Contract for all engines: - `Search(Query) ([]SearchResult, error)` for web results - `SearchImage(Query) ([]SearchResult, error)` for image results - `IsInitialized() bool` for health readiness - `Name() string` for endpoint and stats identity - `GetRateLimiter() *rate.Limiter` for per-engine throttling ### `core.Query` Parsed from query parameters and request headers: - `Text` (`text`) - `LangCode` (`lang`) - `DateInterval` (`date`, format `YYYYMMDD..YYYYMMDD`) - `Filetype` (`file`) - `Site` (`site`) - `Limit` (`limit`, default `25`) - `Start` (`start`, default `0`) - `Filter` (`filter`, default `true`) - `Answers` (`answers`, default `false`) - `ProxyOverride` (`X-Use-Proxy` header: `` or `direct`) - Internal runtime fields: `ProxyURL`, `Insecure` Validation summary: - `start` must be `>= 0` - At least one of `text`, `site`, or `file` must be non-empty - Invalid query parsing is returned as JSON error response ### `core.SearchResult` Single SERP item shape: - `rank` (int) - `url` (string) - `title` (string) - `description` (string) - `ad` (bool) Mega endpoints return `core.MegaSearchResult`, which extends `SearchResult` with: - `engine` (string) ## 4. Request Flow ```text HTTP request -> Fiber router -> handleDedicatedEndpoint / handleMegaEndpoint -> Query.InitFromContext -> ResilientSearcher.SearchPrimary/SearchWithFallback (or mega parallel search) -> CircuitBreaker.AllowRequest -> RateLimiter.Wait -> Proxy policy resolution and proxy selection -> RetryableSearch (backoff/retry loop) -> Engine.Search / Engine.SearchImage Browser path: Browser.Navigate(url) -> DOM parse -> []SearchResult Raw path: raw HTTP request -> goquery parse -> []SearchResult -> De-duplication (mega endpoints) -> Cache.Set (if enabled and cacheable) -> JSON response + X-Cache/X-Proxy-*/X-Fallback-Engine headers ``` ## 5. Browser vs Raw Mode ### Browser Mode (default) - Enabled when `server.raw_requests: false` - Uses Chromium + `go-rod` navigation and page parsing - Supported engines: Google, Yandex, Baidu, Bing, DuckDuckGo - Best compatibility, but heavier resource usage ### Raw HTTP Mode - Enabled when `server.raw_requests: true` - Uses direct HTTP + HTML parsing without launching a browser - Supported engines: Google, Yandex, Baidu - Faster/lighter, but less reliable for anti-bot protected pages and missing image support Mode switch options: - Config: `server.raw_requests` - CLI flag: `--raw` ## 6. Resilience Stack The effective request protection sequence is: 1. Rate limiter (`engine.GetRateLimiter().Wait`) 2. Retry with exponential backoff (`core/retry.go`) 3. Circuit breaker per engine (`core/circuit_breaker.go`) 4. Proxy selection/rotation + health tracking (`core/proxy.go`) 5. Response cache (API-level TTL cache in `core/cache.go`) Important behaviors: - `ErrCaptcha` is non-retryable. - `ErrProxyUnavailable` does not record circuit-breaker failure. - Dedicated endpoints are engine-pure by default (`allow_endpoint_fallback: false`). - Fallback responses are not cached on dedicated endpoints. ## 7. Config Reference Defaults below are the shipped defaults in `config.yaml` (if present). If the config file is missing, fallback defaults from `cmd/root.go` are applied. ### `server` | Key | Default | Description | | --------------------- | --------- | ---------------------------------- | | `server.host` | `0.0.0.0` | API bind host | | `server.port` | `7000` | API bind port | | `server.debug` | `false` | Debug mode, forces headful browser | | `server.verbose` | `true` | Info-level request logs | | `server.raw_requests` | `false` | `true` = raw HTTP mode | | `server.insecure` | `true` | Allow insecure TLS connections | ### `app` | Key | Default | Description | | ------------------ | ------- | ----------------------------- | | `app.timeout` | `15` | Request timeout in seconds | | `app.browser_path` | `""` | Custom browser binary path | | `app.head` | `false` | Headful browser UI | | `app.leakless` | `false` | Force browser process cleanup | | `app.leave_head` | `false` | Keep browser tabs open | | `app.stealth` | `false` | Enable stealth plugin | ### `proxies` | Key | Default | Description | | ---------------------------------- | ------- | ----------------------------------------- | | `proxies.global` | unset | Force single proxy for all engines | | `proxies.entries[]` | empty | Tagged proxy pool entries (`url`, `tags`) | | `proxies.health.failure_threshold` | `3` | Disable proxy after N failures | Per-engine optional proxy tag: - `google.proxy` - `yandex.proxy` - `baidu.proxy` - `bing.proxy` - `duckduckgo.proxy` ### `cache` | Key | Default | Description | | ------------------- | ------- | ------------------------------------- | | `cache.ttl_seconds` | `60` | Response cache TTL (0 disables cache) | | `cache.max_size` | `1000` | Max cached entries | ### `resilience` | Key | Default | Description | | ------------------------------------ | ------- | ------------------------------------------------------ | | `resilience.max_retries` | `2` | Retry attempts per request | | `resilience.allow_endpoint_fallback` | `false` | Allow dedicated endpoints to fallback to other engines | ### `circuit_breaker` | Key | Default | Description | | ---------------------------------- | ------- | ------------------------------------ | | `circuit_breaker.failures` | `5` | Failures before opening circuit | | `circuit_breaker.recovery_seconds` | `60` | Open -> half-open wait time | | `circuit_breaker.successes` | `2` | Half-open successes to close circuit | ### `cors` | Key | Default | Description | | -------------------- | ------------------------------------------------------------ | --------------------------------- | | `cors.enabled` | `true` | Enable CORS middleware | | `cors.allow_origins` | `"*"` | Allowed origins | | `cors.allow_methods` | `"GET, POST, OPTIONS"` | Allowed methods | | `cors.allow_headers` | `"Origin, Content-Type, Accept, Authorization, X-Use-Proxy"` | Allowed headers | | `cors.max_age` | `86400` | Preflight cache max age (seconds) | ### `2captcha` | Key | Default | Description | | ----------------- | ------- | --------------------------- | | `2captcha.apikey` | unset | Optional captcha solver key | ### Engine rate-limit defaults For each engine (`google`, `yandex`, `baidu`, `bing`, `duckduckgo`): | Key | Default | Description | | --------------------------- | --------------- | ----------------------------- | | `.rate_requests` | `4` | Average requests per minute | | `.rate_burst` | `2` | Burst capacity | | `.rate_seconds` | `60` (implicit) | Rate window seconds | | `.selector_timeout` | `5` (implicit) | Selector wait timeout seconds | Google-only additional toggle: - `google.captcha` (default: `true`)