本地优先的笔记应用,数据到底怎么管?——SQLite + 空间隔离 + 内容寻址附件的工程拆解

0. 背景:为什么"数据主权"是硬需求

云笔记把数据放在它家服务器,用的是"使用权"。对一款本地优先 的笔记应用,核心命题是:默认数据就在本机,离线也能完整用,格式开放可迁移。这决定了存储层要优先考虑:

  1. 可移植:数据能从本机拷走、能导出;
  2. 可控:不依赖账号/云,服务停了数据也不丢;
  3. 可离线:全文检索、附件、备份都要在本机闭环。

本文拆解 ShuyoNote 的存储层设计(本地优先笔记),核心是 「权威数据源 + 派生索引」双写 ,以及空间隔离 + 内容寻址附件。

1. 整体存储布局:meta.db + spaces/

数据分两层:

bash 复制代码
{app-data}/
├── meta.db                 # 应用级:工作空间清单 / 模板 / 插件状态 / 同步配置
└── spaces/
    └── {ws_id}/            # 每个工作空间一个独立的 SQLite 库(物理隔离)
        └── .db             # 该空间的所有页面/块/附件索引/关系
attachments/                # 全局内容寻址附件(跨空间去重)

为什么每空间独立库(物理隔离):

  • 隔离:一个空间的数据损坏/迁移不影响其它空间;
  • 导出/导入:单空间能打成自包含 zip(含其附件子集)搬走,不污染其它空间;
  • 多空间并存 :应用级状态(工作空间清单、模板、同步配置)放 meta.db,空间级数据互不干扰。

meta.db 管"有哪些空间、模板、插件、同步配置";spaces/{id}/ 管"这个空间里有什么内容"。两者职责清晰。

2. 数据模型:页 = 一份文档,块是一等公民

把"页"抽象为一棵文档树,块是树的节点:

bash 复制代码
pages(id, parent_id, kind, title, content, ...)   # 页/文件夹,parent_id 构成树
blocks(id, page_id, block_id, ...)                 # 块索引:块 -> 页 反查
backlinks(id, src, dst, kind, ...)                 # 页面级 + 块级引用关系
  • 一页 = 一份文档 :编辑器(Lexical)把页内容序列化存储,块是 Lexical 根级节点,每个顶层块带稳定的 blockId。
  • 块可引用/反链 :((块ID)) 块引用 → blocks 反查;blocks 表维护「块→页」,backlinks 记录页面+块级引用关系,支撑块级反链、关系图。
  • 为什么块独立寻址:块是"可复用"的最小单元(引用、嵌入、反链都要定位到块级),所以给块一个稳定 ID,并建反向索引。

3. 附件:内容寻址(SHA-256)

附件不走"路径-文件名",而是内容寻址:附件按内容哈希存储,相同内容只存一份。

bash 复制代码
attachments/
  ab/                 # 桶:取内容 sha256 的前 2 个十六进制字符,避免单目录文件过多
    3f2a8e...         # 文件名 = 内容的完整 sha256(64 位 hex),内容即地址
  • 去重:跨文件夹/空间,同一张图、同一个文件只存一份(内容寻址天然去重,省空间)。
  • 引用 :页面/块里存 hash 引用,而不是复制字节;引用到页面时用"文件卡片 + 下载"。
  • 可靠:内容即地址,篡改会造成 hash 不匹配(可校验),也是同步增量传输的基础(只传变化的部分)。

代价 :需要一层「hash → 附件元数据」的映射(attachments 表:hash、size、mime、引用计数),并处理孤立附件清理(以引用集为准回收未被引用的字节)。

4. 全文检索:SQLite FTS5 + trigram 做中文

本地要在本机就能搜,不联网。用 SQLite FTS5:

sql 复制代码
CREATE VIRTUAL TABLE post_search USING fts5(title, body, tags, tokenize='trigram');
-- 写入时同步索引;查询时 MATCH + LIKE 兜底
  • trigram 分词 :中文按 3-gram 切分,支持子串检索(中文不靠空格分词,trigram 能覆盖"中文子串")。
  • 可重建 :FTS 是派生索引,可由正文(权威数据源)重建,坏了/旧了能重新生成。
  • 兜底 :短词(不足 3 个字符)FTS 匹配不到,用 LIKE '%xxx%' 兜底(中文常见场景)。

