feat(kebab-app + kebab-store-sqlite): p9-fb-19 search LRU cache + corpus_revision #78

Merged
altair823 merged 2 commits from feat/p9-fb-19-cache into main 2026-05-03 05:08:13 +00:00
Owner

요약

도그푸딩 item 15 — TUI / 같은 process 안에서 동일 query 반복 시 SQLite FTS + Lance + RRF 재계산이 매번 발생하던 비용 해소. in-process LRU 캐시 + monotonic corpus_revision 카운터로 ingest commit 발생 시 모든 entry 자동 stale.

변경

  • SQLite V004: kv (key TEXT PK, value TEXT) STRICT + corpus_revision = '0' seed
  • SqliteStore::corpus_revision() / bump_corpus_revision() — atomic UPDATE
  • ingest_with_config_cancellablenew + updated > 0 시 bump, no-op reingest 보존
  • App.search_cache: Option<Mutex<LruCache<...>>> — capacity from config.search.cache_capacity (default 256, 0 비활성)
  • SearchCacheKey = query_norm (NFKC + trim + lowercase) + mode + k + snippet_chars + embedding_version + chunker_version + corpus_revision
  • App::search — cache lookup → miss 시 search_uncached → put
  • search_uncached_with_config facade + CLI --no-cache flag
  • frozen design §9 표에 corpus_revision row

테스트

  • 신규 3 store unit (fresh=0, monotonic, persist) + 4 app integration (cached repeat, NFKC case/whitespace, --no-cache parity, first ingest bump)
  • cargo test --workspace --no-fail-fast -j 1 exit 0
  • cargo clippy --workspace --all-targets -- -D warnings clean

문서

  • README kebab search: 캐시 동작 + --no-cache 안내
  • docs/SMOKE.md [search] cache_capacity = 256
  • HANDOFF entry
  • spec status planned → in_progress

Out of scope

  • patch-and-merge incremental (RRF 정규화 전체 hit set 기준)
  • SQLite 영속 cache (P+)
  • cross-process cache 공유 (in-process 만; corpus_revision 으로 cross-process 무효화는 O(1))
## 요약 도그푸딩 item 15 — TUI / 같은 process 안에서 동일 query 반복 시 SQLite FTS + Lance + RRF 재계산이 매번 발생하던 비용 해소. in-process LRU 캐시 + monotonic `corpus_revision` 카운터로 ingest commit 발생 시 모든 entry 자동 stale. ## 변경 - **SQLite V004**: `kv (key TEXT PK, value TEXT) STRICT` + `corpus_revision = '0'` seed - **`SqliteStore::corpus_revision()` / `bump_corpus_revision()`** — atomic UPDATE - **`ingest_with_config_cancellable`** — `new + updated > 0` 시 bump, no-op reingest 보존 - **`App.search_cache: Option<Mutex<LruCache<...>>>`** — capacity from `config.search.cache_capacity` (default 256, 0 비활성) - **`SearchCacheKey`** = query_norm (NFKC + trim + lowercase) + mode + k + snippet_chars + embedding_version + chunker_version + corpus_revision - **`App::search`** — cache lookup → miss 시 `search_uncached` → put - **`search_uncached_with_config`** facade + CLI `--no-cache` flag - **frozen design §9** 표에 `corpus_revision` row ## 테스트 - 신규 3 store unit (fresh=0, monotonic, persist) + 4 app integration (cached repeat, NFKC case/whitespace, --no-cache parity, first ingest bump) - `cargo test --workspace --no-fail-fast -j 1` exit 0 - `cargo clippy --workspace --all-targets -- -D warnings` clean ## 문서 - README `kebab search`: 캐시 동작 + `--no-cache` 안내 - docs/SMOKE.md `[search] cache_capacity = 256` - HANDOFF entry - spec status planned → in_progress ## Out of scope - patch-and-merge incremental (RRF 정규화 전체 hit set 기준) - SQLite 영속 cache (P+) - cross-process cache 공유 (in-process 만; corpus_revision 으로 cross-process 무효화는 O(1))
altair823 added 1 commit 2026-05-03 05:02:08 +00:00
도그푸딩 item 15 — TUI / 같은 process 안에서 동일 query 반복 시 SQLite
FTS + Lance + RRF 재계산이 매번 발생하던 비용 해소. in-process LRU
캐시 + 모노토닉 corpus_revision 카운터로 ingest commit 발생 시 모든
entry 자동 stale.

## 핵심 변경

- **SQLite V004 migration**: `kv (key TEXT PRIMARY KEY, value TEXT)
  STRICT` + `corpus_revision = '0'` seed. 미래의 다른 scalar 도 같은
  테이블에 들어갈 수 있는 generic shape.
- **`SqliteStore::corpus_revision()` / `bump_corpus_revision()`** —
  `UPDATE ... CAST AS INTEGER + 1` atomic. INSERT-OR-IGNORE 도 함께
  실행 (V004 seed 가 무슨 이유로 누락된 케이스 paranoid).
