Summary
In local mode (QdrantClient(":memory:") or a path), upserting the same point with the same vector
twice into a Distance.COSINE collection stores two slightly different vectors. The second write differs
from the first in the last bits of some components.
Not applicable to the real server, re-upserting an identical vector gives the same result
Reproduction
Assuming a qdrant server is listening on localhost:6333.
import uuid
from qdrant_client import QdrantClient, models
V = [0.1234567901234, -0.98765432109, 0.5555555555, 0.333333333333]
PID = str(uuid.UUID(hex="ab" * 16))
def probe(client: QdrantClient, name: str) -> tuple[list[float], list[float]]:
if client.collection_exists(name):
client.delete_collection(name)
client.create_collection(
name,
vectors_config={"dense": models.VectorParams(size=4, distance=models.Distance.COSINE)},
)
point = models.PointStruct(id=PID, vector={"dense": V}, payload={})
client.upsert(name, points=[point], wait=True)
first = client.retrieve(name, ids=[PID], with_vectors=True)[0].vector["dense"]
client.upsert(name, points=[point], wait=True) # identical write
second = client.retrieve(name, ids=[PID], with_vectors=True)[0].vector["dense"]
client.delete_collection(name)
return first, second
for label, client in (
("local", QdrantClient(":memory:")),
("server", QdrantClient(url="http://localhost:6333")),
):
first, second = probe(client, "norm_probe")
print(f"{label}: identical={first == second}")
Observed with 1.19.0:
local: identical=False
first : [0.1039525717496872, -0.8316205739974976, 0.46778661012649536, 0.28067195415496826]
second: [0.1039525717496872, -0.8316205739974976, 0.467786580324173, 0.28067195415496826]
server: identical=True
Cause
The cause is that the insert and update paths normalise at different precisions.
Insert (local_collection.py:2413) computes the norm from the incoming Python list, i.e. in float64,
and divides before the cast to float32:
if params.distance == models.Distance.COSINE:
norm = np.linalg.norm(vector) # float64
vector = np.array(vector) / norm if norm > EPSILON else vector
Update (local_collection.py:2678-2685) casts to float32 first and computes the norm from the
already-truncated values:
vector_np = np.array(vector, dtype=np.float32) # cast first
...
if params.distance == models.Distance.COSINE:
norm = np.linalg.norm(vector_np) # float32
vector_np = vector_np / norm if norm > EPSILON else vector_np
Normalising a float32-truncated vector by a float32 norm does not give the same result as normalising in
float64 and then truncating.
Resolution
Easiest one is to cast to np.float64 when updating, or, vise versa, apply cast to np.float32 when inserting.
Summary
In local mode (
QdrantClient(":memory:")or a path), upserting the same point with the same vectortwice into a
Distance.COSINEcollection stores two slightly different vectors. The second write differsfrom the first in the last bits of some components.
Not applicable to the real server, re-upserting an identical vector gives the same result
Reproduction
Assuming a qdrant server is listening on localhost:6333.
Observed with 1.19.0:
Cause
The cause is that the insert and update paths normalise at different precisions.
Insert (
local_collection.py:2413) computes the norm from the incoming Python list, i.e. in float64,and divides before the cast to float32:
Update (
local_collection.py:2678-2685) casts to float32 first and computes the norm from thealready-truncated values:
Normalising a float32-truncated vector by a float32 norm does not give the same result as normalising in
float64 and then truncating.
Resolution
Easiest one is to cast to
np.float64when updating, or, vise versa, apply cast tonp.float32when inserting.