From eedba7ee144c4fa889a820cc2bd936cd5d973cf4 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Tue, 16 Jun 2026 09:44:34 -0700 Subject: [PATCH] Fixed possible index corruption with HNSW vacuuming - resolves #988 Co-authored-by: Bhagyesh Chaturvedi --- CHANGELOG.md | 1 + src/hnsw.h | 1 + src/hnswvacuum.c | 55 +++++++++++++++++++++++++++++++++++++++--------- 3 files changed, 47 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fa998ba..f713448 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ## 0.8.3 (unreleased) +- Fixed possible index corruption with HNSW vacuuming - Fixed performance regression with Hamming distance and Jaccard distance with Postgres 18 ## 0.8.2 (2026-02-25) diff --git a/src/hnsw.h b/src/hnsw.h index 65a74e8..f4492b5 100644 --- a/src/hnsw.h +++ b/src/hnsw.h @@ -412,6 +412,7 @@ typedef struct HnswVacuumState BufferAccessStrategy bas; HnswNeighborTuple ntup; HnswElementData highestPoint; + HnswElementData fallbackPoint; /* Memory */ MemoryContext tmpCtx; diff --git a/src/hnswvacuum.c b/src/hnswvacuum.c index 5de9b43..fe0e980 100644 --- a/src/hnswvacuum.c +++ b/src/hnswvacuum.c @@ -37,17 +37,20 @@ RemoveHeapTids(HnswVacuumState * vacuumstate) { BlockNumber blkno = HNSW_HEAD_BLKNO; HnswElement highestPoint = &vacuumstate->highestPoint; + HnswElement fallbackPoint = &vacuumstate->fallbackPoint; Relation index = vacuumstate->index; BufferAccessStrategy bas = vacuumstate->bas; - HnswElement entryPoint = HnswGetEntryPoint(vacuumstate->index); IndexBulkDeleteResult *stats = vacuumstate->stats; - /* Store separately since highestPoint.level is uint8 */ + /* Store separately since HnswElement level is uint8 */ int highestLevel = -1; + int fallbackLevel = -1; - /* Initialize highest point */ + /* Initialize highest point and fallback point */ highestPoint->blkno = InvalidBlockNumber; highestPoint->offno = InvalidOffsetNumber; + fallbackPoint->blkno = InvalidBlockNumber; + fallbackPoint->offno = InvalidOffsetNumber; while (BlockNumberIsValid(blkno)) { @@ -119,14 +122,31 @@ RemoveHeapTids(HnswVacuumState * vacuumstate) tidhash_insert(vacuumstate->deleted, ip, &found); Assert(!found); } - else if (etup->level > highestLevel && !(entryPoint != NULL && blkno == entryPoint->blkno && offno == entryPoint->offno)) + else if (etup->level > highestLevel) { - /* Keep track of highest non-entry point */ + if (BlockNumberIsValid(highestPoint->blkno)) + { + /* Current highest point becomes fallback */ + fallbackPoint->blkno = highestPoint->blkno; + fallbackPoint->offno = highestPoint->offno; + fallbackPoint->level = highestPoint->level; + fallbackLevel = highestLevel; + } + + /* Keep track of highest point */ highestPoint->blkno = blkno; highestPoint->offno = offno; highestPoint->level = etup->level; highestLevel = etup->level; } + else if (etup->level > fallbackLevel) + { + /* Keep track of second highest point */ + fallbackPoint->blkno = blkno; + fallbackPoint->offno = offno; + fallbackPoint->level = etup->level; + fallbackLevel = etup->level; + } } blkno = HnswPageGetOpaque(page)->nextblkno; @@ -269,12 +289,27 @@ RepairGraphEntryPoint(HnswVacuumState * vacuumstate) /* Get a shared lock */ LockPage(index, HNSW_UPDATE_LOCK, ShareLock); - /* Load element */ - HnswLoadElement(highestPoint, NULL, NULL, index, support, true, NULL); + /* Get latest entry point */ + entryPoint = HnswGetEntryPoint(index); - /* Repair if needed */ - if (NeedsUpdated(vacuumstate, highestPoint)) - RepairGraphElement(vacuumstate, highestPoint, HnswGetEntryPoint(index)); + /* Use fallback point if highest point is entry point */ + if (entryPoint != NULL && entryPoint->blkno == highestPoint->blkno && entryPoint->offno == highestPoint->offno) + { + highestPoint = &vacuumstate->fallbackPoint; + + if (!BlockNumberIsValid(highestPoint->blkno)) + highestPoint = NULL; + } + + if (highestPoint != NULL) + { + /* Load element */ + HnswLoadElement(highestPoint, NULL, NULL, index, support, true, NULL); + + /* Repair if needed */ + if (NeedsUpdated(vacuumstate, highestPoint)) + RepairGraphElement(vacuumstate, highestPoint, entryPoint); + } /* Release lock */ UnlockPage(index, HNSW_UPDATE_LOCK, ShareLock);