# SearXNG 私有搜索服务部署与使用 > 2026-09-16 记录。为 [[AI桌面陪伴助手方案]] 部署一个不绑定 LLM 厂商的联网搜索后端。服务运行在 `cadadmin-server`,只监听宿主机回环地址 `127.0.0.1:18080`,由同机助手调用,不对局域网或公网开放。 --- ## 一、结论与当前状态 当前方案是:**DeepSeek 负责判断何时搜索和组织回答,SearXNG 负责查询互联网。** ```text 用户问题 ↓ DeepSeek 判断是否需要实时资料 ↓ Function Calling: web_search(query) AI Companion 调用 SearXNG JSON API ↓ SearXNG 聚合 360 搜索 / 搜狗等上游结果 ↓ 标题、摘要、URL 结果作为 tool 消息回灌 DeepSeek ↓ 生成正文 + 来源 URL ├─ 网页:显示正文和来源 └─ TTS:只朗读正文,不读网址 ``` 截至 2026-09-16: | 项目 | 状态 | |---|---| | 容器 | `ai-companion-searxng`,运行中 | | 宿主机地址 | `http://127.0.0.1:18080` | | 暴露范围 | 仅服务器回环地址 | | 重启策略 | `unless-stopped` | | JSON API | 已开启 | | 可用上游 | 360 搜索、搜狗 | | 暂不可用 | DuckDuckGo、Brave、Wikipedia 超时;Bing 暂时返回空结果 | | 助手接入 | 已完成并通过真实工具调用验收 | | 镜像 | 固定到已验收的 OCI digest,未使用浮动 `latest` | 真实闭环验收结果:DeepSeek 主动调用 `web_search('DeepSeek 最新消息 2025')`,SearXNG 返回 5 条结果,结果回灌后模型生成带两个来源 URL 的最终回答。 --- ## 二、SearXNG 是什么 SearXNG 是开源的**元搜索引擎**。它本身不维护网页索引,而是把查询转发给多个搜索服务和数据库,再统一整理结果。官方项目强调不追踪、不画像用户,代码采用 AGPL-3.0 许可证。 这意味着: - 自建 SearXNG 不需要维护爬虫和海量网页索引; - 搜索质量、时延和可用性仍依赖上游搜索引擎; - 同一个配置换到不同网络环境,结果可能完全不同; - 自建的主要价值是统一 API、集中控制搜索源、减少对单一商业 API 的绑定,而不是“完全脱离外部搜索服务”。 官方资料: - [SearXNG GitHub](https://github.com/searxng/searxng) - [容器部署文档](https://docs.searxng.org/admin/installation-docker.html) - [`settings.yml` 文档](https://docs.searxng.org/admin/settings/settings.html) - [搜索引擎配置](https://docs.searxng.org/admin/settings/settings_engines.html) - [Search API](https://docs.searxng.org/dev/search_api.html) ### 为什么选择它 | 方案 | 优点 | 缺点 | 本项目结论 | |---|---|---|---| | **SearXNG** | 开源、自建、统一 JSON API、可更换上游、无按次 API Key | 需要运维;上游会限流、验证码或封禁 | **采用** | | DeepSeek 官方搜索 | 模型接入简单 | DeepSeek API 当前没有可直接使用的内置网页搜索,仍需自行实现工具 | 不可单独解决 | | DDGS/DuckDuckGo 库 | 代码少、原型快 | 单一来源,服务器网络下 DuckDuckGo 实测超时 | 不采用 | | Tavily / Brave / Exa | API 稳定,面向 Agent | 需要 Key,可能按量收费,绑定厂商 | 可作为未来备用 | | Vane/Perplexica | 完整开源 AI 搜索应用 | 自带 UI、LLM 编排,与现有助手架构重叠,集成过重 | 只作参考 | 选择 SearXNG 的核心原因不是“绝对免费”,而是把搜索后端抽象成独立服务:以后更换 DeepSeek、本地模型或其他 LLM,不需要重写搜索层。 --- ## 三、部署布局 代码仓库中的配置: ```text ~/ai-companion/deploy/searxng/ ├── docker-compose.yml ├── settings.yml.example # 可提交的模板,密钥是占位符 └── settings.yml # 服务器实际配置,含随机密钥,不进 Git ``` Docker 数据: ```text 容器名:ai-companion-searxng 配置挂载:./settings.yml → /etc/searxng/settings.yml:ro 缓存卷:searxng_searxng-cache → /var/cache/searxng 端口:127.0.0.1:18080 → 容器 8080 ``` 官方文档列出的两个持久化位置是 `/etc/searxng` 和 `/var/cache/searxng`。本项目把配置作为只读单文件挂载,缓存放 named volume。配置必须备份,缓存丢失可以重建。 --- ## 四、从零部署 ### 1. 前提检查 ```bash docker version docker compose version ss -ltn | grep ':18080 ' || true ``` 本次服务器环境:Docker 28.4.0、Docker Compose v2.39.4,`18080` 原先未占用。 ### 2. Compose 文件 `deploy/searxng/docker-compose.yml`: ```yaml services: searxng: # 固定到 2026-09-16 已通过真实搜索验收的镜像。 image: docker.io/searxng/searxng@sha256:f1aca3f61c7ff4552b8d54c0ab7fb458e907b78f51c503210cf450c900adca2c container_name: ai-companion-searxng restart: unless-stopped ports: - "127.0.0.1:18080:8080" volumes: - ./settings.yml:/etc/searxng/settings.yml:ro - searxng-cache:/var/cache/searxng environment: - SEARXNG_BASE_URL=http://127.0.0.1:18080/ volumes: searxng-cache: ``` 为什么不写 `0.0.0.0:18080:8080`:当前服务没有对公网用户设计认证和限流,只供同机助手调用。绑定 `127.0.0.1` 能从网络边界直接阻止其他机器访问。 为什么固定 digest:`latest` 会在下次拉取时静默改变版本,可能带来配置兼容或搜索引擎行为变化。应先在新镜像上完成 API 和真实工具调用验收,再更新 digest。 ### 3. `settings.yml` 模板 ```yaml use_default_settings: engines: keep_only: - bing - bing news - 360search - sogou general: instance_name: AI Companion Search debug: false search: safe_search: 1 formats: - html - json server: secret_key: "__SEARXNG_SECRET__" bind_address: "0.0.0.0" limiter: false public_instance: false image_proxy: false engines: - name: bing disabled: false - name: bing news disabled: false - name: 360search disabled: false - name: sogou disabled: false ``` 关键解释: - `use_default_settings`:继承官方完整默认配置,只覆盖本项目关心的部分,减少升级维护量; - `keep_only`:只加载指定引擎,避免每次等待当前网络必然超时的搜索源; - `disabled: false`:必须显式启用。**只写 `keep_only` 不等于启用引擎**,它会保留默认的禁用状态; - `formats: [html, json]`:JSON API 必须显式开放。官方文档说明,未启用的输出格式会返回 HTTP 403; - `safe_search: 1`:默认使用中等级别安全搜索; - `limiter: false`:仅因为宿主机端口只绑定回环地址且是单用户内部调用;如果对外暴露,不能照抄; - 容器内部 `bind_address` 为 `0.0.0.0`,是为了让 Docker 端口映射能访问;真正的外部边界由宿主机的 `127.0.0.1:18080` 控制。 ### 4. 生成实际配置 不要把真实 `secret_key` 写进仓库: ```bash cd ~/ai-companion/deploy/searxng search_secret=$(openssl rand -hex 32) sed "s/__SEARXNG_SECRET__/${search_secret}/" \ settings.yml.example > settings.yml chmod 600 settings.yml ``` 仓库 `.gitignore` 已包含: ```gitignore deploy/searxng/settings.yml ``` ### 5. 启动 ```bash cd ~/ai-companion/deploy/searxng docker compose pull docker compose up -d docker compose ps ``` 预期端口应显示为: ```text 127.0.0.1:18080->8080/tcp ``` 首次拉取镜像在本次环境中比较慢,但下载一直有进度;不要因为几十秒没有完成就重复启动多个 `docker pull`。 --- ## 五、验证与使用 ### 1. 最小健康验证 SearXNG 没有必要为本项目额外暴露公开健康接口,直接查询 JSON API: ```bash curl -GsS --max-time 20 http://127.0.0.1:18080/search \ --data-urlencode 'q=DeepSeek' \ --data-urlencode 'format=json' \ | jq '.results | length' ``` 返回大于零的数字才算真正可用。**HTTP 200 或容器处于 Up 状态不代表上游搜索可用。** 查看前三条结果: ```bash curl -GsS --max-time 20 http://127.0.0.1:18080/search \ --data-urlencode 'q=DeepSeek 最新消息' \ --data-urlencode 'format=json' \ --data-urlencode 'language=zh-CN' \ | jq -r '.results[0:3][] | .title + " | " + .url' ``` 官方 API 支持 `/` 和 `/search` 两个端点、GET/POST 两种方法;常用参数包括: | 参数 | 用途 | |---|---| | `q` | 必填,搜索词 | | `format` | `json` / `csv` / `rss`,必须在配置中启用 | | `language` | 搜索语言,如 `zh-CN` | | `categories` | 搜索分类 | | `engines` | 临时指定引擎,逗号分隔 | | `pageno` | 页码 | | `time_range` | `day` / `month` / `year`,仅对支持的引擎有效 | | `safesearch` | `0` / `1` / `2` | 指定单一引擎诊断: ```bash curl -GsS http://127.0.0.1:18080/search \ --data-urlencode 'q=DeepSeek' \ --data-urlencode 'format=json' \ --data-urlencode 'engines=sogou' \ | jq '.results | length' ``` ### 2. 从其他电脑临时打开 Web UI 服务没有直接暴露到局域网。需要人工查看时,用 SSH 隧道: ```bash ssh -L 18080:127.0.0.1:18080 \ yonglin@cadadmin-server.taild0f91d.ts.net ``` 然后在本机浏览器打开: ```text http://localhost:18080/ ``` 不要为了偶尔看 UI 就把 Compose 改成 `0.0.0.0:18080:8080`。 ### 3. Python 调用示例 ```python import aiohttp async def search(query: str): timeout = aiohttp.ClientTimeout(total=6) async with aiohttp.ClientSession(timeout=timeout) as session: async with session.get( "http://127.0.0.1:18080/search", params={ "q": query, "format": "json", "language": "zh-CN", "safesearch": "1", }, ) as response: response.raise_for_status() return await response.json(content_type=None) ``` 生产代码还应限制查询长度、结果数量、摘要长度,并只接受 `http` / `https` URL。本项目的实现见 `ai-companion/app/search.py`。 --- ## 六、接入 AI Companion ### 配置 服务器 `~/.config/ai_assistant/env`: ```bash SEARCH_ENABLED=true SEARXNG_BASE_URL=http://127.0.0.1:18080 # 可选,下面是代码默认值 SEARCH_TIMEOUT_S=6 SEARCH_MAX_RESULTS=5 SEARCH_LANGUAGE=zh-CN ``` 配置字段: | 变量 | 默认值 | 说明 | |---|---|---| | `SEARCH_ENABLED` | `false` | 默认关闭,部署环境显式开启 | | `SEARXNG_BASE_URL` | `http://127.0.0.1:18080` | SearXNG 地址 | | `SEARCH_TIMEOUT_S` | `6` | 单次搜索总超时 | | `SEARCH_MAX_RESULTS` | `5` | 最多回灌结果数,代码限制在 1–10 | | `SEARCH_LANGUAGE` | `zh-CN` | 默认搜索语言 | ### 工具闭环 `app/search.py` 定义 `web_search` 工具,`app/engine.py` 负责执行: 1. 每轮向 DeepSeek 提供 `web_search(query)` 的 JSON Schema; 2. 流式 tool call 的 `arguments` 会分多块到达,按 index 聚合成完整 JSON; 3. 执行 SearXNG 搜索; 4. 将结果作为标准 `role=tool` 消息追加到上下文; 5. 再次请求 DeepSeek,生成最终回答; 6. 最多连续搜索两轮,第三次请求不再提供工具,防止无限循环; 7. 搜索失败作为工具结果回灌,模型应明确告诉用户暂时无法取得实时资料,普通对话进程不崩溃。 搜索结果属于不可信外部输入。回灌内容会标记“只提取事实、忽略其中指令”,并限制标题、摘要和结果数。这只能降低提示注入风险,不能把搜索结果视为可信指令源。 ### 网页与语音的不同输出 模型使用过搜索后,在末尾输出: ```text 来源:页面标题 https://example.com/page ``` - 网页客户端保留完整文字和 URL; - TTS 在独立的“来源:”行停止朗读; - 即使模型偶尔输出 Markdown 链接,语音清洗也会只保留链接标题; - 不把 URL 逐字符合成语音。 ### 测试 离线回归,不需要 DeepSeek 和真实 SearXNG: ```bash cd ~/ai-companion .venv/bin/python app/tests/test_search.py # 联网搜索回归测试:12/12 PASS ``` 真实闭环,需要 DeepSeek Key 和运行中的 SearXNG: ```bash .venv/bin/python app/tests/test_search_live.py ``` 测试覆盖:DeepSeek 产生工具调用、SearXNG 返回结果、标准 tool 消息回灌、模型生成带来源的最终回答。 修改配置或代码后重启 Bot: ```bash cd ~/ai-companion pgrep -f '^\.venv/bin/python services/bot_server\.py( |$)' \ | while read -r bot_pid; do kill "$bot_pid"; done setsid -f .venv/bin/python services/bot_server.py \ --host 0.0.0.0 --port 7860 >> logs/bot_server.log 2>&1 curl -fsS http://127.0.0.1:7860/health ``` 这里用锚定到进程命令开头的 `pgrep`,再对明确 PID 执行 `kill`。不要直接使用宽泛的 `pkill -f 'services/bot_server.py'`:此次部署曾因同一条 SSH 命令后半段也包含这个 字符串,导致 `pkill -f` 把远端 shell 一起结束。长期应改用 systemd 管理服务。 --- ## 七、本次部署过程与踩坑 ### 1. 初始状态:容器正常,但搜索结果为零 首次部署后: ```text container: Up HTTP API: 200 results: 0 ``` 日志显示默认引擎全部超时: ```text wikipedia: Timeout brave: Timeout duckduckgo: Timeout wikidata: Timeout google cse: Timeout ``` 结论:只检查容器状态和 HTTP 状态码会产生“服务正常”的假象,必须检查 `results` 数量及实际标题、URL。 ### 2. 分宿主机与容器测试上游 宿主机实测: | 上游 | 结果 | |---|---| | 百度 | 能连接,但搜索请求跳验证码 | | Bing 中国 | HTTP 200,但通过 SearXNG 暂时无结果 | | 360 搜索 | HTTP 200,SearXNG 有结果 | | 搜狗 | HTTP 200,SearXNG 有结果 | | DuckDuckGo HTML | 8 秒超时 | 容器内访问百度成功,说明不是 Docker bridge 完全断网,而是特定境外上游不可达或受限。因此没有改成 host 网络,而是调整搜索源。 ### 3. `keep_only` 不会自动启用引擎 第一次只配置: ```yaml use_default_settings: engines: keep_only: - bing - 360search - sogou ``` 逐个指定 `engines=360search` 或 `engines=sogou` 时有结果,但不指定 `engines` 的聚合查询仍返回 0。 原因:`keep_only` 只限制加载范围,仍继承这些引擎在默认配置中的 `disabled` 状态。补上以下配置后聚合查询恢复: ```yaml engines: - name: 360search disabled: false - name: sogou disabled: false ``` 最终聚合测试先后返回 8、14、5 条结果;数量会随查询和上游状态变化,判断标准应是“大于零且内容合理”,不能写死固定数量。 ### 4. JSON API 默认不一定开放 SearXNG 的默认输出格式通常只有 HTML。如果没有在 `search.formats` 中加入 `json`: ```text /search?q=test&format=json → 403 Forbidden ``` 这是配置行为,不是路由不存在。 ### 5. 搜索源会动态失效 元搜索无法消除上游限制。常见变化: - 某引擎开始返回验证码; - 服务器 IP 被限流; - 引擎页面结构变化,SearXNG 解析器暂时失效; - 境外域名在当前网络超时; - 语言参数或复杂搜索词导致特定引擎零结果。 所以必须保留多引擎、超时、日志和失败降级,不能假定“一次配置永久可用”。 --- ## 八、日常运维 ### 状态和日志 ```bash cd ~/ai-companion/deploy/searxng docker compose ps docker compose logs --tail=100 searxng docker stats --no-stream ai-companion-searxng ``` 确认端口没有意外对外暴露: ```bash docker inspect --format '{{(index (index .HostConfig.PortBindings "8080/tcp") 0).HostIp}}:{{(index (index .HostConfig.PortBindings "8080/tcp") 0).HostPort}}' \ ai-companion-searxng # 必须是 127.0.0.1:18080 ``` ### 重启与停止 ```bash docker compose restart searxng docker compose stop searxng docker compose up -d searxng ``` `docker compose down` 会删除容器和 Compose 网络,但默认不会删除 named volume;不要随意加 `-v`。 ### 升级 当前 Compose 固定 digest。升级步骤: ```bash docker pull docker.io/searxng/searxng:latest docker image inspect docker.io/searxng/searxng:latest \ --format '{{index .RepoDigests 0}}' ``` 把得到的 `searxng/searxng@sha256:...` 更新到 Compose,然后: ```bash docker compose up -d # API 验收 curl -GsS --max-time 20 http://127.0.0.1:18080/search \ --data-urlencode 'q=DeepSeek' \ --data-urlencode 'format=json' \ | jq '.results | length' # DeepSeek 工具闭环验收 cd ~/ai-companion .venv/bin/python app/tests/test_search_live.py ``` 两个测试都通过后再提交新的 digest。官方文档建议升级时检查 Compose 模板和环境变量变化;固定 digest 不意味着永不升级,而是把升级变成显式、可验证的操作。 ### 备份与恢复 需要备份: ```text deploy/searxng/docker-compose.yml deploy/searxng/settings.yml.example 服务器实际的 deploy/searxng/settings.yml(包含 secret,放安全备份) ``` 缓存卷不属于关键数据,可以重新创建。恢复时也可以从模板生成新的随机 secret,不要求保留旧值,因为当前没有外部登录会话依赖它。 --- ## 九、故障排查 | 现象 | 可能原因 | 检查与处理 | |---|---|---| | 连接拒绝 | 容器没启动或端口错误 | `docker compose ps`、`ss -ltn | grep 18080` | | HTTP 403 | `json` 未加入 `search.formats` | 检查 `settings.yml`,重启容器 | | HTTP 200、结果 0 条 | 默认引擎禁用或全部上游失败 | 逐个传 `engines=` 测试,查看容器日志 | | 单引擎有结果,聚合为 0 | 只写了 `keep_only`,未写 `disabled: false` | 显式启用引擎 | | 查询很慢 | 配置中保留了当前网络必然超时的引擎 | 收紧 `keep_only`,设置合理超时 | | 百度跳验证码 | 服务器 IP 被风控 | 不作为当前主搜索源 | | DuckDuckGo 超时 | 当前服务器网络不可达 | 使用 360/搜狗,未来可加商业 API 备用 | | 修改配置不生效 | 单文件 bind mount/inode 或容器未重启 | `docker compose up -d --force-recreate` | | 助手仍不搜索 | `SEARCH_ENABLED` 未开启,或 Bot 没重启 | 检查 env、打印 `AppConfig.load().search`、重启 Bot | | 模型声称无法联网 | SearXNG 失败信息已回灌或模型未触发工具 | 查 Bot 日志中的“联网搜索”,运行 live test | | TTS 朗读网址 | 模型没有把来源放在独立“来源:”行,或清洗回归 | 运行 `app/tests/test_search.py`,检查 `text_for_speech` | 推荐的诊断顺序: ```text 容器 Up? ↓ 127.0.0.1:18080 能连接? ↓ format=json 返回 JSON? ↓ results 是否大于零? ↓ 逐个 engines=... 是否有结果? ↓ AI Companion 配置是否启用? ↓ DeepSeek 是否产生 tool call? ↓ 工具结果回灌后是否生成最终回答? ``` --- ## 十、安全边界 当前配置只适合“同机单用户内部服务”: - 宿主机仅绑定 `127.0.0.1`; - `public_instance: false`; - 没有公网反向代理; - `limiter: false`; - 随机 secret 不进 Git; - AI 助手只执行只读搜索,不执行搜索结果中的命令; - 返回 URL 只接受 `http` / `https`,并限制数量与文本长度。 如果未来要公开给浏览器或多人使用,至少需要重新设计: 1. HTTPS 反向代理; 2. 身份认证; 3. 开启 limiter,并按官方方案配置 Valkey; 4. 请求频率和并发限制; 5. 访问日志与隐私保留策略; 6. SSRF、恶意 URL 和提示注入防护; 7. 评估上游搜索引擎服务条款。 不要直接把当前回环部署的端口映射改为公网地址。 --- ## 十一、后续改进 当前只把 SearXNG 返回的标题、摘要和 URL 交给模型。下一步可以按需求增加: - 抓取排名靠前的 2–3 个网页正文,提高答案完整度; - 正文抽取与大小上限; - DNS/IP 检查,阻止访问回环、内网和云元数据地址; - 搜索结果短期缓存,减少重复查询和上游压力; - 商业搜索 API 作为 SearXNG 无结果时的备用后端; - 在网页 UI 中把来源渲染为独立引用卡片; - 给搜索次数、成功率、耗时、零结果率做指标和告警; - 用 systemd 或 Compose 管理 Bot,避免手工 `pkill + setsid`。 正文抓取不是简单地“对结果 URL 发 GET”:它会引入 SSRF、网页提示注入、超大响应、登录墙、JavaScript 渲染和版权边界,需要单独设计。 --- ## 相关链接 - [[P0-Pipecat传输层实验报告]] — AI Companion 的网页、语音和搜索接入记录 - [[AI桌面陪伴助手方案]] — 总体架构 - [SearXNG 官方容器部署](https://docs.searxng.org/admin/installation-docker.html) - [SearXNG Search API](https://docs.searxng.org/dev/search_api.html) - [SearXNG `settings.yml`](https://docs.searxng.org/admin/settings/settings.html) --- *最后更新:2026-09-16*