Repository navigation
Description
After #8852 (fixed in #8863), a reopened LSM_VECTOR index with quantization NONE loads its graph through the ordinal map recorded next to it and skips the per-vector document read. A PRODUCT-quantized index with nothing deleted still takes the old walk on every reopen, and it also logs a line that says vectors were deleted.
What happens
On reopen with getDeletedCount() == 0, loadPersistedGraphOrDecidePrefix calls reusePersistedGraphDespiteDeletions(gf, true) (the #8852 path). That method returns null at once for PRODUCT and, before it does, logs:
Deleted vectors detected in index %s, which uses PRODUCT quantization: its PQ codes are addressed by the same ordinals the rebuild reassigns, so the graph is rebuilt from scratch rather than reused (issue #7842)
Nothing was deleted, and nothing is rebuilt either: the caller falls through to gf.loadGraph() plus the walk that re-reads every vector's document (the cost #8852 describes: 2.8 s at 1M vectors, 17.9 s at 10M). So the log line is false, and the first search after every reopen pays the O(N) read that #8852 removed for NONE.
The cause is the PRODUCT refusal that #7842 / #7844 added for the deletion case, which is right there: after a deletion the ordinal space has a hole and the PQ codes were built over a dense one. With no deletion the recorded map is exactly the array the PQ codes were built with. The refusal sits above both calls, so it also applies to the no-deletion call, and the writer does not record a map for PRODUCT at all (writeGraphManifest: "a file nothing will ever read", from the #7844 review), which #8852 later made untrue for the no-deletion case.
Measured
Untimed (laptop, not a controlled benchmark), Python bindings on engine 7f65a6e (upstream main of 2026-10-22), SIFT 100k, EUCLIDEAN, pqSubspaces 32, pqClusters 256, build_graph_now(), close, then open in a fresh process and run one vectorNeighbors (k 10, efSearch 100):
| index | line logged on the first search | first search after reopen |
|---|---|---|
| PRODUCT | Deleted vectors detected ... |
0.51 s |
| NONE (same data) | no | 0.32 s |
An idle check of 25 s after that first search showed no further CPU (0.13 s PRODUCT, 0.10 s NONE) and no changed index file, so no rebuild actually happens; the extra 0.19 s is most likely the walk (inferred from the difference, not profiled). I will add a 1M measurement to the fix PR.
Proposed fix
- record the ordinal map for PRODUCT too;
- let the exact-cover (no deletion) call reuse it for PRODUCT, when the PQ codes are loaded and are one per mapped ordinal;
- keep the PRODUCT refusal, and its log line, only for the call that runs because something was deleted;
- an index persisted by an older build has no map for PRODUCT: it keeps taking the walk, without the false line.
I will send the PR for this.
Two smaller things in the same area
Not part of the fix above, seen in my hunt on the same engine commit through SQL (not re-measured for this issue):
- the
vectorNeighborsquery optionrerankis read withopts.getInt("rerank", -1)(SQLFunctionVectorNeighbors), sorerank: 2.5is accepted, while index METADATA refusesrerankOversample2.5; vectorNeighbors(..., -1, ...)returns 0 rows without an error.
Activity
fix(#10568): a PRODUCT-quantized LSM_VECTOR index with nothing delete…
robfrank commentedon Oct 22, 2026
Fixed by #10571. A PRODUCT-quantized
LSM_VECTORindex with nothing deleted now records its ordinal map next to the graph, so a reopen takes the fast ordinal-map path instead of re-reading every vector's document, and it no longer logs "Deleted vectors detected" when nothing was deleted. In the 1M-vector measurement in the PR, the first search after a reopen went from about 2.65 s to about 0.70 s, in line with an unquantized index. After a real deletion, a PRODUCT index still rebuilds as before, and an index persisted by an older build (with no recorded map) keeps the slower path, without the false log line, until it is rebuilt.Still open: the
rerank: 2.5andvectorNeighbors(..., -1, ...)observations from this issue were not addressed by #10571. A review follow-up is tracked in #10584 (the reopen reuse checks the PQ code count, not that the codes belong to the recorded graph).❤️ 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.
fix(ArcadeData#10568): the rerank whole-number check is exact for Big…
fix(ArcadeData#10568): the rerank check builds a BigDecimal from a do…
fix(#10568): a PRODUCT-quantized LSM_VECTOR index with nothing delete…