# minidb：自研嵌入式数据库

> 从通用 KV 库到搜索索引与会话索引的底座——WAL + 快照、独占写锁、持久化索引代与大于内存的全文层。

- 作者：David（道雾轩）
- 专栏：Kimi Code 源码解读（https://daiw.org/manual/kimi-code.md）
- 最后更新：2026-09-19
- 原文：https://daiw.org/manual/kimi-code/minidb
- 转载与引用：请注明出处并附原文链接（https://daiw.org/about/copyright）

`packages/minidb` 仍是最出人意料的一个包：**一个纯 Node.js 实现的嵌入式数据库**，约 2.3 万行，**零运行时依赖**。README 的自我定位没变：

> A **pure-Node.js** (zero native addons) embedded key-value database that mixes ideas from **Redis** (in-memory KV speed, data structures, AOF-style rewrite) and **SQLite** (durable single-file persistence, WAL, indexed queries).

变的是它在项目里的角色。包里新增的 `AGENTS.md` 第一句就是：

> The embedded JSON document store (`MiniDb`) behind kap-server's search index

与 v0.29.1 相比，`src/` 从 27 个文件涨到 61 个，新增的几乎都是为「当索引用」服务的：索引代、维护调度、worker 构建、异步读路径。

## 谁在用它

v0.29.1 时本文写的是「minidb 是为新一代引擎准备的存储层」。现在 v2 已是唯一引擎，实际用到它的是两处：

| 使用方 | 位置 | 用法 |
| --- | --- | --- |
| kap-server 全局搜索 | `search/indexCore.ts` | 单库 `MiniDb`，目录 `search-index`，建词索引与 `ngram` 子串索引 |
| agent-core-v2 会话索引 | `persistence/backends/minidb/miniDbQueryStore.ts` | App 级 `IQueryStore` 的唯一实现，底下是 16 个分片的 `ClusterDb` |

会话记录本身并不进 minidb：每个 agent 的 `wire.jsonl` 仍是追加日志（`IAppendLogStore`，见[记录与回放](https://daiw.org/manual/kimi-code/records-replay)）。minidb 管的是**可查询的派生数据**——跨会话的消息全文、按工作区与时间排序的会话列表。

搜索库的打开参数（`search/indexCore.ts` 第 348 行起）：

```ts
    const opts = {
      dir: this.indexDir,
      valueCodec: 'json',
      valueMode: 'disk',
      fsyncPolicy: 'everysec',
      onLockFail: 'readonly',
      onLockAcquired: (info: { readonly token: string }) => {
        this.lockToken = info.token;
        this.options.onLockToken?.(info.token);
      },
    } as const;
```

值落盘、每秒 fsync、抢不到写锁就只读打开。若打开时判定库已损坏、需要重建，就先 `wipeStoreDir` 再重开——搜索索引是派生数据，丢了可以重算。`[database] search` 缺省开启（`databaseSearchEnabled()` 缺省返回 `true`），此时由 `SearchWorkerHost` 把索引放到 worker 线程里维护。

会话索引那边的两个常量：

```ts
const STORE_SUBDIR = 'query-store';
const SHARD_COUNT = 16;
```

## 不变的底子

v0.29.1 时介绍的核心设计都还在：

| 能力 | 位置 |
| --- | --- |
| 内存索引 + 值指针（`valueMode` 可取 `memory` / `disk` / `auto`） | `store.ts`、`value-reader.ts` |
| 追加写 WAL + group commit + `always` / `everysec` / `no` 三档 fsync | `wal.ts` |
| CRC 分帧（魔数 `MD`），崩溃留下的半截尾部自动截断 | `codec.ts`、`recovery.ts` |
| 非阻塞压实（仿 Redis BGREWRITEAOF 与 Bitcask merge）与快照 | `compaction.ts`、`snapshot.ts` |
| Windows 上 rename 覆盖仍被打开文件的兼容 | `rename-replace.ts` |
| 跳表二级索引、复合索引、日期列索引 | `skiplist.ts`、`compound-index.ts`、`dt-index.ts` |
| TTL（惰性 + 主动） | `store.ts` |
| RESP 服务端（`redis-cli` 可直连） | `server.ts` |
| 多进程分片 `ClusterDb` | `cluster/` |
| 值编码 `buffer` / `string` / `json` | `value-codec.ts` |

「零原生插件」这条约束仍写在 `DESIGN_NOTES.md` 第一节：Kimi Code 要做[单文件二进制分发](https://daiw.org/manual/kimi-code/what-is-kimi-code)，纯 JS 实现打包即可用。CLI 的 SEA 构建甚至把 minidb 的全文构建 worker 作为内嵌资源一起带上（`apps/kimi-code/src/native/minidb-worker.ts`）。

## 为当索引而加的四件事

**1. 独占写锁 + 只读追赶。** `lockfile.ts` 用 `O_EXCL` 建锁，归属按实例令牌（`${pid}:${uuid}`）判断，持有者 PID 已死才算过期。抢锁失败的一方只读打开，用 `catchUpFromWal()` 从 WAL 增量追赶。

**2. 大于内存的全文层。** `text-index/` 在内存里只放词典与增量，倒排表落盘在 `text-postings.ts`。默认分词保留 ASCII 单词与 CJK 单字 / 双字；`trigram.ts` 另提供 2/3-gram 哈希分词，让 `C++`、公式、emoji 这类符号密集的文本也能按子串精确命中——索引只出候选，下游再逐条确认，保证零误报。

**3. 持久化索引代。** 派生状态（存储镜像、各类索引、全文词典与倒排）写成 `generations/g-NNNNNN/`：先写临时目录，再原子发布（rename + 切换 `CURRENT`）。打开时加载最新一代再重放 WAL 增量，不必重新解码全部值、重新分词；代缺失或损坏就自动退回完整恢复。

**4. 维护调度与 worker 构建。** `maintenance.ts` 统一调度压实与建代：每库同时只跑一个重任务，带队列背压、磁盘余量预检、截止时间与取消。语料较大时，全文构建放到 worker 线程（`src/worker/`），主线程只做校验与切换。

外加一套异步读接口（`getAsync` / `searchAsync` / `queryAsync`）和打开过程的状态遥测（`lifecycleStatus()`），目标都是不卡住宿主的事件循环。

## 值得学的地方

1. **`DESIGN_NOTES.md` 记录了每个参考实现教了什么**——Redis、SQLite 的 WAL 文档、NeDB、Bitcask、一个迷你 SQLite 克隆，各自学到的东西列成一张表。
2. **约束驱动的技术选型**。「零原生插件」直接排除了现成方案，也决定了纯 JS 实现是唯一出路。
3. **每个新机制都留了回滚开关**。`OpenOptions` 里的 `indexGenerations: false`、`textBuildWorker: false`、`deferOpenTextBuilds: false` 都能退回旧路径——给存储引擎加复杂优化时，这比什么都重要。

下一篇：[transcript 四层模型](https://daiw.org/manual/kimi-code/transcript)。
