拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Cherry Studio 知识库存储与检索技术设计:从 raw 文件到 per-base 派生索引的实现剖析

Cherry Studio 知识库存储与检索技术设计从 raw 文件到 per-base 派生索引的实现剖析【免费下载链接】cherry-studioAI productivity studio with smart chat, autonomous agents, and 300 assistants. Unified access to frontier LLMs项目地址: https://gitcode.com/GitHub_Trending/ch/cherry-studio本篇技术指南深入解析 Cherry Studio 知识库Knowledge模块的存储与检索实现契约。文档虽然保留在docs/references/knowledge/experiment/路径下但描述的是当前已落地的存储与检索设计包括KnowledgeBase/{baseId}/raw/原始文件布局、index.sqlite派生索引的七表模型、BM25 / 向量 / 混合三种检索模式以及写入不变量与迁移验证机制。读完本文你将掌握 Cherry Studio 知识库主库权威 每库独立派生索引的整体架构理解其路径安全边界、稳定 ID 生成、FTS5 外部内容表同步与倒排索引重建策略可直接对照源码继续深挖。1. 所有权模型谁决定业务条目是否存在Cherry Studio 知识库遵循一条清晰的权责划分原则主数据库表是权威派生索引只是投影。knowledge_base表拥有配置、分组、就绪状态status与error以及可恢复错误knowledge_item表拥有材料身份、层级关系、类型化源数据、生命周期状态与条目级错误。而每个知识库目录下的文件系统仅存放知识库自有的原始字节与一个派生的检索索引。渲染层renderer与检索索引都无权决定哪些业务条目存在——列表、可见性与生命周期决策最终都回到knowledge_item表。这一点可以从 src/main/data/db/schemas/knowledge.ts 的表结构中得到印证knowledge_base记录chunkSize、chunkOverlap、chunkStrategystructured/delimiter、threshold、documentCount等检索配置并通过 CHECK 约束保证status completed时要么同时具备embeddingModelId与dimensions要么两者皆为空knowledge_item则以typefile/url/note/directory和细粒度的statusidle→preparing→processing→reading→embedding→completed/failed/deleting来描述生命周期。这套模型的直接后果是索引行本身永远不授予可见性。任何基于 Concept ID材料相对路径的读取都会先在主库中重新校验该材料对应的knowledge_item是否为已完成且属于同一知识库通过后才允许读取。2. 存储布局raw 材料根与 .cherry 控制目录pathStorage.tssrc/main/features/knowledge/pathStorage.ts负责将每个知识库解析到application.getPath(feature.knowledgebase.data)之下的固定目录结构KnowledgeBase/{baseId}/ raw/ paper.pdf paper.md captured-page.md imported-directory/subtree/file.txt .cherry/ index.sqlite布局要点如下raw/是材料根目录。knowledge_item.data.relativePath是一个相对此根的 POSIX 风格路径存储契约固定为/分隔见PosixRelativeFilePath。文件类型与来源永远从条目读取而不是从路径段推断。材料不会按导入动作类型分子目录目录布局是内部实现细节。目录导入会保留一个顶层前缀并在该前缀下保留源目录的嵌套结构后续对目录内文件的索引与展开都发生在该子树内。.cherry/是保留的控制存储其中index.sqlite是每个库独立的 better-sqlite3 数据库VECTOR_STORE_FILE常量不属于主库的 Drizzle 迁移链。2.1 路径安全边界双重防护所有到达文件系统操作的相对路径必须先通过两层检查assertSafeKnowledgeRelativePath先经KnowledgeRelativePathSchema校验形状不能是绝对路径、不能含空字节、必须是 POSIX 合法段再检查首段是否命中保留前缀.cherry源码注释明确说明直接位于raw/下的合法 Linux 文件名.cherry\x不应被误判因此按首段判定而非折叠分隔符参见 #17429 的修复宿主机侧path.relative检查assertResolvesBelow因为assertSafeKnowledgeRelativePath按 POSIX 读取、path.join按宿主机读取两者对\的解释不同——一个合法存储的..\outside.pdf在 Windows 上可能爬出raw/甚至借deleteKnowledgeItemFiles带走整棵目录树。该函数拒绝落回根、..、..前缀与绝对路径并抛出escapes the material root on this platform。这套防护的针对性测试可以在 pathStorage.test.ts 与 pathStorage.win32.test.ts 中找到。2.2 导入、命名保留与删除导入时字节被复制进知识库copyFileIntoKnowledgeBaseAttmprename 原子提交支持overwrite用于目录展开的自覆盖。文件名与未来会生成的 processed-Markdown 兄弟文件共享同一套保留集合reserveImportedFileRelativePath/collectKnowledgeReservedRelativePaths只有源文件确实会经过文件处理器扩展名在knowledgeFileProcessingExts集合内时才会同时保留其.md工件槽位这样导入时选定的名字永远不会与后续索引作业写入的paper.md冲突。冲突统一以_N后缀解决nextFreeKnowledgeRelativePath。删除整个知识库会移除整个 base 目录deleteKnowledgeBaseDir单条目清理deleteKnowledgeItemFiles只移除被解析子树所拥有的 raw 路径——目录容器按前缀递归删除其余叶子按存储路径逐一 unlink最后pruneEmptyKnowledgeMaterialDirs自底向上清理空目录ENOTEMPTY/ENOENT是良性结果只打日志不中断。3. URL 与笔记快照OKF frontmatter 的写入与剥离URL 与笔记内容同样以 Markdown 形式捕获进raw/。应用写入的快照携带顶层 Open Knowledge FormatOKFfrontmatter当前字段包括type、title、timestampURL 来源额外带resource。实现位于 src/main/features/knowledge/pipeline/sources/okfFrontmatter.tsserializeOkfFrontmatter按 OKF 优先级顺序type → title → resource → timestamp输出键所有值都用 JSON 引号包裹——合法的 YAML 双引号标量保证值里即使含---或#也永远不会形成定界符或注释行stripOkfFrontmatter在索引前移除单个前导块---\n起始、\n---\n闭合正文即使自身以---开头也会原样保留。序列化与剥离必须是精确互逆索引读取快照并剥离该块以恢复规范化的content.text任何漂移都会让每个快照被哈希为已修改。只有快照读取器调用strip用户上传的文件永远不会走这条路径。快照的relativePath会被写回knowledge_item.data完成文件系统与主库的绑定。4. 每库索引 schema七对象模型index.sqlite是独立于主 Drizzle 迁移链的 better-sqlite3 数据库。schema.tssrc/main/features/knowledge/pipeline/vectorstore/indexStore/schema.ts通过KNOWLEDGE_INDEX_SCHEMA_STATEMENTS创建七个对象——六张普通表加一张 FTS5 虚拟表表当前职责meta知识库身份与索引 schema 版本material材料身份、相对路径与当前内容指针content完整规范化文本按内容哈希去重search_unit带字符偏移的有序分块search_text供 FTS 与 embedding 消费的文本投影embedding以 embedding 文本哈希为键的 Float32 向量 BLOBsearch_text_fts基于search_text.text的 Trigram FTS5 外部内容索引打开索引时读取meta.schema_version非空但不匹配则先删库重建这是一个可重建的派生索引再应用当前 DDL新文件/空文件没有存储版本直接初始化为空索引。删除顺序KNOWLEDGE_INDEX_DROP_STATEMENTS严格按子→父先删触发器与 FTS 虚拟表再删search_text/embedding/search_unit/material/content/meta避免在PRAGMA foreign_keys ON下因父表被引用而报错。4.1meta单行身份与防挂载meta固定单行id 1含schema_version、base_id与时间戳。ensureIndexMetaindexMeta.ts首次打开时INSERT OR IGNORE写入身份行随后校验存储的base_id是否等于期望值不匹配直接拒绝——从其他知识库换入的index.sqlite不会被静默挂载。这是唯一的拒绝场景空文件或重建文件没有可冲突的行会作为全新的空索引挂载如果这种情况发生在一个仍有已完成条目的知识库下KnowledgeVectorStoreService会记录不一致日志而不是把空搜索结果当作正常。4.2material检索投影与 Concept IDmaterial.material_id与knowledge_item.id相等relative_path唯一并作为kb_read/kb_manage使用的Concept ID。表很小只是检索投影——展示字段、生命周期状态、来源与错误都留在knowledge_item。schema 层还有防御性 CHECKrelative_path非空、不以/开头、不等于.cherry且不以.cherry/开头。搜索与 Concept ID 解析都会重新校验 material ID 对应的knowledge_item是否已完成且同库索引行本身不授予可见性。getMaterialByRelativePath就是 Concept ID 深度读取背后的查找relative_path唯一调用方再回主库校验可见性后才读取内容。4.3 content 与 search_text哈希去重与共享content_hash sha256(content.text)材料指针指向其当前内容分块引用同一份全文行。content表以content_hash为主键同一全文只存一份INSERT OR IGNOREsearch_text目前每个分块存一个body投影相同 body 文本通过embedding_text_hash共享同一向量。schema 注释明确说明文本已体现规范化规则因此不另存normalization_version也不存text_format因为目前没有消费方分支。4.4 稳定单元身份hashing.tssrc/main/features/knowledge/pipeline/vectorstore/indexStore/hashing.ts定义了全部 ID 规则computeUnitId(materialId, contentHash, unitType, unitIndex, charStart, charEnd)对六元组做 sha256。用相同材料、相同内容、相同分块结果重建必然复现相同单元 IDcomputeSearchTextId(targetType, targetId, kind)对三元组做 sha256分块配置chunker config不嵌入每个 ID物理索引契约变更通过递增KNOWLEDGE_INDEX_SCHEMA_VERSION触发整库重建而不是把配置烤进每个单元 ID详见第 9 节。4.5 FTS 行身份稳定fts_rowid而非隐式 rowidsearch_text_fts是 external-content FTS5 表使用search_text.fts_rowid作为content_rowid而不是 SQLite 隐式 rowid。原因在 schema 注释中写得很清楚隐式 rowid 会在 VACUUM 或表重建时被重新编号导致外部内容索引静默失步#16132 类缺陷的修复与聊天message_fts表同款方案。三个触发器维持同步search_text_aiAFTER INSERT为行分配fts_rowid COALESCE(MAX(fts_rowid), 0) 1并插入 FTS 行。该列设计为可空——NOT NULL 会在触发器运行前拒绝整行search_text_adAFTER DELETE以delete指令清理 FTS 行search_text_auAFTER UPDATE OF text先删后插但fts_rowid稳定不重新分配。search_text_fts_rowid_uniq唯一索引使MAX(fts_rowid) 1成为 O(log N) 查找并大声拒绝重复该分配无竞态的前提是同一知识库的索引写入都被上游的KeyedMutex.runExclusive串行化见 BetterSqlite3Driver.ts。查询时必须用search_text_fts.rowid search_text.fts_rowid回连search_textkind过滤也经由该连接完成不存入 FTS 表。5. 写入不变量5.1 rebuildMaterial单驱动事务内的原子替换KnowledgeIndexStore.rebuildMaterial()KnowledgeIndexStore.ts在一个同步驱动事务内替换单个材料的全部内容、单元、搜索文本与提供的向量。事务内的关键步骤记录材料先前的current_content_hash用于判断 GC 是否可能产生孤儿INSERT OR IGNORE INTO content——内容按哈希不可变已存在则复用upsertmaterial行删除该材料旧的 search_text 与 search_unit写入新单元及 body search_textFTS 由触发器同步写入缺失向量INSERT OR IGNORE已有哈希复用回写current_content_hash仅当确实可能产生孤儿删除了旧单元或内容哈希发生变化时才执行collectIndexGarbage全表反连接清理——把批量索引从 O(K × 表) 优化为 O(K)。embedding 覆盖检查在提交前进行6b 步对向量型知识库每个单元的 embedding 哈希必须能解析到向量否则整个重建回滚。这一检查同时兜住两类故障调用方对分块文本哈希与存储侧对重切片 body 哈希不一致导致的静默缺失以及listExistingEmbeddingHashes竞态调用方在锁外读取哈希GC 并发删掉了它声称存在的向量作业重试时重新嵌入即可收敛。5.2 应用级互斥KeyedMutex 不替代事务同一知识库的高层变更在改变索引、主库状态或知识库自有文件时都持有 Knowledge 的KeyedMutex。该应用互斥用于串行化跨存储的不变量并不替代主库事务——真正的数据一致性仍由主库事务保证。5.3 字符偏移不变量每个单元都存[char_start, char_end)到完整content.text且必须满足content.text.slice(char_start, char_end) search_text 中的单元 bodyrebuildMaterial在写入时先做越界检查charEnd content.length直接抛错而不是让slice()静默截断schema CHECK 再保证char_end char_start。读取侧kb_read返回完整索引内容或受限切片分块检查按unit_index有序返回。readMaterialContent读回current_content_hash指向的原文与单元切片逐字一致。5.4 驱动与向量可移植性§5.6索引存储依赖一个窄同步 SQLite 驱动接口SqliteDriver/SqliteExecutor实现见 BetterSqlite3Driver.ts当前引擎是 better-sqlite3 sqlite-vec。设计决策DDL 全部使用普通 SQLite 表 FTS5无任何引擎专有列类型或函数向量是普通 BLOB 中的裸小端 float32 字节encodeVectorBlob见 vectorBlob.ts维度不在 DDL 里因此语句数组是静态的sqlite-vec 在查询时提供vec_distance_cosine标量函数不存在 ANN/向量虚拟索引——向量检索是暴力扫描详见第 6 节。驱动为每个连接开启外键PRAGMA foreign_keys ON必须在事务外设置。调用方绝不 await 索引 SQL也绝不从索引事务回调返回 Promise——事务必须完全同步执行。6. 检索三种模式与知识库级后处理KnowledgeIndexStore.search()支持bm25、vector、hybrid三种模式单元 body 文本是两个通道共同的检索源BM25走 trigram FTS5MATCH查询对 trigram 车道无法索引的过短 token典型如 1–2 个字符的 CJK 词实现 LIKE 回退全查询都需要回退时退化为LIKE %token%子串扫描并按 body 长度升序更密集的短命中排前主路径上则把 2 字符短词作为LIKE过滤 AND 进 MATCH 查询若过滤把候选全部清零则放宽重试。bm25()得分越低越好返回前取负转为越高越好向量扫描存储的全部 embedding以vec_distance_cosine计算余弦距离升序取 top-K。WHERE dist IS NOT NULL会剔除零范数退化向量其余弦距离未定义否则 NULL/NaN 会排在最前并得满分混合以prefetch topK * 5超量拉取两个通道用**倒数排名融合RRF**合并contribution weight / (RRF_K rank)其中RRF_K 60、向量权重alpha默认 0.5、BM25 权重1 - alpha取 top-K 按融合分降序。基于排名的融合绕开了 cosine 与 BM25 分数尺度不可比的问题。融合实现与测试见 KnowledgeIndexStore.rrf.test.ts。KnowledgeQueryServicesrc/main/features/knowledge/query/KnowledgeQueryService.ts负责知识库级后处理无 embedding 模型的知识库选 BM25向量能力库选混合随后把候选过滤到已完成且同库的条目可选重排rerank裁剪到documentCount ?? 10阈值只作用于相关分最后分配排名。概念读取Concept reads解析material.relative_path、重新校验条目可见性、读取材料完整content.text组织树由knowledge_item.groupId层级构建而不是扫描raw/——再次印证主库权威、索引/文件系统只是投影。7. 删除与空间回收deleteMaterials为每个材料开独立短事务删除级联清 search_unit显式清 body search_text 并经触发器清 FTS每运行DELETE_YIELD_BUDGET_MS 50ms就setImmediate让出一次主进程事件循环防止大目录删除数万行 FTS 触发器的同步执行在 macOS 上变成 beachball之后用单次collectIndexGarbage统一回收孤儿 embedding/content——把旧的 O(材料数 × 表) 批量删除降为 O(N 表)。失败语义也明确了中途失败时已删材料已提交而所有调用方subtreePurge.ts都是先删向量再删knowledge_item行因此重试会精确重新发现剩余材料重复删除是无害 no-op。reclaimSpace()先执行 FTS5optimizeINSERT INTO search_text_fts(search_text_fts) VALUES(optimize)再做阈值门控的VACUUM删除只会给 trigram 条目打墓碑段 blob 仍以活跃行形式留在search_text_fts_data影子表里VACUUM 自己回收不了它们optimize合并并丢弃这些段之后VACUUM 才能把释放页还给操作系统。驱动按 freelist 阈值门控小删除不会为整个索引的段合并付费。8. 源可用性重建路径与拒绝删除不同条目类型的重建前提不同文件根从其知识库自有的 raw 文件重建目录根重新扫描其原始绝对源目录存在data.source中重建URL 与笔记条目走各自的快照/源规划逻辑不需要本地文件/目录探测。重索引reindex在必需的文件或目录源缺失、或无法验证时probeKnowledgeFile/probeKnowledgeSourcePath区分真缺失与暂无法验证后者如瞬时/权限错误拒绝删除向量——绝不臆造所有权宁可保留也不丢失数据。9. Schema 演化IF NOT EXISTS 与版本重建的分界演化策略在 schema 注释中非常明确新增全新表零成本。CREATE ... IF NOT EXISTS在下一次打开时自然执行demand-first表与其首个写入者一起上线不为 v2.x 的猜测预留词汇修改已有表 / CHECK / FTS 绑定 / 触发器必须递增KNOWLEDGE_INDEX_SCHEMA_VERSION当前为 2历史1 → 2 引入fts_rowid代理键并重定 FTS 键即 #16132 修复。store-open 路径检测到不匹配即整库删建知识库随后自然重新索引主库knowledge_base/knowledge_item的变更仍需追加 Drizzle 迁移migrations/sqlite-drizzle因为那些行是用户业务数据不可丢弃重建。正是这种业务数据走迁移、派生数据走重建的分工让索引 schema 演进成本极低。10. 迁移后的索引验证不凭空捏造所有权KnowledgeVectorMigratorsrc/main/data/migration/v2/migrators/KnowledgeVectorMigrator.ts通过同一个 store 工厂写出与当前一致的索引布局构建后校验material / unit / embedding 计数describeIndexCountsmaterials、units、embeddings、unitsMissingEmbedding 四项指标embedding 覆盖缺失数按最终相对路径映射迁移材料保持当前 Concept ID 规则。关键边界情形当旧版目录向量能归因到单个源路径时迁移器合成已完成文件子条目并重映射那些向量当证据不可用时目录保持为失败的directory_not_migrated条目而不是发明文件所有权。迁移构建完成后还会执行PRAGMA wal_checkpoint(TRUNCATE)把 WAL 折回主文件保证index.sqlite自包含可迁移。11. 与知识库其他文档的关系本文是知识库领域文档链的一环其余文档可对照阅读知识库服务概览服务拆分、IPC、存储、条目状态、检索与 Agent 工具知识库操作守卫addItems/deleteItems/reindexItems的守卫与恢复语义知识库工作流架构调度模型、持久化 JobManager 作业、每库变更锁与崩溃语义知识库索引入口 README当前后端形态的源码级入口。如果你需要动手验证本文描述的行为src/main/features/knowledge下有完整的测试矩阵路径安全pathStorage.test.ts/pathStorage.win32.test.ts、索引 schema 与打开schema.test.ts/indexMeta.test.ts、重构建与删除KnowledgeIndexStore.test.ts/KnowledgeIndexStore.integration.test.ts、RRF 融合KnowledgeIndexStore.rrf.test.ts、检索车道KnowledgeIndexStore.search.test.ts以及向量 BLOB 编解码vectorBlob.test.ts可直接作为实现契约的行为级文档使用。【免费下载链接】cherry-studioAI productivity studio with smart chat, autonomous agents, and 300 assistants. Unified access to frontier LLMs项目地址: https://gitcode.com/GitHub_Trending/ch/cherry-studio创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门