ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

caveman:一个极简离线优先的个人知识库工具

caveman:一个极简离线优先的个人知识库工具 1. “caveman” 到底是什么项目先说结论这不是一个考古主题网站也不是原始人生存模拟软件。“caveman” 是我最近在业余时间折腾的一个极简离线优先的个人知识库工具名字取自“穴居人”那种原始、封闭、自给自足的状态——所有数据都存在本地不依赖云端查询快得像石器时代敲石头那样直接。做这个项目的起因很实在。我过去几年试过 Notion、Obsidian、语雀、飞书文档甚至拿 Bear 和 Logseq 凑合过折腾一圈下来发现一个尴尬的事实越“智能”的工具依赖越重。某个笔记应用说崩就崩某天没有网络就连收件箱都打不开更别提那些一个版本更新就给你塞一堆用不上的“AI 能力”的产品。我需要的是一个能让我完全掌控数据、能在任何机器上快速部署、打开就能搜、关闭就不用管的东西。所以 caveman 的目标很简单一个用纯文本存储、带本地索引、支持命令行和简单 Web 界面的知识管理工具全部代码加起来不到两千行没有数据库服务端没有云同步没有插件市场。它解决的核心问题不是“能不能记”而是“记下来的东西能不能在五年后还能打开、还能搜到、还不受平台绑架”。适合谁来参考如果你跟我一样受够了重客户端和大而全的平台想用自己的方式管理笔记和文档如果你想体验一把从零搭一个够用工具的快感甚至你只是好奇为什么有人愿意拿 Python 写一个号称“回到洞穴时代”的笔记系统——那这篇博文应该能给你一些思路。2. 为什么非要把工具做“原始”聊聊设计思路2.1 离线优先不是倒退是反脆弱很多人一听“本地存储、没有同步”就皱眉现在不都讲多端协同吗我手机、平板、电脑都要看呢这个想法没错但你要分清“协同”和“云端依赖”的差别。我最初也做了 WebDAV 同步后来亲手删掉了。理由之一同步是最容易引入隐性 bug 的地方。两处编辑冲突怎么办文件锁怎么实现断网重连后要不要做 diff这些问题不会让工具“不能用”但会让工具“不可信”——你永远不知道当前看到的内容是不是最新的。理由之二我对工具的核心诉求是“低维护”。一个项目如果为了同步要配服务器、配域名、配 HTTPS 证书、处理各种网络异常那我还不如回去用商业笔记软件。caveman 不解决同步问题它只解决“在你这台机器上存储和检索资料”的问题。跨设备共享直接交给 U 盘、移动硬盘或自建的 NAS 共享目录数据是一堆标准 Markdown 文件拿到任何系统上都能读。我个人的理解是离线优先本质上是一种反脆弱设计。网络是工具不是前提数据永久可读才是底线。caveman 的存储层刻意选用了最无聊、最没技术含量但生命周期最长的格式——纯文本。哪怕有一天 Python 从地球上消失我的笔记也仍然是文本文件用记事本就能打开。2.2 为什么用 Python 而不是 Go 或 Rust选 Python 纯属个人偏好和投入产出比的权衡。我不否认 Go 或 Rust 在性能、静态编译、内存占用上有优势但 caveman 对性能的要求远没到那个量级几千篇笔记的全文检索用 SQLite FTS5 在毫秒级就能完成瓶颈根本不在语言而在索引方式。更重要的是开发效率。整个项目从第一个 commit 到可在命令行正常增删改查只花了一个周末。Python 的标准库里有sqlite3、argparse、http.server、pathlib做这种单机小工具几乎没有外部依赖。你要是在选型时纠结“将来并发上来了怎么办”那就不是在做工具是在做预研项目。先跑起来再谈优化。2.3 设计理念数据库只管索引不做存储这是 caveman 和很多笔记软件在架构上最大的不同。传统方式是把正文放进 SQLite 或者别的数据库里文件系统里只有附件或者干脆啥都没有。这种方式的问题在于正文跟你的文件系统脱节了如果哪一天软件崩溃、数据库损坏你要恢复数据就得靠备份如果手动在文件夹里新增了一个 Markdown 文件那还要软件主动导入才能识别。caveman 反着来所有正文都是磁盘上的.md文件目录结构就是你自己的分类结构SQLite 只存两样东西——文件的路径和全文索引用到的分词内容。这样做有几个直接好处任何时刻删掉index.db你的数据也不会有任何损失重新跑一次全量索引就回来了。直接在文件管理器里手工改文件、挪目录、删笔记caveman 下一次操作时能通过比对文件系统与索引状态自动发现变更。数据不锁死在某个私有格式里以后想迁移到任何别的工具省掉导出转换这一步。这种“文件为主、数据库为辅”的设计才是 caveman 敢拿“原始”当卖点的底气。你甚至可以完全不装这个工具用一个普通文件管理器管理这些笔记——caveman 只是帮你加了个搜索和标签的便利层。3. 动手搭一个核心架构与关键模块实现3.1 整体架构长什么样caveman 由四个部分组成存储层磁盘上的一个根目录内部任何层级的.md文件都被视为“笔记”。索引层以 SQLite 数据库存文件元数据和全文索引FTS5 虚拟表。命令行入口负责增删改查、标签管理、重索引、导出等操作。可选 Web 界面基于 Python 标准库http.server写的一个只读搜索页面方便在浏览器里浏览。开个实话Web 界面是整个项目中我最没上心的部分。它存在的意义仅仅是满足“在浏览器里也能搜”这个场景样式简陋到只有一个搜索框加一个结果列表。但你从反面看正因为它可选核心命令行工具才能保持小而美。3.2 文件扫描与变更检测怎么设计caveman 最关键的两个数据结构是文件哈希和路径映射。每次执行caveman refresh时工具会遍历根目录下所有.md文件计算每个文件的 SHA-256 哈希值然后与数据库中记录的上次哈希值做对比。如果路径没变但哈希变了说明内容被外部修改过需要重新索引如果路径是新的但库中没有说明是新增文件如果库中有记录但文件系统里已经没有该路径说明被删除了。这种设计让我可以直接在系统文件管理器里操作笔记而不必每次修改都通过 caveman 的编辑命令。初版我用的是修改时间戳 mtime 做判断后来发现不可靠——某些编辑器不会每次保存都更新 mtime文件复制、同步工具也容易打乱时间信息。后来全部改成哈希对比扫描时间从原来的快得“不真实”变成每 5000 个文件大概耗时 1 秒多但换来的是极其确定的结果。对于知识管理这种低频写入场景准确性的优先级远高于速度。3.3 全文搜索怎么做才像样一开始我用的是sqlite3自带的无 FTS 版本用LIKE %keyword%做模糊匹配。对这种数据量来说其实也不会太慢但有一个致命缺陷它没法做好分词。中文搜索“自动化”匹配不到“自动”也就罢了连“自动化测试”和“自动化部署”在排序上也完全没区分度。后来换成 FTS5建表语句大概是这样的CREATE VIRTUAL TABLE IF NOT EXISTS docs_fts USING fts5( title, body, contentdocs, content_rowidid, tokenizeporter unicode61 );配合contentdocs实现外部内容表模式这样 FTS 表不复制正文只是用来做倒排索引。查询时用MATCH语法再加ORDER BY bm25(docs_fts)做相关性排序。对于中文unicode61默认按单字切分在搜索短词时效果还行如果你想要更智能的中文分词可以自己挂simple分词器外加自定义词典但我实测下来对个人笔记这种精度完全够用。z注意一个细节FTS5 的虚拟表需要手动处理同步删除。外部内容表模式下如果你删除了 docs 表里的行FTS 表不会自动感知需要在应用层触发INSERT INTO docs_fts(docs_fts) VALUES(delete-from-table)。这个是小坑不处理的话会出现搜到已经删除内容的幽灵结果。3.4 标签系统用文件名还是目录结构标签功能我纠结过很久。方案 A 是传统 frontmatter在每个 Markdown 文件顶部加一行tags: python, note。方案 B 是通过目录层级作为隐式标签notes/python/xxx.md就默认带 python 标签。最终我两个都支持但推荐的用法是 B。原因是 B 更贴合“文件即数据”的设计——你移动文件到另一个目录标签就变了根本不需要改文件内容。而 frontmatter 方案里的标签与文件位置完全无关虽然灵活但容易导致笔记散落各处、难归类。实际使用中我把目录作为一级标签把 frontmatter 里的 tags 作为二级补充。搜索时用tag:项目这样的语法过滤整体体验比较顺。4. 从零到跑通实操记录4.1 初始化假设你已经有一个放笔记的目录比如~/caveman_notes。初始化只需要两步pip install caveman caveman init ~/caveman_notes --name my knowledge base这里--name只是给数据库实例起个可读名称。init 会做三件事创建根目录、生成空的 SQLite 索引文件、写入一个config.json用来存根路径等配置。你也可以先创建项目再加笔记命令是反过来的先caveman new project再caveman add往里丢文件就会自动建目录。4.2 导入现有文件如果你已经把旧笔记整理成了 Markdown 文件直接拷进根目录即可然后执行caveman refresh这条命令会全量扫描并增量更新索引。我自己的目录里大概有 4000 多个文件初次索引花了 4 秒左右之后就只看新增和变更的部分通常不到 0.5 秒。最需要注意的是文件名冲突和非法字符。Windows 和 Linux 对文件名的限制不一样如果以后要在多平台间搬运文件名里尽量不要用: * ? |这些符号。我踩过一次坑从 Windows 复制笔记过来发现有几十个文件名因为含?和:直接失败了最后写了个小脚本批量重命名才解决。4.3 日常写入与搜索日常往库里加笔记的命令很直接caveman add 如何用systemd设置定时任务 --tag linux systemd这条命令会做三件事帮你生成一个以合法文件名保存的 Markdown 文件放在根目录下一个疏略分类的目录里自动在tags字段写入linux和systemd最后触发一次局部索引。文件内容的模板是--- tags: [linux, systemd] title: 如何用 systemd 设置定时任务 --- # 如何用 systemd 设置定时任务 从这里开始写模板文件是可以自定义的想改成自己习惯的 frontmatter 格式很简单在config.json里指定模板路径即可。搜索是使用频率最高的命令caveman find systemd 定时任务 caveman find tag:python AND 爬虫前者做全文相关度排序后者做精确的标签过滤。搜索结果默认输出文件绝对路径、最后修改时间、匹配片段。如果你在浏览器里更舒服还可以执行caveman serve它会启动一个只读的 Web 服务默认绑定 127.0.0.1:8008访问后就能在浏览器里搜索浏览。4.4 数据备份策略本地存储给安全带来了新的问题硬盘坏了怎么办笔记本丢了怎么办我把备份思路简化成“冷热双份”。热备份是指保留一个移动硬盘或 NAS 共享盘用 rsync 每天自动把整个~/caveman_notes目录镜像过去。因为所有数据都是文件rsync 天然友好。冷备份则是每隔几个月打包一次 tar.gz 放在一个外部硬盘角落里。备份的核心不是笔记里的 Markdown 文件因为那部分跟普通文件没区别。麻烦的是你不能只备份 Markdown 文件而忽略索引——索引重建虽然只有 4 秒但如果你有大量自定义标签和特殊 frontmatter 字段比如某些笔记记录了长坐标、日期等结构化信息丢索引虽然不丢内容但要重新整理标签体系就很痛苦。所以我的 rsync 命令把整个caveman_notes目录带上包括index.db一行命令的事rsync -av --delete ~/caveman_notes/ /Volumes/Backup/caveman_notes/这里--delete要谨慎它是把本地删除的文件也同步删除到备份端。如果你希望避免误删误同步就把--delete去掉手动清理备份端即可。5. 踩坑实录这些问题不处理会很难受5.1 FTS5 的“幽灵搜索”我前面提过 FTS5 外部内容表的删除同步问题具体现象是你在文件管理器里删掉了一篇笔记refresh之后文件路径和 docs 表都更新了但再搜索时那篇被删笔记仍然会出现在结果里。原因就是 FTS 虚拟表没有被同步执行 DELETE。解决办法是在删除 docs 表记录时主动对 FTS 表执行 same content 的删除操作。在我的实现中删除某文件时会执行INSERT INTO docs_fts(docs_fts, rowid, title, body) VALUES(delete-from-table, :id, :old_title, :old_body);如果你自己造轮子强烈建议在写删除逻辑时就考虑这一点不然后期数据一多就总觉得搜索结果“闹鬼”。5.2 中文分词的无效搜索FTS5 默认的unicode61分词器会按词法把每个汉字拆开。好处是支持单字匹配坏处是“词”的边界不够聪明搜“数据库”时结果里会出现“数据”和“库”两个词各自匹配的噪声结果。虽然 BM25 排序能把相关性高的放在前面但当你有大量包含其中某个单字的笔记时体验会打折扣。我的妥协方案是在正文之外新增一个叫keywords的字段用简单的人工打标来提升准确率。比如这篇关于 systemd 定时任务的笔记我会在 frontmatter 的keywords里写上systemd、timer、cron、linux。搜索时把字段权重调高SELECT title, snippet(docs_fts, 2, b, /b, ..., 12) FROM docs_fts WHERE docs_fts MATCH :query ORDER BY bm25(docs_fts, 10.0, 5.0, 1.0) DESC这里第一个 weight 10 是 title 权重5 是 keywords1 是 body。结果非常明显正文里偶发出现的同音词、同义字的干扰项就少了很多。这不完美但简单可靠。5.3 大文件索引会拖慢 refresh如果你把 PDF、图片也放到笔记目录里refresh 扫描时就相当于做了一次全量 IO 和哈希。PDF 尤其是重灾区一份几百 MB 的 PDF 哈希也算不了几秒但架不住数量多。后来我把根目录下的附件单独建一个_assets子目录在遍历逻辑里跳过这个目录的索引只把它的文件路径记录为“附件引用”。这样大文件不会影响日常搜索体验。5.4 文件移动后标签丢失当你手动在文件管理器中移动笔记文件从linux目录搬到python目录caveman 在下一次 refresh 时通过路径变化会重新解析标签。但如果你依赖的是“目录即标签”的设计就别忘了移动文件后搜索tag:linux那篇笔记会从结果里消失——这是符合预期的但不是每个人都能第一时间反应过来。为了避免这种“消失了”的惊吓我在caveman find里默认给结果附上完整的路径信息。看到结果时你就能立刻判断这到底是“没有这个标签”还是“标签变了”不会对着屏幕疑惑半天。6. 可选的 Web 界面怎么做到够用许多人对命令行工具的印象是“能用但丑”如果不想把自己困在终端里那就整一个最简单的只读 Web 页面。caveman 的 Web 界面用的不是 Flask、Django就是 Python 自带的http.server加一个BaseHTTPRequestHandler子类处理两个请求GET /返回一个静态 HTML 页面GET /search?qxxx返回 JSON 格式的搜索结果。关键在于别把 Web 服务当主力它只是给搜索加了一个图形化壳。整段实现大约 150 行不涉及任何第三方依赖性能即便在树莓派上也能流畅响应。写这块时我还发现http.server是单线程的——并发一多就会阻塞但因为只有我自己访问完全无所谓。如果你也想抄这个思路提醒一句不要在这上面折腾 Session、登录、鉴权本地工具默认绑定 127.0.0.1 就够了。7. 后续还能怎么玩caveman 现在还处在“自己用得很舒服”的阶段下一步想做的方向有三个第一个是给 SQLite 索引加更细粒度的更新时间记录这样能实现“最近一段时间新增了什么笔记”的周报式回顾第二个是把模板系统扩展成支持变量注入比如自动在标题下方插入当前日期和所在目录所属的项目名第三个是做一个极简的caveman stats命令统计总字数、每日新增笔记数量、每周活跃度等让你对知识库的成长有一个量化感知。不过说实话对这种小工具我最深的体会是功能做到“够用”就该停手了。每次增加新功能都会引入新的边界情况和维护负担而知识管理这类低频场景用户要的第一永远是稳定和可预见。后续无论怎么扩展我都尽量以“不破坏纯文本存储”为前提那些必须依赖专有数据库才能实现的功能原则上不碰。这个项目最大的收获不是代码本身而是让我重新审视了工具应有的边界。好的工具不是功能越多越好而是当你不想打开它时它也完全不打扰你当你需要它时它永远都在而且用最朴素的方式给你精确的答案。现在我用 caveman 管理两千多篇工作笔记和三千多篇个人文章素材从来没有因为工具本身原因丢过一次数据也没在维护上花过超出半小时的时间。如果你也被各种“大而全”的平台折腾得够呛我建议你也花一个周末想一想你真正需要的是什么以及什么东西可以不要。
返回列表