关键原则:权威数据源(正文)+ 派生索引(FTS)分离,索引可丢可重建,数据不因索引损坏丢失。

5. 离线 / 多端:WAL + 备份 + 加密

  • WAL 模式 :日志先行,写入快、崩溃安全,且读不阻塞写(可做读写分离 + 只读连接池)。
  • 自动保存:防抖写入,无"保存"按钮。
  • 版本历史:每次保存前快照,可回滚(本地数据的安全网)。
  • 回收站:软删除 + 恢复 + 彻底删除。
  • 整库备份:导出/导入 zip(数据库一致性快照 + 附件目录)。
  • 端到端加密:口令 → Argon2id → 会话内存密钥(不落盘)→ 加密空间库与附件(XChaCha20-Poly1305);锁定态不读。

6. 一段实现(rusqlite + 幂等迁移)

rust 复制代码
// SQLite 连接 + 迁移
let conn = Connection::open(db_path)?;
conn.pragma_update(None, "journal_mode", "WAL")?;
conn.pragma_update(None, "foreign_keys", "ON")?;
conn.execute_batch(r#"
    CREATE TABLE IF NOT EXISTS pages(id INTEGER PRIMARY KEY, parent_id INTEGER, title TEXT, kind TEXT, content TEXT);
    CREATE TABLE IF NOT EXISTS attachments(hash TEXT PRIMARY KEY, size INTEGER, mime TEXT);
    CREATE VIRTUAL TABLE IF NOT EXISTS post_search USING fts5(title, body, tags, tokenize='trigram');
"#)?;

要点:幂等迁移 (IF NOT EXISTS,版本化);外键 + WAL + pragma 初始化 ;派生索引与权威源分离。

7. 取舍与坑

  1. 双写(正文 + FTS)的一致性:FTS 索引写失败要能重建,别让索引成为单点。
  2. 附件内容寻址的引用管理:删页要处理"引用计数",孤立附件靠后台清理,别一把梭删字节。
  3. 每空间独立库 vs 全库一个 db :独立库隔离好、迁移简单,但全空间搜索要跨库合并(对多库做 UNION/FTS 聚合并结果)。
  4. 中文检索:纯 FTS 对不足 3 个字符的中文不友好,必须配 LIKE 兜底。
  5. 备份一致性 :SQLite WAL 下要拿到一致快照(VACUUM INTO / 在线 backup API),别直接拷文件。

8. 结论

本地优先笔记的存储层,核心是权威数据源 + 派生索引分离 、空间物理隔离 、附件内容寻址 、FTS5 中文检索 、WAL + 备份 + 加密。这套设计让"数据真正在自己手里"落地:

  • 数据在本地、格式可导出(能成 Markdown);
  • 索引可重建、备份可还原;
  • 附件去重省空间、内容可校验。

(本文为 ShuyoNote 存储层工程笔记,欢迎评论区交流取舍。)

相关推荐
Lethehong2 小时前
启元 Q1 的控制中枢:输入源优先级仲裁机制拆解
架构·机器人·启元机器人·启元·启元ai
技术原点2 小时前
03 · 战略设计(下):限界上下文与上下文映射
架构
Thomas.Sir2 小时前
第35课:Spring Cloud 2025架构复盘、性能调优 & 面试核心总结
spring cloud·面试·架构
努力实干2 小时前
C++ 面向对象与工程架构
开发语言·c++·架构
aichitang20243 小时前
Meta Muse Spark Open:816B 巨型开放权重多模态 Agent 模型架构、部署实战与社区首测全解
大数据·人工智能·python·算法·ai·架构·spark
阡陌数智3 小时前
Anthropic Claude Fable 5.1 长周期 Agent 原生架构解析:工具调用链路稳定性与 API 聚合平台落地实战
人工智能·语言模型·自然语言处理·架构·推荐算法
艾莉丝努力练剑3 小时前
【AI大模型接入SDK】ChatServer接口实现
c++·人工智能·学习·ai·架构·大模型·sdk
孟健19 小时前
LLM API 安全防线:从 OpenCode 盗刷事件到 LiteLLM 多模型容灾实操
人工智能·后端·架构
smallyoung20 小时前
告别N+1:Spring Boot 一键数据翻译框架体系
java·后端·架构
第404号工位21 小时前
40块钱二手瘦客户端,如何跑满家庭物联网中枢?(拒绝Docker爆盘实录)
linux·架构·node.js