- Milvus version:master
- Deployment mode(standalone or cluster):
- MQ type(rocksmq, pulsar or kafka):
- SDK version(e.g. pymilvus v2.0.0rc2):
- OS(Ubuntu or CentOS):
- CPU/Memory:
- GPU:
- Others:
When using hybrid_search with ordinary vector fields and search iterator v2 enabled, the first page can return valid hits, but the response does not include iterator metadata required
for fetching the next page.
In contrast, normal search with the same iterator v2 parameters returns search_iterator_v2_results.token, last_bound, and session_ts correctly.
import numpy as np
from pymilvus import AnnSearchRequest, DataType, MilvusClient, RRFRanker
COLLECTION_NAME = "hybrid_iterator_metadata_repro"
DIM = 8
NUM_ROWS = 2000
BATCH_SIZE = 5
def print_hits(title, result):
print(f"\n=== {title} ===")
for qi, hits in enumerate(result):
print(f"query {qi}, hits={len(hits)}")
for hit in hits[:BATCH_SIZE]:
print(hit)
def print_iterator_info(title, result):
info = result.get_search_iterator_v2_results_info()
session_ts = result.get_session_ts()
token = getattr(info, "token", "")
last_bound = getattr(info, "last_bound", None)
print(f"\n--- {title} iterator metadata ---")
print(f"token: {token!r}")
print(f"last_bound: {last_bound!r}")
print(f"session_ts: {session_ts!r}")
return token, last_bound, session_ts
def make_ann_requests(query_a, query_b):
req_a = AnnSearchRequest(
data=[query_a],
anns_field="vec_a",
param={"metric_type": "L2", "params": {"nprobe": 16}},
limit=BATCH_SIZE,
)
req_b = AnnSearchRequest(
data=[query_b],
anns_field="vec_b",
param={"metric_type": "L2", "params": {"nprobe": 16}},
limit=BATCH_SIZE,
)
return [req_a, req_b]
def main():
client = MilvusClient("http://localhost:19530")
if client.has_collection(COLLECTION_NAME, timeout=5):
client.drop_collection(COLLECTION_NAME)
schema = client.create_schema(auto_id=False)
schema.add_field("id", DataType.INT64, is_primary=True)
schema.add_field("value", DataType.DOUBLE)
schema.add_field("vec_a", DataType.FLOAT_VECTOR, dim=DIM)
schema.add_field("vec_b", DataType.FLOAT_VECTOR, dim=DIM)
index_params = client.prepare_index_params()
index_params.add_index(
field_name="vec_a",
index_type="IVF_FLAT",
metric_type="L2",
params={"nlist": 128},
)
index_params.add_index(
field_name="vec_b",
index_type="IVF_FLAT",
metric_type="L2",
params={"nlist": 128},
)
client.create_collection(
COLLECTION_NAME,
schema=schema,
index_params=index_params,
consistency_level="Strong",
)
rng = np.random.default_rng(19530)
rows = []
for i in range(NUM_ROWS):
rows.append(
{
"id": i,
"value": float(i),
"vec_a": rng.random(DIM).astype("float32").tolist(),
"vec_b": rng.random(DIM).astype("float32").tolist(),
}
)
client.insert(COLLECTION_NAME, rows)
client.flush(COLLECTION_NAME)
client.load_collection(COLLECTION_NAME)
query_a = rows[0]["vec_a"]
query_b = rows[0]["vec_b"]
iterator_kwargs = {
"iterator": True,
"search_iter_v2": True,
"search_iter_batch_size": BATCH_SIZE,
}
# Control case: a normal single-vector search returns iterator metadata.
normal_page_1 = client.search(
collection_name=COLLECTION_NAME,
data=[query_a],
anns_field="vec_a",
search_params={"metric_type": "L2", "params": {"nprobe": 16}},
limit=BATCH_SIZE,
output_fields=["id", "value"],
**iterator_kwargs,
)
print_hits("normal search iterator page 1", normal_page_1)
normal_token, normal_last_bound, normal_session_ts = print_iterator_info(
"normal search page 1", normal_page_1
)
if normal_token:
normal_page_2 = client.search(
collection_name=COLLECTION_NAME,
data=[query_a],
anns_field="vec_a",
search_params={"metric_type": "L2", "params": {"nprobe": 16}},
limit=BATCH_SIZE,
output_fields=["id", "value"],
search_iter_id=normal_token,
search_iter_last_bound=normal_last_bound,
guarantee_timestamp=normal_session_ts,
**iterator_kwargs,
)
print_hits("normal search iterator page 2", normal_page_2)
print_iterator_info("normal search page 2", normal_page_2)
else:
print("normal search did not return a token; server may not support iterator v2")
# Repro case: ordinary-vector hybrid search can execute the current batch,
# but current server code does not return hybrid-level iterator metadata.
hybrid_page_1 = client.hybrid_search(
collection_name=COLLECTION_NAME,
reqs=make_ann_requests(query_a, query_b),
ranker=RRFRanker(),
limit=BATCH_SIZE,
output_fields=["id", "value"],
**iterator_kwargs,
)
print_hits("hybrid search iterator page 1", hybrid_page_1)
hybrid_token, hybrid_last_bound, hybrid_session_ts = print_iterator_info(
"hybrid search page 1", hybrid_page_1
)
if not hybrid_token or not hybrid_session_ts:
print(
"\nREPRODUCED: hybrid search returned current-batch hits, but did not "
"return token/session_ts needed to request page 2."
)
return
hybrid_page_2 = client.hybrid_search(
collection_name=COLLECTION_NAME,
reqs=make_ann_requests(query_a, query_b),
ranker=RRFRanker(),
limit=BATCH_SIZE,
output_fields=["id", "value"],
search_iter_id=hybrid_token,
search_iter_last_bound=hybrid_last_bound,
guarantee_timestamp=hybrid_session_ts,
**iterator_kwargs,
)
print_hits("hybrid search iterator page 2", hybrid_page_2)
print_iterator_info("hybrid search page 2", hybrid_page_2)
if __name__ == "__main__":
main()
Because token and session_ts are missing, the client cannot construct a valid page-2 request.
The issue appears to be in the Proxy hybrid search path: iterator state is parsed per sub-search, but not promoted to task-level iterator state, and final response metadata is only
populated for the single-query-info search path.
Is there an existing issue for this?
Environment
When using hybrid_search with ordinary vector fields and search iterator v2 enabled, the first page can return valid hits, but the response does not include iterator metadata required
for fetching the next page.
In contrast, normal search with the same iterator v2 parameters returns search_iterator_v2_results.token, last_bound, and session_ts correctly.
Reproduction
Actual Result
Hybrid search returns hits, but iterator metadata is missing:
--- hybrid search page 1 iterator metadata ---
token: ''
last_bound: 0.0
session_ts: 0
Because token and session_ts are missing, the client cannot construct a valid page-2 request.
Expected Result
Hybrid search with iterator v2 should either:
Return valid iterator metadata, including:
so the client can fetch the next page, or
Reject hybrid_search + iterator explicitly if this combination is not supported.
Notes
Normal search + iterator v2 works as expected:
--- normal search page 1 iterator metadata ---
token: '...'
last_bound: 0.17913571000099182
session_ts: 466875486770298891
The issue appears to be in the Proxy hybrid search path: iterator state is parsed per sub-search, but not promoted to task-level iterator state, and final response metadata is only
populated for the single-query-info search path.