# kaiji — CJK 模糊匹配与文本标准化引擎
**kaiji** 是一个高性能 Rust 库,专门解决日语、中文和韩语文本处理中普遍存在的"异体字问题"。
> 斎藤 ≡ 齋藤 ≡ 齊藤 · 渡辺 ≡ 渡邊 ≡ 渡邉 · 吉野家 ≡ 𠮷野家 · 広島 ≡ 廣島
**[▶ 在浏览器中体验 →](https://kent-tokyo.github.io/kaiji/playground/)**
---
## 问题说明
Unicode 标准规范化(NFC / NFD)只能修正字符"形式"上的差异,但对 CJK 语义级异体字(繁简、旧字形、IVS 异体字选择符)毫无作用。结果是:同一个姓名或词语在不同系统中看起来完全不同,尽管人类读者一眼就能识别它们是同一个字。
| eKYC 因旧字形姓名被拒 | OCR 输出 ≠ 用户输入 |
| RAG/LLM 将"齋藤"和"斎藤"嵌入为不同 token | 分词器看到不同字节 |
| 搜索"𠮷野家"返回 0 结果 | EC 数据库存储的是"吉野家" |
| 不动产登记簿无法与 CRM 关联 | 登记簿使用旧字形,CRM 使用新字形 |
各团队用临时的正则表达式列表或大量 `REPLACE()` SQL 应对此问题。始终缺乏一个权威、快速、跨语言的核心解决方案——直到现在。
---
## 性能
在 Apple M2 上测试(单线程,`cargo bench`,优化 release 构建)。
### 批量标准化吞吐量
| 1,000 行 | 42 µs | ~250 MiB/s |
| 100,000 行 | 4.25 ms | ~251 MiB/s |
| **1,000,000 行** | **42 ms** | **~252 MiB/s** |
> 吞吐量在所有批次大小下保持恒定——库的扩展完全线性,无额外开销。
> 外推:**1000 万行 ≈ 420 ms**。
### 单字符串延迟
| 1 字符(斉) | 已是规范形——零分配 | **5.8 ns** |
| 1 字符(齋) | 需要异体字折叠 | **22 ns** |
| 4 字符(斉藤一郎) | 干净输入 | **17 ns** |
| 4 字符(齋藤一郎) | 1 个异体字需折叠 | **50 ns** |
| 30 字符 | 干净输入 | **132 ns** |
| 30 字符 | 混合异体字 | **236 ns** |
| 5,000 字符 | 干净 | **29 µs** |
| 5,000 字符 | 混合异体字 | **36 µs** |
---
## 与现有工具的对比
| [mojimoji](https://github.com/studio-ousia/mojimoji) | 仅全角↔半角 | ✗ | ✗ | 仅 Python |
| [jaconv](https://github.com/ikegami-yukino/jaconv) | 仅假名转换 | ✗ | ✗ | 仅 Python |
| [OpenCC](https://github.com/BYVoid/OpenCC) | 仅中文简繁转换 | ✗(不支持日语) | △ 重型 C++ | C++/Python/Node |
| **kaiji** | **异体字+字宽+IVS+语料检索** | **✓ 300+ 字族** | **✓ 轻量** | **Rust/Python/WASM/CLI** |
kaiji 是第一个将所有这些功能统一在单一可编译到任何平台的 Rust 核心中的库。
---
## kaiji 解决的问题
| **异体字折叠** | 将 300+ 个旧/繁体/变体 CJK 字符映射到其规范新字形(JIS X 0213、人名用汉字、繁体中文) |
| **IVS 剥离** | 移除表意文字变体序列(U+E0100–U+E01EF),这些序列在大多数字体中不可见,但会导致字节级不匹配 |
| **字宽规范化** | 全角 ASCII(ABC→ABC)、半角片假名(ガ→ガ)含浊音合成 |
| **零拷贝快速路径** | 无需转换的输入以借用 `&str` 切片返回——无分配 |
| **模糊匹配** | `matches("齋藤", "斎藤")` → `true` 一行调用 |
| **模块化管道** | 通过 `NormalizerConfig` 或构建器 API 仅启用所需功能 |
---
## 快速上手
### Rust
```toml
[dependencies]
kaiji = { version = "0.1", features = ["japanese"] }
```
```rust
use kaiji::{matches_default, normalize_default, Normalizer};
// 简单的模糊姓名匹配
assert!(matches_default("斎藤", "齋藤").unwrap()); // 斎 vs 齋
assert!(matches_default("渡辺", "渡邊").unwrap()); // 辺 vs 邊
assert!(matches_default("𠮷野家", "吉野家").unwrap()); // tsuchi-yoshi vs kichi
// 标准化(输入已是规范形时零拷贝)
let s = normalize_default("齋藤一郎").unwrap();
assert_eq!(s, "斉藤一郎");
```
### Python
```bash
pip install kaiji
```
```python
import kaiji
kaiji.normalize("齋藤") # "斉藤"
kaiji.matches("斎藤", "齋藤") # True
kaiji.similarity_score("斎藤", "齋藤") # 1.0
n = kaiji.Normalizer(width_normalization=True)
n.normalize("ABC齋藤") # "ABC斉藤"
# 批量标准化(兼容 pandas)
kaiji.normalize_batch(["齋藤", "渡邊", "𠮷野家"])
# → ["斉藤", "渡辺", "吉野家"]
# 语料库索引——N 条记录的模糊搜索
idx = kaiji.Index(["斎藤一郎", "渡辺花子", "佐藤次郎"])
hits = idx.search("齋藤一郎", 0.9) # 阈值 0.0–1.0
hits[0].original # "斎藤一郎"
hits[0].score # 1.0
```
### CLI
```bash
# macOS (Homebrew)
brew tap kent-tokyo/kaiji
brew install kaiji
# 跨平台(Rust)
cargo install kaiji-cli
```
```bash
# 标准输入 → 标准输出标准化
# 含全角转换
# 模糊匹配(exit 0 = 匹配,exit 1 = 不匹配)
kaiji match "斎藤" "齋藤" && echo "matched"
# 相似度分数
kaiji score "斎藤一郎" "斉藤二郎"
# 0.9333
# 以 tsv 或 json 格式批量标准化
```
### JavaScript / TypeScript(WebAssembly)
```bash
npm install kaiji-wasm
```
```js
import init, { normalize, matches, similarity_score, Normalizer } from "kaiji-wasm";
await init();
normalize("齋藤"); // "斉藤"
matches("斎藤", "齋藤"); // true
similarity_score("斎藤", "齋藤"); // 1.0
const n = new Normalizer(true, true, true, false);
n.normalize("ABC齋藤"); // "ABC斉藤"
```
### Go
```bash
# 1. Build the native library
cargo build --release --manifest-path crates/kaiji-c/Cargo.toml
# 2. Add the Go module
go get github.com/kent-tokyo/kaiji/bindings/go
```
```go
import (
"fmt"
kaiji "github.com/kent-tokyo/kaiji/bindings/go"
)
// Set CGO_LDFLAGS to point to the compiled library before building.
// export CGO_LDFLAGS="-L/path/to/crates/kaiji-c/target/release"
result, _ := kaiji.Normalize("齋藤") // "斉藤"
matched, _ := kaiji.Matches("斎藤", "齋藤") // true
score, _ := kaiji.Similarity("斎藤", "齋藤") // 1.0
fmt.Println(result, matched, score)
```
### 构建器 API(推荐,Rust)
```rust
use kaiji::Normalizer;
let n = Normalizer::builder()
.width_normalization(true) // ABC → ABC, ガ → ガ
.fold_variants(true) // 齋 → 斉
.strip_ivs(true) // remove invisible variation selectors
.build();
assert_eq!(n.normalize("ABC齋藤").unwrap(), "ABC斉藤");
assert!(n.matches("渡辺一郎", "渡邊一郎").unwrap());
```
### 语料库索引(N 条记录的模糊搜索)
```toml
[dependencies]
kaiji = { version = "0.1", features = ["japanese", "index"] }
```
```rust
use kaiji::{KaijiIndex, NormalizerConfig};
// Build once — all corpus strings are normalised and stored in an FST
let corpus = vec![
"斎藤一郎".to_string(),
"渡辺花子".to_string(),
"佐藤次郎".to_string(),
];
let index = KaijiIndex::build(corpus, NormalizerConfig::default())?;
// Query — variant-form input still matches the canonical entry
let hits = index.search("齋藤一郎", 0.9)?;
// hits[0].original == "斎藤一郎", hits[0].score == 1.0
```
标准化为相同形式的字符串共享一个槽位——当该槽位匹配时,两个原始字符串都会被返回。结果按 Jaro-Winkler 分数降序排列。
### Cargo 功能特性
```toml
[dependencies]
kaiji = { version = "0.1", features = ["japanese"] } # 默认
kaiji = { version = "0.1", features = ["japanese", "nfkc"] } # + NFKC 规范化
kaiji = { version = "0.1", features = ["japanese", "index"] } # + FST 语料库索引
kaiji = { version = "0.1", features = ["full"] } # 全部功能(不含 index)
```
---
## 使用场景
### 1. NLP / LLM 预处理(数据工程师、ML 工程师)
在分词前对数百万行数据进行标准化,防止异体字产生重复嵌入或误导 RAG 检索管道。以 **~250 MiB/s** 的速度,1000 万行日语姓名 CSV 在不到半秒内即可完成标准化。
### 2. eKYC 与 AML(金融科技 / 安全工程师)
即使用户输入与 OCR 识别的身份证件使用不同的汉字变体,也能实现姓名匹配。消除因字节级不匹配导致的昂贵人工审核队列。
### 3. 不动产登记簿匹配(PropTech)
将充满旧字形的登记簿记录与现代 CRM / GIS 系统关联,无需维护一大堆拖慢数据库性能的 `REPLACE()` SQL 函数。
### 4. 电商搜索(前端 / 搜索工程师)
将 kaiji 编译为 WebAssembly 并在客户端运行:在查询命中索引前静默规范化搜索词,消除因异体字输入导致的"0 结果"问题。
---
## 架构
```
src/
├── lib.rs — public re-exports
├── error.rs — CjkFuzzyError, Result<T>
├── config.rs — NormalizerConfig (#[non_exhaustive])
├── normalizer.rs — Normalizer builder + 4-stage pipeline runner
├── width.rs — Stage 1: fullwidth↔halfwidth, halfwidth-kana, NFKC
├── variants.rs — Stage 2: static OnceLock HashMap (300+ variant→canonical)
├── normalize.rs — Stage 2: normalize() → Cow<'a, str> (zero-copy fast path)
├── matcher.rs — matches() / matches_default()
└── index.rs — KaijiIndex: FST corpus index + Jaro-Winkler search (`index` feature)
```
**管道阶段:**
| 1a | 全角 ASCII → 半角,半角片假名 → 全角(浊音合成) | `width_normalization: true` |
| 1b | Unicode NFKC 规范化 | `nfkc` feature + `nfkc: true` |
| 2 | IVS 剥离 + CJK 语义异体字折叠 | `strip_ivs`, `fold_variants` |
| 3 | 词级上下文转换(OpenCC 风格) | *计划中——`chinese` feature* |
| 4 | 领域规则(地址标准化等) | *计划中* |
异体字映射表在首次调用时通过 `std::sync::OnceLock` 构建,之后无需加锁即可跨线程共享。`normalize()` 在无需替换时借用输入原样返回,热路径上不会产生堆分配。
---
## 路线图
| [完成] | Stage 2: 异体字折叠 + IVS 剥离 |
| [完成] | Stage 1: 字宽规范化(全角、半角片假名、浊音合成) |
| [完成] | `Normalizer` 构建器 API |
| [完成] | Cargo features(`japanese`、`chinese`、`nfkc`、`full`) |
| [完成] | 异体字词典扩展至 446+ 字族 |
| [完成] | `similarity_score()`(基于规范化字符串的 Jaro-Winkler 分数) |
| [完成] | Python 绑定(PyO3 / `pip install kaiji`) |
| [完成] | WebAssembly 构建(wasm-bindgen / `npm install kaiji-wasm`) |
| [完成] | `KaijiIndex` — FST 语料库索引 + Jaro-Winkler 模糊搜索(`index` feature) |
| [完成] | 日语地址标准化——汉数字→阿拉伯数字,丁目/番/号统一(`address` feature) |
| [完成] | Python 绑定——`normalize_batch()`、`Index` 类、`SearchHit`、类型存根(.pyi) |
| [完成] | WASM `KaijiIndex` + `SearchHit`——JavaScript 语料库搜索 |
| [完成] | 浏览器 Playground——GitHub Pages |
| [完成] | Homebrew formula——`brew tap kent-tokyo/kaiji` |
| [完成] | CLI(`kaiji-cli`)——`normalize`、`match`、`score` 子命令 |
| [计划中] | Stage 3: OpenCC 风格词级转换(`chinese` feature) |
详细任务列表请参见 [`tasks/todo.md`](tasks/todo.md)。
---
## 构建与测试
```bash
cargo build
cargo test
cargo test --features nfkc # 含 NFKC 规范化
cargo test --features index # 含 FST 语料库索引
cargo clippy --all-targets -- -D warnings
cargo fmt
cargo bench # 需要 criterion
```
---
## 关于名称
**解字(かいじ)** — "分析字形,还原字的本义"。
该词源自许慎《说文解字》(东汉,约公元 100 年)——中国第一部系统性汉字字典。
《说文解字》的每个条目都从某种书写形式出发,追溯该字的正统结构与含义。
这正是 kaiji 库对 CJK 异体字所做的事情。
---
## 许可证
MIT OR Apache-2.0