Skip to content

feat: add embedding cache to skip re-embedding unchanged code chunks#41

Closed
rajkumarsakthivel wants to merge 2 commits into
mainfrom
feat/embedding-cache
Closed

feat: add embedding cache to skip re-embedding unchanged code chunks#41
rajkumarsakthivel wants to merge 2 commits into
mainfrom
feat/embedding-cache

Conversation

@rajkumarsakthivel

Copy link
Copy Markdown
Collaborator

Content-hash-keyed SQLite cache so identical code is never re-embedded across index runs. On re-index, only cache misses go through the model. Shows cache hit rate in cce index output and cache stats in cce status.

rajkumarsakthivel and others added 2 commits April 27, 2026 12:44
Content-hash-keyed SQLite cache so identical code is never re-embedded
across index runs. On re-index, only cache misses go through the model.
Shows cache hit rate in `cce index` output and cache stats in `cce status`.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
fazleelahhee added a commit that referenced this pull request Apr 27, 2026
Skip re-embedding chunks whose content is unchanged across index runs.
SQLite store keyed by SHA-256 of chunk content; on re-index, only
cache misses go through the embedding model. On a typical re-index
after editing a few files, 95%+ of embeddings come from cache.

Ported from #41 (rajkumarsakthivel) with the following improvements:

1. **Binary float32 storage instead of JSON.** Vectors go through
   struct.pack/unpack — same encoding the sqlite-vec store uses
   elsewhere. ~4× smaller on disk: a 384-dim cache for a 10k-chunk
   project drops from ~60MB to ~15MB.

2. **Thread-safe SQLite connection.** `check_same_thread=False`
   matches the rest of the storage layer. The cache is now called
   from asyncio.to_thread workers (post-parallelism rewrite) — the
   original would have raised in that path.

3. **try/finally for cache.close().** Pipeline now closes the cache
   even if recording cache_hits/cache_misses raises after embed().
   Guarantees no leaked SQLite handle.

4. **prune_orphans(known_hashes).** New method drops cached entries
   whose content hash is no longer present in the live index. Pipeline
   calls it automatically on `cce index --full` so the cache doesn't
   grow monotonically forever (concrete on a high-churn project: prior
   to this, every chunk content variant ever seen accumulated). Refuses
   to operate on an empty known-set as a safety guard.

5. **Tests use pytest.approx for float comparison.** Round-tripping
   through float32 loses sub-bit precision; exact equality would fail
   for non-trivial vectors. Added tests for prune_orphans (with the
   empty-set safety check) — the upstream PR didn't cover pruning.

Other surfaces:
- `cce status` shows cache size and disk footprint when the cache
  exists for the current project
- `cce index` summary line includes "X% cache hit" when at least one
  hit was served, so users see the speedup directly
- README adds section 7 ("Content-Hash Embedding Cache") and the
  roadmap entry; section numbers shifted (Cross-Session Memory is
  now section 8)

Tests: 279 passed (267 prior + 12 new), 4:21s.

Co-Authored-By: Raj <rajkumar.sakti@gmail.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@fazleelahhee

Copy link
Copy Markdown
Owner

Thanks Raj — this is a great feature and the design is sound. We've ported it onto PR #42 in commit a3b06a3 so it lands on top of the broader hardening branch (parallel chunking, batched deletes, etc. would have caused awkward conflicts the other direction). You're credited as a co-author on the commit.

Four quality improvements on the way over:

  1. Binary struct.pack storage instead of JSON. Vectors are now stored the same way the sqlite-vec store does — about 4× smaller on disk (a 384-dim cache for a 10k-chunk project drops from ~60 MB to ~15 MB).
  2. check_same_thread=False on the SQLite connection. The cache is now hit from asyncio.to_thread workers (post-parallelism rewrite); without this the embedder would have raised ProgrammingError from worker threads.
  3. try/finally around cache.close() in the pipeline so the SQLite handle is never leaked if cache_hits = ... raises after embed.
  4. prune_orphans(known_hashes) — drops cached entries whose content hash is no longer present in the live index. Pipeline calls it automatically on cce index --full so the cache doesn't grow forever on high-churn projects. Refuses to operate on an empty known-set as a safety guard.

Tests pass (279 total, 12 new), and the README section + roadmap entry from your PR are preserved verbatim.

Closing in favor of #42. Sorry for the late merge — really nice work.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants