There appears to be a potential memory leak / memory retention issue in SearchIterator when running large search iterations.
SearchIterator uses IteratorCache to keep leftover results between calls to next(). When a cached page contains more rows than the requested batch size, the current implementation splits the cached list using List.subList() and stores the remaining suffix back into the cache.
List<QueryResultsWrapper.RowRecord> retPageRes = cachedPage.subList(0, (int) count);
List<QueryResultsWrapper.RowRecord> leftCachePage = cachedPage.subList((int) count, cachedPage.size());
iteratorCache.cache(cacheId, leftCachePage);
return retPageRes;
In Java, subList() returns a view backed by the original list. This means the cached suffix can still retain a reference to the full original list, including rows that have already been returned to the user. During very large iterations, this can cause already-consumed result rows to remain reachable and prevent them from being garbage collected.
This is especially risky when:
SearchIterator is used for very large topK values.
Each result row contains large output fields.
The iterator repeatedly overfetches and stores leftover cached pages.
The user does not call close() immediately after finishing iteration.
Affected Area
sdk-core/src/main/java/io/milvus/orm/iterator/SearchIterator.java
sdk-core/src/main/java/io/milvus/orm/iterator/IteratorCache.java
Expected Behavior
After a batch of results has been returned by SearchIterator.next(), rows that are no longer needed should become eligible for garbage collection.
The cache should only retain rows that are still needed for future next() calls.
Actual Behavior
The cache may store subList() views that retain the backing list of a larger result page. As a result, already-consumed rows may remain reachable through the cached suffix, causing unnecessary memory retention and potentially leading to increased heap usage or OOM in large iterations.
Possible Approaches
- Store defensive copies in IteratorCache so cached entries never retain subList() backing lists.
- Drain the cache in place after each next() call by copying the returned page and clearing consumed rows from the cached list.
- Add an IteratorCache.drain(cacheId, count) helper to centralize cache consumption, release empty caches, and avoid repeating subList() handling in iterator code.
The third option is likely the cleanest long-term fix because it keeps cache ownership and cleanup logic inside IteratorCache.
If the maintainers agree with this direction, please assign this issue to me. I can raise a PR for the same.
There appears to be a potential memory leak / memory retention issue in SearchIterator when running large search iterations.
SearchIterator uses IteratorCache to keep leftover results between calls to next(). When a cached page contains more rows than the requested batch size, the current implementation splits the cached list using List.subList() and stores the remaining suffix back into the cache.
In Java, subList() returns a view backed by the original list. This means the cached suffix can still retain a reference to the full original list, including rows that have already been returned to the user. During very large iterations, this can cause already-consumed result rows to remain reachable and prevent them from being garbage collected.
This is especially risky when:
SearchIterator is used for very large topK values.
Each result row contains large output fields.
The iterator repeatedly overfetches and stores leftover cached pages.
The user does not call close() immediately after finishing iteration.
Affected Area
sdk-core/src/main/java/io/milvus/orm/iterator/SearchIterator.java
sdk-core/src/main/java/io/milvus/orm/iterator/IteratorCache.java
Expected Behavior
After a batch of results has been returned by SearchIterator.next(), rows that are no longer needed should become eligible for garbage collection.
The cache should only retain rows that are still needed for future next() calls.
Actual Behavior
The cache may store subList() views that retain the backing list of a larger result page. As a result, already-consumed rows may remain reachable through the cached suffix, causing unnecessary memory retention and potentially leading to increased heap usage or OOM in large iterations.
Possible Approaches
The third option is likely the cleanest long-term fix because it keeps cache ownership and cleanup logic inside IteratorCache.
If the maintainers agree with this direction, please assign this issue to me. I can raise a PR for the same.