Open Notebook 架构决策实录 ADR-001:用 SurrealDB 一库承载文档、图谱、向量与后台任务
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-notebookOpen 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任务队列 Celeryworker 一个向量数据库——四个服务。对于想要自己掌控数据的个人用户来说这是实打实的运维负担也是 ADR-001 决定另辟蹊径的直接动因。DecisionSurrealDB 一个服务解决全部四件事决策本身可以用一句话概括使用 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/RedisSQLite LiteFS极致的简单并发能力弱且没有图特性MongoDB Redis Celery工具链熟悉三个服务破坏了自托管简单性优势混合方案Postgres Neo4j两全其美运维成本是自托管用户不愿承担的注意这些方案的取舍逻辑高度一致几乎所有候选路线都栽在「额外服务数量」与「缺少图或向量能力」上。这恰好反衬出决策的核心权衡——用「较年轻的生态」换取「单一可运行服务 开箱即用的图与向量能力」这正是「单用户优先、易于自托管」产品定位的直接投射。仓库落地一版本化迁移与数据库 schema决策不是停留在文档里的一句口号而是沉淀在 open_notebook/database/migrations/ 下从1.surrealql到23.surrealql的数十个版本化迁移文件每个版本都带一个*_down.surrealql回滚文件中。迁移执行机制迁移由两层 Python 封装驱动migrate.py 提供向后兼容的同步包装MigrationManagerasync_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 中「文档 图 向量」三合一的直接证据文档类Schemafullsource表保存来源与全文source_embedding/source_insight保存分块与洞察note保存笔记notebook保存笔记本每个承载语义检索的实体都带向量字段DEFINE FIELD IF NOT EXISTS embedding ON TABLE source_embedding TYPE arrayfloat; DEFINE FIELD IF NOT EXISTS embedding ON TABLE source_insight TYPE arrayfloat; DEFINE FIELD IF NOT EXISTS embedding ON TABLE note TYPE arrayfloat;注意向量被建模为普通数组字段arrayfloat这正是 SurrealDB「一个服务内置向量能力」的体现——不需要单独的向量索引服务后续迁移如 10、13 号还把这些字段调整为optionarrayfloat以适配嵌入缺失的场景。图关系Relation edgenotebook ↔ 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 与 insightDEFINE 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 中surrealdb1.0.4, surreal-commands1.3.1,2,通用服务层位于 api/command_service.pyCommandService.submit_command_job()先确保命令模块被导入因为submit_command会对照本地注册表校验然后调用 surreal-commands 的submit_command(app_name, command_name, command_args)提交任务并返回可追踪的cmd_idget_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_URLWebSocket RPC 端点无由SURREAL_ADDRESS/SURREAL_PORT兜底构造SURREAL_USER认证用户名rootSURREAL_PASSWORD认证密码rootSURREAL_NAMESPACE命名空间open_notebookSURREAL_DATABASE数据库名open_notebookOPEN_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.md1. 同机 Docker Compose推荐SURREAL_URLws://surrealdb:8000/rpc SURREAL_USERroot SURREAL_PASSWORDroot SURREAL_NAMESPACEopen_notebook SURREAL_DATABASEopen_notebook2. Open Notebook 在 Docker、SurrealDB 在宿主机SURREAL_URLws://your-machine-ip:8000/rpc # 或 host.docker.internal SURREAL_USERroot SURREAL_PASSWORDroot SURREAL_NAMESPACEopen_notebook SURREAL_DATABASEopen_notebook3. 两者都在宿主机含单容器部署SURREAL_URLws://localhost:8000/rpc SURREAL_USERroot SURREAL_PASSWORDroot SURREAL_NAMESPACEopen_notebook SURREAL_DATABASEopen_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),仅供参考
上一篇/下一篇内容由系统自动关联
返回资讯列表 →