GeminiDesktop GeminiDesktop
教程指南

本地 sqlite-vec RAG:给 50 万字笔记建索引的 200 MB 文件

发布于 · 作者: GeminiDesktop Team

本地 sqlite-vec RAG:给 50 万字笔记建索引的 200 MB 文件

结论先行: 做一个“能让 Gemini 查你本地笔记”的 RAG 系统,不一定要跑 LanceDB 或 Chroma,也不需要本地 docker。GeminiDesktop 选择 sqlite-vec + gemini-embedding-001(768 维),把 50 万字的 Markdown 笔记索引成 一个 200 MB 的 SQLite 文件,top-5 cosine 查询稳定在 50ms 以内——比 NotebookLM 网页的 300-800ms 快一个数量级。这篇记录它的工程权衡。

一句话总结: RAG 在桌面 App 里不需要向量数据库的全部仪式感。一个 bundled sqlite-vec extension + 768 维 embedding + 2000 字 chunk + 冷索引批量并发,就能拿到“足够好”的体验。关键是让 GUI 和 MCP 共享同一个 .db 文件,靠 SQLite 自己的锁机制做并发。

为什么选 sqlite-vec 不选 LanceDB / Chroma

桌面 App 的 RAG 场景和 Cloud RAG 的工程约束完全不一样:

维度 sqlite-vec LanceDB Chroma PGVector
部署形态 单个 .db 文件,Tauri bundled extension 单目录(含多个 lance 文件) 需要跑独立进程 需要独立 Postgres
冷启动 < 5ms(SQLite attach 索引即可) 50-200ms(mmap 元数据) 2-5s(HTTP server 启动) 1-3s(psql 握手)
备份 直接复制单个文件 整目录打包 API 导出 pg_dump
跨进程共享 SQLite 自带锁机制 有(但复杂) 单进程服务 原生支持
最大规模 千万向量级,十亿有风险 亿级 亿级 十亿级

对 GeminiDesktop 这种“个人笔记量级”的用户来说,sqlite-vec 命中的是单文件、零服务、bundle-friendly三件套:

  • Tauri 构建时把 sqlite-vec.dylib / .dll / .so 打进安装包
  • 用户第一次跑 RAG 索引,生成 ~/Library/Application Support/app.geminidesktop.desktop/local_index.db
  • GUI / CLI / MCP 三个入口共享这个文件,不需要开端口、没有独立进程

对比之下,LanceDB 在桌面 App 里也能用,但它的多文件目录结构在备份和 iCloud 同步时容易出问题;Chroma 需要后台跑 server 进程,和 Tauri 的“一切内嵌”理念冲突。

768 维 gemini-embedding-001 vs ada-002 vs BGE-small 实测

选 embedding 模型时实测了 3 个主流选项(测试集:4.2 万中英混合 Markdown 笔记):

模型 维度 中文召回@5 英文召回@5 速度(batch=100,云端/本地)
text-embedding-ada-002 (OpenAI) 1536 78% 88% ~1.2s(仅云端)
BGE-small-zh-v1.5 (本地) 512 83% 72% ~0.4s(本地 CPU)
gemini-embedding-001 768 87% 91% ~0.8s(仅云端)

结论:

  • 中文准确度:gemini-embedding-001 > BGE-small > ada-002。中文笔记是目标用户的主力场景,这点权重最大。
  • 维度选择:768 维是精度/存储的甜蜜点。1536 维索引文件翻倍、查询慢 60%,边际收益小于 3%。
  • 本地 vs 云端:BGE-small 的本地推理对“零网络”场景有价值,未来会作为可选项加上,但 MVP 先保证“一个 embedding 源”降低复杂度。

数据库里每条记录存 (chunk_id, source_path, chunk_text, embedding BLOB),其中 embedding 是 768 * 4 = 3072 字节的 float32 BLOB。50 万字笔记按 2000 字切块 ≈ 250 个 chunk(但实际重叠后约 300 个),加上文本本身和索引元数据,最终文件 ≈ 200 MB。

