Open Notebook 架构决策实录 ADR-001:用 SurrealDB 一库承载文档、图谱、向量与后台任务
【免费下载链接】open-notebookAn Open Source implementation of Notebook LM with more flexibility and features项目地址: https://gitcode.com/GitHub_Trending/op/open-notebook
Open Notebook 以「单用户优先、可自托管」为设计出发点,在系统设计之初就做出了一个关键架构决策:放弃 Postgres + Redis + Celery + 向量库的传统多服务组合,改用 SurrealDB 作为唯一的数据库。本文以仓库中的架构决策记录 ADR-001: SurrealDB as the database 为骨架,结合仓库内的迁移脚本、数据访问层、Docker 编排与搜索实现源码,逐层还原该决策的来龙去脉:它的上下文与动机、备选方案对比、schema 与迁移落地、向量/全文检索与后台任务的实现方式,以及该决策给自托管用户和开发维护带来的真实代价。读完你会理解为什么本项目把「一个容器」当作基础设施层面最大的优势,也会看到一套把文档、图谱、向量和任务队列塞进单一数据库的完整工程范式。
ADR-001 的角色:架构决策先于代码
在深入源码之前,先理解这份文档在项目中的定位。Open Notebook 在 docs/7-DEVELOPMENT/decisions/ 下维护了一套架构决策记录(ADR),涵盖 SurrealDB 选型、外部依赖策略、Streamlit 到 Next.js 的迁移、后台 worker、发布流程、迁移粒度等多个主题,而ADR-001 是其中编号第一、也是历史最久的一条。
几个值得注意的元信息:
- 状态为 Accepted(已接受),说明该决策已经落地并处于执行中,而非候选方案;
- 日期标注为
2026-07 (retroactive record),即这是一条追溯性记录——决策实际上起源于项目诞生之初,文档说明其长期论证维护在关联 issue(#372、#378、#381)中,并关联根目录的 VISION.md 中描述的 "Platform v-next cluster"(下一代集群化平台)。
换句话说,ADR-001 记录的是一次「项目最初就用错了(或者说用对了?)数据库」级别的根本性选择,其影响贯穿整个仓库的 schema、查询函数与部署形态。
Context:四个数据需求,拒绝四套服务
ADR-001 首先明确描述了项目对存储层的四类核心需求:
- 文档存储:来源(source)及其元数据,包括提取出的全文;
- 图关系:notebook(笔记本)↔ source(来源)↔ note(笔记)之间的关联;
- 向量嵌入:为语义搜索服务的 embedding;
- 后台任务:来源处理、嵌入、播客生成等耗时作业的排队与状态跟踪。
在此基础上,还有一个对产品定位至关重要的约束:面向注重隐私的自托管用户,部署要足够简单。
如果照搬业界主流做法,一套「传统组合」需要同时运维 Postgres(关系数据)+ Redis(任务队列)+ Celery(worker)+ 一个向量数据库——四个服务。对于想要自己掌控数据的个人用户来说,这是实打实的运维负担,也是 ADR-001 决定另辟蹊径的直接动因。
Decision:SurrealDB 一个服务解决全部四件事
决策本身可以用一句话概括:
使用 SurrealDB 作为唯一的数据库:文档、图关系、向量嵌入,以及(通过 surreal-commands 库实现的)任务队列,全部收敛到一个服务里。
仓库 docker-compose.yml 的编排是这个决策最直观的体现:整个栈只有surrealdb与open_notebook两个 service,其中 SurrealDB 仅由一条命令拉起:
command: ["start", "--log", "info", "--user", "${SURREAL_USER:-root}", "--pass", "${SURREAL_PASSWORD:-root}", "rocksdb:/mydata/mydatabase.db"]使用rocksdb作为存储引擎,镜像为surrealdb/surrealdb:v2。ADR-001 同时明确了执行姿态——「Stay with it and work through the challenges」(留下来,把挑战逐一解决),仅在满足 #372 中列出的退出条件时才重新评估选型。根据 ADR 文本,这些退出条件包括:
- 无法工作的并发事务冲突(transaction conflicts that are unworkable);
- 调优也无法修复的性能问题;
- 未打补丁的关键安全漏洞;
- 出现一个兼具同样整合优势的成熟替代方案。
也就是说,这是一条「有纪律的承诺」而非盲目绑定:预设了明确、可证伪的退出标准。
Alternatives considered:四个备选方案为何被否
ADR-001 记录了当时评估过的四条替代路线及其否决理由,整理如下:
| 备选方案 | 优势 | 否决理由 |
|---|---|---|
| PostgreSQL + pgvector | 成熟度与生态最好 | 无法提供图查询能力,且任务队列仍需 Celery/Redis |
| SQLite + LiteFS | 极致的简单 | 并发能力弱,且没有图特性 |
| MongoDB + Redis + Celery | 工具链熟悉 | 三个服务,破坏了自托管简单性优势 |
| 混合方案(Postgres + Neo4j) | 两全其美 | 运维成本是自托管用户不愿承担的 |
注意这些方案的取舍逻辑高度一致:几乎所有候选路线都栽在「额外服务数量」与「缺少图或向量能力」上。这恰好反衬出决策的核心权衡——用「较年轻的生态」换取「单一可运行服务 + 开箱即用的图与向量能力」,这正是「单用户优先、易于自托管」产品定位的直接投射。
仓库落地一:版本化迁移与数据库 schema
决策不是停留在文档里的一句口号,而是沉淀在 open_notebook/database/migrations/ 下从1.surrealql到23.surrealql的数十个版本化迁移文件(每个版本都带一个*_down.surrealql回滚文件)中。
迁移执行机制
迁移由两层 Python 封装驱动:
- migrate.py 提供向后兼容的同步包装(
MigrationManager); - async_migrate.py 是基于官方 Python 客户端与
surrealdb连接层的异步实现:每个.surrealql文件会被读入、剥离--注释后合并成单条查询执行,执行成功后调用bump_version()推进当前版本号;*_down.surrealql则对应降级路径。run_all()会读取当前版本并顺序执行所有待应用的上迁脚本。
这意味着 Open Notebook 的数据层完全由 SurrealQL 声明式驱动:表、字段、约束、事件、函数与索引都写在迁移里,Python 侧只做执行与版本管理。相关并发与启动期的迁移重试行为还有专门的测试覆盖(参见 test_startup_migration_retry.py)。
schema 骨架:文档表、关系边、向量字段
以首个迁移 1.surrealql 为例,可以看到 ADR-001 中「文档 + 图 + 向量」三合一的直接证据:
文档类(Schemafull):source表保存来源与全文,source_embedding/source_insight保存分块与洞察,note保存笔记,notebook保存笔记本,每个承载语义检索的实体都带向量字段:
DEFINE FIELD IF NOT EXISTS embedding ON TABLE source_embedding TYPE array<float>; DEFINE FIELD IF NOT EXISTS embedding ON TABLE source_insight TYPE array<float>; DEFINE FIELD IF NOT EXISTS embedding ON TABLE note TYPE array<float>;注意向量被建模为普通数组字段(array<float>),这正是 SurrealDB「一个服务内置向量能力」的体现——不需要单独的向量索引服务;后续迁移(如 10、13 号)还把这些字段调整为option<array<float>>以适配嵌入缺失的场景。
图关系(Relation edge):notebook ↔ source ↔ note 的关联用 SurrealDB 的原生关系表表达:
DEFINE TABLE IF NOT EXISTS reference TYPE RELATION FROM source TO notebook; DEFINE TABLE IF NOT EXISTS artifact TYPE RELATION FROM note TO notebook;另外还有一类承载多态关联的表(如refers_to,可在 open_notebook/domain/notebook.py 中看到ChatSession.relate_to_notebook/relate_to_source通过relate()在代码层创建边),以及用DEFINE EVENT实现的级联清理——删除 source 时自动清除其 embedding 与 insight:
DEFINE EVENT IF NOT EXISTS source_delete ON TABLE source WHEN ($after == NONE) THEN { delete source_embedding where source == $before.id; delete source_insight where source == $before.id; };仓库落地二:把全文检索与向量检索下沉到数据库内
ADR-001 的价值在检索路径上体现得最充分。SurrealDB 的 SurrealQL 允许用DEFINE FUNCTION把函数直接存在库里,Open Notebook 正是这么做的。
BM25 全文检索基础设施
1.surrealql 中先定义分析器与全文索引:
DEFINE ANALYZER IF NOT EXISTS my_analyzer TOKENIZERS blank,class,camel,punct FILTERS snowball(english), lowercase; DEFINE INDEX IF NOT EXISTS idx_source_title ON TABLE source COLUMNS title SEARCH ANALYZER my_analyzer BM25 HIGHLIGHTS; DEFINE INDEX IF NOT EXISTS idx_source_full_text ON TABLE source COLUMNS full_text SEARCH ANALYZER my_analyzer BM25 HIGHLIGHTS; DEFINE INDEX IF NOT EXISTS idx_note ON TABLE note COLUMNS content SEARCH ANALYZER my_analyzer BM25 HIGHLIGHTS;分析器组合了blank、class、camel、punct四种 tokenizer 与snowball(english)、lowercase两种 filter,索引开启 BM25 评分与高亮。
fn::text_search:数据库内的跨实体聚合
同名迁移中还定义了fn::text_search,它把 source 标题、source 分块、source 全文、source 洞察、note 标题、note 内容六个检索源分别用@1@全文匹配 +search::score(1)打分,再用array::union归并,最后按实体聚合、以相关性倒序截断:
DEFINE FUNCTION IF NOT EXISTS fn::text_search($query_text: string, $match_count: int, $sources:bool, $show_notes:bool) { ... RETURN (SELECT item_id, math::max(relevance) as relevance from $final_results group by item_id ORDER BY relevance DESC LIMIT $match_count); };fn::vector_search:余弦相似度语义检索
向量路径对应fn::vector_search,使用内置的vector::similarity::cosine计算分块/洞察/笔记与查询向量的相似度:
SELECT source as item_id, content, vector::similarity::cosine(embedding, $query) as similarity FROM source_embedding LIMIT $match_countPython 侧调用链
这些库内函数由领域层直接调用。在 open_notebook/domain/notebook.py 中,text_search()通过repo_query执行:
select * from fn::text_search($keyword, $results, $source, $note)而vector_search()(同文件 L809-L839)先用统一嵌入函数generate_embedding把查询文本转成向量,再调用:
SELECT * FROM fn::vector_search($embed, $results, $source, $note, $minimum_score);其中minimum_score默认0.2。这个文件还透露出一个很有价值的工程细节:SurrealDB 的search::highlight在处理大块或多字节文本时可能因字节位置溢出而让整条查询失败(注释中引用 issue #648),因此text_search()在捕获到 "position overflow" 时会自动降级走向量检索,从而保证用户总能拿到结果而不是 500 错误——这正是 ADR-001「留下来解决挑战」在代码层的具体体现。可进一步参考搜索 API 层的封装(api/routers/search.py)。
仓库落地三:surreal-commands 把任务队列也装进数据库
ADR-001 提到 job queueing "via surreal-commands"。这里的核心思想是:后台任务的「队列」本质也是一张 SurrealDB 表,任务以记录(record)形式写入,worker 轮询并更新其状态字段。
依赖声明在 pyproject.toml 中:
"surrealdb>=1.0.4", "surreal-commands>=1.3.1,<2",通用服务层位于 api/command_service.py:CommandService.submit_command_job()先确保命令模块被导入(因为submit_command会对照本地注册表校验),然后调用 surreal-commands 的submit_command(app_name, command_name, command_args)提交任务并返回可追踪的cmd_id;get_command_status()则负责查询任务的 status、result、progress 等字段。
命令模块集中在 commands/ 目录下,按领域拆分为:
- embedding_commands.py — 嵌入计算(文本分块与向量化,底层见 open_notebook/utils/embedding.py,其中
generate_embeddings支持自动批处理与重试); - podcast_commands.py — 播客大纲与逐字稿生成;
- source_commands.py — 来源处理与洞察创建。
领域模型层也大量采用「提交命令即返回」的模式,例如 open_notebook/domain/notebook.py 中add_insight()的文档字符串明确写着:提交create_insight命令,命令内以自动重试逻辑处理事务冲突,随后再异步提交embed_insight命令做向量化。这与 ADR-001 中「事务冲突只是日志噪音、并非失败,通过重试解决」的结论(关联 issue #362、#373)完全对应——重试策略被实现在后台命令而非 API 请求路径上,从而避免把数据库级冲突暴露给前端用户。播客生成的完整作业流可继续阅读 api/podcast_service.py。
运维侧:环境变量、拓扑与安全边界
ADR-001 说「一个容器是自托管用户最大的基础设施优势」,这份承诺的实现细节记录在配套配置文档 database.md 中,并结合 docker-compose.yml 一起看。
标准环境变量
Open Notebook 通过六个环境变量完成与 SurrealDB 的全部对接:
| 变量 | 作用 | 默认值 |
|---|---|---|
SURREAL_URL | WebSocket RPC 端点 | 无(由SURREAL_ADDRESS/SURREAL_PORT兜底构造) |
SURREAL_USER | 认证用户名 | root |
SURREAL_PASSWORD | 认证密码 | root |
SURREAL_NAMESPACE | 命名空间 | open_notebook |
SURREAL_DATABASE | 数据库名 | open_notebook |
OPEN_NOTEBOOK_ENCRYPTION_KEY | 数据库中 API key 的加密密钥 | 必填自设 |
环境变量解析集中在 open_notebook/database/repository.py,其中包含向后兼容逻辑:若只设置了旧式SURREAL_ADDRESS/SURREAL_PORT,会拼出ws://{address}/rpc:{port};用户名/密码亦兼容SURREAL_PASS旧变量。该文件还通过ensure_internal_no_proxy()在 import 阶段把内部 SurrealDB 的 WebSocket 连接排除在系统 HTTP 代理之外(注释引用 issue #1160),避免代理劫持内部流量。
三种部署拓扑
database.md 给出三种典型场景的完整配置(完整安装流程见 docker-compose.md):
1. 同机 Docker Compose(推荐)
SURREAL_URL="ws://surrealdb:8000/rpc" SURREAL_USER="root" SURREAL_PASSWORD="root" SURREAL_NAMESPACE="open_notebook" SURREAL_DATABASE="open_notebook"2. Open Notebook 在 Docker、SurrealDB 在宿主机
SURREAL_URL="ws://your-machine-ip:8000/rpc" # 或 host.docker.internal SURREAL_USER="root" SURREAL_PASSWORD="root" SURREAL_NAMESPACE="open_notebook" SURREAL_DATABASE="open_notebook"3. 两者都在宿主机(含单容器部署)
SURREAL_URL="ws://localhost:8000/rpc" SURREAL_USER="root" SURREAL_PASSWORD="root" SURREAL_NAMESPACE="open_notebook" SURREAL_DATABASE="open_notebook"安全边界的明确警示
database.md 对端口暴露给出了非常具体的警告:官方 docker-compose 把 SurrealDB 端口只绑定在127.0.0.1:8000(见 docker-compose.yml 第 12-18 行的注释——宿主机端口纯粹用于本地调试,如用 Surrealist 或surreal sql连接),因此默认情况下它无法通过宿主机 IP 访问。若确实需要让容器外访问,文档要求刻意地重新发布端口(参考仓库根目录的docker-compose.override.yml.example),并置于防火墙或 SSH 隧道之后、同时改用真实凭据。这与 compose 文件中「默认 root:root 只在零配置本地环境可用,暴露网络前必须在.env中覆盖SURREAL_USER/SURREAL_PASSWORD」的注释互为印证。
多租户与多部署
database.md 还点明了一个扩展性红利:SurrealDB 单实例天然支持多 namespace、多 database。因此要为用户搭建多套 Open Notebook 部署时,无需部署多个数据库进程——只需为不同用户/部署分配不同的 namespace 或 database 即可,这进一步放大了「单一服务」决策的运维价值。
Consequences:为整合付出的真实代价
ADR-001 的 Consequences 部分是全文最坦诚的部分,它没有把决策美化成单方面的胜利,而是并列列出了好处与代价:
收益
- 只需运行一个容器——对自托管用户最大的基础设施优势,与 PDR-001-single-user-first 的产品定位一脉相承。
代价与应对
- 生态较年轻:成熟的调优实践更少,因此项目选择「自己多写文档、并向上游回馈贡献」;
- 并发事务冲突:早期担心的问题在实证中被定性为日志噪音而非真实失败(关联 issue #362、#373),通过后台命令中的自动重试机制消化(代码注释可佐证于 open_notebook/domain/notebook.py);
- 大版本升级成本:跨主版本升级需要刻意安排的迁移工作,ADR 记录中 v3 升级(关联 issue #378)被划入 VISION.md 描述的 "Platform v-next cluster" 一部分。
这套「按版本号管理的 SurrealQL 迁移 + 每个版本配套 down 脚本」的机制,正是为了消化上述升级代价而建立的(参见 open_notebook/database/migrations/ 中 23 个版本及其回滚文件,以及 async_migrate.py 的实现)。
总结:一份可证伪的架构承诺
回看 ADR-001,它给我们提供了一个教科书级的「整合型架构决策」样本:
- 决策前提清晰——四类数据需求 + 一条自托管约束,逻辑自洽;
- 备选方案充分——关系型、嵌入型、多服务混合、双引擎混合全部覆盖,且有明确否决理由;
- 落地证据完整——从 1.surrealql 的 schema、关系边与库内检索函数,到 docker-compose.yml 的单容器编排,再到 command_service.py 的数据库任务队列,仓库每一层都能找到该决策的投影;
- 代价透明可证伪——明确列出退出标准,用实证(事务冲突只是日志噪音)和工程补偿(版本化迁移、自动重试、查询降级)消解风险。
对读者而言,这份 ADR 的最大价值不在于「SurrealDB 比 Postgres 好」这类无法证实的主张,而在于它示范了:当"部署简单性"成为产品的核心约束时,如何用一份单一数据库,把文档、图、向量、队列四件事整合出可用且可持续演进的方案——以及当这条路走到极限(如 Platform v-next 的集群化诉求)时,如何通过版本化迁移与新一轮 ADR 平稳演进。
【免费下载链接】open-notebookAn Open Source implementation of Notebook LM with more flexibility and features项目地址: https://gitcode.com/GitHub_Trending/op/open-notebook
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考