2026-04-15 03:25:06 +03:00
# OpenSERP Architecture
2026-04-26 03:50:48 +03:00
## Overview
2026-04-15 03:25:06 +03:00
2026-06-27 06:10:11 +03:00
OpenSERP is a Go API + CLI for search result extraction from Google, Yandex, Baidu, Bing, DuckDuckGo, and Ecosia.
2026-04-15 03:25:06 +03:00
2026-04-26 03:50:48 +03:00
Execution modes:
2026-04-15 03:25:06 +03:00
2026-04-26 03:50:48 +03:00
- **Browser mode**: default path, headless Chromium via `go-rod` , supported by all engines.
2026-06-27 06:10:11 +03:00
- **Raw HTTP mode**: direct HTTP + `goquery` , currently supported by Google, Yandex, Baidu, and Ecosia.
2026-04-15 03:25:06 +03:00
2026-04-26 03:50:48 +03:00
Browser mode is the primary compatibility path.
2026-04-15 03:25:06 +03:00
2026-04-26 03:50:48 +03:00
## Project Layout
2026-04-15 03:25:06 +03:00
```text
openserp/
2026-04-26 03:50:48 +03:00
├── main.go
├── README.md
├── config.yaml
2026-04-15 03:25:06 +03:00
├── docs/
2026-04-26 03:50:48 +03:00
│ ├── ARCHITECTURE.md
│ ├── CONTRIBUTING.md
│ ├── openapi.yaml
│ └── embed.go
2026-04-15 03:25:06 +03:00
├── cmd/
2026-04-26 03:50:48 +03:00
│ ├── root.go
│ ├── serve.go
2026-04-15 03:25:06 +03:00
│ ├── search.go
2026-04-26 03:50:48 +03:00
│ └── proxy_policy.go
├── core/
│ ├── common.go
│ ├── server.go
│ ├── response.go
│ ├── result.go
│ ├── response_builder.go
│ ├── clusters.go
│ ├── format_markdown.go
│ ├── format_text.go
│ ├── enrichment_domain.go
│ ├── enrichment_domains.yaml
│ ├── middleware.go
│ ├── browser.go
│ ├── http_client.go
│ ├── resilient.go
│ ├── retry.go
│ ├── circuit_breaker.go
│ ├── cache.go
│ ├── proxy.go
│ ├── logger.go
│ └── captcha.go
├── google/
├── yandex/
├── baidu/
├── bing/
├── duckduckgo/
2026-06-27 06:10:11 +03:00
├── ecosia/
2026-04-26 03:50:48 +03:00
└── testutil/
2026-04-15 03:25:06 +03:00
```
2026-04-26 03:50:48 +03:00
## Core Interfaces
2026-04-15 03:25:06 +03:00
### `core.SearchEngine`
2026-04-26 03:50:48 +03:00
All engines implement:
2026-04-15 03:25:06 +03:00
2026-04-26 03:50:48 +03:00
- `Search(context.Context, Query) ([]SearchResult, error)`
- `SearchImage(context.Context, Query) ([]SearchResult, error)`
- `IsInitialized() bool`
- `Name() string`
- `GetRateLimiter() *rate.Limiter`
2026-04-15 03:25:06 +03:00
### `core.Query`
2026-05-28 01:36:52 +03:00
Parsed from query parameters (`text` , `lang` , `region` , `date` , `file` , `site` , `limit` , `start` , `filter` , `features` ) and the `X-Use-Proxy` request header. At least one of `text` , `site` , or `file` must be non-empty.
2026-04-26 03:50:48 +03:00
### Internal `core.SearchResult`
Engine parsers return the older internal shape:
- `Rank`
- `URL`
- `Title`
- `Description`
- `Ad`
2026-05-13 01:30:54 +03:00
HTTP handlers convert this into the public v2 response through `core/response_builder.go` .
2026-04-15 03:25:06 +03:00
2026-04-26 03:50:48 +03:00
## HTTP Request Flow
2026-04-15 03:25:06 +03:00
2026-04-26 03:50:48 +03:00
```text
HTTP request
-> Fiber middleware
-> RequestContextMiddleware
-> CORS
-> RequestLoggerMiddleware
-> handleDedicatedEndpoint / handleMegaEndpoint
-> Query.InitFromContext
-> resolveFormat
-> cache lookup for JSON responses only
-> ResilientSearcher
-> circuit breaker
-> rate limiter
-> proxy policy resolution
-> retry loop
-> engine.Search / engine.SearchImage
-> browser path: Browser.Navigate -> DOM parse -> []SearchResult
-> raw path: HTTP client -> goquery parse -> []SearchResult
-> response enrichment
-> stable IDs
-> normalized URL/display URL
-> pagination position
-> domain_info/classification
-> image metadata extraction
-> mega-only normalized URL dedupe + clusters
-> cache write for eligible JSON responses
-> output serializer: JSON, Markdown, text, or NDJSON
```
2026-04-15 03:25:06 +03:00
2026-04-26 03:50:48 +03:00
## Public API Response
2026-04-15 03:25:06 +03:00
2026-05-13 01:30:54 +03:00
JSON endpoints return a v2 envelope.
2026-04-15 03:25:06 +03:00
2026-04-26 03:50:48 +03:00
Top-level fields:
2026-04-15 03:25:06 +03:00
2026-04-26 03:50:48 +03:00
- `query` : request echo, including `engines_requested`
- `meta` : `request_id` , `requested_at` , `took_ms` , `engines_failed` , `version`
- `results` : normalized web or image results
- `pagination` : `page` , `has_more` , `next_start`
- `clusters` : only on `/mega/search`
2026-04-15 03:25:06 +03:00
2026-04-26 03:50:48 +03:00
Stable ID prefixes:
2026-04-15 03:25:06 +03:00
2026-04-26 03:50:48 +03:00
- `s_` : web search result
- `i_` : image result
- `c_` : mega search URL cluster
2026-04-15 03:25:06 +03:00
2026-04-26 03:50:48 +03:00
`meta.engines_failed` is the only engine status list in the body. Clients can derive responded engines as:
2026-04-15 03:25:06 +03:00
```text
2026-04-26 03:50:48 +03:00
query.engines_requested - meta.engines_failed
2026-04-15 03:25:06 +03:00
```
2026-04-26 03:50:48 +03:00
Dedicated endpoint fallback is represented by:
2026-04-15 03:25:06 +03:00
2026-04-26 03:50:48 +03:00
- `X-Fallback-Engine`
- `results[].engine`
- `meta.engines_failed` containing the primary engine
2026-04-15 03:25:06 +03:00
2026-04-26 03:50:48 +03:00
## Mega Search
2026-04-15 03:25:06 +03:00
2026-04-26 03:50:48 +03:00
`/mega/search` and `/mega/image` run selected engines in parallel.
2026-04-15 03:25:06 +03:00
2026-04-26 03:50:48 +03:00
`/mega/search` behavior:
2026-04-15 03:25:06 +03:00
2026-04-26 03:50:48 +03:00
- Uses `engines` query parameter if provided; otherwise uses all configured engines.
- Skips duplicate engine names.
- Allows partial success; failed engines are listed in `meta.engines_failed` .
- Deduplicates flat results by normalized URL.
- Builds `clusters` from all enriched results before flat dedupe.
- Sorts clusters by score descending, then best rank ascending.
2026-04-15 03:25:06 +03:00
2026-04-26 03:50:48 +03:00
Cluster score:
2026-04-15 03:25:06 +03:00
2026-04-26 03:50:48 +03:00
```text
sum(1 / rank for each occurrence) / engines_queried
```
2026-04-15 03:25:06 +03:00
2026-04-26 03:50:48 +03:00
The score is capped at `1.0` and rounded to two decimals.
2026-04-15 03:25:06 +03:00
2026-04-26 03:50:48 +03:00
## Response Formatting
2026-04-15 03:25:06 +03:00
2026-04-26 03:50:48 +03:00
`resolveFormat` supports:
2026-04-15 03:25:06 +03:00
2026-04-26 03:50:48 +03:00
- `json` (default)
- `markdown`
- `text`
- `ndjson`
The format can be selected with `?format=` or by `Accept` header:
2026-04-15 03:25:06 +03:00
2026-04-26 03:50:48 +03:00
- `text/markdown`
- `text/plain`
- `application/x-ndjson`
2026-04-15 03:25:06 +03:00
2026-04-26 03:50:48 +03:00
Only JSON responses use the response cache. Cached JSON refreshes request-scoped metadata before sending:
2026-04-15 03:25:06 +03:00
2026-04-26 03:50:48 +03:00
- `meta.request_id`
- `meta.requested_at`
- `meta.took_ms`
2026-04-15 03:25:06 +03:00
2026-04-26 03:50:48 +03:00
## Domain Enrichment
2026-04-15 03:25:06 +03:00
2026-04-26 03:50:48 +03:00
`core/enrichment_domain.go` derives:
2026-04-15 03:25:06 +03:00
2026-05-13 01:30:54 +03:00
- `domain_info` : public suffix, SLD, and collapsed category
2026-04-26 03:50:48 +03:00
- `classification` : content type and known source hint
2026-04-15 03:25:06 +03:00
2026-04-26 03:50:48 +03:00
Public suffix parsing uses `golang.org/x/net/publicsuffix` .
2026-04-15 03:25:06 +03:00
2026-04-26 03:50:48 +03:00
Mutable domain category data lives in:
2026-04-15 03:25:06 +03:00
2026-04-26 03:50:48 +03:00
```text
core/enrichment_domains.yaml
```
2026-04-15 03:25:06 +03:00
2026-04-26 03:50:48 +03:00
It can be replaced at runtime:
2026-04-15 03:25:06 +03:00
2026-04-26 03:50:48 +03:00
```bash
OPENSERP_ENRICHMENT_DOMAINS_FILE=/path/to/enrichment_domains.yaml ./openserp serve
```
2026-04-15 03:25:06 +03:00
2026-04-26 03:50:48 +03:00
## Resilience Stack
2026-04-15 03:25:06 +03:00
2026-04-26 03:50:48 +03:00
Request protection sequence:
2026-04-15 03:25:06 +03:00
2026-04-26 03:50:48 +03:00
1. Engine rate limiter
2. Retry with backoff
3. Circuit breaker
4. Proxy policy and proxy health
5. Response cache
2026-04-15 03:25:06 +03:00
2026-04-26 03:50:48 +03:00
Important behaviors:
- `ErrCaptcha` is non-retryable.
- Proxy health is degraded only for proxy/network failures, not parser or captcha errors.
- Dedicated endpoints are engine-pure by default.
- Dedicated fallback is opt-in via `resilience.allow_endpoint_fallback` .
- Fallback responses are not cached on dedicated endpoints.
2026-04-15 03:25:06 +03:00
2026-04-26 03:50:48 +03:00
## Proxy Model
2026-04-15 03:25:06 +03:00
2026-04-26 03:50:48 +03:00
Proxy policy can come from:
2026-04-15 03:25:06 +03:00
2026-04-26 03:50:48 +03:00
- global config
- per-engine config
- per-request `X-Use-Proxy`
2026-04-15 03:25:06 +03:00
2026-04-26 03:50:48 +03:00
Supported request override values:
2026-04-15 03:25:06 +03:00
2026-04-26 03:50:48 +03:00
- `X-Use-Proxy: direct`
- `X-Use-Proxy: <tag>`
2026-04-15 03:25:06 +03:00
2026-04-26 03:50:48 +03:00
Response headers:
2026-04-15 03:25:06 +03:00
2026-04-26 03:50:48 +03:00
- `X-Proxy-Mode` : `off` or `tag_pool`
- `X-Proxy-Tag`
- `X-Proxy-Used`
2026-04-15 03:25:06 +03:00
2026-04-26 03:50:48 +03:00
## Config Reference
2026-04-15 03:25:06 +03:00
2026-04-26 03:50:48 +03:00
Config priority: `CLI flags > OPENSERP_* env vars > config.yaml > defaults` (via Viper).
2026-04-15 03:25:06 +03:00
2026-04-26 03:50:48 +03:00
See [config.yaml ](../config.yaml ) for all available sections and defaults.