Skip to content

Commit a2af8e6

Browse files
fix(index): clear the legacy embeddings table on reset and deindex
reset()'s docstring warns "leaving stale rows here means semantic search can return orphaned hits after a reindex" and deindex()'s docstring promises to remove "the embedding row for any kind," but neither ever touched the legacy embeddings table alongside embedding_index. every artifact deletion or full reindex leaked a stale vector row that never gets cleaned up. search_embeddings (plural, the reader of the legacy table) currently has no callers in src/, so this isn't live search pollution today - but health._check_orphan_embeddings explicitly checks both tables, so a leaked row permanently trips the orphan_embedding fsck warning with no way to clear it via the documented remedy (reindexing), and grows state.db unbounded over a kb's lifetime. add the missing DELETE FROM embeddings to reset()'s executescript, and the missing per-row delete to deindex(), matching the existing embedding_index handling in both. this exact defect and fix were previously submitted as #543 (fix(index): clear the legacy embeddings table on reset and deindex), but that PR was closed unmerged for going stale against a fast-moving test branch, not for anything wrong with the change; the maintainer's closing comment explicitly invited a fresh PR. Fixes #687
1 parent 5534a11 commit a2af8e6

3 files changed

Lines changed: 39 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,13 @@ All notable changes to vouch are documented here. Format follows
4545
artifact the caller could not already retrieve, and it touches no write path.
4646

4747
### Fixed
48+
- **`reset()`/`deindex()` now clear the legacy `embeddings` table too**
49+
(#543 reopened, root-caused): both functions' own docstrings promise to
50+
remove every embedding row for a reindex or a deleted artifact, but
51+
neither ever touched the legacy `embeddings` table alongside
52+
`embedding_index` — a leaked row permanently tripped `fsck`'s
53+
`orphan_embedding` warning with no way to clear it via reindexing, and
54+
grew `state.db` unbounded over a KB's lifetime.
4855
- **`hub_client` ETag lookup is now case-insensitive** (#662): `_request`
4956
flattened `resp.headers` (case-insensitive by design) into a plain
5057
`dict`, so `pull()`'s `resp_headers.get("ETag")` silently returned

src/vouch/index_db.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ def reset(kb_dir: Path) -> None:
122122
"DELETE FROM claims_fts;"
123123
"DELETE FROM pages_fts;"
124124
"DELETE FROM entities_fts;"
125+
"DELETE FROM embeddings;"
125126
"DELETE FROM embedding_index;"
126127
"DELETE FROM query_embedding_cache;"
127128
"DELETE FROM embedding_dupes;"
@@ -202,6 +203,9 @@ def deindex(conn: sqlite3.Connection, *, kind: str, id: str) -> None:
202203
conn.execute("DELETE FROM pages_fts WHERE id = ?", (id,))
203204
elif kind == "entity":
204205
conn.execute("DELETE FROM entities_fts WHERE id = ?", (id,))
206+
conn.execute(
207+
"DELETE FROM embeddings WHERE kind = ? AND id = ?", (kind, id)
208+
)
205209
conn.execute(
206210
"DELETE FROM embedding_index WHERE kind = ? AND id = ?", (kind, id)
207211
)

tests/test_index_db_embeddings.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,34 @@ def test_legacy_search_embeddings_ranks_by_cosine(store: KBStore) -> None:
210210
assert hits[0][1] == "c1"
211211

212212

213+
def test_deindex_removes_the_legacy_embeddings_row(store: KBStore) -> None:
214+
# deindex()'s own docstring promises to remove "the embedding row for
215+
# any kind" but only cleared embedding_index, never the legacy
216+
# `embeddings` table search_embeddings (plural) reads — a deleted
217+
# artifact's vector leaked there forever.
218+
with index_db.open_db(store.kb_dir) as conn:
219+
index_db.index_embedding(
220+
conn, kind="claim", id="ghost", vec=_vec("gone now").tolist()
221+
)
222+
conn.commit()
223+
index_db.deindex(conn, kind="claim", id="ghost")
224+
conn.commit()
225+
assert index_db.search_embeddings(store.kb_dir, _vec("gone now").tolist()) == []
226+
227+
228+
def test_reset_clears_the_legacy_embeddings_table(store: KBStore) -> None:
229+
# reset()'s own docstring warns "leaving stale rows here means semantic
230+
# search can return orphaned hits after a reindex" but never cleared
231+
# the legacy `embeddings` table itself.
232+
with index_db.open_db(store.kb_dir) as conn:
233+
index_db.index_embedding(
234+
conn, kind="claim", id="c1", vec=_vec("will be reset").tolist()
235+
)
236+
conn.commit()
237+
index_db.reset(store.kb_dir)
238+
assert index_db.search_embeddings(store.kb_dir, _vec("will be reset").tolist()) == []
239+
240+
213241
def test_legacy_search_embeddings_rejects_an_empty_query(store: KBStore) -> None:
214242
assert index_db.search_embeddings(store.kb_dir, []) == []
215243

0 commit comments

Comments
 (0)