Skip to content

LSMVectorIndex.remove() scans all vector ids per call: any record update on a vector-indexed type is O(index size), bulk edge-connect degrades quadraticallyΒ #5318

Closed

Description

@tae898

Summary

LSMVectorIndex.remove(Object[], Identifiable) is O(index size): it materializes all vector ids and linearly scans them to find the entries matching one RID. Because DocumentIndexer.updateDocument() fires this on every record update of a vector-indexed type (even when the indexed embedding property is unchanged), any bulk operation that rewrites vertex records of such a type degrades quadratically.

I hit it with GraphBatch bidirectional edges: the deferred incoming-edge connect updates each destination vertex once to attach the IN chunk pointer. With ~213k vectors in the index and ~210k vertex updates, the connect phase ran for hours at ~10-60 edges/sec (measured both contended and solo), fully CPU-bound. The identical graph without a vector index on the vertex type connects at >500k edges/sec.

Environment

Evidence

Log line from the connect phase (earlier, contended run that did complete):

GraphBatch: Connecting 210226 deferred incoming edges...
GraphBatch: Incoming edges connected: 210226 edges in 20068592.8 ms (10 edges/sec)

Thread dump captured mid-crawl on a solo run (one async slot shown; several identical; the thread had >20 minutes of CPU):

"AsyncExecutor-db-1" RUNNABLE cpu=1210211.33ms
  at java.util.stream.IntPipeline.toArray
  at com.arcadedb.index.vector.LSMVectorIndex.remove(LSMVectorIndex.java:3660)
  at com.arcadedb.database.DocumentIndexer.updateDocument(DocumentIndexer.java:635)
  at com.arcadedb.database.LocalDatabase.updateRecordNoLock(LocalDatabase.java:1154)
  at com.arcadedb.database.TransactionContext.commit1stPhase(TransactionContext.java:980)
  ...
  at com.arcadedb.database.async.DatabaseAsyncExecutorImpl$AsyncThread.run

The hot code (LSMVectorIndex.remove, non-BEGUN branch):

final List<Integer> deletedIds = new ArrayList<>();
for (int vectorId : vectorIndex.getAllVectorIds().toArray()) {   // O(index size) per call
    final VectorLocationIndex.VectorLocation loc = vectorIndex.getLocation(vectorId);
    if (loc != null && loc.rid.equals(rid) && !loc.deleted) { ... }
}

210k updates x 213k ids scanned/materialized each = ~4.5e10 operations. The IntStream.toArray allocation per call also explains the multi-core burn (parallel async slots all copying).

Minimal reproduction (pure Java)

  1. Create a vertex type with embedding float[384] and an LSM_VECTOR index; insert 200k vertices with random vectors.
  2. In a second pass, update an unrelated scalar property on every vertex (or create bidirectional edges into them with GraphBatch).
  3. Pass 2 is quadratic; with the vector index dropped, it is linear. Happy to attach a runnable class if useful.

Suggested directions

  1. Keep a RID-to-vectorId reverse map in VectorLocationIndex so remove() is O(k) for a RID with k vectors instead of O(index size).
  2. Independently, DocumentIndexer.updateDocument() could skip vector-index maintenance when the indexed property is unchanged between the old and new record image. Most updates of real workloads (edge pointers, counters, timestamps) never touch the embedding, so this likely removes the entire class of problem.

Found while benchmarking the Python bindings' hybrid workload (vector + SQL + Cypher over one Q&A corpus) for the SciPy paper; the workaround there is unidirectional edges for the vector-indexed type, which avoids the IN-pointer rewrites entirely.

Activity

  1. self-assigned this
    on Jul 17, 2026
  2. added this to the 26.8.1 milestone on Jul 17, 2026
  3. added theissue type on Jul 17, 2026
  4. added 2 commits that reference this issue on Jul 18, 2026
  5. lvca commented on Jul 18, 2026

    @lvca
    Member

    Confirmed and fixed in main.

    There were actually two problems compounding each other:

    1. When a record on a vector-indexed type was updated, ArcadeDB compared the old and new embedding to decide whether to touch the index. Because embeddings are float[], that comparison was by reference, not by content, so an unchanged embedding always looked "changed" and triggered a pointless remove + re-insert into the vector index on every update. Now the comparison is content-aware, so updates that don't touch the embedding (edge pointers, counters, timestamps) skip the vector index entirely.

    2. The vector index remove scanned every vector in the index to find the one belonging to a record (O(index size) per call). It now uses a RID lookup, so removing/replacing a single vector is O(1)-ish regardless of index size. This keeps genuine bulk re-embedding fast too.

    Net effect: bulk operations that rewrite records of a vector-indexed type (including GraphBatch bidirectional edges) are back to linear instead of degrading quadratically. Will ship in the next release.


    ❀️ Thanks for helping make ArcadeDB better! ArcadeDB is free and open source, sustained by its community. If it's useful to you or your company, please consider becoming a sponsor to keep development going.

  6. tae898 commented on Jul 18, 2026

    @tae898
    ContributorAuthor

    Verified on current main (ce9dc7c via my fork sync): the exact pathological shape β€” 50k vertices with a 384-dim LSM_VECTOR index, then 200k bidirectional edges connected into them via GraphBatch β€” now completes the deferred incoming-edge connect as part of a 4.4s edge phase (~46k edges/s end-to-end). Before the fix the same shape ran at 10-60 edges/sec (my 210k-edge corpus took 5.6h). Linear behavior restored; the content-aware embedding comparison also means plain property updates on vector-indexed types no longer touch the index at all in my traces. Thanks for the fast turnaround β€” both fixes land in my 26.8.1.dev3 bindings release today.

  7. lvca commented on Jul 18, 2026

    @lvca
    Member

    Thanks @tae898 for all these reports, without them it would take months to tune and fix our vector index!

  8. added a commit that references this issue on Aug 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions