一个学习用的迷你代码索引器:用纯 Swift 复刻 ragflow 那类"代码索引器"的内核。 目标不是做产品,而是把"代码索引"这件看着唬人的事拆开、祛魅、能自己讲清楚每一行。
面向对象:iOS 开发者。下面会一直用 Xcode / SourceKit 这些你熟的东西当锚点。
我接触过一个 ragflow 这类的代码索引器,它把上百个代码仓库索引起来,
能精确查符号、查引用、查 API 调用方、甚至跨服务追 Feign 调用 / Kafka topic / 共享数据库表。
看清楚之后会发现:它不是一个天才魔法,是一堆成熟技术拼起来的组合体。 拆成三层:
| 层 | 作用 | ragflow 用什么 | 本项目用什么 |
|---|---|---|---|
| ① 结构索引(精确) | 解析代码,抽出符号/引用/接口/表,存起来精确查 | tree-sitter + SQLite | SwiftSyntax + SQLite |
| ② 语义索引(模糊) | 把代码片段变成向量,按"意思"搜 | 开源 RAGFlow embeddings | (Phase 2 待做) |
| ③ 接入层 | 把以上能力暴露成工具给 LLM 调 | MCP 协议封装 | CLI (Phase 3 升级为 MCP) |
真正难、真正值钱的是 ragflow 在多个仓库之间连边(A 服务的 Feign 接口 = B 服务的 controller, 同一个 topic 字符串的两端)。但那是放大题;内核就是上面这三层。
重要事实: 这类索引器的 MCP 通常不索引它自己,只暴露它对目标仓库的索引产出。 所以没法"用 ragflow 读 ragflow 源码"——本项目就是自己把内核搭一遍来理解它。
| ragflow 的能力 | 你早就在用的对应物 |
|---|---|
find_symbol(找定义) |
Xcode 的 Jump to Definition / Open Quickly (⌘⇧O) |
find_references(找引用) |
Xcode 的 Call Hierarchy / Find Callers |
| 结构索引层(存符号的库) | SourceKit-LSP / IndexStoreDB(Xcode 编译时建的 index store) |
也就是说,ragflow 的"结构层"≈ 苹果给 Swift 做的那套东西的跨语言、跨仓库版。 你已经在"用户那一侧"用了好几年,只是没拆开看过后端。
刻意按 ragflow 三层来组织,文件一一对应:
SwiftCodeIndexer/
├── Package.swift 依赖 swift-syntax
└── Sources/SwiftCodeIndexer/
├── Symbol.swift 数据模型:Symbol(声明)+ Reference(引用)
├── SymbolCollector.swift ① 解析层 —— SyntaxVisitor 走语法树抽"声明"(对应 tree-sitter)
├── ReferenceCollector.swift ①(下半场)—— 再走一遍树抽"引用",做出"谁用到了它"
├── IndexStore.swift ② 存储+查询 —— SQLite 两张表(symbols / refs),建表/插入/按名查
└── main.swift ③ CLI 入口 —— index / find / references 三条命令(对应 ragflow 的 MCP 工具)
技术选型说明:
- SwiftSyntax / SwiftParser:苹果官方 Swift 解析器,把源码变成语法树(AST)。
- SQLite3:走系统自带模块
import SQLite3,不引第三方依赖。生成的index.db是普通 SQLite 文件,可用任意 SQLite 浏览器打开——"索引"祛魅:本质就是一张表。
cd ~/Desktop/SwiftCodeIndexer
# 索引:传一个 .swift 文件,或递归索引一个目录(同时抽声明 + 引用)
swift run SwiftCodeIndexer index <文件或目录>
# 查声明:按符号名查(支持子串;精确命中排在前面)—— 这就是你自己的 find_symbol
swift run SwiftCodeIndexer find <名字>
# 查引用:谁用到了这个名字(精确名)—— 这就是你自己的 find_references
swift run SwiftCodeIndexer references <名字>索引库默认生成在当前目录的 index.db(里面有 symbols 和 refs 两张表)。每次 index 会先清空再重建。
find 现在会顺带显示每个符号的引用热度:
$ swift run SwiftCodeIndexer find Symbol
找到 2 个:
[struct] Symbol → Sources/SwiftCodeIndexer/Symbol.swift:5 (7 处引用)
[class] SymbolCollector → Sources/SwiftCodeIndexer/SymbolCollector.swift:9 (2 处引用)
单独看,引用层像是"多存了一张表";但它把这个工具从查字典升级成了查关系。
find <名字>回答 "它定义在哪"(跳转到定义)。references <名字>回答 "谁用到了它"(找调用方)。
两个加起来,你手里就有了一个最小的调用关系数据库——不再只是符号清单,而是符号之间的"边"。
- 改动前的影响面评估(impact analysis)。 最实用。删 / 改 / 重命名一个
send之前,先references send看它被哪些文件、多少处用到,改之前心里有数,而不是改完等编译器报错。 这类工具在大型代码库里最值钱的用法就是这个——"我改这个接口会炸到谁"。 - 找死代码。 一个符号有声明,但
(0 处引用)→ 大概率没人用,可以删。现在find直接把这个数字显示出来了。 - 读陌生代码。 进一个新项目,
references一把列出某个核心类型的所有触点,比全文搜索准—— 它只命中真正的标识符使用,不会命中注释和字符串里的同名文本。
这就是 Xcode 的 Find Callers / Call Hierarchy。你现在等于把那套东西的后端自己搭了一遍: Xcode 前端点一下"谁调用了我",底层查的就是这种引用索引(它的 IndexStoreDB)。
这一版引用是纯按名字撞。最直白的证据就在 find insert 的输出里:
$ swift run SwiftCodeIndexer find insert
找到 2 个:
[func] insert → Sources/SwiftCodeIndexer/IndexStore.swift:41 (2 处引用)
[func] insert → Sources/SwiftCodeIndexer/IndexStore.swift:83 (2 处引用)
两个 insert 重载(一个收 [Symbol]、一个收 [Reference])都显示"2 处引用"——
但全项目对名字 insert 的引用统共就那 2 处,根本分不清哪一处归哪个重载。
因为我们不解析类型、不认作用域,只要名字相同就算一笔。
所以现在的引用结果够用来缩小范围、做粗粒度影响面,但不能当成精确调用图。 从"名字撞上了"升级到"确实是同一个东西",需要类型 / 作用域解析(resolve)—— 而跨文件、尤其跨仓库时,这步正是结构索引最脆弱的一环。ragflow 真正难、真正值钱的功夫, 全花在"确认 A 服务的 Feign 接口就是 B 服务那个 controller"这种跨服务连边上。 你这个 toy 版把这道坎儿摆在了明面上,这比直接用 Xcode 更长见识。
- 自举(Phase 1):索引自己的
Sources/→find Symbol同时命中Symbol(struct)和SymbolCollector(class)。 - 引用(Phase 1.5):自举后
references Symbol返回 7 处使用(类型标注、构造调用),声明点不在其中 ——声明进symbols表、引用进refs表,互不污染;find insert暴露出按名字撞的同名歧义(见上)。 - 真实库:索引 Moya(网络库)→ 66 文件 / 295 符号;
find MultiTarget正确返回enum MultiTarget(精确) +MultiTargetSpec(子串),精确命中排在前面。
结论:结构层在真实第三方代码上跑通,且能正确排序。等价于手搓了一个 Xcode "Open Quickly" + "Find Callers"。
✅ 能做:
- 解析 class / struct / enum / protocol / func 的声明,记录名字、种类、文件、行号。
- 按名精确 + 子串查询,精确命中优先。
- (Phase 1.5) 收集标识符引用(表达式里的调用/读取、类型位置的
: Foo/-> Foo等),references <名字>精确查出"谁用到了它"。声明进symbols表、引用进refs表,互不污染。
❌ 还不能(= 后续学习的动机):
- 引用是纯按名字匹配:只知道源码里写了
send,不知道它指向哪一个send的声明 (同名不同物会一起命中)。这恰恰是 ragflow 跨文件、跨服务连边为何难的缩影。 - 不认
extension、计算属性、typealias、嵌套类型的父子归属。 - 不能按语义搜(如"找网络重试逻辑")——需要向量化。
- 不解析 Objective-C(SwiftSyntax 只吃
.swift)。
- Phase 1 — 结构索引层:SwiftSyntax 解析 → 抽符号 → SQLite 存 → 按名查。(已完成)
- Phase 1.5 —
references <名字>:收集标识符引用,做出"谁调用了它"。纯 Swift,当晚见效。(已完成) 关键体会:跨文件引用其实是"按名字猜"的,这正是 ragflow 跨服务连边为何难的缩影。 - Phase 2 — 语义搜索:给符号/片段算 embedding,实现
search "网络重试逻辑"。 最长见识的一块,需踏出 Swift 调 embedding API + 算 cosine 相似度。 - Phase 3 — MCP 封装:用 MCP SDK 把
find暴露成工具,在 Claude Code 里直接调,复刻 ragflow 最外层。 - Phase 4 — 读 ragflow 真源码:当作"规模化 + 跨服务"的参考架构来读,重点看它怎么连跨服务的边。
推荐坡度:1 → 1.5 → 2,最平缓。