Summary
rebuild_index writes regenerated embeddings to the wrong table, so semantic search silently returns nothing after every reindex/import until artifacts are individually re-written.
Where
src/vouch/health.py:205 (_rebuild_embeddings) calls index_db.index_embedding(...), which writes the legacy embeddings table (index_db.py:123, INSERT OR REPLACE INTO embeddings).
- The reader path
context._retrieve → index_db.search_semantic → search_embedding queries a different table, embedding_index (index_db.py:379).
rebuild_index first calls index_db.reset(), which deletes embedding_index (index_db.py:113) but not embeddings.
- The live write hook
KBStore._embed_and_store (storage.py) correctly populates embedding_index via index_db.put_embedding.
Root cause
reset() clears the table search reads (embedding_index); _rebuild_embeddings then refills only the legacy embeddings table, which no reader queries (search_embeddings plural is dead code — no call sites in src/). So after vouch index, vouch reindex, or import-apply (which all call rebuild_index), embedding/semantic search returns zero hits and retrieval silently degrades to the FTS5/substring fallback.
Why it's a real bug (not intended)
reset()'s own docstring says the embedding write hook should backfill embedding_index on reindex. No test calls rebuild_index and then asserts the embedding backend returns hits — test_search.py accepts any of embedding/fts5/substring (the fallback masks it), and the one embedding-specific integration test never calls rebuild_index. So the current behavior is unintended and untested.
Fix
Write to embedding_index via index_db.put_embedding with the same content_hash + model/version/dim metadata the live hook uses. Regression test rebuilds and asserts each artifact's embedding is readable from embedding_index. PR attached.
Summary
rebuild_indexwrites regenerated embeddings to the wrong table, so semantic search silently returns nothing after every reindex/import until artifacts are individually re-written.Where
src/vouch/health.py:205(_rebuild_embeddings) callsindex_db.index_embedding(...), which writes the legacyembeddingstable (index_db.py:123,INSERT OR REPLACE INTO embeddings).context._retrieve→index_db.search_semantic→search_embeddingqueries a different table,embedding_index(index_db.py:379).rebuild_indexfirst callsindex_db.reset(), which deletesembedding_index(index_db.py:113) but notembeddings.KBStore._embed_and_store(storage.py) correctly populatesembedding_indexviaindex_db.put_embedding.Root cause
reset()clears the table search reads (embedding_index);_rebuild_embeddingsthen refills only the legacyembeddingstable, which no reader queries (search_embeddingsplural is dead code — no call sites insrc/). So aftervouch index,vouch reindex, or import-apply (which all callrebuild_index), embedding/semantic search returns zero hits and retrieval silently degrades to the FTS5/substring fallback.Why it's a real bug (not intended)
reset()'s own docstring says the embedding write hook should backfillembedding_indexon reindex. No test callsrebuild_indexand then asserts the embedding backend returns hits —test_search.pyaccepts any of embedding/fts5/substring (the fallback masks it), and the one embedding-specific integration test never callsrebuild_index. So the current behavior is unintended and untested.Fix
Write to
embedding_indexviaindex_db.put_embeddingwith the samecontent_hash+ model/version/dim metadata the live hook uses. Regression test rebuilds and asserts each artifact's embedding is readable fromembedding_index. PR attached.