Files
kebab/docs/components/store/README.md
th-kim0823 af8c162e09 docs(components): per-group contributor reference (12 그룹)
docs/components/<group>/README.md 12 페이지 + 인덱스 작성. 각 그룹
페이지가 구성 crate 표 + 구조 mermaid + data flow mermaid + 주요
type/trait/함수 시그니처 + 외부 의존 + 핵심 결정 (HOTFIXES + spec
의 "왜" 통합) + 관련 spec/HOTFIXES 링크. 인덱스가 그룹 wiring
다이어그램 + 진입 가이드 보유.

ARCHITECTURE.md 의 ASCII crate 의존 그래프를 mermaid flowchart 로
교체 (등가 정보, Gitea/GitHub 자동 렌더). docs/components/ 진입
링크 추가.

이 layer 는 contributor 향 — 사용자 향 grand picture 는 README.md
의 logical-architecture diagram 그대로 유지. 진척도는 HANDOFF.md,
per-task spec 은 tasks/INDEX.md 가 기존대로 source of truth.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 15:05:32 +09:00

9.1 KiB

Store

persistence layer 두 백엔드 — SQLite (메타 + lexical FTS + jobs + chat) + LanceDB (per-model vector 테이블). 둘이 협조해 partial-write Lance 행이 caller 에 노출되지 않게 two-phase 보장.

구성 crate

Crate 역할
kebab-store-sqlite DocumentStore + JobRepo + ChatSessionRepo + asset writer + FTS5 lexical 쿼리 + eval 저장. refinery migration runner.
kebab-store-vector VectorStore LanceDB 어댑터. per-model chunk_embeddings_<model>_<dim>.lance/ 테이블. SQLite 와 two-phase write 협조.

구조

classDiagram
    class DocumentStore {
        <<trait kebab-core>>
        put_asset(a)
        put_document(d)
        put_blocks(doc, blocks)
        put_chunks(doc, chunks)
        get_document(id)
        list_documents(filter)
    }
    class VectorStore {
        <<trait kebab-core>>
        ensure_table(model, dim) IndexId
        upsert(recs)
        search(query_vec, k, filters) Vec~VectorHit~
        delete_by_chunk_ids(ids)
    }
    class JobRepo {
        <<trait kebab-core>>
        create / update_progress / finish / list
    }
    class ChatSessionRepo {
        <<trait kebab-core>>
        create_session / get_session / list_sessions
        delete_session / append_turn / list_turns
    }
    class SqliteStore {
        +open(cfg) Self
        +run_migrations()
        +put_asset_with_bytes(asset, bytes)
        +corpus_revision() u64
        +bump_corpus_revision()
        +stale_chunk_ids_at(workspace_path)
        +rebuild_chunks_fts()
    }
    class LanceVectorStore {
        +new(cfg, Arc~SqliteStore~) Self
        -tokio::Runtime current-thread
    }
    DocumentStore <|.. SqliteStore
    JobRepo <|.. SqliteStore
    ChatSessionRepo <|.. SqliteStore
    VectorStore <|.. LanceVectorStore
    LanceVectorStore --> SqliteStore : two-phase coord

Data flow — two-phase upsert (write 경로)

flowchart LR
    Caller["kebab-app"]
    PendingIns["1. SQLite INSERT<br/>embedding_records<br/>status='pending'"]
    LanceWrite["2. Lance MergeInsert<br/>(keyed on chunk_id)"]
    CommitFlip["3. SQLite UPDATE<br/>status='committed'"]
    SearchJoin["search() join<br/>WHERE status='committed'<br/>(pending 행 안 보임)"]
    Caller --> PendingIns --> LanceWrite --> CommitFlip
    SearchJoin -.- CommitFlip
    Crash["프로세스 crash<br/>(phase 2 또는 phase 3 사이)"]
    Retry["다음 upsert 가<br/>idempotent 재시도<br/>(MergeInsert 가 chunk_id 로 dedupe)"]
    LanceWrite -.crash.-> Crash --> Retry

Data flow — re-ingest orphan cleanup (delete 경로)

flowchart LR
    Bytes["asset bytes 변경<br/>(같은 workspace_path)"]
    Purge["purge_orphan_at_workspace_path<br/>(SQLite 의 stale chunk_id 수집)"]
    LanceDel["VectorStore::delete_by_chunk_ids<br/>(LanceDB 행 삭제)"]
    Reingest["새 doc_id / chunk_id 로 정상 ingest"]
    Bytes --> Purge --> LanceDel --> Reingest

주요 type / trait / 함수

SqliteStore (kebab-store-sqlite::store):

  • SqliteStore::open(&kebab_config::Config) -> Result<Self>data_dir/sqlite 파일 열고 Mutex<Connection> 으로 wrap.
  • SqliteStore::run_migrations()refinerymigrations/V001..V005 적용.
  • put_asset_with_bytes(asset, bytes) — content-addressable 저장 ({asset_dir}/<aa>/<bb>/<full_hex> blob), assets.workspace_path UNIQUE 충돌 시 purge_orphan_at_workspace_path 후 재삽입 (HOTFIXES P7-3).
  • corpus_revision() -> u64 / bump_corpus_revision() -> Result<u64>kv 테이블의 monotonic counter (p9-fb-19, search cache invalidation key).
  • stale_chunk_ids_at(&WorkspacePath) -> Vec<ChunkId> — re-ingest 시 LanceDB 에서 지울 행 식별.
  • rebuild_chunks_fts() — FTS5 virtual table 풀 리빌드 (corruption 또는 스키마 변경 시).

