feat: sqlite-backed embedding cache -- 2min -> <5sec rebuilds #60

Merged
joshtronic merged 1 commit from feat/rag-embedding-cache into master 2026-05-21 19:54:26 +00:00
Owner

Full re-embed every tick (860 entries / ~2min) was a tax that
didn't earn its keep. Embeddings are deterministic derived data --
same model + same text always produces the same vector -- so
caching them is safe as long as we invalidate when either changes.

New sqlite cache at $IGOR_STATE_DIR/rag-embeddings.sqlite:

CREATE TABLE embeddings (
content_hash TEXT PRIMARY KEY, -- sha256(model_name|text)
embedding BLOB NOT NULL,
created_at TEXT NOT NULL
)

The model name is included in the hash, so switching embedders
auto-invalidates the cache (old hashes become orphans that just
never match again).

Flow on each tick:

  1. Collect all source rows (journal + memory + commit), same as before
  2. Hash each row's text + model -> content_hash
  3. SELECT cached embeddings by hash
  4. Embed only the misses (new journal entries, new commits, edited
    memories)
  5. INSERT new embeddings into cache
  6. Push all rows (cached + new) into Redis (still flush + rebuild)

Self-healing intact:

  • Redis: still fully ephemeral, flushed every tick
  • sqlite cache: durable but trivially reconstructible -- delete the
    file and the next tick rebuilds from journal/memories/git
  • Both layers can be nuked independently without breaking anything

Expected performance after merge:

  • First tick: same ~2min (rebuilds the cache from cold)
  • Steady-state ticks: <5sec (only a few new entries to embed,
    usually 0-3 per tick)
  • Log line "rag: cache hit N/M, need to embed K new" tells the story

Added IGOR_RAG_CACHE_PATH knob in .env.example for visibility.

Co-Authored-By: Claude Opus 4.7 noreply@anthropic.com

Full re-embed every tick (860 entries / ~2min) was a tax that didn't earn its keep. Embeddings are deterministic derived data -- same model + same text always produces the same vector -- so caching them is safe as long as we invalidate when either changes. New sqlite cache at $IGOR_STATE_DIR/rag-embeddings.sqlite: CREATE TABLE embeddings ( content_hash TEXT PRIMARY KEY, -- sha256(model_name|text) embedding BLOB NOT NULL, created_at TEXT NOT NULL ) The model name is included in the hash, so switching embedders auto-invalidates the cache (old hashes become orphans that just never match again). Flow on each tick: 1. Collect all source rows (journal + memory + commit), same as before 2. Hash each row's text + model -> content_hash 3. SELECT cached embeddings by hash 4. Embed only the misses (new journal entries, new commits, edited memories) 5. INSERT new embeddings into cache 6. Push all rows (cached + new) into Redis (still flush + rebuild) Self-healing intact: - Redis: still fully ephemeral, flushed every tick - sqlite cache: durable but trivially reconstructible -- delete the file and the next tick rebuilds from journal/memories/git - Both layers can be nuked independently without breaking anything Expected performance after merge: - First tick: same ~2min (rebuilds the cache from cold) - Steady-state ticks: <5sec (only a few new entries to embed, usually 0-3 per tick) - Log line "rag: cache hit N/M, need to embed K new" tells the story Added IGOR_RAG_CACHE_PATH knob in .env.example for visibility. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
feat: sqlite-backed embedding cache -- 2min -> <5sec rebuilds
All checks were successful
Lint / check-sync (push) Successful in 4s
Lint / check-sync (pull_request) Successful in 4s
b8608d954c
Full re-embed every tick (860 entries / ~2min) was a tax that
didn't earn its keep. Embeddings are deterministic derived data --
same model + same text always produces the same vector -- so
caching them is safe as long as we invalidate when either changes.

New sqlite cache at $IGOR_STATE_DIR/rag-embeddings.sqlite:

  CREATE TABLE embeddings (
      content_hash TEXT PRIMARY KEY,  -- sha256(model_name|text)
      embedding BLOB NOT NULL,
      created_at TEXT NOT NULL
  )

The model name is included in the hash, so switching embedders
auto-invalidates the cache (old hashes become orphans that just
never match again).

Flow on each tick:
  1. Collect all source rows (journal + memory + commit), same as before
  2. Hash each row's text + model -> content_hash
  3. SELECT cached embeddings by hash
  4. Embed only the misses (new journal entries, new commits, edited
     memories)
  5. INSERT new embeddings into cache
  6. Push all rows (cached + new) into Redis (still flush + rebuild)

Self-healing intact:
  - Redis: still fully ephemeral, flushed every tick
  - sqlite cache: durable but trivially reconstructible -- delete the
    file and the next tick rebuilds from journal/memories/git
  - Both layers can be nuked independently without breaking anything

Expected performance after merge:
  - First tick: same ~2min (rebuilds the cache from cold)
  - Steady-state ticks: <5sec (only a few new entries to embed,
    usually 0-3 per tick)
  - Log line "rag: cache hit N/M, need to embed K new" tells the story

Added IGOR_RAG_CACHE_PATH knob in .env.example for visibility.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
joshtronic deleted branch feat/rag-embedding-cache 2026-05-21 19:54:26 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
joshtronic/igor!60
No description provided.