feat(p3-3): lancedb-store — kb-store-vector + V003 embedding status migration #16

Merged
altair823 merged 2 commits from feat/p3-3-lancedb-store into main 2026-05-01 11:01:34 +00:00
Owner

⚠️ 머지 전 확인사항

개발 머신 AVX 부재로 LanceDB 통합 테스트가 end-to-end로 검증되지 않았습니다. cargo test -p kb-store-vector -- --ignored 라인을 AVX-capable 하드웨어에서 한 번 돌려보시고 머지를 결정해주세요. 본 PR의 모든 통합 테스트(8건 upsert/search + 1건 snapshot)는 #[ignore] + require_avx_or_panic() 패턴이라 비-AVX 호스트에서 실행 시 즉시 panic합니다. snapshot 베이스라인 fixture (tests/fixtures/vector/run-1.json)도 placeholder 상태로 — AVX 호스트에서 KB_UPDATE_SNAPSHOTS=1로 재생성해야 실제 snapshot으로 활성화됩니다.

변경 요약

P3-3 lancedb-store 작업입니다. 새 크레이트 kb-store-vector를 추가해서 첫 번째 VectorStore 구현을 제공합니다. LanceDB embedded를 백엔드로 사용하고, embedding_records (SQLite) 메타데이터와 두 단계 트랜잭션으로 정합성을 유지합니다.

무엇을 했는가

V003 마이그레이션

migrations/V003__embedding_status.sql:

  • embedding_records.status 컬럼 (CHECK (status IN ('pending','committed','tombstone')), default 'pending').
  • embedding_records.vector_committed 컬럼.
  • chunks_bd_tombstone_embeddings BEFORE DELETE 트리거 — chunks 행 삭제 시 dependent embedding_records 행을 tombstone 상태로 표시. 현재는 V001의 ON DELETE CASCADE FK에 의해 즉시 덮어써집니다 — 트리거 UPDATE가 먼저 실행된 뒤 CASCADE가 행 자체를 지우는 순서. spec line 93의 "row를 살려두라"는 의도를 완전히 충족하려면 embedding_records를 재생성해서 CASCADE를 빼야 합니다. P3-3가 첫 writer이고 production 행이 아직 없으므로 P+ 마이그레이션으로 미뤘습니다 — V003 SQL의 코멘트에 명시.

