Files
qdrant-client/tests/congruence_tests/test_payload.py
George e32daed967 fix: fix set payload in batch update points, fix operation conversion… (#977)
* fix: fix set payload in batch update points, fix operation conversions, add tests

* new: add some checks

* add comments to test cases

* fix: return comment

* tests: uncomment test due to a fix in core
2025-05-15 19:09:13 +03:00

671 lines
22 KiB
Python

import datetime
import random
import uuid
import grpc
import numpy as np
import pytest
from qdrant_client import QdrantClient
from qdrant_client.http import models
from qdrant_client.http.models import PointStruct
from qdrant_client.http.exceptions import UnexpectedResponse
from tests.congruence_tests.test_common import (
COLLECTION_NAME,
compare_collections,
generate_fixtures,
init_local,
init_remote,
text_vector_size,
initialize_fixture_collection,
)
NUM_VECTORS = 100
def upload(client_1: QdrantClient, client_2: QdrantClient, num_vectors=NUM_VECTORS):
points = generate_fixtures(num_vectors)
client_1.upload_points(COLLECTION_NAME, points, wait=True)
client_2.upload_points(COLLECTION_NAME, points, wait=True)
return points
@pytest.mark.parametrize("prefer_grpc", [True, False])
def test_delete_payload(prefer_grpc):
local_client: QdrantClient = init_local()
initialize_fixture_collection(local_client)
remote_client: QdrantClient = init_remote(prefer_grpc=prefer_grpc)
initialize_fixture_collection(remote_client)
points = upload(local_client, remote_client)
# region delete one point
id_ = points[0].id
local_point = local_client.retrieve(COLLECTION_NAME, [id_])
remote_point = remote_client.retrieve(COLLECTION_NAME, [id_])
assert local_point == remote_point
key = "text_data"
local_client.delete_payload(COLLECTION_NAME, keys=[key], points=[id_])
remote_client.delete_payload(COLLECTION_NAME, keys=[key], points=[id_], wait=True)
assert local_client.retrieve(COLLECTION_NAME, [id_]) == remote_client.retrieve(
COLLECTION_NAME, [id_]
)
# endregion
# region delete multiple points
keys_to_delete = ["rand_number", "text_array"]
ids = [points[1].id, points[2].id]
local_client.delete_payload(COLLECTION_NAME, keys=keys_to_delete, points=ids)
remote_client.delete_payload(COLLECTION_NAME, keys=keys_to_delete, points=ids, wait=True)
compare_collections(local_client, remote_client, NUM_VECTORS)
# endregion
# region delete by filter
payload = points[2].payload
key = "text_data"
value = payload[key]
delete_filter = models.Filter(
must=[models.FieldCondition(key=key, match=models.MatchValue(value=value))]
)
local_client.delete_payload(COLLECTION_NAME, keys=["text_data"], points=delete_filter)
remote_client.delete_payload(
COLLECTION_NAME, keys=["text_data"], points=delete_filter, wait=True
)
compare_collections(local_client, remote_client, NUM_VECTORS)
# endregion
@pytest.mark.parametrize("prefer_grpc", [True, False])
def test_clear_payload(prefer_grpc):
local_client: QdrantClient = init_local()
initialize_fixture_collection(local_client)
remote_client: QdrantClient = init_remote(prefer_grpc=prefer_grpc)
initialize_fixture_collection(remote_client)
points = upload(local_client, remote_client)
points_selector = [point.id for point in points[:5]]
local_client.clear_payload(COLLECTION_NAME, points_selector)
remote_client.clear_payload(COLLECTION_NAME, points_selector)
compare_collections(local_client, remote_client, NUM_VECTORS)
payload = points[42].payload
key = "text_data"
value = payload[key]
points_selector = models.Filter(
must=[models.FieldCondition(key=key, match=models.MatchValue(value=value))]
)
local_client.clear_payload(COLLECTION_NAME, points_selector)
remote_client.clear_payload(COLLECTION_NAME, points_selector)
compare_collections(local_client, remote_client, NUM_VECTORS)
@pytest.mark.parametrize("prefer_grpc", [True, False])
def test_update_payload(prefer_grpc):
local_client: QdrantClient = init_local()
initialize_fixture_collection(local_client)
remote_client: QdrantClient = init_remote(prefer_grpc=prefer_grpc)
initialize_fixture_collection(remote_client)
points = upload(local_client, remote_client)
# region fetch point
id_ = points[0].id
id_filter = models.Filter(must=[models.HasIdCondition(has_id=[id_])])
local_point = local_client.scroll(
COLLECTION_NAME,
scroll_filter=id_filter,
limit=1,
)
remote_point = remote_client.scroll(
COLLECTION_NAME,
scroll_filter=id_filter,
limit=1,
)
assert local_point == remote_point
# endregion
# region set payload
local_client.set_payload(COLLECTION_NAME, {"new_field": "new_value"}, id_filter)
remote_client.set_payload(COLLECTION_NAME, {"new_field": "new_value"}, id_filter)
local_new_point = local_client.scroll(
COLLECTION_NAME,
scroll_filter=id_filter,
limit=1,
)
remote_new_point = remote_client.scroll(
COLLECTION_NAME,
scroll_filter=id_filter,
limit=1,
)
assert local_new_point == remote_new_point
# endregion
# region overwrite payload
local_client.overwrite_payload(COLLECTION_NAME, {"new_field": "overwritten_value"}, id_filter)
remote_client.overwrite_payload(COLLECTION_NAME, {"new_field": "overwritten_value"}, id_filter)
local_new_point = local_client.scroll(
COLLECTION_NAME,
scroll_filter=id_filter,
limit=1,
)
remote_new_point = remote_client.scroll(
COLLECTION_NAME,
scroll_filter=id_filter,
limit=1,
)
assert local_new_point == remote_new_point
# endregion
compare_collections(local_client, remote_client, NUM_VECTORS) # sanity check
def test_not_jsonable_payload():
local_client = init_local()
remote_client = init_remote()
vector_size = 2
vectors_config = models.VectorParams(size=vector_size, distance=models.Distance.COSINE)
initialize_fixture_collection(local_client, vectors_config=vectors_config)
initialize_fixture_collection(remote_client, vectors_config=vectors_config)
# subset of types from pydantic.json.ENCODERS_BY_TYPE (pydantic v1)
# is not supported by grpc
payloads = [
{"bytes": b"123"},
{"date": datetime.date(2021, 1, 1)},
{"datetime": datetime.datetime(2021, 1, 1, 1, 1, 1)},
{"time": datetime.time(1, 1, 1)},
{"timedelta": datetime.timedelta(seconds=1)},
{"decimal": 1.0},
{"frozenset": frozenset([1, 2])},
{"set": {1, 2}},
{"uuid": uuid.uuid4()},
]
points = [
models.PointStruct(id=i, vector=[random.random(), random.random()], payload=payload)
for i, payload in enumerate(payloads)
]
for point in points: # for better debugging
local_client.upsert(COLLECTION_NAME, [point])
remote_client.upsert(COLLECTION_NAME, [point])
compare_collections(local_client, remote_client, len(points))
local_client.delete_collection(collection_name=COLLECTION_NAME)
local_client.create_collection(
collection_name=COLLECTION_NAME,
vectors_config=vectors_config,
)
remote_client.delete_collection(collection_name=COLLECTION_NAME)
remote_client.create_collection(
collection_name=COLLECTION_NAME,
vectors_config=vectors_config,
)
point_ids = []
for point in points:
point.payload = None
point_ids.append(point.id)
local_client.upsert(COLLECTION_NAME, [point])
remote_client.upsert(COLLECTION_NAME, [point])
for point_id, payload in zip(point_ids, payloads):
local_client.set_payload(
COLLECTION_NAME,
payload,
models.Filter(must=[models.HasIdCondition(has_id=[point_id])]),
)
remote_client.set_payload(
COLLECTION_NAME,
payload,
models.Filter(must=[models.HasIdCondition(has_id=[point_id])]),
)
compare_collections(local_client, remote_client, len(points))
for point_id, payload in zip(point_ids[::-1], payloads):
local_client.overwrite_payload(
COLLECTION_NAME,
payload,
models.Filter(must=[models.HasIdCondition(has_id=[point_id])]),
)
remote_client.overwrite_payload(
COLLECTION_NAME,
payload,
models.Filter(must=[models.HasIdCondition(has_id=[point_id])]),
)
compare_collections(local_client, remote_client, len(points))
@pytest.mark.parametrize("prefer_grpc", [True, False])
def test_set_payload_with_key(prefer_grpc):
local_client = init_local()
remote_client = init_remote(prefer_grpc=prefer_grpc)
vector_size = 2
vectors_config = models.VectorParams(size=vector_size, distance=models.Distance.COSINE)
initialize_fixture_collection(local_client, vectors_config=vectors_config)
initialize_fixture_collection(remote_client, vectors_config=vectors_config)
vector = np.random.rand(vector_size).tolist()
def set_payload(payload, new_payload, key):
local_client.upsert(
collection_name=COLLECTION_NAME,
points=[
PointStruct(
id=9999,
payload=payload,
vector=vector,
),
],
wait=True,
)
remote_client.upsert(
collection_name=COLLECTION_NAME,
points=[
PointStruct(
id=9999,
payload=payload,
vector=vector,
),
],
wait=True,
)
local_client.set_payload(
collection_name=COLLECTION_NAME,
payload=new_payload,
points=[9999],
key=key,
)
remote_client.set_payload(
collection_name=COLLECTION_NAME,
payload=new_payload,
points=[9999],
key=key,
)
compare_collections(local_client, remote_client, 1)
# update an existing field in nested array
payload = {"nest": [{"a": "100", "b": "200"}]}
new_payload = {"a": "101"}
key = "nest[0]"
set_payload(payload, new_payload, key)
# can't modify a non-existing array element
key = "nest[1]"
new_payload = {"d": "404"}
set_payload(payload, new_payload, key)
# add new field to a dict in nested array
key = "nest[].nest"
set_payload(payload, new_payload, key)
# add new field to a dict in nested array for all array elements
key = "nest[]"
set_payload(payload, new_payload, key)
# add new key to an empty payload
payload = {}
set_payload(payload, new_payload, key)
# can't add fields to an array
payload = {"nest": [{"a": [], "b": "200"}]}
new_payload = {"a": "101"}
key = "nest[0].a[]"
set_payload(payload, new_payload, key)
# add key to a deeply nested dict
payload = {"a": {"b": {"c": {"d": {"e": 1}}}}}
new_payload = {"f": 2}
key = "a.b.c.d"
set_payload(payload, new_payload, key)
# replace an array with a dict
payload = {"a": []}
new_payload = {}
key = "a.b"
set_payload(payload, new_payload, key)
# replace an array with a dict of arrays
payload = {"a": []}
new_payload = {}
key = "a.b[0]"
set_payload(payload, new_payload, key)
# can't replace a dict with an empty dict
payload = {"a": [[{"a": 1}]]}
new_payload = {}
key = "a[0][0]"
set_payload(payload, new_payload, key)
# modify a dict in a deeply nested array
payload = {"a": [[{"a": "w"}]]}
new_payload = {"b": "q"}
key = "a[0][0]"
set_payload(payload, new_payload, key)
# replace an array with an empty dict
payload = {"a": []}
new_payload = {}
key = "a.b"
set_payload(payload, new_payload, key)
# replace a dict with a nested array
payload = {"a": {"c": [{"d": 1}]}}
new_payload = {"a": 1}
key = "a.c[][]"
set_payload(payload, new_payload, key)
payload = {"": "xc"}
new_payload = {"": "bbb"}
key = ""
local_client.upsert(
collection_name=COLLECTION_NAME,
points=[
PointStruct(
id=9999,
payload=payload,
vector=vector,
),
],
wait=True,
)
remote_client.upsert(
collection_name=COLLECTION_NAME,
points=[
PointStruct(
id=9999,
payload=payload,
vector=vector,
),
],
wait=True,
)
# region invalid path blank key
with pytest.raises(ValueError):
local_client.set_payload(
collection_name=COLLECTION_NAME,
payload=new_payload,
points=[9999],
key=key,
)
with pytest.raises((UnexpectedResponse, grpc.RpcError)): # type: ignore
remote_client.set_payload(
collection_name=COLLECTION_NAME,
payload=new_payload,
points=[9999],
key=key,
)
# endregion
# region invalid path blank key in filter
filter_ = models.Filter(
must=[models.FieldCondition(key="", match=models.MatchValue(value="xc"))]
)
with pytest.raises(ValueError):
local_client.set_payload(
collection_name=COLLECTION_NAME, payload=new_payload, points=filter_
)
with pytest.raises((UnexpectedResponse, grpc.RpcError)): # type: ignore
remote_client.set_payload(
collection_name=COLLECTION_NAME, payload=new_payload, points=filter_
)
# endregion
# region correct way of setting payload for a blank key
filter_ = models.Filter(
must=[models.FieldCondition(key='""', match=models.MatchValue(value="xc"))]
)
remote_client.set_payload(collection_name=COLLECTION_NAME, payload=new_payload, points=filter_)
local_client.set_payload(collection_name=COLLECTION_NAME, payload=new_payload, points=filter_)
compare_collections(local_client, remote_client, 1)
# endregion
@pytest.mark.parametrize("prefer_grpc", [True, False])
def test_upsert_operation(prefer_grpc):
local_client: QdrantClient = init_local()
initialize_fixture_collection(local_client)
remote_client: QdrantClient = init_remote(prefer_grpc=prefer_grpc)
initialize_fixture_collection(remote_client)
def do_upsert_operation(client: QdrantClient, op: models.UpsertOperation):
client.batch_update_points(collection_name=COLLECTION_NAME, update_operations=[op])
vector = np.random.rand(text_vector_size).tolist()
upsert_points_batch = models.UpsertOperation(
upsert=models.PointsBatch(
batch=models.Batch(
ids=[
1,
],
vectors={"text": [vector]},
payloads=[{"key": "value"}],
)
)
)
do_upsert_operation(local_client, upsert_points_batch)
do_upsert_operation(remote_client, upsert_points_batch)
compare_collections(local_client, remote_client, 10)
upsert_points_list = models.UpsertOperation(
upsert=models.PointsList(
points=[models.PointStruct(id=2, vector={"text": vector}, payload={"key": "value"})]
)
)
do_upsert_operation(local_client, upsert_points_list)
do_upsert_operation(remote_client, upsert_points_list)
compare_collections(local_client, remote_client, 10)
@pytest.mark.parametrize("prefer_grpc", [True, False])
def test_delete_operation(prefer_grpc):
local_client: QdrantClient = init_local()
initialize_fixture_collection(local_client)
remote_client: QdrantClient = init_remote(prefer_grpc=prefer_grpc)
initialize_fixture_collection(remote_client)
num_vectors = 100
upload(local_client, remote_client, num_vectors)
ids_to_delete = [1, 2, 3]
op = models.DeleteOperation(delete=models.PointIdsList(points=ids_to_delete))
local_client.batch_update_points(collection_name=COLLECTION_NAME, update_operations=[op])
remote_client.batch_update_points(collection_name=COLLECTION_NAME, update_operations=[op])
compare_collections(local_client, remote_client, num_vectors)
assert local_client.count(collection_name=COLLECTION_NAME).count == (
num_vectors - len(ids_to_delete)
)
@pytest.mark.parametrize("prefer_grpc", [True, False])
def test_delete_and_clear_payload_operation(prefer_grpc):
local_client: QdrantClient = init_local()
remote_client: QdrantClient = init_remote(prefer_grpc=prefer_grpc)
initialize_fixture_collection(
local_client, vectors_config=models.VectorParams(size=2, distance=models.Distance.COSINE)
)
initialize_fixture_collection(
remote_client, vectors_config=models.VectorParams(size=2, distance=models.Distance.COSINE)
)
points = [
models.PointStruct(
id=i,
vector=[random.random(), random.random()],
payload={"random_digit": random.randint(0, 9)},
)
for i in range(10)
]
local_client.upsert(COLLECTION_NAME, points)
remote_client.upsert(COLLECTION_NAME, points)
ids_to_delete = [1, 2, 3]
delete_payload_op_points = models.DeletePayloadOperation(
delete_payload=models.DeletePayload(keys=["random_digit"], points=ids_to_delete)
)
local_client.batch_update_points(
collection_name=COLLECTION_NAME, update_operations=[delete_payload_op_points]
)
remote_client.batch_update_points(
collection_name=COLLECTION_NAME, update_operations=[delete_payload_op_points]
)
compare_collections(local_client, remote_client, 10)
ids_to_delete = [4, 5, 6]
delete_payload_op_filter = models.DeletePayloadOperation(
delete_payload=models.DeletePayload(
keys=["random_digit"],
filter=models.Filter(must=[models.HasIdCondition(has_id=ids_to_delete)]),
)
)
local_client.batch_update_points(
collection_name=COLLECTION_NAME, update_operations=[delete_payload_op_filter]
)
remote_client.batch_update_points(
collection_name=COLLECTION_NAME, update_operations=[delete_payload_op_filter]
)
compare_collections(local_client, remote_client, 10)
ids_to_clear = [7, 8, 9]
clear_payload_op = models.ClearPayloadOperation(
clear_payload=models.PointIdsList(points=ids_to_clear)
)
local_client.batch_update_points(
collection_name=COLLECTION_NAME, update_operations=[clear_payload_op]
)
remote_client.batch_update_points(
collection_name=COLLECTION_NAME, update_operations=[clear_payload_op]
)
compare_collections(local_client, remote_client, 10)
@pytest.mark.parametrize("prefer_grpc", [True, False])
def test_update_or_delete_vectors_operation(prefer_grpc):
local_client: QdrantClient = init_local()
initialize_fixture_collection(local_client)
remote_client: QdrantClient = init_remote(prefer_grpc=prefer_grpc)
initialize_fixture_collection(remote_client)
num_vectors = 10
upload(local_client, remote_client, num_vectors)
new_vector = {"text": np.random.randn(text_vector_size).round(3).tolist()}
update_vectors_op = models.UpdateVectorsOperation(
update_vectors=models.UpdateVectors(points=[models.PointVectors(id=1, vector=new_vector)])
)
local_client.batch_update_points(
collection_name=COLLECTION_NAME, update_operations=[update_vectors_op]
)
remote_client.batch_update_points(
collection_name=COLLECTION_NAME, update_operations=[update_vectors_op]
)
compare_collections(local_client, remote_client, num_vectors)
ids_to_delete = [1, 2, 3]
delete_vectors_op_points = models.DeleteVectorsOperation(
delete_vectors=models.DeleteVectors(points=ids_to_delete, vector=["text"])
)
local_client.batch_update_points(
collection_name=COLLECTION_NAME, update_operations=[delete_vectors_op_points]
)
remote_client.batch_update_points(
collection_name=COLLECTION_NAME, update_operations=[delete_vectors_op_points]
)
compare_collections(local_client, remote_client, num_vectors)
ids_to_delete = [4, 5, 6]
delete_vectors_op_filter = models.DeleteVectorsOperation(
delete_vectors=models.DeleteVectors(
filter=models.Filter(must=[models.HasIdCondition(has_id=ids_to_delete)]),
vector=["text"],
)
)
local_client.batch_update_points(
collection_name=COLLECTION_NAME, update_operations=[delete_vectors_op_filter]
)
remote_client.batch_update_points(
collection_name=COLLECTION_NAME, update_operations=[delete_vectors_op_filter]
)
compare_collections(local_client, remote_client, num_vectors)
@pytest.mark.parametrize("prefer_grpc", [True, False])
def test_set_or_overwrite_payload_operation(prefer_grpc):
local_client: QdrantClient = init_local()
initialize_fixture_collection(local_client)
remote_client: QdrantClient = init_remote(prefer_grpc=prefer_grpc)
initialize_fixture_collection(remote_client)
num_vectors = 10
upload(local_client, remote_client, num_vectors)
new_payload = {"text_data": "new_value"}
set_payload_op_points_no_key = models.SetPayloadOperation(
set_payload=models.SetPayload(points=[1], payload=new_payload)
)
local_client.batch_update_points(
collection_name=COLLECTION_NAME, update_operations=[set_payload_op_points_no_key]
)
remote_client.batch_update_points(
collection_name=COLLECTION_NAME, update_operations=[set_payload_op_points_no_key]
)
compare_collections(local_client, remote_client, num_vectors)
new_nested_payload = {"nested_data": "new_nested_value"}
set_payload_op_filter_key = models.SetPayloadOperation(
set_payload=models.SetPayload(
filter=models.Filter(must=[models.HasIdCondition(has_id=[1])]),
payload=new_nested_payload,
key="text_data",
)
)
local_client.batch_update_points(
collection_name=COLLECTION_NAME, update_operations=[set_payload_op_filter_key]
)
remote_client.batch_update_points(
collection_name=COLLECTION_NAME, update_operations=[set_payload_op_filter_key]
)
compare_collections(local_client, remote_client, num_vectors)
new_payload = {"text_data": {"some_key": "overwritten_value"}}
overwrite_payload_op_points_no_key = models.OverwritePayloadOperation(
overwrite_payload=models.SetPayload(points=[1], payload=new_payload)
)
local_client.batch_update_points(
collection_name=COLLECTION_NAME, update_operations=[overwrite_payload_op_points_no_key]
)
remote_client.batch_update_points(
collection_name=COLLECTION_NAME, update_operations=[overwrite_payload_op_points_no_key]
)
compare_collections(local_client, remote_client, num_vectors)