docs: add architecture, OpenAPI spec, Swagger UI, and contributing guide

This commit is contained in:
Rustem Kamalov
2026-04-15 03:25:06 +03:00
parent e1142ac23b
commit a9986fd8fe
7 changed files with 1373 additions and 1 deletions

View File

@@ -23,14 +23,22 @@ jobs:
go-version: "1.24"
cache: true
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
- name: Run unit tests
run: go test -race -count=1 ./...
- name: Run go vet
run: go vet ./...
- name: Lint OpenAPI spec
run: npx --yes @redocly/cli lint docs/openapi.yaml
- name: Build
run: go build .
- name: Run golangci-lint
uses: golangci/golangci-lint-action@v6
uses: golangci/golangci-lint-action@v6

120
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,120 @@
# Contributing to OpenSERP
## Development Setup
### Prerequisites
- Go 1.24+
- Chromium/Chrome (only required for browser-mode work and integration tests)
- Optional: Docker
### Clone, build, run
```bash
git clone https://github.com/karust/openserp.git
cd openserp
go build -o openserp .
./openserp serve
```
### Test commands
Unit tests (default, no browser/network assumptions):
```bash
go test -race ./...
```
Integration tests (explicitly enabled):
```bash
OPENSERP_INTEGRATION_TESTS=1 go test -race -timeout=120s ./...
```
Notes:
- Integration tests are gated by `testutil.RequireIntegration(t)`.
- Do not create browser instances in `init()` or package-level variables.
## Adding a New Search Engine
### 1) Create engine package
Create a new folder (example: `myengine/`) with:
- `myengine/url.go` (`BuildURL`, and `BuildImageURL` when image support exists)
- `myengine/search.go` (browser mode implementation)
- `myengine/search_raw.go` (optional raw mode implementation)
### 2) Implement `core.SearchEngine`
Your engine type must implement:
- `Search(core.Query) ([]core.SearchResult, error)`
- `SearchImage(core.Query) ([]core.SearchResult, error)`
- `IsInitialized() bool`
- `Name() string`
- `GetRateLimiter() *rate.Limiter`
Use the existing engines (for example `google/`) as the reference pattern.
### 3) Register the engine in server wiring
Update [`cmd/serve.go`](cmd/serve.go):
- Add engine spec in `browserEngineSpecs()`
- Add raw-mode handling if raw support exists
### 4) Add config block
Update [`config.yaml`](config.yaml) with your engine section:
- `rate_requests`
- `rate_burst`
- optional `proxy` tag
- optional engine-specific fields
### 5) Add tests
- URL builder tests (table-driven)
- Parser tests (prefer deterministic fixtures in `testdata/`)
- Integration tests guarded by `testutil.RequireIntegration(t)`
## Code Style and Quality Checks
Run these before opening a PR:
```bash
gofmt -w .
go vet ./...
golangci-lint run
go test -race ./...
```
Guidelines:
- Return `error` values instead of panicking in library code.
- Reuse existing patterns in `core/` and existing engines.
- Add comments only for non-obvious decisions (why, not what).
## Test Categories
- Unit tests: deterministic tests that run with `go test ./...` and do not require browser/network.
- Integration tests: live/browser/network dependent tests gated by `OPENSERP_INTEGRATION_TESTS=1`.
When adding tests, keep unit and integration behavior clearly separated.
## Pull Request Process
For each PR:
1. Describe what changed and why.
2. Link the related issue (if available).
3. Include or update tests for behavior changes.
4. Include updated docs when API/config/contracts change.
If you change API behavior, update:
- [`docs/openapi.yaml`](docs/openapi.yaml)
- [`README.md`](README.md)
- [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) when flow/design changes

View File

