PR #78 머지 (e9e37fd). 머지 후 spec status 갱신.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
77 lines
2.9 KiB
Markdown
77 lines
2.9 KiB
Markdown
---
|
|
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<LruCache<CacheKey, Vec<SearchHit>>>` 필드. 호출자 (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 갱신 동반.
|