Files
kebab/docs/components/embed/README.md
altair823 5e6d977281 docs: CLAUDE 3구역 정책 + DOCS.md 지도 + component README task-spec 링크 정리
doc-reorg(C) Phase 4b+5:
- CLAUDE.md: doc-list 에 DOCS.md/CHANGELOG 추가 + zones(living/계약/증거) 정책 +
  "새 plan/handoff/per-feature spec 누적 금지(결정→HOTFIXES, 구조→ARCHITECTURE,
  릴리스→CHANGELOG)" 명문화. §Spec contract 재작성(task spec 삭제 반영: 유일
  frozen 계약 = 설계 doc, 구현 진실 = 코드). stale per-task-spec 참조 정정.
- docs/components/*/README.md (12): 삭제될 tasks/p*/ task-spec markdown 링크
  제거/plain-text 화 + 흡수된 crate(kebab-normalize→parse-md::normalize,
  kebab-parse-types→parse-md::types) 참조 정정.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Mc6W1fgsrbFKTsqA6P8La
2026-06-27 12:46:29 +00:00

116 lines
6.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Embed
> Chunk text → 단위 정규화된 벡터. trait + impl 패턴으로 future swap (candle / ollama-embed) 가능.
## 구성 crate
| Crate | 역할 |
|-------|------|
| `kebab-embed-local` | `FastembedEmbedder` — fastembed-rs 위 ONNX-backed local 임베더. default `multilingual-e5-small` 384d. |
## 구조
```mermaid
classDiagram
class Embedder {
<<trait kebab-core>>
model_id() EmbeddingModelId
model_version() EmbeddingVersion
dimensions() usize
embed(inputs) Vec~Vec~f32~~
}
class EmbeddingInput {
text: &str
kind: EmbeddingKind
}
class EmbeddingKind {
<<enum>>
Document
Query
}
class FastembedEmbedder {
+new(config) Result~Self~
-inner: Mutex~TextEmbedding~
-model_id, version, dimensions, batch_size
}
class MockEmbedder {
feature = "mock"
deterministic test double
}
Embedder <|.. FastembedEmbedder
Embedder <|.. MockEmbedder
Embedder ..> EmbeddingInput
EmbeddingInput ..> EmbeddingKind
```
## Data flow
```mermaid
flowchart LR
Chunks["Vec~Chunk~<br/>(kebab-chunk)"]
Inputs["EmbeddingInput<br/>{text, kind}"]
Prefix["E5 prefix<br/>Document → 'passage: '<br/>Query → 'query: '"]
Batch["batch by config.batch_size"]
Onnx["fastembed TextEmbedding<br/>(ONNX session, Mutex)"]
L2["L2 정규화<br/>(fastembed 내장)"]
Vec["Vec~Vec~f32~~<br/>unit norm, finite"]
Inputs --> Prefix --> Batch --> Onnx --> L2 --> Vec
Chunks -.text.-> Inputs
Query["사용자 query string<br/>(kebab-search)"] -.text+Query.-> Inputs
Vec --> VStore["kebab-store-vector"]
Vec --> Search["kebab-search<br/>(query 경로)"]
```
## 주요 type / trait / 함수
**Trait** (`kebab-core`):
- `Embedder::embed(&self, inputs: &[EmbeddingInput<'_>]) -> Result<Vec<Vec<f32>>>` — 출력 shape `inputs.len()` × `dimensions()`. 결과 벡터 모두 L2 = 1 + finite.
- `EmbeddingInput { text: &str, kind: EmbeddingKind }` — kind = `Document` / `Query` (E5 prefix 분기).
- `EmbeddingModelId(String)`, `EmbeddingVersion(String)``model_id × version × dim` 으로 vector store 테이블 분리.
**FastembedEmbedder** (`kebab-embed-local`):
- `FastembedEmbedder::new(config: &kebab_config::Config) -> Result<Self>` — 모델 파일 캐시 위치 = `{model_dir}/fastembed/`. 첫 호출 시 ONNX + tokenizer 다운로드. `config.models.embedding.dimensions` 가 실제 모델 차원과 다르면 즉시 `Err` (런타임 silent mismatch 회피).
- `Mutex<TextEmbedding>` 으로 inner 세션 직렬화 — fastembed 4.9 가 `&self` 지만 보수적 lock. kebab-app 의 indexer 가 어차피 순차 batch 라 contention 없음.
- E5 prefix 자동 적용: `Document``"passage: "`, `Query``"query: "` (§11.3).
- L2 정규화 = fastembed 내장 (`transformer_with_precedence`). 별도 정규화 안 함, 단 `assert_unit_norm` 테스트로 invariant pin.
**테스트 도구** (`kebab-core`, feature `mock`):
- `assert_vector_shape(&[Vec<f32>], expected_dims)` — 길이 + finite 검증.
- `assert_unit_norm(&[Vec<f32>], tolerance)` — L2 norm 이 `1.0 ± tolerance`. f32 384d 권장 tol = `5e-4`.
- `MockEmbedder` (feature `mock`, default OFF) — 테스트용 deterministic double. 실 어댑터는 `kebab-embed-local` 또는 future P+ adapter 가 담당.
## 외부 의존
- `kebab-embed-local``kebab-core` + `kebab-config`, `fastembed`, `anyhow`.
- 외부 lib: `fastembed-rs` (ONNX wrapper, Hugging Face 모델 다운로드 포함). 로컬 ORT runtime.
- 외부 서비스: 첫 호출 시 모델 다운로드 (Hugging Face). 그 후 오프라인.
## 핵심 결정
- **`Embedder` trait + 테스트 도구가 `kebab-core` 에 직접 거주 (`mock` feature)**.
**왜**: `kebab-store-vector`, `kebab-search` 등 downstream 은 `use kebab_core::Embedder` 로 의존 — 별도 re-export shim 불필요. `MockEmbedder` / `assert_vector_shape` / `assert_unit_norm` 은 default-OFF `mock` feature 뒤에 둠. 과거의 순수 facade `kebab-embed``kebab-core` 로 fold-in 되어 삭제됨 (crate 그래프는 [`docs/ARCHITECTURE.md`](../../ARCHITECTURE.md) 참조). 어댑터 코드는 `kebab-embed-local` 또는 future `kebab-embed-<provider>` 로.
- **`multilingual-e5-small` 384d default**.
**왜**: 한국어 + 영어 동시 강함, ONNX 작음 (~120MB), 384d 가 retrieval 정확도/저장 비용 균형 좋음. e5 prefix 컨벤션 (`"passage: "` / `"query: "`) 으로 같은 모델이 doc + query 두 모드 cover.
- **L2 정규화 = fastembed 내장에 위임**.
**왜**: fastembed 4.x 가 `transformer_with_precedence` 에서 이미 L2. 두 번 정규화 = 비용 + numerical drift. invariant 가 깨지면 `assert_unit_norm` 테스트가 즉시 실패 — fastembed 가 default 바꾸면 회귀 잡힘.
- **`Mutex<TextEmbedding>` 보수적 직렬화**.
**왜**: fastembed `&self` API 라 in principle 병렬 가능, 그러나 ORT Session 의 thread-safety 가 backend 별로 다름. indexer 가 어차피 순차 batch 라 contention 없음. profiling 에서 병목 보이면 그때 풀음.
- **dim mismatch = 생성자에서 즉시 fail**.
**왜**: `config.models.embedding.dimensions = 384` 가 실제 모델 차원과 다르면 첫 `embed` 호출에서야 발견 → 운영 시 ingest 절반 진행 후 죽음. 생성자에서 검증 = early exit, 사용자가 즉시 config 수정.
- **모델 캐시 = `{model_dir}/fastembed/` 고정 서브디렉토리**.
**왜**: spec literal. `model_dir``{data_dir}/models` default → 사용자가 한 곳에 모든 모델 캐시. fastembed 외 어댑터 (candle / ggml / ...) 는 자기 서브디렉토리 사용해서 충돌 회피.
- **`MockEmbedder` feature gate (default OFF)**.
**왜**: production binary 가 mock 코드를 포함 안 함. test crate 가 `features = ["mock"]` 로 명시 opt-in.
## 관련 spec / HOTFIXES
- frozen 설계 §7.1 (helper input types `EmbeddingInput`/`EmbeddingKind`), §7.2 (`Embedder` trait), §11.3 (E5 prefix), §6.4 (`models.embedding`), §9 (versioning): [`docs/superpowers/specs/2026-04-27-kebab-final-form-design.md`](../../superpowers/specs/2026-04-27-kebab-final-form-design.md)
- task specs: 삭제됨(2026-06-27 doc-reorg) — 설계는 frozen 계약, 동작은 tasks/HOTFIXES.md, 상세 git history.
- HOTFIXES: 이 그룹은 머지 후 deviation 없음.