--- phase: P9 component: kebab-search + kebab-app task_id: p9-fb-19 title: "Search result cache (in-memory LRU + index_version invalidation)" status: completed depends_on: [] unblocks: [] contract_source: ../../docs/superpowers/specs/2026-04-27-kebab-final-form-design.md contract_sections: [§7 search, §9 versioning] source_feedback: p9-dogfooding-feedback.md item 15 --- # p9-fb-19 — Search cache ## Goal 같은 query 반복 시 SQLite FTS + Lance + RRF 재계산 회피. 우선 in-memory LRU + `index_version` bump 기반 단순 invalidation. ## Allowed dependencies - `lru = "0.12"` (검증된 LRU crate). ## Public surface `kebab-app::App` 에 `Mutex>>` 필드. 호출자 (CLI / TUI) 는 변경 없이 cached 결과 받음. ```rust struct CacheKey { query_norm: String, // NFKC + trim + lowercase mode: SearchMode, k: u32, snippet_chars: u32, embedding_version: String, chunker_version: String, index_version: u64, } ``` CLI: `kebab search --no-cache "..."` 로 강제 bypass. ## Behavior contract - LRU capacity: 256 entry (cfg.search.cache_capacity, default 256). 메모리 한정 — 1 entry ≈ 5KB → 1.3MB 상한. - normalize: query 정규화 후 같은 entry. 사용자 입력 trim 차이가 redundant compute 안 만듦. - `index_version`: SQLite `kv` 테이블의 `kv['index_version']` 단조 증가 카운터. ingest 가 1 chunk 라도 변경하면 +1. embedding 만 추가/삭제도 +1. bump 시 cache 의 모든 entry 가 stale (index_version 키 비교). - LRU evict / stale entry 는 next miss 시 자동 garbage. 명시적 wipe API 도 제공 (`App::clear_search_cache()`). - TTL: in-memory LRU 라 process 수명. 영속 cache (SQLite) 는 P+. ## Test plan | kind | description | |------|-------------| | unit | 같은 query 2 회 → 두번째 cache hit | | unit | ingest → index_version+1 → 같은 query stale → recompute | | unit | NFKC 정규화: "Foo" / "FOO" / " Foo " 같은 entry | | unit | LRU evict: capacity+1 entry 삽입 → 가장 오래된 evict | | integration | `--no-cache` flag 가 cache bypass | ## DoD - [ ] `cargo test -p kebab-search -p kebab-app` 통과 - [ ] `index_version` SQLite 컬럼 + ingest 가 bump - [ ] frozen design §9 versioning 에 `index_version` 추가 - [ ] README — `--no-cache` 안내 ## Out of scope - patch-and-merge incremental (사용자가 말한 "추가만 끼워넣기") — P+ task. 우선 stale 시 전체 recompute. - SQLite 영속 cache — P+. - per-process 공유 cache (RwLock 다른 process 간) — P+. ## Risks / notes - patch-and-merge 가 더 효율적이지만 RRF normalization 이 hit set 전체 기준 (`2/(k+1)`) 이라 incremental 어려움. 우선 단순 LRU 가 도그푸딩 막힘 해결. - `index_version` 신규 — versioning cascade (§9) 에 새 차원 추가. 기존 5 개 (parser/chunker/embedding/prompt_template/index 그 자체 의미 다름) 와 구분 필요. 명확화 작업이 spec 갱신 동반.