- **`kebab-app::ingest_with_config_cancellable`** — `new + updated > 0`
  시 bump, no-op (skipped-only) reingest 는 cache 보존.
- **`App.search_cache: Option<Mutex<LruCache<SearchCacheKey, Vec<
  SearchHit>>>>`** — `config.search.cache_capacity` (default 256, 0
  비활성). `lru = "0.12"` workspace dep 추가.
- **`SearchCacheKey`** = `query_norm` (NFKC + trim + lowercase) +
  `mode` + `k` + `snippet_chars` + `embedding_version` (vector/hybrid
  만, lexical 은 빈 문자열) + `chunker_version` + `corpus_revision`
  snapshot.
- **`App::search`** rewrite — cache 활성 시 lookup → miss 면 기존
  `search_uncached` 호출 후 put. cache 비활성이거나 lock 실패면
  straight-line.
- **`App::search_uncached`** (rename of pre-fb-19 `search` body) +
  `search_uncached_with_config` facade — CLI `kebab search --no-cache`
  로 진입.
- **`Config.search.cache_capacity: usize`** field, `#[serde(default)]`
  로 기존 config 호환.
- **CLI `--no-cache`** flag — 디버깅용 (CLI 는 매 호출이 새 process
  라 사실상 no-op 이지만 spec 명시 + 향후 long-lived process 호환).
- **frozen design §9 versioning** 표에 `corpus_revision` row 추가
  (기존 `index_version` 라벨과 다른 차원: 라벨은 retrieval 형상,
  corpus_revision 은 ingest commit ack).

## 테스트

- `kebab-store-sqlite` 신규 3 unit (fresh=0, monotonic bump, persist
  across reopen)
- `kebab-app` 신규 4 integration (cached repeat 같은 hits, NFKC 정규화
  로 case/whitespace collapse, --no-cache parity, first ingest bumps
  corpus_revision)
- 워크스페이스 전체 `cargo test --workspace --no-fail-fast -j 1` exit 0
- `cargo clippy --workspace --all-targets -- -D warnings` clean

## 문서

- README `kebab search` 행: 캐시 동작 + `--no-cache` 안내 + corpus_
  revision 무효화 메커니즘
- docs/SMOKE.md `[search]` 절에 `cache_capacity` 라인 추가
- HANDOFF: 2026-05-03 entry
- spec status planned → in_progress

## Out of scope

- patch-and-merge incremental (RRF 정규화 전체 hit set 기준이라 어려움)
- SQLite 영속 cache (P+)
- 다른 process 간 cache 공유 (in-process 만 — corpus_revision 이
  cross-process 무효화는 O(1))

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

회차 1 — cache + corpus_revision 패턴 정석. SQLite atomic UPDATE, NFKC + trim + lowercase 정규화, lexical 모드 embedding_version 빈 문자열 처리, run-loop 통합, --no-cache CLI 까지 spec 의 모든 facet cover. 7 신규 테스트 (3 store + 4 app integration) 도 fresh / stale / NFKC / parity / bump 골고루.

actionable nit 2 건 — (a) mutex poison 시 silently bypass 의 가시성 부족, (b) spec naming (index_version) 과 impl (corpus_revision) 차이의 doc 누락.