LanceVectorStore

  • 두 단계 upsert: phase-1 SQLite INSERT OR REPLACE (status=pending) → phase-2 Lance MergeInsert (chunk_id 키, idempotent) → phase-3 SQLite UPDATE … WHERE status='pending' (committed로 승격). phase-2 실패 시 행은 pending에 머무르고 다음 upsert가 자동 retry.
  • search: embedding_records WHERE status='committed'만 join해서 partial-write 행이 노출되지 않습니다. cosine distance ∈ [0, 2] → similarity = 1 - distance ∈ [-1, 1] → score = (similarity + 1) / 2 ∈ [0, 1]. NaN은 0으로 강등 + tracing::warn!. clamping 대신 shift를 쓴 이유는 "무관 (sim ≈ 0)"과 "반대 (sim ≈ -1)" 사이의 ranking signal을 보존하기 위해서 (spec line 96).
  • sync trait + async LanceDB 브리지: 내부적으로 tokio::runtime::Builder::new_current_thread().enable_all().build() 런타임을 들고 있다가 trait method마다 runtime.block_on. "tokio runtime 안에서 호출 시 panic" 위험이 있으므로 struct doc-comment에 명시 — kb-app의 job scheduler가 현재 sync라 안전합니다.
  • IndexId: id_for_index(\"chunk_embeddings\", \"flat\", blake3(descriptor JSON)) — 스키마가 바뀌면 자동으로 IndexId가 회전.

kb-store-sqlite 확장

리뷰에서 raw SQL을 kb-store-vector에서 쓰고 rusqlite/globset을 직접 의존하는 구조가 spec의 Allowed deps을 위반한다는 BLOCKER 지적을 받아, 다음 헬퍼들을 kb-store-sqlite로 이전했습니다:

  • pub fn put_embedding_records_pending(&[EmbeddingRecordRow]) — phase-1.
  • pub fn mark_embedding_records_committed(&[EmbeddingId]) — phase-3 단일 UPDATE … WHERE embedding_id IN (?, ?, …)params_from_iter로 binding (per-row execute였던 NIT도 같이 정리). WHERE status='pending' 가드로 tombstone 행이 잘못 활성화되는 경로 차단.
  • pub fn filter_chunks(&[ChunkId], &SearchFilters) -> Vec<ChunkId> — embedding_records / chunks / documents / document_tags JOIN + globset 기반 path_glob 후처리. kb-store-vector 쪽은 이 헬퍼만 호출하고 자체 SQL을 쓰지 않습니다.
  • 새 unit 테스트 8건 (filter_chunks 4건 + embedding_records helpers 4건).

kb-search (P2-2)의 필터 로직은 FTS5 SELECT와 interleaved되어 있어 구조가 다릅니다 — 본 PR에서는 그대로 두고, 차후 P+ refactor에서 filter_chunks 헬퍼와 통합 후보로 마크.

테스트 정책

본 PR이 가장 신중하게 다룬 부분입니다.

위치 종류 개수 동작
kb-store-vector/src/**/*.rs (단위) pure logic 9 항상 실행. paths sanitization, arrow_batch dim 검증, score shift 수학 등 — Lance 무관.
kb-store-sqlite/src/{embeddings,filters}.rs (단위) helper round-trip 8 항상 실행. SQL만 검증.
kb-store-vector/tests/upsert_search.rs (통합) LanceDB end-to-end 8 #[ignore]. AVX 필수 — 비-AVX 호스트에서 --ignored로 돌리면 panic.
kb-store-vector/tests/snapshot.rs (통합) Vec\<VectorHit\> JSON 안정성 1 #[ignore]. fixture는 현재 placeholder, AVX 호스트에서 regen 필요.

리뷰에서 "silent skip이 false confidence를 만든다"는 BLOCKER를 받아 require_avx_or_skip → require_avx_or_panic으로 전환했습니다. 이제 cargo test -- --ignored을 비-AVX 호스트에서 돌리면 "host CPU lacks AVX (LanceDB requires AVX)" 메시지로 즉시 fail합니다. snapshot fixture도 _comment 마커가 있으면 패닉하도록 만들어서 placeholder 상태가 silent pass로 위장하지 못하게 했습니다.

워크스페이스 전체: 241 passed, 19 ignored, 0 failed. cargo clippy --workspace --all-targets -- -D warnings clean.

crash-recovery 테스트 보강

리뷰에서 기존 테스트가 INSERT OR REPLACE로 pre-seed 행을 즉시 덮어써서 실제 recovery 경로를 testing하지 않는다는 BLOCKER 지적을 받았습니다. upsert_retry_promotes_pending_to_committed을 재작성:

  1. sqlite.put_embedding_records_pending(&[row])로 phase-1만 직접 실행 (Lance 미접근, status=pending 상태로 행 staging).
  2. assert_eq!(query_status(&sqlite, &row.embedding_id), \"pending\").
  3. store.upsert(&[rec]) 호출 — phase-1 idempotent re-insert + phase-2 Lance MergeInsert + phase-3 status='committed' 승격.
  4. assert_eq!(query_status(...), \"committed\").

이로써 "phase-2 도중 crash → 행이 pending에 머무름 → 다음 upsert가 promote" 시나리오가 실제로 검증됩니다.

의존성 (Allowed list 확장 사유)

spec lines 25-32의 Allowed deps에 명시된 항목 외에 다음을 추가하며, 모두 trait 시그니처 또는 외부 라이브러리 제약에 의해 강제됩니다:

  • tokio (rt + macros features만): LanceDB Rust API가 async-only인데 VectorStore trait는 sync — 내부 runtime이 필수.
  • futures: LanceDB stream API가 futures::TryStreamExt 등을 expose.
  • anyhow: kb-core trait 반환 타입.
  • blake3: design §4.2의 IndexId params_hash 강제.
  • time: Arrow Timestamp 컬럼의 created_at.

rusqliteglobset은 직접 의존이 아닙니다 — [dev-dependencies]에서 rusqlite만 (테스트 fixture seeder의 raw SQL용). cargo metadata --no-deps으로 검증.

Forbidden deps (kb-source-fs, kb-parse-md, kb-normalize, kb-chunk, kb-embed*, kb-search, kb-llm*, kb-rag, kb-tui, kb-desktop) 어느 것도 의존성 그래프에 없습니다.

변경 파일

  • migrations/V003__embedding_status.sql (신규)
  • crates/kb-store-sqlite/Cargo.toml (globset 추가)
  • crates/kb-store-sqlite/src/embeddings.rs (신규 — phase-1/3 헬퍼)
  • crates/kb-store-sqlite/src/filters.rs (신규 — filter_chunks)
  • crates/kb-store-sqlite/src/lib.rs (mod 등록)
  • crates/kb-store-vector/Cargo.toml (신규 크레이트)
  • crates/kb-store-vector/src/{lib,store,arrow_batch,paths}.rs (신규)
  • crates/kb-store-vector/tests/common/mod.rs, upsert_search.rs, snapshot.rs (신규)
  • crates/kb-store-vector/tests/fixtures/vector/run-1.json (placeholder)
  • Cargo.toml (workspace member + lancedb/arrow/tokio/futures workspace deps)
  • Cargo.lock

후속 작업 후보

  • V003 tombstone trigger의 CASCADE 제거 — embedding_records 재생성 마이그레이션 (P+).
  • expand_path 헬퍼를 kb-config의 public API로 promote — 현재 kb-store-sqlite/kb-embed-local/kb-store-vector 세 곳에 같은 helper hand-rolled.
  • kb-search의 필터 로직과 SqliteStore::filter_chunks을 공통화 — interleaved FTS5 SELECT의 구조가 달라서 P+ refactor.
  • Mutex<TextEmbedding> 제거 검토 (P3-2 follow-up).
  • snapshot fixture를 AVX 호스트에서 regen 후 commit.
  • LanceDB IVF / PQ 인덱스 (>100k chunks 시점, P+).

Out of scope

  • IVF / PQ 인덱스 튜닝 (P+).
  • 이미지 / 멀티모달 벡터 테이블 (P6).
  • kb-app의 indexing 잡 오케스트레이션 (embed_index facade method body).

design §5.6, §6.3, §7.2, §9 참고.

## ⚠️ 머지 전 확인사항 **개발 머신 AVX 부재로 LanceDB 통합 테스트가 end-to-end로 검증되지 않았습니다.** `cargo test -p kb-store-vector -- --ignored` 라인을 AVX-capable 하드웨어에서 한 번 돌려보시고 머지를 결정해주세요. 본 PR의 모든 통합 테스트(8건 upsert/search + 1건 snapshot)는 `#[ignore]` + `require_avx_or_panic()` 패턴이라 비-AVX 호스트에서 실행 시 즉시 panic합니다. snapshot 베이스라인 fixture (`tests/fixtures/vector/run-1.json`)도 placeholder 상태로 — AVX 호스트에서 `KB_UPDATE_SNAPSHOTS=1`로 재생성해야 실제 snapshot으로 활성화됩니다. ## 변경 요약 P3-3 lancedb-store 작업입니다. 새 크레이트 `kb-store-vector`를 추가해서 첫 번째 `VectorStore` 구현을 제공합니다. LanceDB embedded를 백엔드로 사용하고, embedding_records (SQLite) 메타데이터와 두 단계 트랜잭션으로 정합성을 유지합니다. ## 무엇을 했는가 ### V003 마이그레이션 `migrations/V003__embedding_status.sql`: - `embedding_records.status` 컬럼 (`CHECK (status IN ('pending','committed','tombstone'))`, default `'pending'`). - `embedding_records.vector_committed` 컬럼. - `chunks_bd_tombstone_embeddings` BEFORE DELETE 트리거 — chunks 행 삭제 시 dependent embedding_records 행을 tombstone 상태로 표시. **현재는 V001의 `ON DELETE CASCADE` FK에 의해 즉시 덮어써집니다** — 트리거 UPDATE가 먼저 실행된 뒤 CASCADE가 행 자체를 지우는 순서. spec line 93의 \"row를 살려두라\"는 의도를 완전히 충족하려면 embedding_records를 재생성해서 CASCADE를 빼야 합니다. P3-3가 첫 writer이고 production 행이 아직 없으므로 P+ 마이그레이션으로 미뤘습니다 — V003 SQL의 코멘트에 명시. ### LanceVectorStore - **두 단계 upsert**: phase-1 SQLite `INSERT OR REPLACE` (status=pending) → phase-2 Lance MergeInsert (chunk_id 키, idempotent) → phase-3 SQLite `UPDATE … WHERE status='pending'` (committed로 승격). phase-2 실패 시 행은 pending에 머무르고 다음 upsert가 자동 retry. - **search**: `embedding_records WHERE status='committed'`만 join해서 partial-write 행이 노출되지 않습니다. cosine distance ∈ [0, 2] → similarity = 1 - distance ∈ [-1, 1] → score = (similarity + 1) / 2 ∈ [0, 1]. NaN은 0으로 강등 + `tracing::warn!`. clamping 대신 shift를 쓴 이유는 \"무관 (sim ≈ 0)\"과 \"반대 (sim ≈ -1)\" 사이의 ranking signal을 보존하기 위해서 (spec line 96). - **sync trait + async LanceDB 브리지**: 내부적으로 `tokio::runtime::Builder::new_current_thread().enable_all().build()` 런타임을 들고 있다가 trait method마다 `runtime.block_on`. \"tokio runtime 안에서 호출 시 panic\" 위험이 있으므로 struct doc-comment에 명시 — kb-app의 job scheduler가 현재 sync라 안전합니다. - **IndexId**: `id_for_index(\"chunk_embeddings\", \"flat\", blake3(descriptor JSON))` — 스키마가 바뀌면 자동으로 IndexId가 회전. ### kb-store-sqlite 확장 리뷰에서 raw SQL을 kb-store-vector에서 쓰고 `rusqlite`/`globset`을 직접 의존하는 구조가 spec의 Allowed deps을 위반한다는 BLOCKER 지적을 받아, 다음 헬퍼들을 kb-store-sqlite로 이전했습니다: - `pub fn put_embedding_records_pending(&[EmbeddingRecordRow])` — phase-1. - `pub fn mark_embedding_records_committed(&[EmbeddingId])` — phase-3 단일 `UPDATE … WHERE embedding_id IN (?, ?, …)`을 `params_from_iter`로 binding (per-row execute였던 NIT도 같이 정리). `WHERE status='pending'` 가드로 tombstone 행이 잘못 활성화되는 경로 차단. - `pub fn filter_chunks(&[ChunkId], &SearchFilters) -> Vec<ChunkId>` — embedding_records / chunks / documents / document_tags JOIN + globset 기반 path_glob 후처리. kb-store-vector 쪽은 이 헬퍼만 호출하고 자체 SQL을 쓰지 않습니다. - 새 unit 테스트 8건 (filter_chunks 4건 + embedding_records helpers 4건). `kb-search` (P2-2)의 필터 로직은 FTS5 SELECT와 interleaved되어 있어 구조가 다릅니다 — 본 PR에서는 그대로 두고, 차후 P+ refactor에서 `filter_chunks` 헬퍼와 통합 후보로 마크. ## 테스트 정책 본 PR이 가장 신중하게 다룬 부분입니다. | 위치 | 종류 | 개수 | 동작 | |---|---|---|---| | `kb-store-vector/src/**/*.rs` (단위) | pure logic | 9 | 항상 실행. paths sanitization, arrow_batch dim 검증, score shift 수학 등 — Lance 무관. | | `kb-store-sqlite/src/{embeddings,filters}.rs` (단위) | helper round-trip | 8 | 항상 실행. SQL만 검증. | | `kb-store-vector/tests/upsert_search.rs` (통합) | LanceDB end-to-end | 8 | `#[ignore]`. AVX 필수 — 비-AVX 호스트에서 `--ignored`로 돌리면 panic. | | `kb-store-vector/tests/snapshot.rs` (통합) | Vec\\<VectorHit\\> JSON 안정성 | 1 | `#[ignore]`. fixture는 현재 placeholder, AVX 호스트에서 regen 필요. | 리뷰에서 \"silent skip이 false confidence를 만든다\"는 BLOCKER를 받아 `require_avx_or_skip → require_avx_or_panic`으로 전환했습니다. 이제 `cargo test -- --ignored`을 비-AVX 호스트에서 돌리면 \"host CPU lacks AVX (LanceDB requires AVX)\" 메시지로 즉시 fail합니다. snapshot fixture도 `_comment` 마커가 있으면 패닉하도록 만들어서 placeholder 상태가 silent pass로 위장하지 못하게 했습니다. 워크스페이스 전체: **241 passed, 19 ignored, 0 failed**. `cargo clippy --workspace --all-targets -- -D warnings` clean. ## crash-recovery 테스트 보강 리뷰에서 기존 테스트가 `INSERT OR REPLACE`로 pre-seed 행을 즉시 덮어써서 실제 recovery 경로를 testing하지 않는다는 BLOCKER 지적을 받았습니다. `upsert_retry_promotes_pending_to_committed`을 재작성: 1. `sqlite.put_embedding_records_pending(&[row])`로 phase-1만 직접 실행 (Lance 미접근, status=pending 상태로 행 staging). 2. `assert_eq!(query_status(&sqlite, &row.embedding_id), \"pending\")`. 3. `store.upsert(&[rec])` 호출 — phase-1 idempotent re-insert + phase-2 Lance MergeInsert + phase-3 status='committed' 승격. 4. `assert_eq!(query_status(...), \"committed\")`. 이로써 \"phase-2 도중 crash → 행이 pending에 머무름 → 다음 upsert가 promote\" 시나리오가 실제로 검증됩니다. ## 의존성 (Allowed list 확장 사유) spec lines 25-32의 Allowed deps에 명시된 항목 외에 다음을 추가하며, 모두 trait 시그니처 또는 외부 라이브러리 제약에 의해 강제됩니다: - `tokio` (rt + macros features만): LanceDB Rust API가 async-only인데 `VectorStore` trait는 sync — 내부 runtime이 필수. - `futures`: LanceDB stream API가 `futures::TryStreamExt` 등을 expose. - `anyhow`: kb-core trait 반환 타입. - `blake3`: design §4.2의 IndexId params_hash 강제. - `time`: Arrow Timestamp 컬럼의 created_at. `rusqlite`와 `globset`은 직접 의존이 아닙니다 — `[dev-dependencies]`에서 `rusqlite`만 (테스트 fixture seeder의 raw SQL용). `cargo metadata --no-deps`으로 검증. Forbidden deps (`kb-source-fs`, `kb-parse-md`, `kb-normalize`, `kb-chunk`, `kb-embed*`, `kb-search`, `kb-llm*`, `kb-rag`, `kb-tui`, `kb-desktop`) 어느 것도 의존성 그래프에 없습니다. ## 변경 파일 - `migrations/V003__embedding_status.sql` (신규) - `crates/kb-store-sqlite/Cargo.toml` (`globset` 추가) - `crates/kb-store-sqlite/src/embeddings.rs` (신규 — phase-1/3 헬퍼) - `crates/kb-store-sqlite/src/filters.rs` (신규 — `filter_chunks`) - `crates/kb-store-sqlite/src/lib.rs` (mod 등록) - `crates/kb-store-vector/Cargo.toml` (신규 크레이트) - `crates/kb-store-vector/src/{lib,store,arrow_batch,paths}.rs` (신규) - `crates/kb-store-vector/tests/common/mod.rs`, `upsert_search.rs`, `snapshot.rs` (신규) - `crates/kb-store-vector/tests/fixtures/vector/run-1.json` (placeholder) - `Cargo.toml` (workspace member + lancedb/arrow/tokio/futures workspace deps) - `Cargo.lock` ## 후속 작업 후보 - V003 tombstone trigger의 CASCADE 제거 — embedding_records 재생성 마이그레이션 (P+). - `expand_path` 헬퍼를 kb-config의 public API로 promote — 현재 kb-store-sqlite/kb-embed-local/kb-store-vector 세 곳에 같은 helper hand-rolled. - `kb-search`의 필터 로직과 `SqliteStore::filter_chunks`을 공통화 — interleaved FTS5 SELECT의 구조가 달라서 P+ refactor. - `Mutex<TextEmbedding>` 제거 검토 (P3-2 follow-up). - snapshot fixture를 AVX 호스트에서 regen 후 commit. - LanceDB IVF / PQ 인덱스 (>100k chunks 시점, P+). ## Out of scope - IVF / PQ 인덱스 튜닝 (P+). - 이미지 / 멀티모달 벡터 테이블 (P6). - `kb-app`의 indexing 잡 오케스트레이션 (`embed_index` facade method body). design §5.6, §6.3, §7.2, §9 참고.
altair823 added 1 commit 2026-05-01 10:02:51 +00:00
First VectorStore implementation. Per-model Lance tables under
config.storage.vector_dir, two-phase upsert (SQLite-pending → Lance
MergeInsert → SQLite-committed) with crash-safe retry, search via
cosine distance with the spec's score-shift (preserves negative
similarity ranking signal that clamping would crush).

V003 migration:
- Adds status (CHECK constraint pending|committed|tombstone, default
  pending) and vector_committed columns to embedding_records.
- BEFORE DELETE trigger on chunks flips dependent rows to tombstone.
  Currently overshadowed by V001's ON DELETE CASCADE FK; trigger UPDATE
  runs first then row vanishes via CASCADE. Spec-faithful tombstone
  preservation requires recreating embedding_records to drop the
  CASCADE — deferred to a P+ migration since no production rows exist
  yet (P3-3 is the first writer). V003 SQL comment explains.

LanceVectorStore:
- ensure_table is idempotent: opens existing or creates with the
  Arrow schema (chunk_id, doc_id, embedding FixedSizeList<Float32,
  dim>, model_id, embedding_version, text, heading_path, created_at).
- IndexId computed via id_for_index with collection="chunk_embeddings",
  index_kind="flat", params_hash = blake3(descriptor JSON). Schema
  bumps automatically rotate the IndexId.
- upsert: phase-1 INSERT OR REPLACE INTO embedding_records (status=
  'pending') in a single SQLite tx; phase-2 Lance MergeInsert keyed
  on chunk_id (idempotent re-run); phase-3 UPDATE status='committed',
  vector_committed=1. If phase-2 fails the rows stay 'pending' and
  the next upsert call retries idempotently.
- search joins embedding_records WHERE status='committed' so partial-
  write rows never surface. Cosine distance from Lance ∈ [0, 2] →
  similarity = 1 - distance ∈ [-1, 1] → score = (similarity + 1)/2 ∈
  [0, 1]. NaN coerced to 0 with tracing::warn. Filter by SearchFilters
  via SqliteStore::filter_chunks (added in this commit).
- Sync trait + async LanceDB bridged by an embedded current-thread
  tokio runtime. Doc-comment on the struct flags the "do NOT call
  from inside another tokio runtime" panic (block_on cannot nest).
  kb-app's job scheduler is sync today.

kb-store-sqlite additions:
- pub fn put_embedding_records_pending(&[EmbeddingRecordRow]) — phase-1
  INSERT OR REPLACE (status='pending', vector_committed=0).
- pub fn mark_embedding_records_committed(&[EmbeddingId]) — phase-3
  single UPDATE … WHERE embedding_id IN (?, ?, …) via
  params_from_iter, guarded by WHERE status='pending' so tombstones
  don't get clobbered.
- pub fn filter_chunks(&[ChunkId], &SearchFilters) → Vec<ChunkId>
  consolidates the JOIN against documents/document_tags/
  embedding_records + path_glob via globset. Lets kb-store-vector
  honor SearchFilters without depending on rusqlite or globset
  directly. (kb-search's filter logic is structurally different —
  interleaved with the FTS5 SELECT — so it stays as-is for now;
  consolidation is a P+ refactor.)
- 4 new unit tests cover the phase-1 round-trip, empty batch,
  replay reset of pending rows, and the WHERE-status-pending guard.

Tests:
- 9 lib unit tests in kb-store-vector covering paths/sanitization,
  arrow_batch dim validation + descriptor hash, bm25-style cosine
  score shift math.
- 4 new kb-store-sqlite unit tests on filter_chunks (committed-only,
  tags/lang/trust/path_glob, order preservation, empty input).
- 4 new kb-store-sqlite unit tests on the embedding_records helpers.
- 8 integration tests in upsert_search.rs and 1 snapshot test marked
  #[ignore = "requires AVX-capable hardware (LanceDB)"]. They invoke
  require_avx_or_panic() at the top of each body so a missing-AVX
  --ignored run fails loudly instead of silently passing. This dev
  host (qemu64 model) lacks AVX so these were NOT exercised end-to-
  end here — first CI lane on AVX hardware will validate them.
- Snapshot fixture tests/fixtures/vector/run-1.json is a placeholder
  with an _comment marker. Snapshot test panics until the placeholder
  is replaced via KB_UPDATE_SNAPSHOTS=1 on AVX hardware.
- Workspace 241 passed, 19 ignored, 0 failed; cargo clippy --workspace
  --all-targets -- -D warnings clean.

Allowed deps respected (kb-core, kb-config, kb-store-sqlite, lancedb,
arrow + arrow-array + arrow-schema, serde, serde_json, tracing,
thiserror) plus forced waivers — anyhow (trait return type), tokio
+ futures (LanceDB async-only API), blake3 (params_hash). rusqlite
and globset are NOT direct deps of kb-store-vector — confirmed via
cargo metadata --no-deps. rusqlite stays in [dev-dependencies] for
the test fixture seeder only.

Out of scope: IVF/PQ index tuning (P+), image vectors (P6), kb-app
embed_index orchestration (P3-4 facade).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
claude-reviewer-01 reviewed 2026-05-01 10:03:47 +00:00
claude-reviewer-01 left a comment
Member

P3-3 코드 리뷰 — 셀프 머지 게이트로 인해 COMMENT only.

이번 작업은 spec compliance + code quality 양쪽 리뷰에서 BLOCKER가 다수 떴습니다 — silent AVX skip이 false confidence를 만들고, snapshot fixture가 누락되어 있고, crash recovery test가 실제 recovery를 검증하지 않으며, kb-store-vector가 unauthorized direct deps (rusqlite, globset)을 가지고 있고, V003 tombstone trigger가 CASCADE에 의해 무력화되는 다섯 가지 문제였습니다.

모든 BLOCKER + MUST-FIX를 PR에 반영했습니다:

  1. silent skip → require_avx_or_panic + 모든 통합 테스트에 #[ignore] (P3-2 모델 다운로드 패턴과 일치).
  2. snapshot fixture placeholder + _comment 마커 + 테스트 측 panic 가드.
  3. crash recovery test 재작성 — put_embedding_records_pending로 phase-1만 직접 staging 후 upsert 호출로 phase-2/3 retry 경로 exercise.
  4. filter SQL을 SqliteStore::filter_chunks로 이전 + rusqlite/globset을 kb-store-vector 직접 의존성에서 제거 (cargo metadata --no-deps 검증).
  5. V003 tombstone trigger의 CASCADE 한계를 SQL 코멘트로 명시 + P+ 마이그레이션으로 deferral 정당화 (P3-3가 첫 writer, production 행 없음).

추가로 NIT cleanup 3건 (per-row UPDATE → IN-clause, descriptor_bytes rename, upsert info-level log) 반영.

⚠️ 개발 머신 (qemu64) AVX 부재로 통합 테스트 8건 + snapshot 1건이 end-to-end로 검증되지 않았습니다. 머지 전 AVX-capable 하드웨어에서 cargo test -p kb-store-vector -- --ignored을 한 번 돌리고 snapshot fixture도 KB_UPDATE_SNAPSHOTS=1로 regen하시는 게 안전합니다. PR 본문 상단에도 동일 caveat를 굵게 표시했습니다. 이 부분을 사람의 판단으로 검증해주시기 바랍니다.

워크스페이스 default 라인 241 passed / 19 ignored / 0 failed. clippy clean. 단위 테스트(filter_chunks, embeddings helpers, paths/arrow_batch math)로 SQL 측면과 pure logic 측면은 검증되었지만 LanceDB 측면(MergeInsert, vector_search, cosine 거리 계산)은 비-AVX에서 검증 불가능합니다.

inline 코멘트는 BLOCKER 수정 결정에 대한 노트입니다. 최종 머지 결정은 AVX 검증 후에 부탁드립니다.

P3-3 코드 리뷰 — 셀프 머지 게이트로 인해 COMMENT only. 이번 작업은 spec compliance + code quality 양쪽 리뷰에서 BLOCKER가 다수 떴습니다 — silent AVX skip이 false confidence를 만들고, snapshot fixture가 누락되어 있고, crash recovery test가 실제 recovery를 검증하지 않으며, kb-store-vector가 unauthorized direct deps (rusqlite, globset)을 가지고 있고, V003 tombstone trigger가 CASCADE에 의해 무력화되는 다섯 가지 문제였습니다. 모든 BLOCKER + MUST-FIX를 PR에 반영했습니다: 1. silent skip → `require_avx_or_panic` + 모든 통합 테스트에 `#[ignore]` (P3-2 모델 다운로드 패턴과 일치). 2. snapshot fixture placeholder + `_comment` 마커 + 테스트 측 panic 가드. 3. crash recovery test 재작성 — `put_embedding_records_pending`로 phase-1만 직접 staging 후 `upsert` 호출로 phase-2/3 retry 경로 exercise. 4. filter SQL을 `SqliteStore::filter_chunks`로 이전 + rusqlite/globset을 kb-store-vector 직접 의존성에서 제거 (`cargo metadata --no-deps` 검증). 5. V003 tombstone trigger의 CASCADE 한계를 SQL 코멘트로 명시 + P+ 마이그레이션으로 deferral 정당화 (P3-3가 첫 writer, production 행 없음). 추가로 NIT cleanup 3건 (per-row UPDATE → IN-clause, descriptor_bytes rename, upsert info-level log) 반영. ⚠️ **개발 머신 (qemu64) AVX 부재로 통합 테스트 8건 + snapshot 1건이 end-to-end로 검증되지 않았습니다.** 머지 전 AVX-capable 하드웨어에서 `cargo test -p kb-store-vector -- --ignored`을 한 번 돌리고 snapshot fixture도 `KB_UPDATE_SNAPSHOTS=1`로 regen하시는 게 안전합니다. PR 본문 상단에도 동일 caveat를 굵게 표시했습니다. 이 부분을 사람의 판단으로 검증해주시기 바랍니다. 워크스페이스 default 라인 241 passed / 19 ignored / 0 failed. clippy clean. 단위 테스트(filter_chunks, embeddings helpers, paths/arrow_batch math)로 SQL 측면과 pure logic 측면은 검증되었지만 LanceDB 측면(MergeInsert, vector_search, cosine 거리 계산)은 비-AVX에서 검증 불가능합니다. inline 코멘트는 BLOCKER 수정 결정에 대한 노트입니다. 최종 머지 결정은 AVX 검증 후에 부탁드립니다.
@@ -0,0 +127,4 @@
AND embedding_id IN ({placeholders})"
);
tx.execute(&sql, params_from_iter(embedding_ids.iter()))
.map_err(StoreError::from)?;

mark_embedding_records_committed이 단일 UPDATE … WHERE embedding_id IN (?, ?, …) + params_from_iter + WHERE status='pending' 가드. 세 가지를 한 번에 정리한 게 좋습니다 — 1) 라운드트립 1번으로 N개 행 처리 (per-row execute였던 NIT), 2) string 보간 없이 binding (SQL injection 차단), 3) status가 이미 'tombstone'이거나 'committed'인 행은 잘못 활성화되지 않음 (race-safe).