SQLite migrations (migrations/):

  • V001 — full P1 schema: assets, documents, document_tags, blocks, chunks, embedding_records, jobs, ingest_runs, answers, eval_runs, eval_query_results.
  • V002chunks_fts (FTS5 virtual, unicode61 remove_diacritics 2) + chunks_ai/ad/au sync triggers. 본문은 frozen 설계 §5.5 verbatim.
  • V003embedding_records.status lifecycle marker (pending/committed) + idx_embed_status. P3-3 two-phase write 의 SQLite 측.
  • V004kv (key TEXT PK, value TEXT) + corpus_revision = '0' seed (p9-fb-19).
  • V005chat_sessions + chat_turns (FK ON DELETE CASCADE) + idx_chat_turns_session (p9-fb-17). spec PR 의 V004 가 p9-fb-19 의 kv 와 충돌해서 V005 로 시프트.

LanceVectorStore (kebab-store-vector::store):

  • LanceVectorStore::new(&Config, Arc<SqliteStore>) -> Result<Self>Arc<SqliteStore> 공유로 two-phase coord.
  • VectorStore trait 구현. 모든 메서드 안에서 private current-thread tokio::runtime::Runtimeblock_on (sync trait → async LanceDB 브리지).
  • 테이블 명: chunk_embeddings_<sanitized_model>_<dim>.lance/ (예: chunk_embeddings_multilingual-e5-small_384.lance/). 모델/차원 변경 시 자동 새 테이블 = embeddings 분리.
  • delete_by_chunk_ids(&[ChunkId]) — default impl 빈 no-op (P7-3 follow-up 으로 LanceVectorStore 만 override).

외부 의존

  • kebab-store-sqlitekebab-core + kebab-config, rusqlite (bundled feature, libsqlite3 시스템 dep 회피), refinery, serde_json, time, blake3, globset.
  • kebab-store-vectorkebab-core + kebab-config + kebab-store-sqlite, lancedb, arrow (Lance schema), tokio (current-thread runtime).
  • 외부 서비스: 없음. 모든 저장 on-disk.

핵심 결정

  • rusqlite bundled feature. : 시스템 libsqlite3 의존을 single-binary 약속이 거부. bundled 가 SQLite source 를 같이 빌드 → kebab 깔면 추가 install 0.

  • per-model Lance 테이블. : 임베딩 모델 swap 시 이전 모델의 vector 가 그대로 남아 있어야 (re-embed 유예) + 같은 chunk 가 두 모델로 동시에 indexed 가능. 테이블 명에 <model>_<dim> 인코딩 → EmbeddingModelId 변경 = 새 테이블, 기존은 read-only 유지.

  • two-phase write (SQLite first, Lance second, status flip). : LanceDB 는 transactional commit 없음 (MergeInsert 도 read-after-write 보장 안 함). SQLite 의 embedding_records.status 가 truth 역할 — searchWHERE status='committed' 로 join 해서 partial-write Lance 행 보이지 않음. 프로세스 crash 후 재 ingest 가 idempotent (Lance MergeInsert 가 chunk_id dedupe).

  • delete_by_chunk_ids 의 default no-op + Lance override. : VectorStore trait 의 default impl 가 빈 no-op 이라 test fake / 미래 다른 backend 가 default 그대로 컴파일됨 (behavioural breaking change 없음). Lance 만 override.

  • byte-edit re-ingest 시 SQLite + Lance 양쪽 stale 청소. : P7-3 의 hot bug — assets.workspace_path UNIQUE 와 upsert_asset_rowON CONFLICT(asset_id) gap 때문에 byte 가 바뀐 같은 path 의 자산이 ingest 실패. purge_orphan_at_workspace_path 가 SQLite 측 stale chunk_id 수집, follow-up PR 이 VectorStore::delete_by_chunk_ids 추가해 LanceDB 에서도 같은 chunk_id 삭제. 둘 다 안 청소하면 orphan vector 누적.

  • chat session storage = V005 (V004 와 충돌). : spec PR 가 chat_sessions 를 V004 로 잡았는데 같은 시점 p9-fb-19 가 kv 테이블을 V004 로 가져감. refinery 는 numeric 충돌 = hard fail. p9-fb-17 가 V005 로 시프트, HOTFIXES 기록.

  • SqliteStoreJobRepo + ChatSessionRepo 도 구현. : 모두 같은 SQLite 파일 안에 있고 같은 Mutex<Connection> 공유. 별 crate 로 분리하면 connection 두 개 필요 → SQLite 가 single-writer 라 contention 만 늘어남. trait 별로는 분리, impl 은 한 store.

  • FTS5 본문 spec verbatim + CI diff-check. : tokenizer (unicode61 remove_diacritics 2) + trigger 본문이 frozen 설계 §5.5 와 byte-for-byte 동일해야 wire 동작 일치. CI 가 diff 검출.

관련 spec / HOTFIXES