Repository navigation
Description
Summary
Follow-up to #6815, spun out of the review on #6872.
#6815 fixed Select.timeout() for the four sources SelectExecutor.buildIterator() can return, by moving the deadline to the consumer (SelectExecutor.checkForTimeout()). The vector k-NN search is a fifth plan shape that never goes through buildIterator() at all, and it consults no deadline whatsoever.
Where
engine/src/main/java/com/arcadedb/query/select/SelectExecutor.java - executeVector() calls neither startTimeout() nor checkForTimeout().
Select.timeout() returns this and nearestTo() is declared on Select, so this compiles and silently ignores the timeout:
db.select().fromType("Product")
.timeout(1, TimeUnit.MILLISECONDS, true) // silently dropped
.nearestTo("embedding", query, 25)
.vertices();SelectVectorBuilder.vertices()/documents() go straight to new SelectExecutor(select).executeVector(), bypassing every path #6815 touched.
Suggested fix
Arm startTimeout() at the head of executeVector() and check the deadline in its two loops: the per-index neighbour search, and the result-assembly loop (each iteration loads a record and may run the post-filter WHERE).
Note the granularity limit: a single LSMVectorIndex.findNeighborsFromVector() call is not interruptible from executeVector(), so the deadline can only be honoured between indexes and per assembled result. That is still the difference between a no-op and a bound, but it will not cut short one long search inside a single index.
Why this was not fixed in #6872
The fix itself is three lines, but I could not write a regression test for it that is stable under this repo's timing discipline, so it was pulled back out rather than shipped unverified.
executeVector() is eager - it returns a fully materialized List. The iterator-path tests in #6872 stay stable because the consumer paces the lazy iterator with its own Thread.sleep(), making the elapsed time deterministic and the assertion one-sided in the safe direction. No such hook exists here: the caller cannot slow an eager call down, so any "a 1 ms budget expired" assertion is a straight wall-clock race that a faster machine turns red.
Measured on the PR branch (warm JVM, best of 10 runs, dim 8, k == n, with a post-filter WHERE):
| products | best elapsed |
|---|---|
| 50 | 0.25 ms |
| 1,000 | 1.29 ms |
| 5,000 | 3.67 ms |
A 50-vector fixture finishes in a quarter of a 1 ms budget - a candidate test written that way passed 5/5 in isolation on a cold JVM and then failed in the warm full-suite run, which is exactly the failure mode CLAUDE.md forbids. Even 5,000 vectors only buys a ~3.7x margin, which is not enough to be safe, and paying a much larger fixture for a bigger margin is a poor trade.
So this needs either:
- a deterministic hook that lets a test control the elapsed time inside the measured window (e.g. driving the deadline through a seam rather than the wall clock), or
- an accepted decision that the vector timeout ships with the granularity caveat and only the load-invariant assertions (a generous timeout must not truncate; no timeout must return the full
k).
Scope / confidence
High on the defect. executeVector() is the only SelectExecutor entry point with no deadline, and the chain above is reachable from the public fluent API.
Activity
fix(#6873): bound the vector k-NN path with the native Select timeout (…
robfrank commentedon Aug 28, 2026
Fixed in #6877, merged to
main.The vector k-NN search was the one query shape that never consulted the deadline at all. #6815 moved the native
Selecttimeout onto the consumer side, which covered the four ordinary result sources, butnearestTo()runs down a separate path that was never wired up - soselect().timeout(...).nearestTo(...)compiled, returned normally, and silently ignored the budget you asked for.The vector path now arms the deadline and checks it in three places: once per bucket index searched, once between the last index search and the merge/sort of the candidates, and once per result assembled.
exceptionOnTimeoutkeeps its usual meaning - either throw aTimeoutExceptionor stop and return what has already been assembled. One caveat carried over from the report: a single index search is not interruptible from the outside, so the deadline is honoured between indexes and per result, not in the middle of one long search inside a single index. That is still the difference between no bound at all and a bound.❤️ 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.