# DHT 元数据搜索服务 这是一个用于持续发现持久化索引和搜索 BitTorrent DHT 元数据的 Rust 服务 当前已经具备可即时启停的分布式 DHT 采集 Metadata 下载 RocksDB 精确去重 Tantivy 全文搜索 内容聚合 可用性验证 HTTP API Web 搜索界面 运行诊断 配置管理 备份恢复和 Docker 部署能力 ## 项目结构 ```text src/search/ 最终运行的采集存储搜索和接口应用 src/crawler/ 可独立复用的 DHT 协议与 Metadata 获取基础库 src/web/ 基于 Vue 和 shadcn-vue 的搜索界面 opencodes/ 不参与构建且不得修改的参考项目 ``` RocksDB 是唯一权威数据源 Tantivy 索引可以从 RocksDB 完整重建 Tantivy 使用代际影子索引处理 Schema 文档格式损坏和缺失等全量重建 用户过滤规则通过可恢复扫描只增量更新真正变化的搜索文档 两类进度都可以在搜索提示与系统诊断页查看 重复发现只会精确更新 RocksDB 同一个内容组每六小时最多触发一次 Tantivy 动态状态刷新 新内容过滤变化和可用性验证仍会立即更新索引 搜索结果返回前会从 RocksDB 补全当前精确状态 因此界面数字保持最新但最近发现次数和热度的全局排序允许最多六小时的索引快照延迟 应用全部配置和内容隐藏规则统一位于 [`config.toml`](config.toml) 诊断页默认聚合全部采集器的 DHT 下载和失败原因指标 也可以切换到单个采集器查看其历史吞吐和进程资源 独立的采集器页面负责节点状态启停与每节点 DHT 配置 关闭采集器不会影响已有 RocksDB 数据补建索引和本地搜索 ## 分布式运行 同一个程序通过 `[service].role` 支持三种运行角色 | 角色 | 行为 | |---|---| | `standalone` | 默认单机模式 同时运行存储搜索和本机采集器 | | `coordinator` | 唯一存储搜索协调器 不启动 DHT | | `collector` | 只运行 DHT 采集有效性验证和持久待发送箱 | 协调器和采集器必须使用相同的 `DHT_SEARCH_CLUSTER_TOKEN` 环境变量 值至少三十二个字符 内部接口使用独立端口且不得无保护暴露 公网中继必须同时使用防火墙或反向代理来源 IP 白名单限制采集器来源 采集器第一次连接时自动生成并持久化节点 ID 协调器保存每个节点的期望配置和修订号 Metadata 在采集器本地 RocksDB 待发送箱落盘后才确认下载成功 协调器断开或积压达到高水位时采集器会暂停并在恢复后继续 采集器心跳同时上报 DHT Peer 失败分类队列和当前进程资源 协调器把集群汇总与逐节点快照写入可删除的诊断历史 Metadata 待发送箱以最多 32 条和 1 MiB 原始编码软上限组成批次 超过软上限的单条 Metadata 仍会独立发送 采集器最多并发 8 个批次并使用 Zstandard 压缩 协调器限制请求体和解压后数据为 16 MiB 只有收到数量一致的完整批次结果后才从本地 RocksDB 原子确认删除 失败批次继续保留并按有上限的指数退避重试 诊断页的本次运行接收和发送表示所选采集器容器从本次启动开始的非回环网络接口累计字节 包含 DHT UDP Peer Metadata 下载 协调器上传 心跳和配置同步 全部采集器视图显示各采集器最近上报值的合计 不包含宿主机其他服务并在采集器容器重启后重新计数 本地启动一个协调器和两个采集器 ```shell export DHT_SEARCH_CLUSTER_TOKEN="请替换为至少三十二个字符的随机令牌" docker compose -f compose.cluster.yaml up -d --build ``` PowerShell 使用 `$env:DHT_SEARCH_CLUSTER_TOKEN = "请替换为至少三十二个字符的随机令牌"` 打开 `http://127.0.0.1:18080/system` 查看两个节点 再到配置页分别启用和修改参数 内部协议使用批量 infohash 租约避免多个采集器重复下载 RocksDB 仍是唯一权威数据源 租约丢失或上传重试只影响额外网络开销 不会生成重复记录 ## 本地运行 Windows 可以在仓库根目录执行 ```powershell scripts\run.bat ``` 脚本会在当前窗口同时启动 Rust 后端和 Vite 前端 按一次 `Ctrl+C` 即可统一停止 默认地址 | 服务 | 地址 | |---|---| | Web 开发界面 | `http://127.0.0.1:5173` | | HTTP API | `http://127.0.0.1:8080` | | 健康检查 | `http://127.0.0.1:8080/health` | | 就绪检查 | `http://127.0.0.1:8080/ready` | | 内部采集接口 | `127.0.0.1:8081` | 也可以分别启动 ```powershell cargo run -p dht-search --bin dht-search -- --config config.toml cd src/web bun install bun run dev -- --host 127.0.0.1 ``` Windows 构建 RocksDB 需要 LLVM 并让 `LIBCLANG_PATH` 指向包含 `libclang.dll` 的目录 ## Docker 镜像使用 Bun Rust 和 `debian:trixie-slim` 三个阶段构建 最终容器只运行非 root `dht-search` 进程 ```shell docker build --platform linux/amd64 -t dht-search:dev . docker compose up -d docker compose logs -f ``` 默认 Compose 行为 - `config.toml` 映射到 `/dht-search/config.toml` - `dht-search-data` 命名卷保存 RocksDB Tantivy SQLite 日志和检查点 - HTTP 只发布到宿主机 `127.0.0.1:8080` - DHT UDP 发布到宿主机 `12313/udp` - 容器文件句柄上限为 `65536` - 停止宽限时间为 120 秒 端口冲突时可以临时覆盖 ```powershell $env:DHT_HTTP_BIND = "127.0.0.1:18080" $env:DHT_UDP_PORT = "22313" docker compose up -d ``` 常用管理命令 ```shell docker compose ps docker compose logs -f docker compose stop docker compose start docker compose down ``` `docker compose down` 不删除数据卷 不要执行 `docker compose down --volumes` 除非已经确认权威数据不再需要 导出并传输镜像 ```shell docker save dht-search:dev -o dht-search-dev.tar docker load -i dht-search-dev.tar ``` 如果宿主机启用了 Xray TProxy 等全局透明代理 DHT 流量可能需要单独旁路 判断和处理方式见 [`docs/xray-transparent-proxy.md`](docs/xray-transparent-proxy.md) ## 测试 单个规则和私有状态机测试放在对应 Rust 模块底部 跨层公开契约测试放在 crate 的 `tests/` 目录 真实 DHT 长时间运行远程部署和浏览器交互不进入默认 `cargo test` 默认 Rust 验证命令 ```powershell $env:LIBCLANG_PATH = "$PWD\.tools\libclang\clang\native" cargo fmt --all --check cargo test --workspace --all-targets --all-features cargo clippy --workspace --all-targets --all-features -- -D warnings ``` 手动验证高延迟限速和请求失败条件下的持久待发送箱批量清空 ```powershell cargo test -p dht-search --all-features compressed_batches_drain_a_mixed_outbox_over_a_limited_link -- --ignored --nocapture ``` Web 验证命令 ```shell cd src/web bun run typecheck bun run build ``` ## 性能基线 基准使用 Windows x86_64 release 构建和确定性合成数据 内容重复比例为十分之一 每批索引 1000 个内容文档 | 记录数 | 内容文档 | RocksDB 写入 | Tantivy 索引 | 总磁盘 | 峰值内存 | |---:|---:|---:|---:|---:|---:| | 10,000 | 9,000 | 102,433 条/秒 | 9,050 文档/秒 | 21.88 MiB | 53.07 MiB | | 100,000 | 90,000 | 86,452 条/秒 | 8,423 文档/秒 | 174.24 MiB | 182.23 MiB | | 1,000,000 | 900,000 | 75,248 条/秒 | 5,435 文档/秒 | 1.49 GiB | 433.14 MiB | 百万级查询 P95 | 查询类型 | P95 | |---|---:| | 中文关键词 | 4.774 ms | | 英文关键词 | 7.746 ms | | 文件路径片段 | 1.063 ms | | 精确 infohash | 0.031 ms | | 最近收录排序 | 3.367 ms | | 大小扩展名过滤 | 6.641 ms | 百万级基准中普通搜索过滤排序精确哈希索引吞吐磁盘和峰值内存均达到当前目标 大型种子会先按文件大小降序和规范化路径稳定排序 最多索引 2048 个文件且完整路径文本总量不超过 256 KiB 以优先覆盖主体内容并限制极端 Metadata 的索引放大 搜索索引对标题使用最多 10 字符的有限 N-Gram 文件名使用 2 至 3 字符 Basic 倒排和独立四字符位置字段 长文件名关键词通过短语查询保证连续匹配 完整路径仅按目录段和单词分词 普通文本查询会按各字段策略拆分 `*.iso` 一类扩展名快捷查询直接使用扩展名词项索引并可与普通关键词组合 搜索侧不执行正则或任意通配符扫描并在首次写入前延迟创建 Tantivy writer 每条记录包含 2048 个文件的极端基准中 450 个内容文档的 Tantivy 索引为 19.84 MiB 平均每文档 46.2 KiB 峰值内存为 113.08 MiB 运行诊断中的 `index_refresh_scheduled` 和 `index_refresh_suppressed` 分别表示重复发现实际安排和被时间桶合并的索引刷新数 `index_documents_written` 和 `index_documents_skipped` 用于确认待索引任务最终是否产生 Tantivy 文档写入 ### 真实索引空间测量 使用 `dht-index-inspect` 可以只读统计 `CURRENT` 指向的活动 Tantivy 索引 不会创建 writer 或修改索引 ```powershell cargo run -p dht-search --bin dht-index-inspect -- data/search-index ``` 2026-08-11 在远端 624801 个活动文档上的测量覆盖 99.9998% 的索引文件字节 总索引为 14.71 GiB 其中物理文档 1036396 个 删除文档比例为 39.71% 按物理文档比例估算相同 Schema 全新重建约为 8.87 GiB | 字段或组件 | 占用 | 比例 | |---|---:|---:| | 文件名 N-Gram `file_names` | 9.02 GiB | 61.34% | | 别名 N-Gram `aliases` | 1.73 GiB | 11.78% | | 标题 N-Gram `name` | 1.73 GiB | 11.75% | | 已停用查询字段 `regex_text` | 0.83 GiB | 5.62% | | 精确文件名 `exact_file_names` | 0.64 GiB | 4.35% | | 路径分词 `files_text` | 0.30 GiB | 2.04% | | 全部 stored fields | 0.20 GiB | 1.39% | | 全部 fast fields | 0.01 GiB | 0.09% | 空间瓶颈是文件名 N-Gram 不是 stored field fast field 热度或可用性字段 新 Schema 已删除没有查询调用方的 `regex_text` 并停止在 `aliases` 中重复索引代表标题 不同 infohash 提供的其他标题仍保留为可搜索别名 精确文件名字段暂时保留用于相关性排序 在 10 万条确定性记录的相同数据集上对比文件名 N-Gram `2..8` 与 `2..4` 后 `file_names` 从 18232514 字节降到 5068096 字节 单字段下降 72.20% 其中词典下降 91.20% 倒排表下降 65.64% 总 Tantivy 索引下降 12.01% 索引吞吐从每秒 8079 文档提高到 9015 文档 两组常规查询结果数量保持一致 P95 查询多数变化很小 文件路径片段从 0.213 ms 增加到 0.326 ms 简单 `2..4` 会把长关键词拆成多个四字符词项 测试已确认多个文件分别包含零散片段时 Tantivy 候选可能误匹配 为避免读取 RocksDB 复核候选破坏稳定分页 实验方案将 2 至 3 字符片段保存为无词频基础倒排 将四字符片段保存到独立位置字段 长关键词使用短语查询 多文件零散片段误匹配测试被精确拦截 连续长文件名搜索保持通过 在相同 10 万条数据上 拆分位置方案的两个文件名字段合计从 18232514 字节降到 5584568 字节 下降 69.37% 总索引下降 11.51% 索引吞吐从每秒 8079 文档提高到 8934 文档 文件路径片段 P95 为 0.500 ms 位置数据为 1240496 字节 每条 100 个高度重复短文件名的压力数据中 拆分位置方案空间基本持平但索引吞吐提高约 32% 说明收益取决于真实文件名长度和多样性 按远端字段占比线性推算 相同 Schema 的全新索引可能从约 8.87 GiB 降到约 5.1 GiB 生产索引已经切换到拆分位置方案 Schema 或文档格式变化时通过影子索引从 RocksDB 全量重建 旧索引在重建完成和原子切换前继续提供搜索 最终体积峰值磁盘和真实查询延迟仍需通过远端重建验证 ## 相关文档 - 当前实施状态和后续计划见 [`TODOS.md`](TODOS.md) - 开发约定和架构边界见 [`AGENTS.md`](AGENTS.md) - 应用命令参数 API 和基准工具见 [`src/search/README.md`](src/search/README.md) - DHT 基础库用法和指标见 [`src/crawler/README.md`](src/crawler/README.md) - Web 开发说明见 [`src/web/README.md`](src/web/README.md)