Skip to content

feat: optional derived vector index (#49) - #85

Merged
valiantone merged 7 commits into
mainfrom
feat/vector-index-49
Aug 28, 2026
Merged

feat: optional derived vector index (#49)#85
valiantone merged 7 commits into
mainfrom
feat/vector-index-49

Conversation

@alphakenz

@alphakenz alphakenz commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

What

Adds an optional, disposable, rebuildable derived vector index (Chroma backend) that accelerates HotMem search while SQLite/files/bundles/manifests remain canonical storage.

Why

Issue #49 (M5: Performance + Inspectors). HotMem needs optional vector acceleration for large stores, but the filesystem-first architecture requires that the index never becomes a source of truth: HotMem must run with no vector backend installed, index loss must never lose memory, and the default response shape and ranking contract must be preserved whether acceleration is present or absent.

Changes

  • src/hotmem/vector_index.py (new)VectorIndex protocol (upsert/search/delete/is_stale/status/clear/apply_rebuild_marker), NullVectorIndex (default; never imports chromadb — proven via subprocess test), ChromaVectorIndex (lazy import, persists under <mount>/hotmem-vector-index/, cosine space), get_vector_index() factory that degrades to Null with a warning when chromadb is absent, and rebuild_vector_index() that reads only SQLite rows — embeddings reused, never recomputed, zero backing-file I/O.
  • Ranking contract preserved — the index only supplies oversampled cosine candidate ids, unioned with FTS match ids; MemoryDB.search_by_ids() (new, src/hotmem/db.py) re-fetches candidates with the identical TTL-live/archived predicates and cosine_sim UDF, and the unchanged hybrid scorer (cosine + FTS + importance) recomputes final ranking → byte-identical results vs fallback.
  • Staleness detection — cheap store fingerprint (COUNT, MAX(rowid), MAX event seq) compared against rebuild_marker.json; catches inserts, deletes, INSERT OR REPLACE rewrites, and rowid reuse. Stale/missing/erroring index → silent fallback to the deterministic SQLite scan.
  • EndpointsPOST /v1/vector-index/rebuild (admin; emits index.rebuilt event; 400 vector_index_disabled / vector_dependency_missing), GET /v1/vector-index/status (backend, dependency availability, counts, staleness), DELETE /v1/vector-index (clear). /v1/search response shape untouched.
  • Config surfacecreate_app(vector_backend=...), hotmem serve --vector-index {none,chroma} (default none).
  • Packaging — optional vector extra (chromadb>=1.0,<2) in pyproject.toml; uv.lock updated additively. No version bump (auto-tag workflow fires on version changes).
  • CI — new vector job (py 3.13) installing .[dev,vector] so the real Chroma path is exercised.
  • Docs — api.md "Vector Index (optional, derived)" section stating the index is never canonical; architecture.md note; CHANGELOG Unreleased entry.
  • Teststests/test_vector_index.py (28 tests) covering all issue acceptance criteria: no-backend operation, rebuild parity across mixed seeds (inline/file-backed w+wo summary/archived/TTL-expired), stale-index fallback, index-loss safety, no eager file reads (SpyAdapter proves zero reads on a 2 MB backing file), deterministic fallback, endpoint behavior, and real-Chroma tests. Plus a golden additive test proving identical scores/ordering/shape with vs. without acceleration.

Also in this PR (v0.2.4 scope)

Release

  • Version bumped to 0.2.4 in its own commit (pyproject + __version__ +
    CHANGELOG, plus uv.lock refresh); merging auto-tags v0.2.4 via auto-tag.
  • Closes Optional derived vector index #49 — this PR is the implementation of the optional derived vector index.

- VectorIndex protocol + NullVectorIndex default (zero vector deps)
- Optional ChromaVectorIndex backend (lazy import, hotmem[vector] extra)
- Index supplies oversampled cosine candidates; SQLite re-scores with the
  identical hybrid formula, preserving the /v1/search ranking contract
- Rebuild reads SQLite only (embeddings reused, never recomputed, no file
  I/O); rebuild marker records the store fingerprint for staleness
- Admin endpoints: POST /v1/vector-index/rebuild (emits index.rebuilt),
  GET /v1/vector-index/status, DELETE /v1/vector-index
- hotmem serve --vector-index {none,chroma}; CI vector job; docs + changelog
@alphakenz
alphakenz requested a review from valiantone August 27, 2026 09:48
@valiantone valiantone added this to the v0.2.4 milestone Aug 28, 2026
@valiantone valiantone added enhancement New feature or request area:storage Storage adapter abstraction + filesystem adapters v0.2.x HotMem post-v0.2 follow-up work labels Aug 28, 2026
The rebuild marker now records the store fingerprint taken BEFORE the
all_rows() read. A mutation landing mid-rebuild (concurrent insert) thus
changes the live fingerprint and the marker reads stale, so search takes
the deterministic fallback until the next rebuild. Snapshotting after the
read could bless an index that silently misses rows inserted during the
rebuild. Adds a regression test simulating the mid-rebuild insert.

@valiantone valiantone left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

_stream computed line_start = offset + pos in (carry+chunk) coordinates,
overstating file offsets by the carry length for every line after a
chunk-spanning line. Affected unsupported_reason offsets and byte_ranges
(feeding FileInspection -> API/MCP/CLI). Offsets now use base + pos with
base = offset - len(carry), verified for exact parity against the spike's
C scanner (bench/native_spike). Adds a chunk-boundary regression test;
discovered by the native helper spike (#48, #84).
…, formatting (#91)

- gen_corpus.py: malformed-line accounting used a hardcoded 38 instead of
  the actual 41 bytes (40 + newline); counter now derives from the line.
  The insertion threshold is unaffected (drift began after insertion), so
  the committed manifest stays valid for regenerated corpora.
- run_bench.py: report rows column now takes the first arm with a
  measured row count instead of blindly the first arm; reformatted.
@valiantone
valiantone merged commit 1f2c4b5 into main Aug 28, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:storage Storage adapter abstraction + filesystem adapters enhancement New feature or request v0.2.x HotMem post-v0.2 follow-up work

Projects

None yet

2 participants