회차 1 — cache + corpus_revision 패턴 정석. SQLite atomic UPDATE, NFKC + trim + lowercase 정규화, lexical 모드 embedding_version 빈 문자열 처리, run-loop 통합, --no-cache CLI 까지 spec 의 모든 facet cover. 7 신규 테스트 (3 store + 4 app integration) 도 fresh / stale / NFKC / parity / bump 골고루. actionable nit 2 건 — (a) mutex poison 시 silently bypass 의 가시성 부족, (b) spec naming (index_version) 과 impl (corpus_revision) 차이의 doc 누락.
@@ -88,1 +129,4 @@
// `None` (cache disabled — every search hits the retrievers).
let search_cache = NonZeroUsize::new(config.search.cache_capacity)
.map(|cap| Mutex::new(LruCache::new(cap)));
Ok(Self {

spec p9-fb-19 의 제목 "Search result cache (in-memory LRU + index_version invalidation)" 는 무효화 카운터를 index_version 으로 칭하지만, 구현은 corpus_revision 으로 다른 이름을 채택했습니다 (이미 §9 에 다른 의미의 index_version 라벨이 있어 충돌 회피).

SearchCacheKey doc 에 한 줄 추가하면 미래 reader 가 spec / impl 사이의 naming 차이를 즉시 파악할 수 있습니다:

/// Note: spec p9-fb-19 calls this `index_version` but the impl renamed
/// to `corpus_revision` to avoid confusion with the existing
/// `IndexVersion` newtype (§9, embedding-index identity label —
/// completely different concept). HOTFIXES entry tracks the rename.

선택: tasks/HOTFIXES.md 에 한 줄 entry 추가 ("p9-fb-19 spec 의 index_version → impl 의 corpus_revision 으로 rename").

spec p9-fb-19 의 제목 `"Search result cache (in-memory LRU + index_version invalidation)"` 는 무효화 카운터를 `index_version` 으로 칭하지만, 구현은 `corpus_revision` 으로 다른 이름을 채택했습니다 (이미 §9 에 다른 의미의 `index_version` 라벨이 있어 충돌 회피). `SearchCacheKey` doc 에 한 줄 추가하면 미래 reader 가 spec / impl 사이의 naming 차이를 즉시 파악할 수 있습니다: ``` /// Note: spec p9-fb-19 calls this `index_version` but the impl renamed /// to `corpus_revision` to avoid confusion with the existing /// `IndexVersion` newtype (§9, embedding-index identity label — /// completely different concept). HOTFIXES entry tracks the rename. ``` 선택: `tasks/HOTFIXES.md` 에 한 줄 entry 추가 ("p9-fb-19 spec 의 index_version → impl 의 corpus_revision 으로 rename").

if let Ok(mut guard) = cache.lock() 가 mutex poison 시 silently fall through 합니다 (cache 활성인데 lock 실패 = 마치 cache 비활성처럼 동작). 정확성 측면에선 OK 이지만 (search 결과 항상 신선), poison 자체가 이전 panic 의 흔적이라 한 번은 알리는 게 좋습니다.

제안: 두 lock 호출 모두 .lock().unwrap_or_else(|e| { tracing::warn!(...); e.into_inner() }) 로 recovery. cache 가 poison 됐어도 다음 호출이 정상 사용 가능하고, 한 번은 warn 로그가 남음.

또는 그냥 .expect("search_cache mutex poisoned") 로 fail-fast — single-process facade 의 invariant 위반은 실제로 fatal 이라는 입장도 가능. (1) 이 graceful, (2) 가 명시적.

`if let Ok(mut guard) = cache.lock()` 가 mutex poison 시 silently fall through 합니다 (cache 활성인데 lock 실패 = 마치 cache 비활성처럼 동작). 정확성 측면에선 OK 이지만 (search 결과 항상 신선), poison 자체가 이전 panic 의 흔적이라 한 번은 알리는 게 좋습니다. 제안: 두 lock 호출 모두 `.lock().unwrap_or_else(|e| { tracing::warn!(...); e.into_inner() })` 로 recovery. cache 가 poison 됐어도 다음 호출이 정상 사용 가능하고, 한 번은 warn 로그가 남음. 또는 그냥 `.expect("search_cache mutex poisoned")` 로 fail-fast — single-process facade 의 invariant 위반은 실제로 fatal 이라는 입장도 가능. (1) 이 graceful, (2) 가 명시적.
altair823 added 1 commit 2026-05-03 05:07:53 +00:00
- `App::search` 의 두 cache.lock() 호출이 mutex poison 시 silently
  bypass 하던 것을 `unwrap_or_else(|e| { warn!; e.into_inner() })`
  recovery 로 교체. cache 가 poison 됐어도 다음 호출은 정상이고
  한 번은 warn 로그가 남아 panic 흔적 추적 가능. lookup 후 lock
  drop → retriever 호출 → 재 lock 으로 lock granularity 도 짧게.
- `clear_search_cache` 도 같은 recovery 패턴.
- `SearchCacheKey` doc 에 spec 와 impl 의 naming 차이 (index_version
  vs corpus_revision) 명시 + HOTFIXES entry 추가. spec 의 index_
  version 명칭이 design §9 의 기존 `IndexVersion` newtype (embedding
  -index identity 라벨) 과 충돌해서 corpus_revision 으로 rename.

7 tests/search_lexical 통과. clippy clean.

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

회차 2 — nit 2 건 깔끔히 반영.

  • mutex poison recovery () — cache 가 panic 후에도 정상 사용 가능, 한 번 warn 로그
  • lookup 후 lock drop → retriever → 재 lock 으로 lock granularity 짧게 (concurrent search 에 도움)
  • clear_search_cache 도 같은 패턴
  • SearchCacheKey doc 에 spec (index_version) ↔ impl (corpus_revision) 차이 명시
  • HOTFIXES entry: 2026-05-03 rename rationale (IndexVersion newtype 와 collision 회피)

추가 지적 없음. 머지 OK.

회차 2 — nit 2 건 깔끔히 반영. - mutex poison recovery () — cache 가 panic 후에도 정상 사용 가능, 한 번 warn 로그 - lookup 후 lock drop → retriever → 재 lock 으로 lock granularity 짧게 (concurrent search 에 도움) - clear_search_cache 도 같은 패턴 - SearchCacheKey doc 에 spec (index_version) ↔ impl (corpus_revision) 차이 명시 - HOTFIXES entry: 2026-05-03 rename rationale (IndexVersion newtype 와 collision 회피) 추가 지적 없음. 머지 OK.
altair823 merged commit e9e37fda6f into main 2026-05-03 05:08:13 +00:00
altair823 deleted branch feat/p9-fb-19-cache 2026-05-03 05:08:14 +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#78