[draft] hnsw: URL→node index, durable zombie reclaim, persist-then-swap, async hnsw-compact - #47
Draft
andreimarinescu wants to merge 3 commits into
Draft
[draft] hnsw: URL→node index, durable zombie reclaim, persist-then-swap, async hnsw-compact#47andreimarinescu wants to merge 3 commits into
andreimarinescu wants to merge 3 commits into
Conversation
…ot-swapped full persist, zombie-transit traversal
Per-URL node index makes MarkURLPassagesInvalid O(k) and LookupVectorByURL O(1)
(live nodes only); valid/zombie counts are maintained, not scanned.
Incremental persists now rewrite every node dirtied since the last persist
(invalidated, or handed a back-link), so reclaim and reconcile survive a
restart and shutdown no longer needs a full persist. Persist encodes per
window under the read lock instead of holding it for the whole write.
Full persists write into the inactive node slot ('v'+0x02) and only then
point meta at it (HSW2 carries the slot; HSW1 is still emitted for slot
0x01). The previous graph stays loadable throughout and the two key ranges
never share tombstones.
searchLayer treats zombies as transit-only (traversed, never admitted to the
ef window) and invalidating the entry point relocates it to the highest-level
live node.
…own checkpoint; reclaim on by default /admin/hnsw-compact now returns 202 and runs CompactPersist in the background (409 while in flight, ?wait=1 blocks for the result); state, phase, persist progress and ETA are published as /stats.hnsw_compact. The old slot is cleared only after the swap. /admin/checkpoint and /admin/frontier-clear lift the 60 s WriteTimeout like hnsw-compact already did. The crawler checkpoint uses TryPersistFrom (skips a tick while a full persist runs) and also fires on dirty-only changes; shutdown is one more incremental checkpoint instead of a full persist that never completed at production scale. A stale slot left by a crash mid-swap is cleared after load. COSIFT_ZOMBIE_RECLAIM defaults to on (0/false/off disables); reclaim and zombie counts are exposed in /stats and /metrics. COSIFT_PEBBLE_COMPACTIONS sets Pebble MaxConcurrentCompactions (default 1).
…/PEBBLE docs for reclaim default, slots, compaction knob
andreimarinescu
marked this pull request as draft
September 2, 2026 16:15
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
URL→node index (O(k) reclaim).
HNSWkeepsbyURL map[string][]int32of live node ids(full-string keys sharing the node's string bytes; ~1 GB at 16M URLs).
MarkURLPassagesInvalidis O(k) instead of an O(N) scan under the write lock,
LookupVectorByURLis O(1) and returns thefirst live passage (previously the first match, which after an invalidation was a zombie's
empty vector —
serve_search.go's callers only checkok), and valid/zombie counts aremaintained rather than walked (
/statsno longer scans 73M nodes every 5 s).Durable incremental persist. The 60 s checkpoint only wrote
nodes[lastN:]; invalidationsand back-links on older nodes were lost on every restart until a full persist — so reclaim would
have silently undone itself at each restart (reconcile can't catch duplicates, the URL is still
live). Every node whose vector was cleared or whose neighbor list gained an entry is tracked in a
dirty set and rewritten by the next checkpoint (+~10 blobs per insert). Shutdown is one more
incremental checkpoint instead of the full persist that has never completed at production scale
(
TimeoutStopSec=30). Persist encodes per ~1 GiB window under the read lock and writes outsideit, so writers stall for one window instead of the whole persist (which, via RWMutex writer
preference, would have stalled every new search for the duration of a full persist under live
crawl).
Persist-then-swap. Node blobs live in one of two slots (
'v'+0x01,'v'+0x02); a full persist(
hnsw-compact,hnsw-rebuild) writes the next generation into the inactive slot, points meta atit (HSW2 = HSW1 + slot byte; HSW1 is still emitted while the graph sits in slot 0x01, so a store
that has never swapped stays readable by v0.2.x), then clears the old slot. This removes both the
no-graph-on-disk window (Op 1 wiped disk before writing) and the tombstone storm that made Op 1's
persist run at 12 MB/s for 5h49m: fresh writes never land under the DeleteRange. A crash mid-swap
leaves the old graph loadable; the stale slot is cleared after the next load.
Zombie-transit traversal + entry-point relocation.
searchLayertraverses zombies but neveradmits them to the ef window (they used to fill it with +Inf entries — the 08-26 "dense returns 0
candidates" failure), and invalidating the entry point relocates it to the highest-level live
node. Recall on an 80 %-zombie graph is now equal to clean.
Async
/admin/hnsw-compact. Returns 202 and runsCompactPersistin the background (409 whilein flight,
?wait=1for the old synchronous shape); phase / node counts / persist progress / ETAin
/stats.hnsw_compactandcosift_hnsw_compact_running. The weekly script polls that insteadof holding a connection (Op 1's response died on the ~6 h connection); threshold 30→15 %; the
box-only timer unit is now in
deploy/systemd/./admin/checkpointand/admin/frontier-clearlift the 60 s WriteTimeout like hnsw-compact already did.
COSIFT_PEBBLE_COMPACTIONSexposes PebbleMaxConcurrentCompactions(default 1, unchanged;the single slot is the 128K-SSTable and persist-throughput cause on the box).
COSIFT_ZOMBIE_RECLAIMdefaults to on (0/false/offdisables);/stats.hnsw_reclaimed_total,cosift_hnsw_reclaimed_total,cosift_hnsw_zombie_nodes.