资讯动态

自建知识管理工具atlas:SQLite+FTS5实现可检索的知识地图

发布时间:2026/10/20 9:38:37 来源:尧图企业网站定制
接手过太多知识管理工具从最早的文件夹笔记到后来的双链笔记、白板工具我一度以为“工具越多知识越整”。结果恰恰相反越换工具资料越散旧笔记像一座座废弃的孤岛救不回来也沉不下去。后来我花了大概三周时间自己动手写了一个叫atlas的个人知识地图项目核心思路特别朴素把每一条知识当作地图上的一个“地点”把笔记之间的引用关系当作“道路”再用一套轻量级的本地服务把这些东西组织成可检索、可漫游、可回溯的知识册。这篇文章就把 atlas 从设计动机、存储模型、核心实现到日常使用流完整拆开讲一遍适合正在折腾知识管理、想自建个人知识库、或者对轻量级本地应用感兴趣的朋友参考。1. 从“信息孤岛”到“知识地图”atlas 的定位与设计初衷1.1 传统笔记工具到底卡在哪我见过太多人在笔记软件里收藏了上千篇文章但真正需要的时候翻不出来。问题不在于“没存”而在于“存进去之后知识之间没有连接”。文件夹式的笔记天然是一棵倒立的树一个节点只能有一个父节点可真实世界的知识是网状的。比如我研究“SQLite 全文索引优化”这个问题时它同时关联数据库、搜索引擎、文本处理三条线树状结构根本无法表达这种多父节点的关联。后来有了双链笔记双向链接确实解决了一部分问题但很多人用着用着就变成了“链接堆砌”——每条笔记都在疯狂加[[链接]]最后关系图密密麻麻一团乱麻根本看不出知识的主次和结构。我自己就经历过这个阶段花了两个月给旧笔记补链接结果发现关系图看起来像一碗炒面对我回忆知识没有任何帮助。1.2 atlas 的核心理念把知识当“地点”把笔记当“路标”做 atlas 之前我反复问自己一个问题地图软件是怎么帮人找路的答案很朴素有明确的地点坐标有道路连通有图层切换。我想把同样的逻辑搬到知识管理里于是定了三个核心概念地点Node一条独立的知识条目拥有标题、摘要、正文和一组坐标标签。道路Link两条知识之间的关联关系分为“引用”“扩展”“对比”“前置依赖”四种类型。图层Layer同一个知识库的不同观察视角比如“按时间看”“按主题看”“按项目看”。这样设计的好处是它没有强制要求你把所有知识都织成一张大网而是允许知识以“景点”的方式松散存在只有当你主动建立关系时道路才会出现。这避免了双链笔记那种“为链接而链接”的焦虑。1.3 这个项目适合谁不适合谁说实话atlas 不是要替代所有笔记软件它更适合下面几类人研究者或写作者需要反复整理同一批素材并提炼结构需要跨项目沉淀经验的工程师希望把踩坑记录变成可检索的技术地图对数据有强控制欲的人希望笔记以裸文件方式存储随时可以导出自持。反过来如果你只想要一个“打开就写、写完就走”的快速记录工具atlas 反而是多余的。它在读、查、关联上的优势远大于在“速记”上的优势。认清边界再动手能省掉很多无用功。2. 技术底座与存储模型用最简单的方式搭出一套可扩展的框架2.1 为什么选 SQLite 全文索引不选重型数据库第一版 atlas 我差点用了 PostgreSQL后来冷静下来算了一笔账个人知识库的量级通常就是几千到几万条文本记录单机访问没有并发写入压力这种情况下引入独立数据库服务完全是在给自己找运维麻烦。SQLite 单文件、零配置、备份就是一个cp命令天然适合这种“个人级数据仓库”场景。但 SQLite 自带的LIKE查询在文本检索上非常弱所以我在表结构之外给正文和标题单独建了 FTS5 全文索引。这里有个容易被忽略的细节FTS5 默认的分词器对中文支持不好中文文本会被整句切分搜一个词往往匹配不到。实际使用中我给 FTS5 配了tokenize unicode61同时自己在写入时用 jieba 做了粗粒度分词把分词结果存到单独一列再丢给 FTS5。这样既保留了 SQLite 的轻量又让中文搜索达到了可用水平。2.2 三层数据模型实体、关系、视图atlas 的数据模型围绕三个表展开结构非常简单-- 实体表一条知识条目 CREATE TABLE entities ( id TEXT PRIMARY KEY, title TEXT NOT NULL, summary TEXT, body TEXT, layer TEXT DEFAULT default, tags TEXT, created_at TEXT, updated_at TEXT ); -- 关系表记录两条实体间的有向关联 CREATE TABLE links ( id INTEGER PRIMARY KEY AUTOINCREMENT, source_id TEXT NOT NULL, target_id TEXT NOT NULL, link_type TEXT NOT NULL, -- cite / extend / contrast / prerequisite note TEXT, created_at TEXT ); -- 索引表FTS 全文索引 CREATE VIRTUAL TABLE entity_fts USING fts5( title, summary, body, contententities, content_rowidrowid );这里我把layer做成了实体上的一个字段而不是单独一张表。原因是大多数笔记实际上只属于一个主视角把图层做成字段可以少做一次关联查询。只有当图层之间出现大量交叉归属时才值得拆成多对多关系表。这也是我当时权衡过的点先满足 80% 的简单需求不提前引入复杂性。2.3 目录结构设计让数据裸文件可读可迁移数据表结构再清晰如果数据只能通过程序访问一旦工具停止维护知识就被锁死了。所以我在 atlas 里做了一个很“笨”的设计每个实体除了进入数据库还会同步落盘为一个 Markdown 文件放在atlas_data/entities/目录下文件名就是实体 ID文件头部用 YAML 存元信息正文就是纯 Markdown。--- id: 20240512-sqlite-fts5 title: SQLite FTS5 中文分词实践 layer: tech tags: [数据库, 全文检索, 中文分词] created_at: 2024-05-12 --- 我在给 atlas 搭建全文索引时遇到中文分词匹配不到的问题...这样做的直接好处是就算 atlas 某天彻底不能运行我依然能用任何文本编辑器打开这些 Markdown 文件用 grep 检索内容甚至写个脚本迁移到其他工具。对个人知识库来说数据的长期可读性比软件功能的华丽程度重要得多。3. 从零手写一个可用版本核心接口与关键实现3.1 环境准备与初始化atlas 的后端我用 Python 实现依赖非常克制fastapi提供 HTTP APIsqlite3标准库操作数据库jieba做中文分词markdown做渲染。前端先不做复杂界面用一套简单的 Web 页面展示关系图和列表。初始化时先建数据库和目录结构核心代码如下import sqlite3 from pathlib import Path def init_atlas(data_dir: Path): data_dir.mkdir(parentsTrue, exist_okTrue) (data_dir / entities).mkdir(exist_okTrue) conn sqlite3.connect(data_dir / atlas.db) conn.executescript( CREATE TABLE IF NOT EXISTS entities (...); CREATE TABLE IF NOT EXISTS links (...); CREATE VIRTUAL TABLE IF NOT EXISTS entity_fts USING fts5(...); ) conn.commit() return conn3.2 实体管理接口增删改查背后的约束实体管理的核心不是 CRUD 本身而是 CRUD 过程中的数据一致性。我在设计接口时额外做了三件事标题唯一性校验同一标题的笔记重复出现是知识库混乱的最大来源。我在写入时先对标题做一次规范化处理去掉首尾空格、统一全角半角再查重重名直接拒绝写入并提示已有实体 ID。正文分词幂等每次更新正文都会重新分词并同步到 FTS 索引保证搜索结果不滞后。落盘同步数据库写入成功后同步把 Markdown 文件写回磁盘目录。这两步不是原子操作所以我先写内存数据库、再落盘如果落盘失败则在下次启动时通过比对updated_at自动补偿。def create_entity(conn, entity: dict): normalized_title entity[title].strip().lower() existed conn.execute( SELECT id FROM entities WHERE lower(title) ?, (normalized_title,) ).fetchone() if existed: raise ValueError(ftitle conflict with {existed[id]}) # 写入 entities FTS 落盘 Markdown ...3.3 关系构建与图遍历怎么做到“相邻知识一目了然”关系的价值在于“发现相邻知识”。我实现了一个简单的两跳图遍历接口给定一个实体 ID找出它直接关联的所有实体再找这些实体之间相互关联的情况最后按关联强度排序返回。def neighbors(conn, entity_id: str, depth: int 2): visited set() queue [(entity_id, 0)] result [] while queue: current, dist queue.pop(0) if current in visited or dist depth: continue visited.add(current) rows conn.execute( SELECT target_id, link_type, note FROM links WHERE source_id ? UNION ALL SELECT source_id, link_type, note FROM links WHERE target_id ? , (current, current)).fetchall() for target, link_type, note in rows: if target not in visited: result.append((target, dist 1, link_type, note)) queue.append((target, dist 1)) return result这个实现朴素但在几千条数据量级下性能完全够用。真正需要优化的点在于结果的组织我按“直接关联”和“间接关联”分成两组展示避免把关系图搞成一团乱麻。这也是 atlas 和那些自动把所有节点全画出来的关系图工具的关键差异——关系图要克制只显示当前节点两跳以内的邻居才能起到导航作用。3.4 视图层热力图、时间线、关系图的取舍视图层我做了三个列表视图支持按图层、标签、时间筛选展示标题和摘要适合快速扫读。关系视图基于上面说的两跳邻居接口渲染当前节点的局部关系网络适合漫游时发现意外关联。时间线视图按created_at聚合每月的笔记数量直观看出自己的知识产出节奏。这里我没有做花哨的热力图原因是前期数据量不够时热力图很容易变成一张“全绿”的无意义图片。等数据积累到上千条再考虑也不迟。做工具的教训就是不要为了界面好看而造数据指标要和真实使用习惯对齐。4. 日常使用流设计不改变习惯的前提下完成知识收敛4.1 导入流程旧笔记批量迁移工具做得再好如果迁移成本太高最后还是会吃灰。我把导入流程专门做成了一个独立脚本支持从 Markdown 文件夹批量导入。导入逻辑很简单递归扫描目录下所有.md文件读 YAML 头部的元信息正文写入body文件名作为标题再把目录名映射成图层名。python -m atlas.importers.markdown_folder \ --source ~/Documents/old_notes \ --map-dir-layer notes:writing, tech:dev实际跑下来我迁移了 600 多篇旧笔记其中大概有 15% 出现标题重复或 YAML 头缺失脚本会把这些文件单独放到一个conflicts/目录下不会直接覆盖已有数据。这个“先隔离再处理”的思路帮我避免了很多数据事故。4.2 每日记录与周度回捞知识管理最大的敌人不是工具不够好而是“没有固定节奏”。我给 atlas 设计了一个非常轻量的使用循环每日记录用极简的“随手记”接口写下当天的收获只填标题和正文不强制打标签、不强制建链接。每周回捞每周末抽半小时把本周随手记的条目做一次梳理该建关系的建关系该合并的合并。回捞这个动作是 atlas 整个体系里价值最高的部分。随手记的时候我最放松记录的是原始想法回捞的时候我带着整理心态能发现很多原本没意识到的联系。比如我有一篇随手记写“SQLite 锁等待超时”另一篇写“FastAPI 异步接口并发问题”回捞时才发现两者背后都指向同一个根源单连接串行写入。于是我把这两篇用contrast关系连了起来还顺手补了一篇综合笔记。4.3 标签 vs 关系我踩过的“标签膨胀”坑我最开始给 atlas 设计了完整的标签体系结果用了两周就发现标签数量膨胀到收不住——“性能”和“性能优化”是两个标签“数据库”和“数据库优化”又是两个。后来我彻底转变了用法标签只表达“稳定身份”关系表达“临时关联”。标签只保留固定的领域词比如“数据库”“前端”“写作”不再新增细粒度标签。凡是临时的、主题性的关联全部通过“关系”来表达。这个转变非常关键。标签是慢变量关系是快变量。把两者职责分开后维护成本立刻降了下来。如果你也在做自己的知识库强烈建议先想清楚这个边界。5. 实测中遇到的典型问题与排查链路5.1 全文索引突然搜不到刚写入的内容有一天我写完一条笔记立刻搜索刚出现的关键词结果什么都搜不到。第一反应是索引没建但检查 FTS 表发现数据是有的。接着排查发现我在写入实体时先更新了entities表再把分词结果插入entity_fts但这两个操作在同一个事务里而 FTS5 的外部内容表模式需要保证content表和 FTS 索引的同步顺序。问题根因是FTS5 在contententities模式下删除和更新操作需要显式同步删除旧索引我只处理了新增漏了更新场景。修复方案是给 FTS 表挂了触发器CREATE TRIGGER entities_ai AFTER INSERT ON entities BEGIN INSERT INTO entity_fts(rowid, title, summary, body) VALUES (new.rowid, new.title, new.summary, new.body); END; CREATE TRIGGER entities_ad AFTER DELETE ON entities BEGIN INSERT INTO entity_fts(entity_fts, rowid, title, summary, body) VALUES (delete, old.rowid, old.title, old.summary, old.body); END; CREATE TRIGGER entities_au AFTER UPDATE ON entities BEGIN INSERT INTO entity_fts(entity_fts, rowid, title, summary, body) VALUES (delete, old.rowid, old.title, old.summary, old.body); INSERT INTO entity_fts(rowid, title, summary, body) VALUES (new.rowid, new.title, new.summary, new.body); END;这里还想提醒一句如果是用unicode61分词器中文检索依然可能因为分词粒度对不上而搜不到。我当时最终方案是写入时用 jieba 分词后把分词结果以空格连接存进body_tokens字段再对该字段建 FTS。实测之后类似“全文索引”“中文分词”这种复合词都能稳定命中。5.2 关系环导致的递归死循环给关系图加“两跳遍历”时我一开始没有加visited集合结果笔记 A 引用 B、B 引用 C、C 又引用 A 时遍历直接陷入死循环接口超时。后来加了访问标记问题解决。但这里真正要小心的不是“防止死循环”这个动作本身而是环状关系在知识库中是常态不是异常。所以在遍历结果里我保留了环状关系的展示只是不让它继续递归扩展。这样做的好处是读者能直观看到“三个知识点互为支撑”而不是被工具强行抹平。5.3 SQLite 并发写入冲突与锁等待atlas 早期版本直接在 FastAPI 的异步接口里用同一个 SQLite 连接执行写入。当我在浏览器里连续快速创建多条笔记时偶尔会出现database is locked错误。排查过程分了三步先确认是否有长事务没提交。检查后发现没有。再确认是否多个连接同时写。发现是同一个连接被异步任务并发调用。解决方案是给写入操作加一个全局写锁并设置busy_timeout。write_lock threading.Lock() def safe_write(conn, sql, params): with write_lock: conn.execute(sql, params) conn.commit()个人知识库的写入频率本身不高加锁带来的性能损失可以忽略不计但换来的是绝对稳定的写入行为。如果你也打算在本地 Web 服务里用 SQLite建议一开始就把“单写者”这个模型确定下来否则后面会遇到各种奇怪的锁问题。6. 给想把 atlas 用起来的人一些后置经验与建议6.1 先小规模跑通再全量迁移这是我踩过最大的坑换来的经验。一开始我雄心勃勃想把过去五年的笔记一次性迁移进 atlas结果导入后发现大量旧笔记质量参差不齐要么是随手粘贴的链接要么是残缺的摘抄把整个库的检索体验都拉低了。建议的做法是先只迁移最近三个月的笔记跑通“记录—回捞—检索—关联”这个完整循环确认自己真的能用起来再分批导入历史数据。同时导入历史数据时不要把内容原封不动塞进去最好在导入时顺手做一次“修剪”——删掉无意义的收藏把值得长期留存的提炼成完整条目。6.2 定期巡检与导出备份策略数据安全这件事再怎么强调都不过分。atlas 的数据包含 SQLite 数据库文件和 Markdown 裸文件我的备份策略是每天凌晨执行一次快照保留最近 30 天版本#!/usr/bin/env bash # /usr/local/bin/atlas_backup.sh BACKUP_DIR$HOME/atlas_backups/$(date %Y%m%d) mkdir -p $BACKUP_DIR cp -r $HOME/atlas_data $BACKUP_DIR/ find $HOME/atlas_backups -type d -mtime 30 -exec rm -rf {} \;除了自动备份我还坚持每月手动导出一次全量 Markdown 文件放到另一块移动硬盘上。这么做不是为了应对硬盘损坏而是给自己留一条“随时可以脱离工具独立阅读”的后路。知识管理工具最怕的就是变成单向门存进去容易拿出来难。6.3 下一步可以怎么扩展atlas 目前还处于“个人可用”的阶段后面有几个我计划延伸的方向也供你参考定时自动抓取针对我经常查阅的几类技术文档站点每周自动抓取更新内容生成“变更摘要”实体并和现有的技术笔记自动建立extend关系。多人共建在现有实体表上增加owner字段把同步方式改为基于 Git 的协作流让团队里每个人维护自己负责的主题图层。自然语言查询接入一个本地跑得动的小模型把“我上周看过哪些关于 SQLite 的笔记”这类自然语言问题转成查询条件降低检索门槛。这些扩展都建立在现有数据模型之上不需要推翻重来。这也是我把数据模型设计得克制的原因结构越简单扩展的空间反而越大。最后分享一个实际使用中的心得atlas 真正改变我的不是“搜索更快”或“关系更清晰”而是让我养成了一个习惯——每周固定时间回捞整理当周的零散记录。工具只是提供了一个固定节奏的容器真正让知识沉淀下来的是那个雷打不动的整理动作。如果你也想建一套自己的知识地图不妨也从“先定节奏、再做工具”开始可能比你想的更有效。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