@@ -4,12 +4,14 @@ import (
"encoding/json"
"errors"
"fmt"
"html"
"runtime"
"sort"
"strings"
"time"
"github.com/gofiber/fiber/v2"
apidocs "github.com/karust/openserp/docs"
"github.com/sirupsen/logrus"
"golang.org/x/time/rate"
)
@@ -84,6 +86,9 @@ func NewServerWithOptions(host string, port int, opts ServerOptions, searchEngin
}
app.Use(RequestLoggerMiddleware())
app.Get("/openapi.yaml", serv.handleOpenAPISpec)
app.Get("/docs", serv.handleSwaggerUI)
app.Get("/docs/", serv.handleSwaggerUI)
app.Get("/health", serv.handleHealthCheck)
app.Get("/stats", serv.handleStats)
app.Get("/stats/cache", serv.handleCacheStats)
@@ -563,6 +568,45 @@ func (s *Server) applyProxyHeaders(c *fiber.Ctx, meta ProxyExecutionMeta) {
c.Set("X-Proxy-Used", used)
}
func (s *Server) handleOpenAPISpec(c *fiber.Ctx) error {
c.Set("Content-Type", "application/yaml; charset=utf-8")
return c.Send(apidocs.OpenAPIYAML)
}
func (s *Server) handleSwaggerUI(c *fiber.Ctx) error {
const specPath = "/openapi.yaml"
page := fmt.Sprintf(`<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>OpenSERP API Docs</title>
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5/swagger-ui.css" />
<style>
body { margin: 0; background: #f6f8fb; }
#swagger-ui { max-width: 1200px; margin: 0 auto; }
</style>
</head>
<body>
<div id="swagger-ui"></div>
<script src="https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js" crossorigin></script>
<script>
window.onload = function () {
window.ui = SwaggerUIBundle({
url: %q,
dom_id: "#swagger-ui",
deepLinking: true,
displayRequestDuration: true,
presets: [SwaggerUIBundle.presets.apis],
});
};
</script>
</body>
</html>`, html.EscapeString(specPath))
c.Set("Content-Type", "text/html; charset=utf-8")
return c.SendString(page)
}
func (s *Server) Listen() error {
return s.app.Listen(s.addr)
}

View File

