diff --git a/examples/contracts/log_indexer.py b/examples/contracts/log_indexer.py index 15fdbd17d..0ad420694 100644 --- a/examples/contracts/log_indexer.py +++ b/examples/contracts/log_indexer.py @@ -66,27 +66,29 @@ def get_closest_vector(self, text: str) -> dict | None: def add_log(self, log: str, log_id: int) -> None: key = log_id if key in self.log_vector_ids: - self.vector_store.get_by_id(self.log_vector_ids[key]).value = StoreValue( - text=log, log_id=key - ) - return + # VecDBElement.key (the stored vector used by knn()) is + # read-only - only .value can be updated in place. Re-embedding + # requires removing the stale entry and inserting a fresh one, + # otherwise a changed log stays ranked by its old embedding. + self.vector_store.get_by_id(self.log_vector_ids[key]).remove() emb = self.get_embedding(log) vector_id = self.vector_store.insert(emb, StoreValue(text=log, log_id=key)) self.log_vector_ids[key] = vector_id + self.removed_log_ids[key] = False @gl.public.write def update_log(self, log_id: int, log: str) -> None: key = log_id if key in self.log_vector_ids: - self.vector_store.get_by_id(self.log_vector_ids[key]).value = StoreValue( - text=log, log_id=key - ) - return + # See add_log: .key is read-only, must remove+reinsert to + # actually re-embed rather than leaving the stale vector. + self.vector_store.get_by_id(self.log_vector_ids[key]).remove() emb = self.get_embedding(log) vector_id = self.vector_store.insert(emb, StoreValue(text=log, log_id=key)) self.log_vector_ids[key] = vector_id + self.removed_log_ids[key] = False @gl.public.write def remove_log(self, id: int) -> None: diff --git a/tests/integration/icontracts/tests/test_log_indexer.py b/tests/integration/icontracts/tests/test_log_indexer.py index 8db4802c1..3bf38dc67 100644 --- a/tests/integration/icontracts/tests/test_log_indexer.py +++ b/tests/integration/icontracts/tests/test_log_indexer.py @@ -108,3 +108,65 @@ def test_log_indexer(setup_validators): assert float(closest_vector_log_2["similarity"]) > 0.99 assert closest_vector_log_2["id"] == 3 assert closest_vector_log_2["text"] == "This is the third log" + + # ######################################## + # ### Removed log_id becomes visible again + # ### after being re-added + # ######################################## + transaction_response_remove_log_2 = contract.remove_log(args=[3]).transact() + assert tx_execution_succeeded(transaction_response_remove_log_2) + + # tombstoned: must not be found anymore + closest_vector_after_remove = contract.get_closest_vector( + args=["This is the third log"] + ).call() + assert ( + closest_vector_after_remove is None + or closest_vector_after_remove["id"] != 3 + ) + + # re-add the same log_id via add_log (takes the "already indexed" + # in-place-update branch, since log_id 3 is still in log_vector_ids) + transaction_response_readd_log_2 = contract.add_log( + args=["This is the third log, again", 3] + ).transact() + assert tx_execution_succeeded(transaction_response_readd_log_2) + + # must be visible again — the tombstone from remove_log must have + # been cleared, not left permanently set + closest_vector_after_readd = contract.get_closest_vector( + args=["This is the third log, again"] + ).call() + assert closest_vector_after_readd is not None + assert closest_vector_after_readd["id"] == 3 + assert closest_vector_after_readd["text"] == "This is the third log, again" + + # ######################################## + # ### Updating an existing log_id must + # ### re-embed, not just re-label + # ######################################## + # log_id 3 currently holds "This is the third log, again" — an + # unrelated topic. If add_log's "already indexed" branch only updated + # .value (the old bug: VecDBElement.key, the stored vector, is + # read-only and can't be changed in place) instead of removing and + # re-inserting with a fresh embedding, this log would still be found + # by queries about its OLD topic and missed by queries about its new + # one, despite .text correctly reporting the new content. + transaction_response_retopic_log_2 = contract.add_log( + args=["Quantum computers use superconducting qubits", 3] + ).transact() + assert tx_execution_succeeded(transaction_response_retopic_log_2) + + # found by a query about the NEW topic — this alone proves re-embedding + # happened: the OLD embedding ("This is the third log, again") has + # nothing in common with quantum computing, so this could only match + # if a fresh embedding was actually computed and stored. + closest_vector_new_topic = contract.get_closest_vector( + args=["How do quantum computers work"] + ).call() + assert closest_vector_new_topic is not None + assert closest_vector_new_topic["id"] == 3 + assert ( + closest_vector_new_topic["text"] + == "Quantum computers use superconducting qubits" + )