Query returns zero rank at the documented equality boundary
Summary
Qdrant 1.19.0 returns an incorrect score for a Discover query when a candidate's positive and negative context similarities are exactly equal.
The documented Discover formula assigns a context rank of +1 when:
positive_similarity >= negative_similarityHowever, when the two similarities are exactly equal, Qdrant contributes 0 instead of +1.
Environment
- Qdrant:
qdrant/qdrant:v1.19.0 - Distance:
Dot - Vector dimension: 2
- API: REST
POST /collections/{collection_name}/points/query - Query type: public
discover - Search mode:
exact=true - Filter: none
- Quantization: disabled
Steps to Reproduce
Start Qdrant v1.19.0 on port 16355:
docker run --rm --name qdrant-discover-tie-repro \
-p 127.0.0.1:16355:6333 \
qdrant/qdrant:v1.19.0Install the dependency:
pip install requests==2.34.2Run this standalone Python script:
import json
import math
import uuid
import requests
BASE = "http://127.0.0.1:16355"
COLLECTION = "discover_tie_" + uuid.uuid4().hex[:8]
TARGET = [1.0, 0.0]
POINTS = {
100: [1.0, 0.0], # positive context
101: [0.0, 1.0], # negative context
0: [0.95, 0.10],
1: [0.70, 0.40],
2: [0.40, 0.70],
3: [0.10, 0.95],
4: [0.50, 0.40],
}
MUTANT = [0.50, 0.50]
def call(method, path, **kwargs):
response = requests.request(
method,
BASE + path,
timeout=30,
**kwargs,
)
response.raise_for_status()
return response.json() if response.content else {}
def dot(left, right):
return sum(a * b for a, b in zip(left, right))
def sigmoid(value):
return 0.5 * (1.0 + value / (1.0 + abs(value)))
def expected(values):
rows = []
for point_id, vector in values.items():
if point_id in (100, 101):
continue
positive = dot(values[100], vector)
negative = dot(values[101], vector)
# Documented rule: equality belongs to the positive zone.
rank = 1 if positive >= negative else -1
score = sigmoid(dot(TARGET, vector)) + rank
rows.append((point_id, score))
return sorted(rows, key=lambda row: (-row[1], row[0]))
def query():
response = call(
"POST",
f"/collections/{COLLECTION}/points/query",
json={
"query": {
"discover": {
"target": TARGET,
"context": [
{
"positive": 100,
"negative": 101,
}
],
}
},
"limit": 5,
"params": {
"exact": True,
},
},
)
return [
(int(point["id"]), float(point["score"]))
for point in response["result"]["points"]
]
def matches(actual, expected_rows):
if [point_id for point_id, _ in actual] != [
point_id for point_id, _ in expected_rows
]:
return False
return all(
math.isclose(actual_score, expected_score, abs_tol=3e-5)
for (_, actual_score), (_, expected_score)
in zip(actual, expected_rows)
)
try:
call(
"PUT",
f"/collections/{COLLECTION}",
json={
"vectors": {
"size": 2,
"distance": "Dot",
}
},
)
call(
"PUT",
f"/collections/{COLLECTION}/points?wait=true",
json={
"points": [
{"id": point_id, "vector": vector}
for point_id, vector in POINTS.items()
]
},
)
seed_actual = query()
seed_expected = expected(POINTS)
values = dict(POINTS)
values[4] = MUTANT
call(
"PUT",
f"/collections/{COLLECTION}/points/vectors?wait=true",
json={
"points": [
{
"id": 4,
"vector": MUTANT,
}
]
},
)
mutant_actual = query()
mutant_expected = expected(values)
print(json.dumps({
"seed_actual": seed_actual,
"seed_expected": seed_expected,
"mutant_actual": mutant_actual,
"mutant_expected": mutant_expected,
}, indent=2))
assert matches(seed_actual, seed_expected), (
"The no-tie seed did not match the documented formula"
)
assert not matches(mutant_actual, mutant_expected), (
"The equality-boundary anomaly was not reproduced"
)
print("BUG REPRODUCED")
finally:
requests.delete(
f"{BASE}/collections/{COLLECTION}",
timeout=30,
)Actual Behavior
Before the update, the no-tie seed follows the documented formula.
After changing point 4 from:
[0.50, 0.40]to:
[0.50, 0.50]the positive and negative similarities are both 0.50.
The expected score is:
sigmoid(0.5) + 1 = 1.6666667Qdrant instead returns:
sigmoid(0.5) + 0 = 0.6666667The equality case contributes 0 instead of +1.
Expected Behavior
When:
positive_similarity == negative_similaritythe candidate should be treated as belonging to the positive zone because the documented rule uses >=.
The expected score for the mutated point is approximately:
1.6666667Possible Solution
Change the Discover context comparison so that equality contributes the positive rank:
if positive_score >= negative_score {
1
} else {
-1
}A regression test should cover:
- positive similarity greater than negative similarity;
- positive similarity less than negative similarity;
- positive similarity exactly equal to negative similarity.
Context (Environment)
This affects applications using Discover queries for context-based ranking or recommendation.
The equality case can change both the returned score and the ordering of results. The issue reproduces with exact search, no filters, no payload indexes, no quantization, and no ANN approximation.
A single stored vector coordinate update is sufficient to create the equality boundary.
Detailed Description
The seed data contains positive and negative context points:
positive context: [1.0, 0.0]
negative context: [0.0, 1.0]Point 4 initially has vector:
[0.50, 0.40]Its positive similarity is greater than its negative similarity, so the documented rank is +1.
After replacing it with:
[0.50, 0.50]both similarities become exactly equal. According to the documented >= rule, the rank should remain +1.
Instead, Qdrant returns a score corresponding to a rank contribution of 0.
The behavior reproduced deterministically across fresh collections.
Possible Implementation
Add an integration test using the Python reproducer above and verify that the equality case returns the same positive-zone rank as the positive_similarity > negative_similarity case.
The test should assert the exact score and result order before and after the vector mutation.
Source: qdrant/qdrant