mirror of
https://github.com/milvus-io/milvus.git
synced 2026-07-21 10:15:43 +00:00
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:
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):
|
||||
|
||||
Reference in New Issue
Block a user