package search import ( "context" "encoding/json" "fmt" "net/http" ) // bochaProfile 是博查 Bocha(design/27 §2 A 档「中文优先」;分册 02 官方原文确证)。 // // POST `/v1/web-search` + `Authorization: Bearer`;响应 `webPages.value[]` 的 // `name/url/snippet/summary/datePublished` —— **与 SearXNG 字段近乎同名**,归一化最省事。 // // 两个诚实边界(分册 02): // // - 官方文档域名为 `api.bocha.cn`,而官网首页 curl 样例仍用 `api.bochaai.com`, // 两者关系官方未说明 ⇒ 默认取**文档现行口径** `api.bocha.cn`(见 config 的默认值), // 两域都可由 `WEB_SEARCH_BOCHA_BASE_URL` 覆盖。 // - 中文检索质量**未实测**(无 key)。 type bocha struct{ cfg Config } // bochaPath 是 Web Search 端点路径(分册 02:POST https://api.bocha.cn/v1/web-search)。 const bochaPath = "/v1/web-search" // bochaDefaultCount / bochaMaxCount 是请求条数口径(分册 02 官方原文: // 「返回条数,可填范围 1-50,单次最大 50,默认 10」)。 const ( bochaMaxCount = 50 bochaDefaultCount = 10 ) // newBocha 造一家博查 provider。 func newBocha(cfg Config) Provider { return &bocha{cfg: cfg} } // Name 实现 Provider。 func (p *bocha) Name() string { return NameBocha } // Search 实现 Provider。 func (p *bocha) Search(ctx context.Context, q Query) ([]Result, error) { // summary=true 换取中文摘要(分册 02:默认 false 不给 summary); // freshness 用官方推荐的 noLimit(官方原文:「推荐使用 noLimit。搜索算法会自动 // 进行时间范围的改写,效果更佳」)。 payload := map[string]any{ "query": q.Text, "summary": true, "freshness": "noLimit", "count": bochaClampCount(q.Count), } body, err := json.Marshal(payload) if err != nil { return nil, fmt.Errorf("search: bocha: encode request: %w", err) } data, err := do(ctx, p.cfg, request{ Method: http.MethodPost, URL: baseURLJoin(p.cfg.BaseURL, bochaPath), Headers: map[string]string{"Authorization": "Bearer " + p.cfg.APIKey}, Body: body, }) if err != nil { return nil, err } return extractResults(data, bochaSpec()) } // bochaClampCount 把调用方要的条数收进官方允许区间(分册 02:1-50,默认 10)。 func bochaClampCount(n int) int { if n <= 0 { return bochaDefaultCount } if n > bochaMaxCount { return bochaMaxCount } return n } // bochaSpec 是博查的响应形状(分册 02「响应字段」官方原文确证)。 // `summary` 只在请求里 `summary=true` 时才有 —— 我们恒置 true。 func bochaSpec() spec { return spec{ Arrays: []string{"webPages.value"}, Title: []string{"name"}, Link: []string{"url"}, Snippet: []string{"snippet"}, Content: []string{"summary"}, Published: []string{"datePublished"}, // 官方字段表未列评分(`id` 是排序 ID、不是分数)⇒ 不给 Score 候选。 } }