spec(fb-39b): embedding model upgrade design

- multilingual-e5-small (384 dim) → multilingual-e5-large (1024 dim)
- Cascade: embedding_version bump → fb-23 incremental ingest
  re-embeds all chunks
- Migration policy: dim mismatch detection at LanceVectorStore::open
  → error.v1 (code = embedding_dim_mismatch) + hint
  "kebab reset --vector-only && kebab ingest"
- Config defaults flip (model + dimensions). User TOML pinning small
  preserves backwards-compat
- bge-m3 deferred (fastembed enum 미포함, UserDefinedEmbeddingModel
  ONNX path 별도)
- Release trigger: 0.6 → 0.7 minor bump per CLAUDE.md cascade rule

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
th-kim0823
2026-05-10 22:59:03 +09:00
parent 240120ee80
commit 2c3461c465

View File

@@ -0,0 +1,198 @@
---
title: "p9-fb-39b — Embedding model upgrade design (multilingual-e5-large)"
phase: P9
component: kebab-embed-local + kebab-store-vector + kebab-config + kebab-app
task_id: p9-fb-39b
status: design
target_version: 0.7.0
contract_source: ../../docs/superpowers/specs/2026-04-27-kebab-final-form-design.md
contract_sections: [§4 search, §5 storage, §9 versioning cascade]
date: 2026-05-10
---
# p9-fb-39b — Embedding model upgrade
## Goal
fb-39 의 lever 적용 — embedding model 을 `multilingual-e5-small` (384 dim) 에서 `multilingual-e5-large` (1024 dim) 로 업그레이드. 도그푸딩 한국어 corpus 의 retrieval precision 개선.
fb-39 가 측정 도구 (P@5 / P@10) 를 추가했으므로, 본 PR 머지 후 small vs large 비교 가능.
`bge-m3` 검토했으나 fastembed 4.9.1 의 `EmbeddingModel` enum 에 미포함 — `UserDefinedEmbeddingModel` ONNX 직접 로드 path 는 별도 작업 (fb-39c 후보). 본 PR scope = e5-large 만.
## Behavior contract
### Embedding model
- 신규 default: `multilingual-e5-large` (1024 dim).
- `kebab-embed-local::resolve_model` 에 신규 arm:
```rust
"multilingual-e5-large" => Ok(EmbeddingModel::MultilingualE5Large),
```
기존 `multilingual-e5-small` arm 그대로 (backwards-compat opt-out).
### Config defaults
- `Config::defaults().models.embedding.model`: `"multilingual-e5-small"``"multilingual-e5-large"`.
- `Config::defaults().models.embedding.dimensions`: `384``1024`.
- `kebab init` 가 생성하는 config.toml 템플릿 동일 갱신.
기존 user TOML 이 `model = "multilingual-e5-small"` 또는 `dimensions = 384` 명시한 경우 그대로 유지 — `serde` 가 user value 우선. opt-out 가능.
### Cascade
- `embedding_version`: 자동 변경 (config.models.embedding.model 값 그대로 wire 에 emit). `multilingual-e5-small``multilingual-e5-large`.
- fb-23 incremental ingest: 4-input match (blake3 + parser_version + chunker_version + embedding_version) 에서 embedding_version 깨짐 → 모든 chunk 재-embed. text/parse/chunk 비용 회피, embed 비용만 발생.
- `eval_runs.config_snapshot_json`: 새 version 자동 기록. 비교 시 동일 version 끼리.
- design §9 cascade rule 의 5 키 중 `embedding_version` 변경 — binary release 트리거 (CLAUDE.md `Versioning cascade` 룰).
### Migration policy
LanceDB stored vectors 의 dim 과 `config.models.embedding.dimensions` 가 mismatch 면:
- `LanceVectorStore::open` (또는 첫 호출) 가 비교 → mismatch 시 신규 `ErrorV1`:
- `code = "embedding_dim_mismatch"`
- `message`: `"vector index dim 384 vs config dim 1024"`
- `hint`: `"기존 vector index 가 4-dim, config 는 N-dim. 'kebab reset --vector-only && kebab ingest' 로 재구축."`
- CLI: exit 1 + error.v1 stderr (또는 비-`--json` 모드 plain stderr).
- silent migration / auto-wipe 안 함 — 사용자 명시 동의 필요.
remediation flow:
```
$ kebab search "..."
error: vector index dim 384 vs config dim 1024
Hint: 기존 vector index 가 384-dim, config 는 1024-dim.
'kebab reset --vector-only && kebab ingest' 로 재구축.
$ kebab reset --vector-only
[wipe LanceDB + SQLite embedding_records]
$ kebab ingest
[full re-embed with new model — fastembed downloads e5-large ONNX (~1.3 GB) on first run]
```
### Wire shape
신규 wire field 없음. `error.v1.code` 의 valid value namespace 에 `"embedding_dim_mismatch"` 추가 (string, enum 아님 — additive).
## Allowed / forbidden dependencies
- `kebab-embed-local`: 신규 dep 없음. fastembed enum variant 추가만.
- `kebab-store-vector`: 신규 dep 없음. LanceDB schema reader 사용.
- `kebab-config`: 신규 dep 없음. defaults 값 변경.
- `kebab-app`: 신규 dep 없음. error propagation.
`kebab-core` 의 다른 `kebab-*` 의존 금지 룰 그대로.
## Public surface delta
### kebab-embed-local (`lib.rs`)
```rust
fn resolve_model(name: &str) -> Result<EmbeddingModel> {
match name {
"multilingual-e5-small" => Ok(EmbeddingModel::MultilingualE5Small),
"multilingual-e5-large" => Ok(EmbeddingModel::MultilingualE5Large), // 신규
other => anyhow::bail!(/* ... */),
}
}
```
### kebab-config (defaults + TOML 템플릿)
```rust
EmbeddingCfg {
provider: "fastembed".to_string(),
model: "multilingual-e5-large".to_string(),
dimensions: 1024,
// ... 기타 ...
}
```
generated config.toml 템플릿 도 같이 갱신.
### kebab-store-vector (`lib.rs` 또는 신규 helper)
```rust
impl LanceVectorStore {
pub fn open(...) -> Result<Self> {
// 기존 open 로직 ...
let stored_dim = read_schema_vector_dim(&table)?;
if stored_dim != config_dim {
anyhow::bail!(StructuredError(ErrorV1 {
code: "embedding_dim_mismatch".to_string(),
message: format!("vector index dim {stored_dim} vs config dim {config_dim}"),
hint: Some(format!(
"기존 vector index 가 {stored_dim}-dim, config 는 {config_dim}-dim. \
'kebab reset --vector-only && kebab ingest' 로 재구축."
)),
// ...
}));
}
Ok(...)
}
}
```
(정확한 LanceDB schema reading API 는 구현 시 확인 — `Table::schema()` 또는 `arrow_schema::Schema` 직접 inspect.)
## Test plan
| kind | description |
|------|-------------|
| unit (kebab-embed-local) | `resolve_model("multilingual-e5-large")` returns Ok |
| unit (kebab-embed-local) | `check_dim(1024, 1024)` ok |
| unit (kebab-embed-local) | `check_dim(384, 1024)` Err — message mentions both dims |
| unit (kebab-config) | `Config::defaults().models.embedding.model == "multilingual-e5-large"` |
| unit (kebab-config) | `Config::defaults().models.embedding.dimensions == 1024` |
| unit (kebab-config) | TOML `model = "multilingual-e5-small"` deserialize 정상 (backwards-compat) |
| unit (kebab-config) | 생성된 config.toml 템플릿 안 `model = "multilingual-e5-large"`, `dimensions = 1024` |
| unit (kebab-store-vector) | mismatch fixture (384-dim stored + 1024 cfg) → `embedding_dim_mismatch` ErrorV1 |
| 통합 (kebab-cli) | mismatch scenario — pre-existing 384-dim DB + new config → exit 1 + error.v1 stderr (`code = embedding_dim_mismatch`) + hint mentions reset --vector-only |
| 통합 (kebab-cli) | small config 로 fresh ingest + search → 정상 (backwards-compat path 검증) |
`multilingual-e5-large` 모델 다운로드 회피 위해 unit/integration 테스트는 fixture 또는 mock — 실 모델 호출 안 함. 첫 도그푸딩 시 사용자가 fastembed cache 다운로드.
## Implementation steps (high-level)
1. `kebab-embed-local::resolve_model` arm + check_dim 단위 테스트.
2. `kebab-store-vector` dim mismatch detection + ErrorV1 + 단위 테스트.
3. `kebab-config` defaults flip + TOML 템플릿 + 단위 테스트.
4. `kebab-cli` integration: mismatch error.v1 wire + backwards-compat path 통합 테스트.
5. README + SMOKE + design + HOTFIXES + status flip.
5 task. 단일 PR, single 세션 가능.
## Risks / notes
- **첫 실행 모델 다운로드**: e5-large ONNX ~1.3 GB. fastembed cache (`config.storage.model_dir/fastembed/`) 에 자동 다운로드 (첫 호출 시). progress 표시 없음 — 사용자 침묵 latency. `kebab doctor` 또는 README 에 경고 안내.
- **Search/ingest latency**: e5-large 가 e5-small 대비 ~3-4× embedding 시간. ingest 비용 증가 (one-time + 신규 docs). search 시 query embed per-call 증가.
- **Disk usage**: vector dim 2.6× → LanceDB 약 2.7× 증가.
- **HOTFIXES entry**: dim mismatch UX (error.v1 + reset --vector-only flow) 가 frozen design 안 명시 안 된 신규 동작 — HOTFIXES 한 항목 추가.
- **eval comparison**: fb-39 P@k 가 측정 도구. 도그푸딩 corpus + golden 의 expected_chunk_ids 채워서 small vs large 정량 비교 별도 (PR 안 의무 아님).
- **fb-23 incremental ingest 와의 상호작용**: embedding_version 변경 → 모든 doc 재-embed. fb-23 의 unchanged path 는 한 번도 hit 안 함 (예상 동작).
- **release trigger**: design §9 cascade rule 의 `embedding_version` 변경 → CLAUDE.md `Versioning cascade` 룰에 따라 binary 0.6 → 0.7 minor bump 필요.
## Out of scope
- bge-m3 또는 user-defined ONNX path (fb-39c 후보).
- Other lever (RRF / cross-encoder / chunk policy).
- Auto-migration / background re-vector.
- LanceDB schema migration tooling (별도 wipe + re-ingest).
- multi-model coexistence (한 KB 안 small + large 동시).
- precision 정량 비교 의무 (별도 도그푸딩).
## Documentation updates (implementation PR 동시)
- `README.md` `[models.embedding]` config 섹션 — default 변경 + small opt-out 안내 + dim mismatch 시 reset 명령 안내.
- `docs/SMOKE.md` — upgrade walkthrough (`kebab reset --vector-only && kebab ingest` 시퀀스 + 첫 ONNX 다운로드 latency 경고).
- `docs/superpowers/specs/2026-04-27-kebab-final-form-design.md` §5 storage / §9 versioning 적절 절 — 새 default + dim 1024 명시.
- `tasks/HOTFIXES.md` — dim mismatch UX entry.
- `tasks/p9/p9-fb-39-retrieval-precision-tuning.md` banner — fb-39b lever 적용 (embedding upgrade) ✅ 추가 (단 spec status 는 fb-39 frozen).
- `tasks/p9/p9-fb-39b-embedding-upgrade.md` 신규 task spec (만들거나, fb-39 sub-task 로 frontmatter 처리).
- `tasks/INDEX.md` — fb-39b 행 추가 ✅.
- 본 PR 머지 후 `chore: bump version 0.6 → 0.7` + tag (CLAUDE.md release 절차).