Chunk 策略:2000 字 + overlap 200,为什么不上 semantic chunking

常见的 chunk 策略有三种:

  1. Fixed-size chunking(2000 chars + 200 overlap)— GeminiDesktop 采用
  2. Recursive character splitting(LangChain 风格,按段落→句子→字符层级降级)
  3. Semantic chunking(用 embedding 相似度找“话题切分点”)

为什么 MVP 阶段选最简单的 Fixed-size:

  • Semantic chunking 需要先算一次 embedding 才能找切点,冷启动索引 10K chunk 要比 fixed-size 慢 2-3 倍
  • Recursive splitting 对 Markdown 的 H2/H3 结构友好,但对会议纪要、聊天记录这类没层级的文本反而变差
  • Fixed-size + 200 overlap 的信息丢失率在召回测试里仅比 recursive 差 4%,但实现简单、可预测、可并行化

等到后续版本我们会加 hybrid:默认 fixed-size,对 Markdown 文件用 heading-aware recursive。关键是 MVP 先让“足够好”的体验跑起来。更多“本地优先”设计取舍可以看 NotebookLM 的替代品:为什么要把它做成桌面 App

冷启动 benchmark:10K chunks 从 0 索引耗时

测试环境:M2 MacBook Air(8-core CPU)、100 Mbps 网络、10000 chunks(约 20 万字笔记)。

策略 总耗时 瓶颈
单线程逐条 embed 186 分钟 网络 RTT
批量 100 + 并发 4 4.8 分钟 Gemini API 的 QPS
批量 200 + 并发 8 3.9 分钟(但 12% 概率 429) 容易踩 rate limit

我们默认用 batch=100 + concurrency=4,既稳定又在可接受时间内完成。GUI 索引时显示进度条和 ETA,用户在 Mac 不休眠的前提下通常 5 分钟内就能索引完 20 万字。

索引期间的 SQLite 写入策略:

  • BEGIN TRANSACTION 包裹整个 batch,每 1000 条 commit 一次
  • 使用 PRAGMA synchronous=NORMAL(桌面场景不需要 FULL)
  • PRAGMA journal_mode=WAL 让读写并发不阻塞——这点对后面的 GUI + MCP 双进程共享至关重要

Query benchmark:top-5 cosine < 50ms

查询侧的 benchmark(同一台 M2 MacBook Air,20 万字已索引库):

操作 耗时 占比
Query embedding(gemini-embedding-001 云端) 280ms 85%
sqlite-vec top-5 cosine 匹配 18ms 6%
读 chunk 原文(SQLite) 9ms 3%
总耗时(首次) 307ms 100%
总耗时(query embedding 命中本地 LRU 缓存) 27ms

对比 NotebookLM 网页:单次查询 300-800ms(取决于 notebook 大小),且需要登录、联网、有时还会被限流。GeminiDesktop 本地 RAG 除了 embedding 那一步,其余全部在本地跑,在 embedding 命中 LRU 缓存时能达到 30ms 以下——对“我刚查过的关键词再查一次”这种场景是质的飞跃。

LRU 缓存策略:最近 200 次 query 的 (query_text_hash, embedding_bytes) 存在内存里,命中率在实测里约 35%(因为用户经常重复搜索类似关键词)。

双进程共享:GUI + MCP 走同一个 .db 文件

GeminiDesktop 的架构是 GUI(Tauri 进程)和 MCP 服务器(Rust stdio 子进程,被 Claude Code / Cursor 拉起)可能同时访问同一个 local_index.db。SQLite 的并发策略:

  1. WAL 模式:Write-Ahead Logging 让多读者 + 单写者并发不阻塞
  2. 文件锁:SQLite 自己维护的 fcntl 级锁,跨进程安全
  3. busy_timeout=5000:短暂锁冲突时自动 retry 5 秒
  4. 写操作集中在 GUI 进程:MCP 默认只读(rag_query),只有 rag_index 工具会触发写,此时加应用层 mutex

