三天,1173 次搜索,我的 AI 助手每次要 10 条结果,有 1171 次拿到了满满的 10 条。
同一段时间,SearXNG 的统计页上,Google 那一路的错误率是 15%。
两个数都很好看。但日志里还有第三个数:每次搜索后端到底返回了多少条原始结果。这个数恰好等于 25 的,有 685 次,占 58%。而三个引擎全在场的时候,它应该是 45 左右。
也就是说,有一半多的搜索,Google 根本没出场,而上游下游两个指标都没告诉我。
这是我第二次在同一套自建搜索上栽跟头。八月那次更难看:我修好了一台搜索服务,报了「修好了」,结果修的那台根本不在调用链上。两件事是同一个病,第 3 章讲。想直接拿判读方法的看第 2 章和最后的清单。
1. 25 这个数是怎么来的
先交代一下这东西。SearXNG 是个开源的「元搜索」:你给它一个查询,它同时去问 Google、Bing、Yandex 这些引擎,把结果合并去重再还给你。我的 agent 联网搜索就靠它。
它每次返回的 JSON 里有个 results 数组。我的 agent 拿到之后按分数排序,截前 10 条用,日志里顺手记一句:
SearXNG search '...': 10 results (from 25 raw, limit 10)
那个 from 25 raw 就是截断之前的原始条数。
我把这三天的 raw 计数做了个直方图,排前几的是:
| raw | 次数 |
|---|---|
| 25 | 685 |
| 24 | 68 |
| 22 | 68 |
| 45 | 42 |
| 44 | 25 |
| 35 | 25 |
一个数独占 58%,这就很可疑了。正常情况下合并去重之后的条数应该散一点,不会这么整齐。
去现场打了十次探针,每次看是哪些引擎贡献了结果。答案特别规整:google cse 20 条,yandex 15 条,bing 7 到 10 条。加起来 42 到 45。
这就能对上账了:
raw 满载 ≈ 每个参战引擎一页的条数之和
= 20 (google cse) + 15 (yandex) + 10 (bing) = 45
少了 google cse:15 + 10 = 25
就是一个加法。直方图上那个 25 的大尖峰,就是「google cse 缺席、只剩 yandex 和 bing」这个状态的指纹。
反过来用更有意思:知道每个引擎一页给几条,看 raw 计数的直方图,就能读出哪个引擎在哪些时候缺席。 不用开 SearXNG 的后台,agent 这一侧的日志就够了。你自己的实例每家给几条不一定跟我一样,探针打一次就知道。
2. 15% 和 58%,到底哪个是真的
去翻 SearXNG 的容器日志,原因写得很直白:
google cse: Our systems have detected unusual traffic from your network. (suspended_time=180)
Google 觉得我这边流量不正常,回了 429。SearXNG 收到之后把这个引擎罚停 180 秒,这 3 分钟里所有查询都不再问它。
那 15% 错误率是怎么回事?我的理解是:错误率只统计这个引擎真正发出去的请求。触发罚停的那一下算一次错误,之后 180 秒它压根没发请求,自然也不会再有错误。罚停期间的缺席,在错误率里是隐形的。
所以这两个数量的是两件事:
错误率 = 失败的请求 ÷ 它发出去的请求 → 15%
缺席率 ≈ raw 恰好掉到「少它那一档」的调用 ÷ 总调用 = 685 ÷ 1173 ≈ 58%
我关心的是后一个。我想知道的是「我搜的时候它在不在」,它发出去的请求成功率多少,我其实不在乎。
再拆一层更能说明问题。我把调用按小时分了一下,一小时 50 次以上的算忙时:
| 调用数 | raw=25 | 占比 | |
|---|---|---|---|
| 忙时(≥50 次/小时) | 973 | 651 | 67% |
| 闲时 | 200 | 34 | 17% |
忙的时候三次里两次没有 Google,闲的时候六次里一次。忙时基本是我同时开了好几个 agent 在查资料,搜得越猛,Google 越嫌你烦,罚停越频繁。偏偏最需要搜得好的就是这种时候。
然后是最隐蔽的那一层:下游那个「10 条结果」把这一切都盖住了。 25 条照样够截 10 条,agent 每次都拿到满的,从它的角度看搜索一直很健康。少了 Google 之后前 10 条质量掉了多少,这个我没量,第 4 章会说。
这里可以抽出一条通用的判据,看任何聚合系统的健康数字都能用:
错误率只能告诉你「问了的时候成不成」,它不会告诉你「该问的时候问没问」。 任何带熔断、罚停、降级的系统,都要另外看一个「缺席」的数,最好是从调用方那一侧数出来的。
3. 八月:实例全绿,修的却是另一台
说回八月那次,因为这个病我其实早就见过一次了。
当时我觉得搜索后端「老是挂」。先看实例:容器跑了四周没重启过,没有被 OOM 杀过,HTTP 200,响应 2.35 秒。全绿。
真正的问题在引擎层。/stats/errors 拉出来:
| 引擎 | 错误率 | 类型 |
|---|---|---|
| mojeek | 95% | 403 |
| duckduckgo | 75% | CAPTCHA |
| brave | 70% | 429 |
| baidu | 15% | CAPTCHA |
| startpage | 5% | CAPTCHA |
| 0% | — |
Google 错误率 0%,结果恒为 0。这个组合我后来专门记了一条判读规则:
- 引擎名出现在返回的
unresponsive_engines里 → 被反爬封了 - 不在 unresponsive 里、错误率 0%、但结果永远是 0 → 解析器过期
第二种最阴。HTTP 200,没异常,没日志,就是解析不出东西。那次的根因是镜像落后上游两个月,搜索引擎改了页面结构,旧解析器就瞎了。起了一个新版临时容器同题对拍,google 从 0 条变成 44 条,根因当场锁死。
还有两个小发现顺便记一下:
- 被封的引擎换住宅代理没用。brave、mojeek、startpage 直连和走代理的返回一模一样,人家认的是整体指纹(TLS、HTTP/2、行为模式),换个出口 IP 骗不过去。
- 响应从 2.35 秒降到 0.90 秒,主要功劳是把那几家被封的直接禁了。被封的引擎每次都要等满超时才放弃,聚合结果得等最慢的那个回来。
修完之后,同一批 10 个查询,结果中位数从 36 涨到 44,空结果 0/10。我很高兴地报告:修好了。
然后被追问了一句「真的修好了吗」。我让 agent 真跑一次搜索工具,返回 {"web": []}。空的。
原来这台机器上有两个 SearXNG:一台在 NAS 上,一台在本机的容器里。agent 读的是 .env 里写的地址,指向本机那台。我看了 systemd 的服务配置、看了覆盖文件、看了进程的环境变量,三处都写着 NAS 的地址,就认定它生效了。实际上 .env 的优先级比这三处都高,这三处显示的都是被压掉的值。
我花了一上午,把一台不在调用链上的机器修得很漂亮。
最尴尬的是,「看运行时实际生效的值,别看配置文件写了什么」这条原则,是我那天上午刚从 Google 解析器那件事里总结出来的。我对 SearXNG 的配置严格执行了它,对 agent 的配置没执行。
回头看这两次是一个病。八月是「实例健康」盖住了「引擎死了」,这次是「每次都返回 10 条」盖住了「Google 一半时间不在」。指标挂在哪一层,就只能替那一层作证。
4. 这次没做完的地方
几处我没量,结论先不说死:
- 质量损失没量。 我只知道 58% 的调用少了 Google 那 20 条,没测过前 10 条的相关性因此掉了多少。可能 Bing 加 Yandex 已经够用,也可能差很多。
- 忙时的阈值是我拍的。 「一小时 50 次」只是为了把两种状态分开,换个阈值比例会变,方向应该不会变。
- 两边日志的时间是我手动对齐的。 容器日志是 UTC,agent 日志是本地时间。我按时区换算后看到罚停高峰和调用高峰大体重合,但只是目测,没做逐条匹配。
- 罚停时长和缺席次数没逐条对上。 容器日志三天里 Google 这条 429 记了 79 次,79 × 180 秒 ≈ 4 小时。调用是扎堆的,这 4 小时能不能刚好罩住那 685 次,我没逐条对,也不排除罚停有我没搞懂的累加机制。
- 中文引擎现在全挂着。 今天的探针里 baidu、quark、sogou 一直在 unresponsive 里,统计页上 baidu 和 sogou 都是 90% 的 CAPTCHA。也就是说中文查询这会儿也是靠那三家英文引擎撑着的,影响多大同样没量。
5. 下次怀疑自建搜索不对劲,按这个顺序查
- 先确认你修的是调用方真正在用的那一台。去调用方的代码里看它最终读的是哪个值,systemd 和进程环境变量显示的可能是被压掉的那个。
- 跑一次调用方自己的搜索工具,看它返回什么。curl 后端不算数。
- 打一次探针,记下每个引擎一页给几条,把满载 raw 加出来。
- 在调用方日志里拉 raw 计数的直方图,找那个突出的尖峰,用第 3 步的加法反推谁缺席。
- 把缺席率按忙时、闲时分开算。只在忙时缺席的,基本就是被限流罚停了。
- 某个引擎错误率 0% 但结果恒为 0,先怀疑解析器过期,起个新版临时实例对拍。
- 被 CAPTCHA 或 429 封的引擎直接禁掉,别指望换代理。
- 中文引擎至少挂两家,单押一家会被罚停搞出整段空窗。