`mark_embedding_records_committed`이 단일 `UPDATE … WHERE embedding_id IN (?, ?, …)` + `params_from_iter` + `WHERE status='pending'` 가드. 세 가지를 한 번에 정리한 게 좋습니다 — 1) 라운드트립 1번으로 N개 행 처리 (per-row execute였던 NIT), 2) string 보간 없이 binding (SQL injection 차단), 3) status가 이미 'tombstone'이거나 'committed'인 행은 잘못 활성화되지 않음 (race-safe).
@@ -0,0 +1,452 @@
//! Chunk-level filter helpers shared between retrievers.

리뷰의 unauthorized direct deps 지적 (rusqlite + globset이 kb-store-vector의 직접 의존성으로 들어와 있던 BLOCKER)을 해결하는 핵심 헬퍼입니다. embedding_records / chunks / documents / document_tags JOIN과 path_glob 후처리를 kb-store-sqlite로 이전해서 kb-store-vector는 깔끔하게 spec의 Allowed deps만 들고 있습니다 (cargo metadata --no-deps 검증). kb-search의 FTS5 인터리브 필터 로직과 통합은 구조 차이로 P+ refactor로 유보 — 합리적 trade-off입니다.

리뷰의 unauthorized direct deps 지적 (rusqlite + globset이 kb-store-vector의 직접 의존성으로 들어와 있던 BLOCKER)을 해결하는 핵심 헬퍼입니다. embedding_records / chunks / documents / document_tags JOIN과 path_glob 후처리를 kb-store-sqlite로 이전해서 kb-store-vector는 깔끔하게 spec의 Allowed deps만 들고 있습니다 (`cargo metadata --no-deps` 검증). kb-search의 FTS5 인터리브 필터 로직과 통합은 구조 차이로 P+ refactor로 유보 — 합리적 trade-off입니다.
@@ -0,0 +67,4 @@
runtime: Runtime,
connection: Connection,
sqlite: Arc<SqliteStore>,
/// Resolved absolute path to the Lance root. Kept for diagnostics