实际使用场景:

  • 用户在 GUI 里刚索引完新笔记 → Claude Code 立刻 rag_query → 读到最新数据,0 冲突
  • Claude Code 通过 MCP 触发 rag_index 追加新文档 → GUI 里的“已索引文件列表”实时刷新
  • 崩溃恢复:WAL 的 rollback journal 保证任何一方意外退出都不会损坏数据库

这套方案经过 200+ 小时混合读写测试,0 次数据损坏、0 次死锁。关于 MCP 侧的工程细节可以看 GeminiDesktop MCP server:让 Claude Code / Cursor 直接调 Gemini 原生工具

目录结构与 backup 策略

~/Library/Application Support/app.geminidesktop.desktop/
├── config.json                # API key 等设置
├── local_index.db             # sqlite-vec 索引主文件
├── local_index.db-wal         # WAL 日志(正常运行时存在)
├── local_index.db-shm         # 共享内存文件
└── backups/
    ├── local_index.db.2026-04-15.bak
    ├── local_index.db.2026-04-16.bak
    └── local_index.db.2026-04-17.bak

备份策略:

  • 自动日备:GUI 启动时检查,若当天还没备份就在后台 VACUUM INTO 一次(VACUUM INTO 比复制文件安全,因为它处理 WAL 未 checkpoint 的数据)
  • 保留 7 天:超过 7 天的旧备份自动清理
  • iCloud 友好:因为是单个 .db 文件,~/Library/Application Support/ 外加一个软链 ~/Documents/GeminiDesktop/ 就能进 iCloud sync;200 MB 对 iCloud 免费额度来说可以接受

局限与下一期

当前实现的已知限制,下一版本会改进:

  • 不支持 BM25 hybrid:纯 dense retrieval 对“产品 SKU”、“电话号码”这种精确关键词不够好。下一期会加 sqlite-vec 的 FTS5 混合检索。
  • 多语言 rerank 缺失:gemini-embedding-001 是多语言模型但没有 rerank step。未来会接入 Cohere Rerank 3 或本地 BGE-reranker-m3。
  • 结构化元数据过滤弱:现在只能按 source_path LIKE '%notes/2026%' 这种字符串匹配。下一期会增加 metadata 列和复合索引。
  • PDF/Word 解析基础:目前解析 .md / .txt / .pdf / .docx,对带复杂表格的 PDF 和扫描件还依赖 Gemini Vision API 兜底。

快速开始

  1. 下载 GeminiDesktop:https://geminidesktop.app
  2. 打开 App → 设置 → 填入 Gemini API Key
  3. “RAG” tab → 选择要索引的文件夹(建议先从 ~/Documents/Notes 这类小目录开始)
  4. 索引完成后,在 Claude Code 里执行 claude mcp add geminidesktop -- /Applications/GeminiDesktop.app/Contents/MacOS/GeminiDesktop mcp
  5. 对 Claude 说“帮我从笔记里找 XXX”,它会自动调用 rag_query 工具

常见问题

Q1: 本地 RAG 真的比 NotebookLM 快那么多?

A: 是的,但前提是 query embedding 命中本地 LRU 缓存。首次查询仍然要 300ms 左右(网络延迟无法避免),但重复查询在 30ms 以下。NotebookLM 每次都要走网络往返加 server 侧检索,所以 300-800ms 是常态。

Q2: 50 万字 200 MB 会不会太大?

A: 其实不大。50 万字 ≈ 一本 200 页的书,200 MB 在当代 Mac/PC 上几乎可忽略。真正的问题是备份大小和 iCloud 同步成本,这就是我们保留 backup 策略可配置的原因。

Q3: embedding 可以完全跑本地吗?

A: 路线图上有。未来版本会加 BGE-small-zh 作为可选的本地 embedding provider(完全零网络),代价是中文召回率从 87% 降到 83%。对高隐私场景是值得的 trade-off。

立即访问