qdrant / qdrant/qdrant

Payload range filtering misbehaves at float32 minimum finite value (FLT_MIN), with inconsistent results between gRPC and REST

Open
#8,617 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug
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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.