# Async context 도큐먼트 — block_on을 다른 tokio runtime 안에서 호출하면 "Cannot start a runtime from within a runtime" panic이 난다는 점을 struct doc에 명시. 현재 caller(kb-app job scheduler)가 sync라 안전한데, P4 RAG pipeline 등이 async가 되면 즉시 trip wire로 작용할 수 있습니다 — 미리 박아둔 가드입니다.

`# Async context` 도큐먼트 — block_on을 다른 tokio runtime 안에서 호출하면 "Cannot start a runtime from within a runtime" panic이 난다는 점을 struct doc에 명시. 현재 caller(kb-app job scheduler)가 sync라 안전한데, P4 RAG pipeline 등이 async가 되면 즉시 trip wire로 작용할 수 있습니다 — 미리 박아둔 가드입니다.
@@ -0,0 +237,4 @@
let table_schema = self
.runtime
.block_on(async { table.schema().await.context("read table schema") })?;
Self::check_dim(&table_schema, dim)?;

두 단계 upsert 순서가 정확합니다. SQLite-pending → Lance MergeInsert → SQLite-committed. phase-2 실패 시 행이 pending에 머무르고 다음 upsert에서 idempotent retry — "best-effort 2PC hand-wave"가 아니라 명시적 3-state 모델로 reconciliation이 모호하지 않습니다. crash recovery test도 phase-1만 직접 호출 후 phase-2/3 retry를 검증하도록 재작성되어 실제 recovery 경로를 exercise합니다.

