KiwiFS搜索三层架构源码剖析:grep、FTS5与可插拔向量存储
KiwiFS搜索三层架构源码剖析:grep、FTS5与可插拔向量存储
KiwiFS 是一个面向 AI Agent 和团队的 Markdown 文件系统(Markdown filesystem for agents and teams)。它的搜索系统由三层组成:grep 纯文本搜索、SQLite FTS5 全文索引、可插拔向量存储,三者通过 Reciprocal Rank Fusion(RRF)融合成一次混合检索。本文带你读懂这套 KiwiFS 搜索架构的设计思路与源码实现,即使你刚接触搜索技术也能轻松上手。
为什么搜索要分三层?
不同的查询场景,对搜索的要求完全不同:
| 场景 | 适合的技术 | 特点 |
|---|---|---|
| 精确找一段文字、一个错误信息 | grep 式逐行匹配 | 零依赖、永远可用 |
| 按关键词相关性排序("哪个文档最讲部署?") | FTS5 + BM25 | 快、可离线、纯 Go |
| 找"意思相近"的内容(同义词、跨语言) | 向量语义搜索 | 需要 Embedding 模型 |
KiwiFS 的策略是:每一层都能独立工作,组合起来更强,任何一层缺位都不影响整体——这正是它源码里"降级而非失败"(degrade, don't fail)的设计哲学。
三层架构的总入口定义在 internal/bootstrap/bootstrap.go 的 buildSearcher 函数中:配置了 sqlite/fts5 引擎就用 FTS5 索引,索引打开失败或引擎未配置时,自动回退到 grep,保证搜索功能永不缺席。
第一层:grep——零依赖的兜底全文搜索
grep 引擎的实现只有 128 行,源码位于 internal/search/grep.go:
- 纯 Go 实现:用
filepath.WalkDir遍历知识根目录,只扫.md文件,逐行做大小写不敏感的子串匹配,不依赖任何外部数据库或 C 库; - 性能保护:每个文件最多保留 20 条匹配(
maxMatchesPerFile),防止单个大文件"霸占"结果集; - 可取消:遍历每个文件前都会检查
context,HTTP 请求一旦取消,搜索立刻停止; - 无状态:
Index/Reindex都是空操作——查询时直接读文件,永远不需要维护索引,也就永远不会"索引损坏"。
源码注释说得很直白:"Zero external deps — works everywhere, fast for up to ~5000 files"。它就是 KiwiFS 搜索架构中的"保底方案":小规模知识库下足够快,大规模场景则交给第二层。
第二层:SQLite FTS5——BM25 排序的全文索引
FTS5 是 SQLite 内置的全文搜索模块,KiwiFS 在 internal/search/sqlite.go 中用它构建了主搜索索引,有几个值得新手学习的设计:
1. contentless 虚拟表
CREATE VIRTUAL TABLE IF NOT EXISTS docs USING fts5(
path UNINDEXED, content, content='',
tokenize = 'porter unicode61 remove_diacritics 1'
);
(见 internal/search/sqlite.go)content='' 表示 FTS5 只存倒排索引、不复制正文——正文本来就躺在 .md 文件里,索引体积减半;porter 词干器让 "deploy" 和 "deployment" 都能命中。
2. 读写分离连接池
索引存放在 <root>/.kiwi/state/search.db,写入连接池限制为 1 个连接(串行化所有写),读取连接池按 CPU 核数打开且标记 query_only。得益于 WAL 模式,搜索永远不用等索引写入完成,查询与索引更新互不阻塞。
3. 聪明的查询解析
internal/search/sqlite.go 的 buildFTS5Query 会判断用户输入:如果包含 AND/OR/NOT/NEAR 等 FTS5 操作符,原样透传;否则按空格分词、给每个词加引号做隐式 AND 匹配——用户输入连字符、撇号、冒号都不会报语法错误,还有整词短语回退机制兜底。
4. BM25 + 信任度加权
排序使用 bm25(docs) 相关性分数(见 internal/search/sqlite.go),同时支持 recency_weight 把"最近修改"混入排序,还能通过 internal/search/search.go 中的 TrustSearcher 接口对 verified、source-of-truth 页面做信任度加权——已核实的知识页会自然浮到前面。
索引坏了怎么办?Reindex() 可以整体重建,因为文件才是唯一事实来源,索引只是可随时重建的派生物——这是 KiwiFS 的核心信条。
第三层:可插拔向量存储——语义搜索
第三层回答的是 grep 和 FTS5 回答不了的问题:"哪些文档说的是同一件事,尽管用词完全不同?"
1. 极窄的存储接口
internal/vectorstore/vectorstore.go 定义了 Store 接口,只有 7 个方法(Upsert/RemoveByPath/Search/Reset/Count 等)。正因为接口足够小,6 种存储后端都能以少量代码接入(见 internal/vectorstore/factory.go):
- 本地零依赖:
sqlite(向量存 BLOB、L2 归一化后点积即余弦相似度) - 专用向量库:
qdrant、pinecone、weaviate - 关系数据库:
pgvector、milvus
2. 8 种 Embedding 提供方的自由组合
同一工厂函数 buildEmbedder 支持 openai、ollama、http、cohere、voyage、bedrock、vertex,以及纯本地 ONNX 模型(完全离线、零网络依赖,见 internal/vectorstore/factory.go)。本地 SQLite 向量库适合约 5 万个 chunk 以内的知识库,超过后只需在配置中换成真正的 ANN 存储,上层代码一行不用改——这就是"可插拔"的含义。
3. 带上下文的分块
文档被切成 chunk 后再嵌入,且每个 chunk 会预拼上标题、路径、frontmatter 等上下文模板(ContextTemplate),避免"孤立段落"丢失语义。向量库为空时,服务启动会自动在后台全量重建。
三层如何协同:混合搜索与 RRF 融合
调用 GET /api/kiwi/search?mode=hybrid 时,internal/hybrid/hybrid.go 会并发发起 FTS5 与向量两路检索(每路先取 50 个候选),再用 Reciprocal Rank Fusion 融合:
score(文档) = Σ 1 / (60 + 该文档在每路结果中的排名)
实现见 internal/search/fusion.go。RRF 的妙处在于:它只消费"排序"而不消费"分数",所以 BM25 分数和余弦相似度根本不需要校准量纲就能合并;常数 k=60 来自 2009 年 SIGIR 论文的经典结论,无需针对自己的知识库调参。
混合结果里每条都标注了 fts_rank 与 semantic_rank(见 internal/api/handlers_search.go),调试检索时你能立刻看出"这一条到底是哪一层找到的"。更有安全感的是降级策略:向量服务没配置或超时,接口照样返回纯 FTS 结果,而不是 503——engines 字段会如实告知你本次哪些引擎实际参与了。
如何为你的知识库选择搜索方案
在 .kiwi/config.toml 中配置即可,配置结构定义在 internal/config/config.go:
| 你的情况 | 推荐配置 |
|---|---|
| 知识库 < 5000 篇、追求极简 | engine = "grep"(或不开 FTS5 自动回退) |
| 团队 Wiki、需要相关度排序与元数据过滤 | engine = "sqlite"(默认推荐) |
| 需要"按意思找文档" | 在 [search.vector] 下开启 embedder + store |
一个最小化的语义搜索配置长这样:
[search.vector]
enabled = true
[search.vector.embedder]
provider = "ollama" # 或 openai / onnx / http ...
model = "nomic-embed-text"
[search.vector.store]
provider = "sqlite" # 或 qdrant / pgvector / pinecone ...
完整的接口文档(/api/kiwi/search、/api/kiwi/search/semantic、/api/kiwi/search/verified 三个端点)参见 docs/API.md,架构全景可看 docs/ARCHITECTURE.md。
小结
KiwiFS 的搜索三层架构可以浓缩成三句话:
- grep 兜底——零依赖、永远可用,小库足够快(internal/search/grep.go);
- FTS5 主力——BM25 排序 + 信任度加权 + 读写分离,文件即事实、索引可随时重建(internal/search/sqlite.go);
- 向量增强——极窄的 Store 接口 + 可插拔的 embedder/存储后端,语义搜索缺位时优雅降级(internal/vectorstore/、internal/hybrid/hybrid.go)。
这套"每层独立可用、组合更强"的设计,正是 KiwiFS 既适合新手快速上手、又能平滑扩展到生产环境的原因。
