Repository navigation
Description
Summary
The VectorLocationIndex maintains an in-memory ConcurrentHashMap that accumulates VectorLocation entries on every put() call. Stale entries (from re-embedding unchanged or updated vertices) are never removed from memory — not by DROP INDEX, not by index recreation, and not by GC. They persist until the database is closed and reopened.
In production, this caused 9.3 million VectorLocation objects (~970MB heap) for only ~4,029 active vectors, leading to OutOfMemoryError on a 1500MB heap.
Environment
- ArcadeDB: 26.5.1
- JVM: OpenJDK 21.0.5
- OS: macOS / Linux
- Index config:
LSM_VECTOR, 384 dimensions, COSINE, INT8 quantization
Reproduction
Steps
- Create a vertex type with
embeddingproperty +LSM_VECTORindex - Store 1,000 embeddings
- Re-store the same 1,000 embeddings 10 times (simulating periodic sync cycles)
- Heap histogram shows ~10,000
VectorLocationobjects DROP INDEX+CREATE INDEX— stale entries still in memorydb.close()+db.open()— memory cleared
Expected behavior
After DROP INDEX, the in-memory ConcurrentHashMap entries for that index should be cleared. Alternatively, ArcadeDB should compact/remove stale entries automatically (standard LSM-tree compaction).
Actual behavior
Stale entries persist in the ConcurrentHashMap until the database is closed and reopened. DROP INDEX only removes the disk files; the in-memory map is untouched.
Heap histogram evidence (production)
jcmd <PID> GC.class_histogram
num #instances #bytes class name
1: 9,353,958 299,326,656 java.util.concurrent.ConcurrentHashMap$Node
2: 9,310,795 297,945,440 com.arcadedb.index.vector.VectorLocationIndex$VectorLocation
4: 9,308,069 223,393,656 com.arcadedb.database.RID
5: 9,316,716 149,067,456 java.lang.Integer
Total: ~970MB for 9.3M objects. Active vectors: ~4,029. Bloat ratio: ~2,300x.
Impact
- Memory: ~970MB of unreachable-but-pinned heap for a 4,000-vector dataset
- GC pressure: G1 old gen fills rapidly, causing sustained UNHEALTHY status (90%+ heap)
- OOM: After weeks of 10-minute sync cycles, heap exhausts and the JVM crashes
- Customer impact: Desktop app on 4GB VMs crashes after ~2-3 weeks of continuous operation
Workaround
Close and reopen the database after rebuilding vector indexes at startup:
db.command("sql", "DROP INDEX ${typeName}_embedding_idx")
db.command("sql", "CREATE INDEX ${typeName}_embedding_idx ON ${typeName} (embedding) LSM_VECTOR METADATA { ... }")
db.close()
db = DatabaseFactory(dbPath).open() // Fresh in-memory stateSuggested fixes
DROP INDEXshould clear the in-memory index map — currently only removes disk files- Automatic compaction for LSM_VECTOR — standard LSM-tree compaction would merge segments and remove tombstoned entries
COMPACT INDEXSQL command — allow explicit compaction of vector indexes- Don't load tombstoned entries on startup — when opening the database, skip stale entries when rebuilding the in-memory map
Related
- We observed this with
all-MiniLM-L6-v2384-dimensional embeddings with INT8 quantization - The
count(*)query onindex:<name>returns only active entries (4,029), not the in-memory total (9.3M) — making the bloat invisible to SQL-level monitoring - Related issues: HNSW Metadata Persistence Issue #2735 (HNSW metadata persists after DROP INDEX), Concurrent UPDATE of multi-page records (large embeddings) with LSM_VECTOR index causes "Invalid pointer to a chunk" and "Concurrent modification on page" errors. #3135 (stale ordinal mappings), Missing images when vector neighbors query #3722 (missing vectors after restart), Crash (OOM?) during graph rebuild #3868 (OOM during graph rebuild)
Activity
KyaniteSolutions commentedon Jul 28, 2026
Java reproduction script:
To run:
javac -cp arcadedb-engine-26.5.1.jar VectorIndexBloatRepro.java && java -cp .:arcadedb-engine-26.5.1.jar VectorIndexBloatReprolvca commentedon Jul 29, 2026
Fixed in #5521 - thanks for the heap histogram and the reproduction script, they made this quick to pin down.
What was actually wrong. Your diagnosis was right about the symptom and there were two causes behind it:
A tombstoned vector id kept its whole
VectorLocationresident - the location, its RID, the map node and a slot in the reverse index, ~90 bytes - purely to carry adeletedboolean. Nothing ever reads a dead id's offset again, so that memory bought nothing. A superseded id is now one bit in a compact set: ~1.2MB for the 9.3M ids that were costing you ~970MB. It also means opening the database no longer pulls every tombstoned page entry into memory.Each of your updates was being indexed twice - once while the commit serialized the record, once replaying the operations queued on the transaction. A normal LSM index collapses that onto the same key, but a vector put mints a new vector id every time, so every re-embedding cycle burned two ids and wrote two entries plus two tombstones per vertex. That is why your bloat ratio was roughly double what the arithmetic suggested. Updates now mint exactly one.
Measured on 100 vertices re-embedded 10 times: 2100 resident locations before, 100 after, and a reopen loads 100 instead of 2100.
Your workaround is no longer needed.
DROP INDEXdid already clear the in-memory map (there is a regression test for it now), but the close-and-reopen dance was doing the real work of dropping the accumulated tombstones. That accumulation is gone.On the disk side, which the issue did not mention but your workload will hit. The index file grows with every write and nothing reclaimed it, so after weeks of 10-minute sync cycles you would have had a multi-GB index for ~4K live vectors.
COMPACT INDEX <name>is the reclaim path - and while verifying it for this fix we found it was broken in four ways (it resurrected deleted vectors, duplicated them on a second run, never dropped the file it replaced, and left the index unreadable after a restart). All four are fixed in the same PR. On the test workload it takes 9 pages / 2304KB / 10500 entries down to 1 page / 256KB / 500 entries.Compaction is not automatic - it stays an explicit operation for now - so for a workload like yours the recommendation is to run
COMPACT INDEXon the vector index periodically, e.g. after a sync cycle, instead of dropping and recreating the index at startup.fix(engine) #5516: LSM_VECTOR in-memory bloat, and a compaction that …
fix(engine) #5568: stop the location index from evicting live vectors…
fix(engine) #5568: report the location index's retained heap, log the…
fix(engine) #5568: publish the vector index location map atomically i…
feat(engine): an LSM_VECTOR index compacts itself once its file is mo…
fix(engine) #5588: restore the delta-scan tombstone guard the comment…
perf(engine) #5588: lay the vector location index out in primitive ar…
fix(engine) #5516: LSM_VECTOR in-memory bloat, and a compaction that …
fix(engine) #5568: publish the vector index location map atomically i…
feat(engine): an LSM_VECTOR index compacts itself once its file is mo…