From f662be4b974e5e7cb9f9b71d4c89dfb83045f357 Mon Sep 17 00:00:00 2001 From: Aaron Paterson Date: Fri, 23 Oct 2026 08:21:00 -0500 Subject: [PATCH] Replace a deleted DiskANN medoid with a node of the graph When the medoid was deleted, diskann_medoid_handle_delete() took the first other rowid of _vectorsNN as the new one, which can be a vector still waiting in the buffer, with no node: every search then started from a node it could not read, found nothing in the graph, and returned only the buffer until the next flush. The replacement is now the first other node of _diskann_nodesNN, and a failed step of that query is returned instead of emptying the graph's medoid. --- sqlite-vec-diskann.c | 12 +++++++----- tests/test-diskann.py | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 5 deletions(-) diff --git a/sqlite-vec-diskann.c b/sqlite-vec-diskann.c index 5bd298bc..7d573ee5 100644 --- a/sqlite-vec-diskann.c +++ b/sqlite-vec-diskann.c @@ -314,7 +314,9 @@ static int diskann_medoid_set(vec0_vtab *p, int vec_col_idx, /** * Called when deleting a vector. If the deleted vector was the medoid, - * pick a new one (the first available vector, or set to empty if none remain). + * pick a new one: the first remaining node of the graph, or set to empty if + * none remain. A vector still in the buffer has no node, so it cannot be the + * entry point of a search. */ static int diskann_medoid_handle_delete(vec0_vtab *p, int vec_col_idx, i64 deletedRowid) { @@ -326,7 +328,7 @@ static int diskann_medoid_handle_delete(vec0_vtab *p, int vec_col_idx, if (!isEmpty && currentMedoid == deletedRowid) { sqlite3_stmt *stmt = NULL; char *zSql = sqlite3_mprintf( - "SELECT rowid FROM " VEC0_SHADOW_VECTORS_N_NAME " WHERE rowid != ?1 LIMIT 1", + "SELECT rowid FROM " VEC0_SHADOW_DISKANN_NODES_N_NAME " WHERE rowid != ?1 LIMIT 1", p->schemaName, p->tableName, vec_col_idx); if (!zSql) return SQLITE_NOMEM; @@ -340,10 +342,10 @@ static int diskann_medoid_handle_delete(vec0_vtab *p, int vec_col_idx, i64 newMedoid = sqlite3_column_int64(stmt, 0); sqlite3_finalize(stmt); return diskann_medoid_set(p, vec_col_idx, newMedoid, 0); - } else { - sqlite3_finalize(stmt); - return diskann_medoid_set(p, vec_col_idx, -1, 1); } + sqlite3_finalize(stmt); + if (rc != SQLITE_DONE) return rc; + return diskann_medoid_set(p, vec_col_idx, -1, 1); } return SQLITE_OK; } diff --git a/tests/test-diskann.py b/tests/test-diskann.py index 4e651604..ae9c8f1a 100644 --- a/tests/test-diskann.py +++ b/tests/test-diskann.py @@ -889,6 +889,41 @@ def test_diskann_delete_preserves_graph_connectivity(db): f"Only {len(reachable)}/{len(all_rowids)} nodes reachable" +def test_diskann_delete_medoid_with_rows_in_buffer(db): + """Deleting the medoid while rows wait in the buffer makes another node + of the graph the medoid, so every node is still found by its own vector.""" + db.execute(""" + CREATE VIRTUAL TABLE t USING vec0( + emb float[8] INDEXED BY diskann( + neighbor_quantizer=int8, n_neighbors=8, buffer_threshold=10 + ) + ) + """) + import random + random.seed(11) + vectors = {} + for rowid in [*range(100, 110), 1, 2, 3]: + vectors[rowid] = _f32([random.uniform(-1, 1) for _ in range(8)]) + db.execute("INSERT INTO t(rowid, emb) VALUES (?, ?)", [rowid, vectors[rowid]]) + assert db.execute("SELECT count(*) FROM t_diskann_buffer00").fetchone()[0] == 3 + + medoid = db.execute( + "SELECT value FROM t_info WHERE key = 'diskann_medoid_00'" + ).fetchone()[0] + db.execute("DELETE FROM t WHERE rowid = ?", [medoid]) + + new_medoid = db.execute( + "SELECT value FROM t_info WHERE key = 'diskann_medoid_00'" + ).fetchone()[0] + nodes = [r[0] for r in db.execute("SELECT rowid FROM t_diskann_nodes00")] + assert new_medoid in nodes + for rowid in nodes: + rows = db.execute( + "SELECT rowid FROM t WHERE emb MATCH ? AND k = 1", [vectors[rowid]] + ).fetchall() + assert [r[0] for r in rows] == [rowid] + + # ====================================================================== # Update scenarios # ======================================================================