test: fix flaky bfloat16 pagination test by pinning DISKANN search_list_size (#49033)

## Summary

Fixes a flaky test in `test_search_bfloat16_with_pagination_default`
that failed at ~40% rate (2/5 runs locally), with overlaps of 76–79%
against the 80% threshold.

issue: #49030

### Root Cause

When `offset` is set, Milvus proxy computes `queryTopK = limit + offset`
and passes it directly to DISKANN as the search depth. Without an
explicit `search_list_size`, DISKANN scales its graph traversal
proportionally to `queryTopK`:

| Call | Internal topk | Graph depth |
|------|--------------|-------------|
| Page 1 (offset=100, limit=100) | 200 | shallow (5× less than full) |
| Page 3 (offset=300, limit=100) | 400 | medium (2.5× less than full) |
| Full search (limit=1000) | 1000 | deep |

Boundary candidates at the edge of each page differ between the
paginated and full searches, producing ~76–79% overlap and flaky
failures.

### Fix

Pin `search_list_size=1200` on **both** paginated and full searches.
DISKANN uses this as a fixed exploration budget regardless of `topk`, so
both calls examine the same candidate pool and agree on boundary
positions.

Additional improvements:
- Per-page overlap threshold: 80% → 90% (reflects actual overlap with
matched search depth)
- Added overall recall assertion (≥95%) to validate cross-page coverage
- Expanded docstring explaining the DISKANN pagination consistency model

### Test Plan

- [x] Reproduced original failure: 2/5 runs failed (40% rate), overlap
76–79%
- [x] Verified fix: 5/5 runs passed after pinning `search_list_size`
- [x] Tested against Milvus `master-20260413` on standalone instance

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Signed-off-by: yanliang567 <82361606+yanliang567@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
yanliang567
2026-04-17 10:59:54 +08:00
committed by GitHub
co-authored by Claude Sonnet 4.6
parent b4da574b03
commit 32cc35982d
@@ -210,15 +210,31 @@ class TestMilvusClientSearchPagination(TestMilvusClientV2Base):
"""
target: test search bfloat16 vectors with pagination
method: 1. connect and create a collection
2. search bfloat16 vectors with pagination
3. search with offset+limit
4. compare with the search results whose corresponding ids should be the same
expected: search successfully and ids is correct
2. search bfloat16 vectors with pagination (with explicit search_list_size)
3. search a full reference (same search_list_size) without pagination
4. compare: per-page overlap >= 90%, and overall recall across all pages >= 95%
expected: search successfully and ids are consistent
Note on DISKANN pagination consistency:
When offset is set, Milvus internally searches with topk = limit + offset.
Without an explicit search_list_size, DISKANN uses a search depth proportional
to topk — so paginated searches (e.g. topk=200 for page 1) explore far fewer
graph nodes than the full search (topk=1000). This causes boundary candidates
(those at the edge of each page) to differ between calls, producing ~76-79%
overlap and flaky failures against an 80% threshold.
Fix: pin search_list_size=1200 on both paginated and full searches so both
explore the same set of candidate neighbours. Per-page overlap then rises
to ~95%+ and overall recall across all pages reaches ~99%+.
"""
client = self._client()
# 1. Create collection with schema
collection_name = self.collection_name
# Pin search_list_size so paginated and full searches explore the same DISKANN
# candidate pool, eliminating topk-driven depth differences.
diskann_search_list_size = 1200 # must be >= limit * pages (1000)
# 2. Search with pagination for 10 pages
limit = 100
pages = 10
@@ -226,7 +242,7 @@ class TestMilvusClientSearchPagination(TestMilvusClientV2Base):
all_pages_results = []
for page in range(pages):
offset = page * limit
search_params = {"offset": offset}
search_params = {"offset": offset, "params": {"search_list_size": diskann_search_list_size}}
search_res_with_offset, _ = self.search(
client,
collection_name,
@@ -244,8 +260,8 @@ class TestMilvusClientSearchPagination(TestMilvusClientV2Base):
)
all_pages_results.append(search_res_with_offset)
# 3. Search without pagination
search_params_full = {}
# 3. Full reference search — same search_list_size so candidate pools match
search_params_full = {"params": {"search_list_size": diskann_search_list_size}}
search_res_full, _ = self.search(
client,
collection_name,
@@ -255,17 +271,25 @@ class TestMilvusClientSearchPagination(TestMilvusClientV2Base):
limit=limit * pages
)
# 4. Compare results - verify pagination results overlap with full search results
for p in range(pages):
page_res = all_pages_results[p]
for i in range(default_nq):
page_ids = [page_res[i][j].get('id') for j in range(limit)]
# 4. Validate results
for i in range(default_nq):
all_page_ids = set()
for p in range(pages):
page_ids = [all_pages_results[p][i][j].get('id') for j in range(limit)]
ids_in_full = [search_res_full[i][p * limit:p * limit + limit][j].get('id') for j in range(limit)]
intersection_ids = set(ids_in_full).intersection(set(page_ids))
overlap_ratio = len(intersection_ids) / limit * 100
log.debug(f"page[{p}], nq[{i}], overlap: {overlap_ratio}%")
assert overlap_ratio >= 80, \
f"bfloat16 pagination overlap too low: {overlap_ratio}% (page={p}, nq={i})"
assert overlap_ratio >= 90, \
f"bfloat16 pagination per-page overlap too low: {overlap_ratio}% (page={p}, nq={i})"
all_page_ids.update(page_ids)
# Overall recall: union of all paginated results vs full search
full_ids = {search_res_full[i][j].get('id') for j in range(limit * pages)}
overall_recall = len(all_page_ids & full_ids) / len(full_ids) * 100
log.debug(f"nq[{i}], overall recall: {overall_recall:.1f}%")
assert overall_recall >= 95, \
f"bfloat16 pagination overall recall too low: {overall_recall:.1f}% (nq={i})"
@pytest.mark.tags(CaseLabel.L0)
def test_search_sparse_with_pagination_default(self):