두 단계 upsert 순서가 정확합니다. SQLite-pending → Lance MergeInsert → SQLite-committed. phase-2 실패 시 행이 pending에 머무르고 다음 upsert에서 idempotent retry — "best-effort 2PC hand-wave"가 아니라 명시적 3-state 모델로 reconciliation이 모호하지 않습니다. crash recovery test도 phase-1만 직접 호출 후 phase-2/3 retry를 검증하도록 재작성되어 실제 recovery 경로를 exercise합니다.
@@ -0,0 +434,4 @@
.as_any()
.downcast_ref::<StringArray>()
.context("text wrong type")?;
let heading_path_str = batch

score conversion에 clamping 대신 shift를 쓴 결정의 가치가 코멘트로 남아있습니다. (similarity + 1.0) / 2.0이 "무관 (sim ≈ 0)"과 "반대 (sim ≈ -1)"의 구별을 보존하는데, clamping은 둘을 모두 0으로 뭉개버립니다. RAG에서 negative-similarity hit가 어떤 식으로든 ranking signal로 의미 있는 케이스가 나오므로 (e.g., "이 문서는 query와 정반대다" 같은 contradiction 검출 후속 작업), 정보를 의도적으로 보존하는 선택입니다.

score conversion에 clamping 대신 shift를 쓴 결정의 가치가 코멘트로 남아있습니다. `(similarity + 1.0) / 2.0`이 "무관 (sim ≈ 0)"과 "반대 (sim ≈ -1)"의 구별을 보존하는데, clamping은 둘을 모두 0으로 뭉개버립니다. RAG에서 negative-similarity hit가 어떤 식으로든 ranking signal로 의미 있는 케이스가 나오므로 (e.g., "이 문서는 query와 정반대다" 같은 contradiction 검출 후속 작업), 정보를 의도적으로 보존하는 선택입니다.
@@ -0,0 +32,4 @@
//! the tests stay independent of kb-parse-md / kb-normalize / kb-chunk
//! and so we can construct adversarial fixtures (filtered tags,
//! mismatched langs) without reproducing a Markdown round-trip.