@@ -3,6 +3,7 @@ package core
import (
"encoding/json"
"errors"
"io"
"net/http"
"net/http/httptest"
"strings"
@@ -72,6 +73,52 @@ func requestWithHeader(t *testing.T, s *Server, path string, header string, valu
return resp
}
func TestOpenAPISpecEndpoint(t *testing.T) {
engine := &engineMock{name: "google", initialized: true}
srv := NewServerWithOptions("127.0.0.1", 7107, DefaultServerOptions(), engine)
resp := request(t, srv, "/openapi.yaml")
if resp.StatusCode != http.StatusOK {
t.Fatalf("expected /openapi.yaml to return 200, got %d", resp.StatusCode)
}
if got := resp.Header.Get("Content-Type"); !strings.Contains(got, "application/yaml") {
t.Fatalf("expected YAML content-type, got %q", got)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
t.Fatalf("read /openapi.yaml body: %v", err)
}
if !strings.Contains(string(body), "openapi: 3.0.3") {
t.Fatalf("expected OpenAPI version marker in body")
}
}
func TestDocsEndpointServesSwaggerUI(t *testing.T) {
engine := &engineMock{name: "google", initialized: true}
srv := NewServerWithOptions("127.0.0.1", 7108, DefaultServerOptions(), engine)
resp := request(t, srv, "/docs")
if resp.StatusCode != http.StatusOK {
t.Fatalf("expected /docs to return 200, got %d", resp.StatusCode)
}
if got := resp.Header.Get("Content-Type"); !strings.Contains(got, "text/html") {
t.Fatalf("expected HTML content-type, got %q", got)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
t.Fatalf("read /docs body: %v", err)
}
content := string(body)
if !strings.Contains(content, "SwaggerUIBundle") {
t.Fatalf("expected SwaggerUI bundle script on docs page")
}
if !strings.Contains(content, "/openapi.yaml") {
t.Fatalf("expected docs page to reference /openapi.yaml")
}
}
func TestInvalidQueryParametersReturnJSONError(t *testing.T) {
engine := &engineMock{name: "google", initialized: true}
srv := NewServerWithOptions("127.0.0.1", 7104, DefaultServerOptions(), engine)

265
docs/ARCHITECTURE.md Normal file
View File

@@ -0,0 +1,265 @@
# 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: `<tag>` 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 |
| --- | --- | --- |
| `<engine>.rate_requests` | `4` | Average requests per minute |
| `<engine>.rate_burst` | `2` | Burst capacity |
| `<engine>.rate_seconds` | `60` (implicit) | Rate window seconds |
| `<engine>.selector_timeout` | `5` (implicit) | Selector wait timeout seconds |
Google-only additional toggle:
- `google.captcha` (default: `true`)

8
docs/embed.go Normal file
View File

@@ -0,0 +1,8 @@
package docs
import _ "embed"
// OpenAPIYAML is the bundled OpenAPI specification served by /openapi.yaml.
//
//go:embed openapi.yaml
var OpenAPIYAML []byte

880
docs/openapi.yaml Normal file
View File

@@ -0,0 +1,880 @@
openapi: 3.0.3
info:
title: OpenSERP API
version: 0.6.3
description: >
OpenSERP provides dedicated and multi-engine search endpoints for Google, Yandex,
Baidu, Bing, and DuckDuckGo. Responses are normalized into a shared schema and
include runtime metadata via response headers.
license:
name: MIT
url: https://opensource.org/licenses/MIT
servers:
- url: http://127.0.0.1:7000
description: Local default server
security: []
tags:
- name: Search
description: Dedicated per-engine search endpoints
- name: Mega
description: Cross-engine aggregated search endpoints
- name: Health
description: Health and readiness endpoints
- name: Stats
description: Runtime statistics endpoints
- name: Docs
description: OpenAPI and Swagger UI endpoints
paths:
/{engine}/search:
get:
tags: [Search]
operationId: searchWeb
summary: Search web results from a specific engine
description: >
Engine path values are `google`, `yandex`, `baidu`, `bing`, and `duck` (`duck` maps
to DuckDuckGo internally).
parameters:
- $ref: "#/components/parameters/EnginePath"
- $ref: "#/components/parameters/TextQuery"
- $ref: "#/components/parameters/LangQuery"
- $ref: "#/components/parameters/DateQuery"
- $ref: "#/components/parameters/FileQuery"
- $ref: "#/components/parameters/SiteQuery"
- $ref: "#/components/parameters/LimitQuery"
- $ref: "#/components/parameters/StartQuery"
- $ref: "#/components/parameters/FilterQuery"
- $ref: "#/components/parameters/AnswersQuery"
- $ref: "#/components/parameters/UseProxyHeader"
responses:
"200":
description: Search results
headers:
X-Cache:
$ref: "#/components/headers/XCache"
X-Fallback-Engine:
$ref: "#/components/headers/XFallbackEngine"
X-Proxy-Mode:
$ref: "#/components/headers/XProxyMode"
X-Proxy-Tag:
$ref: "#/components/headers/XProxyTag"
X-Proxy-Used:
$ref: "#/components/headers/XProxyUsed"
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/SearchResult"
examples:
default:
value:
- rank: 1
url: https://go.dev/doc/
title: The Go Programming Language
description: Official Go language documentation.
ad: false
"503":
$ref: "#/components/responses/ServiceUnavailableError"
"404":
$ref: "#/components/responses/NotFoundError"
"500":
$ref: "#/components/responses/InternalServerError"
/{engine}/image:
get:
tags: [Search]
operationId: searchImages
summary: Search image results from a specific engine
parameters:
- $ref: "#/components/parameters/EnginePath"
- $ref: "#/components/parameters/TextQuery"
- $ref: "#/components/parameters/LangQuery"
- $ref: "#/components/parameters/DateQuery"
- $ref: "#/components/parameters/FileQuery"
- $ref: "#/components/parameters/SiteQuery"
- $ref: "#/components/parameters/LimitQuery"
- $ref: "#/components/parameters/StartQuery"
- $ref: "#/components/parameters/FilterQuery"
- $ref: "#/components/parameters/AnswersQuery"
- $ref: "#/components/parameters/UseProxyHeader"
responses:
"200":
description: Image search results
headers:
X-Cache:
$ref: "#/components/headers/XCache"
X-Fallback-Engine:
$ref: "#/components/headers/XFallbackEngine"
X-Proxy-Mode:
$ref: "#/components/headers/XProxyMode"
X-Proxy-Tag:
$ref: "#/components/headers/XProxyTag"
X-Proxy-Used:
$ref: "#/components/headers/XProxyUsed"
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/SearchResult"
examples:
default:
value:
- rank: 1
url: https://upload.wikimedia.org/example/golang.png
title: Golang logo image
description: Example image result
ad: false
"503":
$ref: "#/components/responses/ServiceUnavailableError"
"404":
$ref: "#/components/responses/NotFoundError"
"500":
$ref: "#/components/responses/InternalServerError"
/mega/search:
get:
tags: [Mega]
operationId: megaSearch
summary: Search across multiple engines in parallel
parameters:
- $ref: "#/components/parameters/TextQuery"
- $ref: "#/components/parameters/LangQuery"
- $ref: "#/components/parameters/DateQuery"
- $ref: "#/components/parameters/FileQuery"
- $ref: "#/components/parameters/SiteQuery"
- $ref: "#/components/parameters/LimitQuery"
- $ref: "#/components/parameters/StartQuery"
- $ref: "#/components/parameters/FilterQuery"
- $ref: "#/components/parameters/AnswersQuery"
- $ref: "#/components/parameters/EnginesQuery"
- $ref: "#/components/parameters/UseProxyHeader"
responses:
"200":
description: Aggregated and de-duplicated results
headers:
X-Cache:
$ref: "#/components/headers/XCache"
X-Proxy-Mode:
$ref: "#/components/headers/XProxyMode"
X-Proxy-Tag:
$ref: "#/components/headers/XProxyTag"
X-Proxy-Used:
$ref: "#/components/headers/XProxyUsed"
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/MegaSearchResult"
examples:
default:
value:
- rank: 1
url: https://go.dev/doc/
title: The Go Programming Language
description: Official documentation.
ad: false
engine: google
- rank: 2
url: https://pkg.go.dev/
title: Go Packages
description: Go package documentation.
ad: false
engine: bing
"400":
$ref: "#/components/responses/BadRequestError"
"500":
$ref: "#/components/responses/InternalServerError"
/mega/image:
get:
tags: [Mega]
operationId: megaImageSearch
summary: Image search across multiple engines in parallel
parameters:
- $ref: "#/components/parameters/TextQuery"
- $ref: "#/components/parameters/LangQuery"
- $ref: "#/components/parameters/DateQuery"
- $ref: "#/components/parameters/FileQuery"
- $ref: "#/components/parameters/SiteQuery"
- $ref: "#/components/parameters/LimitQuery"
- $ref: "#/components/parameters/StartQuery"
- $ref: "#/components/parameters/FilterQuery"
- $ref: "#/components/parameters/AnswersQuery"
- $ref: "#/components/parameters/EnginesQuery"
- $ref: "#/components/parameters/UseProxyHeader"
responses:
"200":
description: Aggregated and de-duplicated image results
headers:
X-Cache:
$ref: "#/components/headers/XCache"
X-Proxy-Mode:
$ref: "#/components/headers/XProxyMode"
X-Proxy-Tag:
$ref: "#/components/headers/XProxyTag"
X-Proxy-Used:
$ref: "#/components/headers/XProxyUsed"
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/MegaSearchResult"
examples:
default:
value:
- rank: 1
url: https://upload.wikimedia.org/example/gopher.png
title: Go Gopher
description: Example image
ad: false
engine: duckduckgo
"400":
$ref: "#/components/responses/BadRequestError"
"500":
$ref: "#/components/responses/InternalServerError"
/mega/engines:
get:
tags: [Mega]
operationId: listMegaEngines
summary: List available engines and runtime state
responses:
"200":
description: Engine list
content:
application/json:
schema:
$ref: "#/components/schemas/MegaEnginesResponse"
examples:
default:
value:
total: 5
engines:
- name: google
initialized: true
circuit_state: closed
- name: bing
initialized: true
circuit_state: closed
"404":
$ref: "#/components/responses/NotFoundError"
/health:
get:
tags: [Health]
operationId: healthCheck
summary: Service health status
responses:
"200":
description: Healthy or degraded service
content:
application/json:
schema:
$ref: "#/components/schemas/HealthStatus"
examples:
healthy:
value:
status: healthy
uptime: 1h2m3s
engines:
- name: google
initialized: true
status: ready
system:
goroutines: 32
memory_mb: 128
go_version: go1.24.6
"503":
description: Unhealthy service
content:
application/json:
schema:
$ref: "#/components/schemas/HealthStatus"
examples:
unhealthy:
value:
status: unhealthy
uptime: 12m10s
engines:
- name: google
initialized: false
status: not_initialized
system:
goroutines: 14
memory_mb: 96
go_version: go1.24.6
"404":
$ref: "#/components/responses/NotFoundError"
/stats:
get:
tags: [Stats]
operationId: getStats
summary: Combined cache, proxy, and circuit-breaker stats
responses:
"200":
description: Runtime statistics
content:
application/json:
schema:
$ref: "#/components/schemas/StatsResponse"
examples:
default:
value:
cache:
status: true
entries: 10
hits: 200
misses: 15
bypasses: 4
evictions: 0
ttl_seconds: 60
max_size: 1000
proxy:
configured_count: 2
healthy_count: 2
unhealthy_count: 0
tags:
default:
configured: 2
healthy: 2
entries:
- proxy: http://proxy1:8080
tags: [default]
healthy: true
failures: 0
disabled: false
engines:
google:
tag: default
selected_proxy: pooled
circuit_breakers:
- engine: google
state: closed
failure_count: 0
last_changed: 2026-04-15T08:00:00Z
"404":
$ref: "#/components/responses/NotFoundError"
/stats/cache:
get:
tags: [Stats]
operationId: getCacheStats
summary: Cache statistics only
responses:
"200":
description: Cache status
content:
application/json:
schema:
$ref: "#/components/schemas/CacheStats"
examples:
enabled:
value:
status: true
entries: 1
hits: 1
misses: 2
bypasses: 1
evictions: 0
ttl_seconds: 60
max_size: 1000
disabled:
value:
status: false
"404":
$ref: "#/components/responses/NotFoundError"
/stats/proxy:
get:
tags: [Stats]
operationId: getProxyStats
summary: Proxy pool and per-engine proxy policy statistics
responses:
"200":
description: Proxy stats payload
content:
application/json:
schema:
$ref: "#/components/schemas/ProxyStats"
examples:
default:
value:
configured_count: 1
healthy_count: 1
unhealthy_count: 0
tags:
us:
configured: 1
healthy: 1
entries:
- proxy: http://proxy-us:8080
tags: [us]
healthy: true
failures: 0
disabled: false
engines:
google:
tag: us
selected_proxy: pooled
yandex:
selected_proxy: direct
"404":
$ref: "#/components/responses/NotFoundError"
/stats/cb:
get:
tags: [Stats]
operationId: getCircuitBreakerStats
summary: Circuit breaker state per engine
responses:
"200":
description: Circuit breaker stats payload
content:
application/json:
schema:
$ref: "#/components/schemas/CircuitBreakerStatsResponse"
examples:
default:
value:
circuit_breakers:
- engine: google
state: open
failure_count: 5
last_changed: 2026-04-15T08:00:00Z
retry_in: 43
"404":
$ref: "#/components/responses/NotFoundError"
/openapi.yaml:
get:
tags: [Docs]
operationId: getOpenAPISpec
summary: Get raw OpenAPI YAML
responses:
"200":
description: OpenAPI YAML
content:
application/yaml:
schema:
type: string
"404":
$ref: "#/components/responses/NotFoundError"
/docs:
get:
tags: [Docs]
operationId: getSwaggerUI
summary: Swagger UI for interactive API docs
responses:
"200":
description: HTML page loading Swagger UI from CDN
content:
text/html:
schema:
type: string
"404":
$ref: "#/components/responses/NotFoundError"
components:
parameters:
EnginePath:
name: engine
in: path
required: true
description: Search engine endpoint alias (`duck` is DuckDuckGo).
schema:
type: string
enum: [google, yandex, baidu, bing, duck]
TextQuery:
name: text
in: query
required: false
description: >
Search query text. At least one of `text`, `site`, or `file` must be non-empty.
schema:
type: string
example: golang
LangQuery:
name: lang
in: query
required: false
description: Language code (engine-specific behavior).
schema:
type: string
example: EN
DateQuery:
name: date
in: query
required: false
description: Date interval in `YYYYMMDD..YYYYMMDD` format.
schema:
type: string
pattern: "^[0-9]{8}\\.\\.[0-9]{8}$"
example: 20250101..20250131
FileQuery:
name: file
in: query
required: false
description: File extension filter (for engines that support it).
schema:
type: string
example: PDF
SiteQuery:
name: site
in: query
required: false
description: Site/domain filter.
schema:
type: string
example: github.com
LimitQuery:
name: limit
in: query
required: false
description: Maximum results to return.
schema:
type: integer
default: 25
example: 10
StartQuery:
name: start
in: query
required: false
description: Pagination offset (must be >= 0).
schema:
type: integer
minimum: 0
default: 0
example: 20
FilterQuery:
name: filter
in: query
required: false
description: Duplicate filtering flag (primarily used by Google parser behavior).
schema:
type: boolean
default: true
example: true
AnswersQuery:
name: answers
in: query
required: false
description: Include answer box style results when supported.
schema:
type: boolean
default: false
example: false
EnginesQuery:
name: engines
in: query
required: false
description: >
Comma-separated engine list for mega endpoints. If omitted, all available engines are used.
schema:
type: string
example: google,bing,duckduckgo
UseProxyHeader:
name: X-Use-Proxy
in: header
required: false
description: >
Request-scoped proxy override. Use `direct` to disable proxy for the request or
pass a proxy tag name (for example `us`) to force a tagged proxy pool.
schema:
type: string
examples:
direct:
value: direct
tag:
value: us
headers:
XCache:
description: Cache status when cache is enabled (`HIT`, `MISS`, `BYPASS`).
schema:
type: string
XFallbackEngine:
description: Engine name used when dedicated endpoint fallback served the response.
schema:
type: string
XProxyMode:
description: Effective proxy mode for the request (`off` or `tag_pool`).
schema:
type: string
enum: [off, tag_pool]
XProxyTag:
description: Effective proxy tag when `X-Proxy-Mode=tag_pool`.
schema:
type: string
XProxyUsed:
description: Effective proxy target used (`direct`, masked proxy, `pooled`, `multiple`, `mixed`).
schema:
type: string
responses:
BadRequestError:
description: Invalid request parameters
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
examples:
invalidEngines:
value:
error: bad_request
code: 400
message: No valid search engines specified
ServiceUnavailableError:
description: Search failed and no result could be produced
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
examples:
primaryFailed:
value:
error: service_unavailable
code: 503
message: all search engines failed
NotFoundError:
description: Endpoint not found
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
examples:
notFound:
value:
error: not_found
code: 404
message: Cannot GET /unknown
InternalServerError:
description: Internal error while parsing/handling request
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
examples:
parseError:
value:
error: server_error
code: 500
message: invalid syntax
schemas:
SearchResult:
type: object
required: [rank, url, title, description, ad]
properties:
rank:
type: integer
example: 1
url:
type: string
example: https://go.dev/doc/
title:
type: string
example: The Go Programming Language
description:
type: string
example: Official Go language documentation.
ad:
type: boolean
example: false
MegaSearchResult:
allOf:
- $ref: "#/components/schemas/SearchResult"
- type: object
required: [engine]
properties:
engine:
type: string
example: google
ErrorResponse:
type: object
required: [error, code]
properties:
error:
type: string
example: service_unavailable
code:
type: integer
example: 503
message:
type: string
example: all search engines failed
EngineHealth:
type: object
required: [name, initialized, status]
properties:
name:
type: string
example: google
initialized:
type: boolean
example: true
status:
type: string
enum: [ready, not_initialized, circuit_open]
example: ready
HealthStatus:
type: object
required: [status, uptime, engines, system]
properties:
status:
type: string
enum: [healthy, degraded, unhealthy]
uptime:
type: string
example: 1h12m3s
engines:
type: array
items:
$ref: "#/components/schemas/EngineHealth"
system:
type: object
additionalProperties: true
properties:
goroutines:
type: integer
memory_mb:
type: integer
go_version:
type: string
CacheStatsEnabled:
type: object
required: [status, entries, hits, misses, bypasses, evictions, ttl_seconds, max_size]
properties:
status:
type: boolean
enum: [true]
entries:
type: integer
hits:
type: integer
misses:
type: integer
bypasses:
type: integer
evictions:
type: integer
ttl_seconds:
type: integer
max_size:
type: integer
CacheStatsDisabled:
type: object
required: [status]
properties:
status:
type: boolean
enum: [false]
CacheStats:
oneOf:
- $ref: "#/components/schemas/CacheStatsEnabled"
- $ref: "#/components/schemas/CacheStatsDisabled"
ProxyTagSummary:
type: object
required: [configured, healthy]
properties:
configured:
type: integer
healthy:
type: integer
ProxyStatsEntry:
type: object
required: [proxy, tags, healthy, failures, disabled]
properties:
proxy:
type: string
example: http://proxy-us:8080
tags:
type: array
items:
type: string
healthy:
type: boolean
failures:
type: integer
disabled:
type: boolean
ProxyEngineStats:
type: object
required: [selected_proxy]
properties:
tag:
type: string
selected_proxy:
type: string
example: direct
ProxyStats:
type: object
required: [configured_count, healthy_count, unhealthy_count, tags, entries]
properties:
configured_count:
type: integer
healthy_count:
type: integer
unhealthy_count:
type: integer
tags:
type: object
additionalProperties:
$ref: "#/components/schemas/ProxyTagSummary"
entries:
type: array
items:
$ref: "#/components/schemas/ProxyStatsEntry"
engines:
type: object
additionalProperties:
$ref: "#/components/schemas/ProxyEngineStats"
CircuitBreakerStat:
type: object
required: [engine, state, failure_count, last_changed]
properties:
engine:
type: string
example: google
state:
type: string
enum: [closed, open, half-open]
failure_count:
type: integer
last_changed:
type: string
format: date-time
retry_in:
type: integer
description: Seconds until next half-open attempt (present when state is open).
CircuitBreakerStatsResponse:
type: object
required: [circuit_breakers]
properties:
circuit_breakers:
type: array
items:
$ref: "#/components/schemas/CircuitBreakerStat"
StatsResponse:
type: object
required: [cache, proxy, circuit_breakers]
properties:
cache:
$ref: "#/components/schemas/CacheStats"
proxy:
$ref: "#/components/schemas/ProxyStats"
circuit_breakers:
type: array
items:
$ref: "#/components/schemas/CircuitBreakerStat"
MegaEngineInfo:
type: object
required: [name, initialized]
properties:
name:
type: string
example: google
initialized:
type: boolean
circuit_state:
type: string
enum: [closed, open, half-open]
MegaEnginesResponse:
type: object
required: [engines, total]
properties:
engines:
type: array
items:
$ref: "#/components/schemas/MegaEngineInfo"
total:
type: integer