Payload range filtering misbehaves at float32 minimum finite value (FLT_MIN), with inconsistent results between gRPC and REST
Nobody has claimed this yet.
- Dominant language
- Rust
- Stars
- 34.7k
- Forks
- 2.7k
- Avg merge
- 1d 18h
- Merged PRs (30d)
- 187
Description
Current Behavior
Payload range filtering behaves incorrectly for a point whose payload value is the minimum finite float32 value:
FLOAT_MIN = -3.4028234663852886e38
I insert a point with:
payload = {
"c15": FLOAT_MIN,
"scores_array": [1.0],
}
Then I query it with range filters on c15.
Expected comparisons around the stored value should behave consistently:
c15 <= FLOAT_MIN -> match
c15 < FLOAT_MIN -> no match
c15 >= FLOAT_MIN -> match
c15 > FLOAT_MIN -> no match
c15 == FLOAT_MIN (implemented as gte=FLOAT_MIN and lte=FLOAT_MIN) -> match
Actual results differ by transport.
gRPC:
c15 <= FLOAT_MIN -> [1]
c15 < FLOAT_MIN -> [1] (incorrect)
c15 >= FLOAT_MIN -> [] (incorrect)
c15 > FLOAT_MIN -> []
c15 == FLOAT_MIN -> [] (incorrect)
REST:
c15 <= FLOAT_MIN -> [] (incorrect)
c15 < FLOAT_MIN -> []
c15 >= FLOAT_MIN -> [1]
c15 > FLOAT_MIN -> [1] (incorrect)
c15 == FLOAT_MIN -> [] (incorrect)
This is not only an equality problem. Ordering itself becomes inconsistent near the boundary, and the behavior differs between gRPC and REST for the same stored value and logically equivalent query conditions.
Steps to Reproduce
Start a local Qdrant server with REST on port 6333 and gRPC on port 6334.
Run the script below.
Observe the output for both prefer_grpc=True and prefer_grpc=False.
Compare actual results with the expected range semantics.
Code:
"""
Minimal reproduction of Qdrant float32 boundary filtering bug.
The issue: comparisons with FLT_MIN (~ -3.4028234663852886e+38) behave inconsistently
between gRPC and REST, and equality checks fail entirely.
Expected behavior: a point with value == FLT_MIN should be included in:
- value <= FLT_MIN
- value >= FLT_MIN
- value == FLT_MIN
Actual behavior differs per transport (see assertions).
"""
from qdrant_client import QdrantClient
from qdrant_client.http.models import (
Distance,
FieldCondition,
Filter,
PointStruct,
Range,
VectorParams,
)
COLLECTION_NAME = "float_min_boundary_repro"
FLOAT_MIN = -3.4028234663852886e38 # float32 minimum finite value
def run_test(prefer_grpc: bool):
transport = "gRPC" if prefer_grpc else "REST"
client = QdrantClient(
host="127.0.0.1",
port=6333,
grpc_port=6334,
prefer_grpc=prefer_grpc,
timeout=30,
)
# Cleanup previous run
try:
client.delete_collection(COLLECTION_NAME)
except Exception:
pass
# Create collection with dummy vectors
client.create_collection(
collection_name=COLLECTION_NAME,
vectors_config=VectorParams(size=2, distance=Distance.DOT),
)
# Insert a single point with c15 = FLOAT_MIN and a non-empty scores_array
point = PointStruct(
id=1,
vector=[0.1, 0.1],
payload={
"c15": FLOAT_MIN,
"scores_array": [1.0],
},
)
client.upsert(collection_name=COLLECTION_NAME, points=[point], wait=True)
def query_ids(range_obj: Range):
"""Return point IDs matching c15 range condition."""
scroll_filter = Filter(must=[FieldCondition(key="c15", range=range_obj)])
points, _ = client.scroll(
COLLECTION_NAME, scroll_filter=scroll_filter, limit=10
)
return [p.id for p in points]
print(f"\n--- Qdrant float32 boundary test ({transport}) ---")
print(f"Stored c15 = {FLOAT_MIN}")
# Define test cases: (description, range, expected_ids)
tests = [
("c15 <= FLOAT_MIN", Range(lte=FLOAT_MIN), [1]),
("c15 < FLOAT_MIN", Range(lt=FLOAT_MIN), []),
("c15 >= FLOAT_MIN", Range(gte=FLOAT_MIN), [1]),
("c15 > FLOAT_MIN", Range(gt=FLOAT_MIN), []),
("c15 == FLOAT_MIN", Range(gte=FLOAT_MIN, lte=FLOAT_MIN), [1]),
]
all_passed = True
for label, rng, expected in tests:
actual = query_ids(rng)
status = "PASS" if actual == expected else "FAIL"
if status == "FAIL":
all_passed = False
print(f"{label}: expected {expected}, got {actual} -> {status}")
if not all_passed:
print(f"\n[BUG DETECTED] Inconsistent or incorrect filtering for float32 min value.")
client.delete_collection(COLLECTION_NAME)
if name == "main":
print("Reproducing Qdrant float32 boundary comparison bug...")
run_test(prefer_grpc=True) # gRPC transport
run_test(prefer_grpc=False) # REST transport
Observed output:
Reproducing Qdrant float32 boundary comparison bug...
--- Qdrant float32 boundary test (gRPC) ---
Stored c15 = -3.4028234663852886e+38
c15 <= FLOAT_MIN: expected [1], got [1] -> PASS
c15 < FLOAT_MIN: expected [], got [1] -> FAIL
c15 >= FLOAT_MIN: expected [1], got [] -> FAIL
c15 > FLOAT_MIN: expected [], got [] -> PASS
c15 == FLOAT_MIN: expected [1], got [] -> FAIL
[BUG DETECTED] Inconsistent or incorrect filtering for float32 min value.
--- Qdrant float32 boundary test (REST) ---
Stored c15 = -3.4028234663852886e+38
c15 <= FLOAT_MIN: expected [1], got [] -> FAIL
c15 < FLOAT_MIN: expected [], got [] -> PASS
c15 >= FLOAT_MIN: expected [1], got [1] -> PASS
c15 > FLOAT_MIN: expected [], got [1] -> FAIL
c15 == FLOAT_MIN: expected [1], got [] -> FAIL
[BUG DETECTED] Inconsistent or incorrect filtering for float32 min value.
Expected Behavior
For a stored payload value exactly equal to FLOAT_MIN:
lte=FLOAT_MIN should match
lt=FLOAT_MIN should not match
gte=FLOAT_MIN should match
gt=FLOAT_MIN should not match
gte=FLOAT_MIN and lte=FLOAT_MIN should match exactly
The result should also be consistent across gRPC and REST.
Possible Solution
This may be a boundary handling or numeric conversion bug in range filtering for float payloads near the minimum finite float32 value.
Possible areas to inspect:
conversion of float payload values during indexing or storage
conversion of range endpoints between REST and gRPC paths
comparison logic for extreme negative float values
whether float payloads are normalized to float32 in storage while range bounds are compared in another representation
It may also be related to broader range-boundary precision issues already discussed in:
#2356
#5389
#7955
#8538
However, this reproduction is specifically about float payloads at FLT_MIN and also shows inconsistent behavior between gRPC and REST.
Context (Environment)
This affects correctness of payload filtering at numeric boundaries.
In particular:
exact equality checks fail
strict and non-strict inequalities become contradictory
REST and gRPC do not agree on the result
This makes numeric filtering unreliable for edge-case float payload values and complicates fuzzing, regression testing, and any application that depends on exact range semantics.
Qdrant version:qdrant:v1.17.0
qdrant-client version:1.17.0
Python version:3.11.5
OS:Ubuntu
Detailed Description
The inserted payload value is the minimum finite float32 value, which is a valid finite number.
For such a value, the ordering relationships should remain logically consistent:
if x == FLOAT_MIN, then x < FLOAT_MIN must be false
if x == FLOAT_MIN, then x > FLOAT_MIN must be false
if x == FLOAT_MIN, then both x <= FLOAT_MIN and x >= FLOAT_MIN must be true
Instead, the current behavior violates these relationships, and the violations differ depending on whether the request uses gRPC or REST.
This suggests the issue is not just normal floating-point approximation at the API level, but a transport-specific or implementation-specific bug in boundary comparison.
Possible Implementation
One possible direction is to ensure both transports use the same canonical numeric comparison path for payload range filtering and that extreme finite float values are preserved consistently end-to-end.
It would also help to add regression tests for:
min and max finite float32 values
exact closed-range equality queries (gte=v and lte=v)
consistency between REST and gRPC
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with the supplied Python reproduction against Qdrant v1.17.0, comparing REST and gRPC results for FLT_MIN. Inspect the payload float conversion, range-endpoint handling, and comparison paths mentioned in the issue, along with issues #2356, #5389, #7955, and #8538. Done means strict and inclusive comparisons have the expected semantics and produce identical results over REST and gRPC, with regression coverage for float32 boundaries and closed-range equality.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python, rust
- Domain
- api, databases
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 50/100