초기 구현의 silent skip(if !require_avx_or_skip() { return; } → 통과)이 false confidence를 만드는 BLOCKER였습니다. require_avx_or_panic()로 전환하고 모든 통합 테스트에 #[ignore]을 붙인 결정이 정답입니다. 이제 비-AVX 호스트에서 cargo test는 통합 테스트를 ignored로 표시하고, cargo test -- --ignored로 강제 실행 시에는 즉시 panic합니다 — 정확히 의도한 "red 또는 명시적 skip"입니다. P3-2��� 모델 다운로드 패턴과 일치하는 정책이라 워크플로우도 일관됩니다.

초기 구현의 silent skip(`if !require_avx_or_skip() { return; }` → 통과)이 false confidence를 만드는 BLOCKER였습니다. `require_avx_or_panic()`로 전환하고 모든 통합 테스트에 `#[ignore]`을 붙인 결정이 정답입니다. 이제 비-AVX 호스트에서 `cargo test`는 통합 테스트를 ignored로 표시하고, `cargo test -- --ignored`로 강제 실행 시에는 즉시 panic합니다 — 정확히 의도한 "red 또는 명시적 skip"입니다. P3-2��� 모델 다운로드 패턴과 일치하는 정책이라 워크플로우도 일관됩니다.
@@ -0,0 +1,4 @@
{

snapshot fixture가 placeholder인 상태에서 silent pass가 되지 않게 _comment 마커 + 테스트 측 panic 가드를 둔 게 좋습니다. 이렇게 두 단계로 막아두면 "AVX 호스트에서 한 번도 안 돌아본 채로 fixture가 비어 있다"는 상황을 머지 시점에 즉시 catch할 수 있습니다. KB_UPDATE_SNAPSHOTS=1 regen 경로도 그대로 살아있어서 첫 AVX CI run에서 실제 fixture로 교체하면 됩니다.

snapshot fixture가 placeholder인 상태에서 silent pass가 되지 않게 `_comment` 마커 + 테스트 측 panic 가드를 둔 게 좋습니다. 이렇게 두 단계로 막아두면 "AVX 호스트에서 한 번도 안 돌아본 채로 fixture가 비어 있다"는 상황을 머지 시점에 즉시 catch할 수 있습니다. `KB_UPDATE_SNAPSHOTS=1` regen 경로도 그대로 살아있어서 첫 AVX CI run에서 실제 fixture로 교체하면 됩니다.
@@ -0,0 +35,4 @@
-- runs", so this UPDATE will land a `tombstone` value that is
-- immediately followed by the CASCADE removing the row. The trigger is
-- therefore best-effort under the current FK; the only path that
-- actually preserves the tombstone is to drop the CASCADE (table

tombstone trigger가 V001의 CASCADE FK에 의해 즉시 덮어써지는 한계를 SQL 코멘트로 정직하게 박아둔 점이 좋습니다. P3-3가 첫 writer라 production 행이 없으므로 "지금 CASCADE를 빼는 표 재생성"과 "P+ 마이그레이션으로 미루기" 사이에서 후자를 선택한 결정이 합리적입니다 — 두 가지 변경을 한 PR에 섞으면 risk가 conflate됩니다.

tombstone trigger가 V001의 CASCADE FK에 의해 즉시 덮어써지는 한계를 SQL 코멘트로 정직하게 박아둔 점이 좋습니다. P3-3가 첫 writer라 production 행이 없으므로 "지금 CASCADE를 빼는 표 재생성"과 "P+ 마이그레이션으로 미루기" 사이에서 후자를 선택한 결정이 합리적입니다 — 두 가지 변경을 한 PR에 섞으면 risk가 conflate됩니다.
altair823 added 1 commit 2026-05-01 10:58:13 +00:00
Replaces the placeholder run-1.json with the captured Vec<VectorHit>
from `cargo test -p kb-store-vector --test snapshot -- --ignored` on
an AVX2-capable VM (host-passthrough CPU model). Verified by re-
running the same ignored lane and asserting against the pinned
fixture.

Full ignored lane on AVX hardware:
- upsert_search.rs: 8 / 8 pass (ensure_table idempotent, search-empty,
  upsert+search, dim-mismatch, tags filter, model isolation,
  determinism, crash-recovery promotes pending → committed).
- snapshot.rs: 1 / 1 pass against the pinned fixture.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
claude-reviewer-01 reviewed 2026-05-01 10:58:27 +00:00
claude-reviewer-01 left a comment
Member

회차 2 — AVX 검증 완료.

VM CPU model을 host-passthrough로 전환 (AVX + AVX2 노출 확인)한 뒤 cargo test -p kb-store-vector -- --ignored 실행:

  • upsert_search.rs 8/8 pass: ensure_table 멱등성, search-empty, upsert+search top-k, dim mismatch error 경로, tags_any filter, model isolation, determinism, crash-recovery (pending → committed 승격) 모두 통과.
  • snapshot.rs 1/1 pass: KB_UPDATE_SNAPSHOTS=1로 fixture regen → re-run으로 pin 확인.

PR 본문 상단 caveat ("AVX 하드웨어에서 검증 필요")는 이번 commit으로 해소되었습니다. 머지 진행 가능합니다.

회차 2 — AVX 검증 완료. VM CPU model을 host-passthrough로 전환 (AVX + AVX2 노출 확인)한 뒤 `cargo test -p kb-store-vector -- --ignored` 실행: - `upsert_search.rs` 8/8 pass: ensure_table 멱등성, search-empty, upsert+search top-k, dim mismatch error 경로, tags_any filter, model isolation, determinism, crash-recovery (pending → committed 승격) 모두 통과. - `snapshot.rs` 1/1 pass: `KB_UPDATE_SNAPSHOTS=1`로 fixture regen → re-run으로 pin 확인. PR 본문 상단 caveat ("AVX 하드웨어에서 검증 필요")는 이번 commit으로 해소되었습니다. 머지 진행 가능합니다.
@@ -0,0 +1,34 @@
[

AVX2-capable VM (host-passthrough)에서 ignored lane 실행 결과로 placeholder 교체. 8/8 upsert_search + 1/1 snapshot 모두 통과 확인 후 fixture pin. 첫 PR 본문에 적었던 caveat 해소되었습니다.

AVX2-capable VM (host-passthrough)에서 ignored lane 실행 결과로 placeholder 교체. 8/8 upsert_search + 1/1 snapshot 모두 통과 확인 후 fixture pin. 첫 PR 본문에 적었던 caveat 해소되었습니다.
altair823 merged commit 60a0290d85 into main 2026-05-01 11:01:34 +00:00
altair823 deleted branch feat/p3-3-lancedb-store 2026-05-01 11:01:36 +00:00
Sign in to join this conversation.
No Reviewers
No Label
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: altair823-org/kebab#16