What happened
src/vouch/index_db.py's reset() and deindex() never clear the
legacy embeddings table, even though it's the table
search_embeddings() actually queries for semantic search and
index_embedding() writes to.
# src/vouch/index_db.py: reset()
def reset(kb_dir: Path) -> None:
"""Drop everything; the rebuild caller re-populates.
...
Leaving stale rows here means semantic search can return
orphaned hits after a reindex.
"""
with open_db(kb_dir) as conn:
conn.executescript(
"DELETE FROM claims_fts;"
"DELETE FROM pages_fts;"
"DELETE FROM entities_fts;"
"DELETE FROM embedding_index;"
"DELETE FROM query_embedding_cache;"
"DELETE FROM embedding_dupes;"
"DELETE FROM prov_edges;"
"DELETE FROM index_meta WHERE key LIKE 'embedding_%';"
"DELETE FROM index_meta WHERE key LIKE 'prov_%';"
)
Note there's no DELETE FROM embeddings; here, despite the docstring
explicitly warning about exactly this failure mode ("Leaving stale rows
here means semantic search can return orphaned hits after a reindex").
# src/vouch/index_db.py: deindex()
def deindex(conn: sqlite3.Connection, *, kind: str, id: str) -> None:
"""Remove every derived index row for a deleted artifact.
... the embedding row for any kind (every put_* calls
_embed_and_store, so an embedding may exist for a relation too) ...
"""
...
conn.execute(
"DELETE FROM embedding_index WHERE kind = ? AND id = ?", (kind, id)
)
conn.execute(
"DELETE FROM prov_edges WHERE src_id = ? OR dst_id = ?", (id, id)
)
deindex()'s own docstring says it removes "the embedding row for any
kind," but it only deletes from embedding_index — never from the
legacy embeddings table that search_embeddings() actually reads:
def search_embeddings(kb_dir: Path, query_vec: list[float], *, limit: int = 10):
...
with open_db(kb_dir) as conn:
rows = conn.execute("SELECT kind, id, vec FROM embeddings").fetchall()
What you expected
Both reset() and deindex() should clear the embeddings table the
same way they clear embedding_index, so a reindex or an artifact
deletion doesn't leave a permanently-orphaned vector behind.
Reproduction
import tempfile, pathlib
from vouch.storage import KBStore
from vouch import index_db
d = pathlib.Path(tempfile.mkdtemp())
store = KBStore.init(d)
with index_db.open_db(store.kb_dir) as conn:
index_db.index_embedding(conn, kind="claim", id="ghost", vec=[0.1, 0.2, 0.3])
conn.commit()
with index_db.open_db(store.kb_dir) as conn:
index_db.deindex(conn, kind="claim", id="ghost")
conn.commit()
with index_db.open_db(store.kb_dir) as conn:
print(conn.execute("SELECT kind, id FROM embeddings").fetchall())
# [('claim', 'ghost')] -- still there after deindex
with index_db.open_db(store.kb_dir) as conn:
index_db.index_embedding(conn, kind="claim", id="ghost2", vec=[0.4, 0.5, 0.6])
conn.commit()
index_db.reset(store.kb_dir)
with index_db.open_db(store.kb_dir) as conn:
print(conn.execute("SELECT kind, id FROM embeddings").fetchall())
# [('claim', 'ghost'), ('claim', 'ghost2')] -- both still there after reset
Concrete failure scenario
Delete any artifact that had an embedding. Its vector row in embeddings
is never removed, so it:
- Shows up as a semantic-search hit forever — even after running
vouch index (a full reindex), since health.rebuild_index() calls
reset() then only re-populates rows for artifacts that still exist
via INSERT OR REPLACE; it can never remove a stale row.
- Permanently trips
health.fsck's orphan_embedding finding, whose
own code comment says "Both the legacy embeddings table and the
newer embedding_index table are checked" — but nothing ever cleans
the legacy one, so the finding can never actually be resolved by the
documented remedy (reindexing).
Environment
- vouch version:
test branch @ current HEAD
- Python version: 3.11+
- OS: any
- Host: any — affects semantic search and
vouch fsck generally
.vouch/ state
Not required to reproduce — the repro above uses a fresh temp KB.
Anything else
This exact defect and fix were previously submitted as #543
(fix(index): clear the legacy embeddings table on reset and deindex)
but closed unmerged on 2026-07-29 purely for going stale against test
(a CHANGELOG.md conflict) — the maintainer's closing comment
explicitly said this wasn't a judgment on the change. Re-verified
independently against current test HEAD: the bug is still live in
both reset() and deindex().
Suggested fix: add "DELETE FROM embeddings;" to reset()'s
executescript, and add
conn.execute("DELETE FROM embeddings WHERE kind = ? AND id = ?", (kind, id))
in deindex() alongside the existing embedding_index delete.
What happened
src/vouch/index_db.py'sreset()anddeindex()never clear thelegacy
embeddingstable, even though it's the tablesearch_embeddings()actually queries for semantic search andindex_embedding()writes to.Note there's no
DELETE FROM embeddings;here, despite the docstringexplicitly warning about exactly this failure mode ("Leaving stale rows
here means semantic search can return orphaned hits after a reindex").
deindex()'s own docstring says it removes "the embedding row for anykind," but it only deletes from
embedding_index— never from thelegacy
embeddingstable thatsearch_embeddings()actually reads:What you expected
Both
reset()anddeindex()should clear theembeddingstable thesame way they clear
embedding_index, so a reindex or an artifactdeletion doesn't leave a permanently-orphaned vector behind.
Reproduction
Concrete failure scenario
Delete any artifact that had an embedding. Its vector row in
embeddingsis never removed, so it:
vouch index(a full reindex), sincehealth.rebuild_index()callsreset()then only re-populates rows for artifacts that still existvia
INSERT OR REPLACE; it can never remove a stale row.health.fsck'sorphan_embeddingfinding, whoseown code comment says "Both the legacy
embeddingstable and thenewer
embedding_indextable are checked" — but nothing ever cleansthe legacy one, so the finding can never actually be resolved by the
documented remedy (reindexing).
Environment
testbranch @ current HEADvouch fsckgenerally.vouch/stateNot required to reproduce — the repro above uses a fresh temp KB.
Anything else
This exact defect and fix were previously submitted as #543
(
fix(index): clear the legacy embeddings table on reset and deindex)but closed unmerged on 2026-07-29 purely for going stale against
test(a
CHANGELOG.mdconflict) — the maintainer's closing commentexplicitly said this wasn't a judgment on the change. Re-verified
independently against current
testHEAD: the bug is still live inboth
reset()anddeindex().Suggested fix: add
"DELETE FROM embeddings;"toreset()'sexecutescript, and addconn.execute("DELETE FROM embeddings WHERE kind = ? AND id = ?", (kind, id))in
deindex()alongside the existingembedding_indexdelete.