Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,13 @@ All notable changes to vouch are documented here. Format follows
artifact the caller could not already retrieve, and it touches no write path.

### Fixed
- **`reset()`/`deindex()` now clear the legacy `embeddings` table too**
(#543 reopened, root-caused): both functions' own docstrings promise to
remove every embedding row for a reindex or a deleted artifact, but
neither ever touched the legacy `embeddings` table alongside
`embedding_index` — a leaked row permanently tripped `fsck`'s
`orphan_embedding` warning with no way to clear it via reindexing, and
grew `state.db` unbounded over a KB's lifetime.
- **`recall`/`capture` no longer crash on malformed numeric config
values** (#488 reopened, root-caused): both `load_config()` functions
passed `max_chars`/`min_observations`/`dedup_window_seconds` straight
Expand Down
4 changes: 4 additions & 0 deletions src/vouch/index_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ def reset(kb_dir: Path) -> None:
"DELETE FROM claims_fts;"
"DELETE FROM pages_fts;"
"DELETE FROM entities_fts;"
"DELETE FROM embeddings;"
"DELETE FROM embedding_index;"
"DELETE FROM query_embedding_cache;"
"DELETE FROM embedding_dupes;"
Expand Down Expand Up @@ -202,6 +203,9 @@ def deindex(conn: sqlite3.Connection, *, kind: str, id: str) -> None:
conn.execute("DELETE FROM pages_fts WHERE id = ?", (id,))
elif kind == "entity":
conn.execute("DELETE FROM entities_fts WHERE id = ?", (id,))
conn.execute(
"DELETE FROM embeddings WHERE kind = ? AND id = ?", (kind, id)
)
conn.execute(
"DELETE FROM embedding_index WHERE kind = ? AND id = ?", (kind, id)
)
Expand Down
28 changes: 28 additions & 0 deletions tests/test_index_db_embeddings.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,34 @@ def test_legacy_search_embeddings_ranks_by_cosine(store: KBStore) -> None:
assert hits[0][1] == "c1"


def test_deindex_removes_the_legacy_embeddings_row(store: KBStore) -> None:
# deindex()'s own docstring promises to remove "the embedding row for
# any kind" but only cleared embedding_index, never the legacy
# `embeddings` table search_embeddings (plural) reads — a deleted
# artifact's vector leaked there forever.
with index_db.open_db(store.kb_dir) as conn:
index_db.index_embedding(
conn, kind="claim", id="ghost", vec=_vec("gone now").tolist()
)
conn.commit()
index_db.deindex(conn, kind="claim", id="ghost")
conn.commit()
assert index_db.search_embeddings(store.kb_dir, _vec("gone now").tolist()) == []


def test_reset_clears_the_legacy_embeddings_table(store: KBStore) -> None:
# reset()'s own docstring warns "leaving stale rows here means semantic
# search can return orphaned hits after a reindex" but never cleared
# the legacy `embeddings` table itself.
with index_db.open_db(store.kb_dir) as conn:
index_db.index_embedding(
conn, kind="claim", id="c1", vec=_vec("will be reset").tolist()
)
conn.commit()
index_db.reset(store.kb_dir)
assert index_db.search_embeddings(store.kb_dir, _vec("will be reset").tolist()) == []


def test_legacy_search_embeddings_rejects_an_empty_query(store: KBStore) -> None:
assert index_db.search_embeddings(store.kb_dir, []) == []

Expand Down
Loading