Environment
- Milvus Version:
harbor.milvus.io/milvusdb/milvus:3.0-20260806-955185ef (commit 955185ef9c23162c7784f5fed5e588be37c03bfb)
- Deployment Mode: distributed cluster on Kubernetes, 2 QueryNodes
- MQ / Streaming: external Woodpecker service
- SDK: pymilvus
3.0.1rc74
- Architecture: amd64
- Index / Metric: FLAT / COSINE
The original failure was TestSearchV2Shared::test_search_with_expression in branch 3.0 build 237.
Reproduction
The following self-contained script reduces the original wide test schema to four fields: INT64 PK, nullable FLOAT, VARCHAR(512), and FLOAT_VECTOR.
#!/usr/bin/env python3
"""Reproduce COSINE results that are not globally score-sorted."""
from __future__ import annotations
import argparse
import time
import numpy as np
from pymilvus import DataType, MilvusClient
ROWS = 10_000
DIM = 128
NQ = 2
PAYLOAD_BYTES = 512
BATCH_SIZE = 500
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--uri", default="http://127.0.0.1:19530")
args = parser.parse_args()
client = MilvusClient(uri=args.uri)
collection_name = f"repro_cosine_order_{time.time_ns()}"
rng = np.random.default_rng(20260806)
vectors = rng.standard_normal((ROWS, DIM), dtype=np.float32)
vectors /= np.linalg.norm(vectors, axis=1, keepdims=True)
schema = client.create_schema(enable_dynamic_field=False)
schema.add_field("id", DataType.INT64, is_primary=True)
schema.add_field("value", DataType.FLOAT, nullable=True)
schema.add_field("payload", DataType.VARCHAR, max_length=PAYLOAD_BYTES)
schema.add_field("vector", DataType.FLOAT_VECTOR, dim=DIM)
try:
client.create_collection(collection_name, schema=schema)
for start in range(0, ROWS, BATCH_SIZE):
client.insert(
collection_name,
[
{
"id": row,
"value": None if row < ROWS // 5 else float(row),
"payload": f"{row:08d}" + "x" * (PAYLOAD_BYTES - 8),
"vector": vectors[row].tolist(),
}
for row in range(start, start + BATCH_SIZE)
],
)
client.flush(collection_name)
index_params = client.prepare_index_params()
index_params.add_index("vector", index_type="FLAT", metric_type="COSINE")
client.create_index(collection_name, index_params=index_params)
client.load_collection(collection_name)
query_vectors = rng.standard_normal((NQ, DIM), dtype=np.float32)
query_vectors /= np.linalg.norm(query_vectors, axis=1, keepdims=True)
result_sets = client.search(
collection_name,
data=query_vectors.tolist(),
anns_field="vector",
search_params={"metric_type": "COSINE", "params": {}},
limit=ROWS,
filter="",
)
for query_index, results in enumerate(result_sets):
for index in range(len(results) - 1):
before = results[index]
after = results[index + 1]
if before["distance"] < after["distance"]:
print(
"FAIL: COSINE results are not in descending order\n"
f"query={query_index} indexes={index}/{index + 1}\n"
f"before: id={before['id']} distance={before['distance']:.9f}\n"
f"after: id={after['id']} distance={after['distance']:.9f}"
)
return 1
print(f"PASS: {NQ}x{ROWS} COSINE results are in descending order")
return 0
finally:
if client.has_collection(collection_name):
client.drop_collection(collection_name)
if __name__ == "__main__":
raise SystemExit(main())
Run it against the small-segment boundary configuration:
python repro_search_cosine_order.py --uri http://127.0.0.1:19530
Trigger Conditions
- Frequency: deterministic in repeated local runs and also observed in Jenkins
- Required in the minimized case:
- 10,000 rows
- 2 query vectors
- multiple small sealed segments
- Does NOT happen when:
- the same image and script use the default segment configuration;
- the row count is reduced to 5,000;
- the VARCHAR payload is reduced enough that fewer sealed segments are produced
- The filter is empty, so expression evaluation is not required to trigger the ordering problem.
Expected Behavior
For COSINE, every hit list returned by search should be monotonically sorted in descending score order:
distance[i] >= distance[i + 1]
FLAT is used, so ANN recall is not involved.
Actual Behavior
On the boundary configuration, the deterministic script returns an adjacent inversion:
FAIL: COSINE results are not in descending order
query=1 indexes=3153/3154
before: id=5989 distance=0.042794593
after: id=7175 distance=0.042794596
The result at index 3154 has a larger COSINE score than the result at index 3153.
The same image, pymilvus version, test code, seed, and data pass on the default configuration:
PASS: 2x10000 COSINE results are in descending order
The original pytest failure is consistent with the minimized script:
tests/python_client/milvus_client/test_milvus_client_search_v2.py:2188:
AssertionError: distances not in descending order for COSINE metric
Error Logs
No server error is returned. The RPC succeeds, but the result order is incorrect.
Non-default Configuration
Failing boundary configuration:
common:
storage:
enablev2: true
useLoonFFI: true
dataCoord:
segment:
maxSize: 64
sealProportion: 0.05
sealProportionJitter: 0
queryNode:
segcore:
storageV2:
cellTargetSizeBytes: 1048576
mmap:
vectorField: true
vectorIndex: true
scalarField: true
scalarIndex: true
growingMmapEnabled: true
Passing control:
dataCoord:
segment:
maxSize: 1024
sealProportion: 0.12
sealProportionJitter: 0.1
Analysis Hints
- Boundary and default use the same Milvus image; the relevant difference is segment shape/count.
- The failure remains after reducing the schema to one vector field and a fixed VARCHAR payload.
- FLAT/COSINE and an empty filter rule out ANN recall and expression correctness as causes.
- The inversion is very small but violates the search API ordering invariant and fails deterministically at the same IDs/scores for this seed.
- This may be in cross-segment result reduction/merge or score conversion rather than per-segment FLAT search.
- Searches for existing issues did not find an exact match.
Environment
harbor.milvus.io/milvusdb/milvus:3.0-20260806-955185ef(commit955185ef9c23162c7784f5fed5e588be37c03bfb)3.0.1rc74The original failure was
TestSearchV2Shared::test_search_with_expressionin branch 3.0 build 237.Reproduction
The following self-contained script reduces the original wide test schema to four fields: INT64 PK, nullable FLOAT, VARCHAR(512), and FLOAT_VECTOR.
Run it against the small-segment boundary configuration:
Trigger Conditions
Expected Behavior
For COSINE, every hit list returned by search should be monotonically sorted in descending score order:
FLAT is used, so ANN recall is not involved.
Actual Behavior
On the boundary configuration, the deterministic script returns an adjacent inversion:
The result at index 3154 has a larger COSINE score than the result at index 3153.
The same image, pymilvus version, test code, seed, and data pass on the default configuration:
The original pytest failure is consistent with the minimized script:
Error Logs
No server error is returned. The RPC succeeds, but the result order is incorrect.
Non-default Configuration
Failing boundary configuration:
Passing control:
Analysis Hints