Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions qdrant_client/local/local_collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@
validate_filter,
)
from qdrant_client.local.payload_value_extractor import value_by_key, parse_uuid
from qdrant_client.local.payload_value_setter import set_value_by_key
from qdrant_client.local.payload_value_setter import delete_value_by_key, set_value_by_key
from qdrant_client.local.persistence import CollectionPersistence
from qdrant_client.local.utils import last_argmax, swap_remove
from qdrant_client.local.sparse import (
Expand Down Expand Up @@ -2937,8 +2937,7 @@ def delete_payload(
for point_id in ids:
idx = self.ids[point_id]
for key in keys:
if key in self.payload[idx]:
self.payload[idx].pop(key)
delete_value_by_key(self.payload[idx], parse_json_path(key))
self._persist_by_id(point_id)

def clear_payload(
Expand Down
49 changes: 49 additions & 0 deletions qdrant_client/local/payload_value_setter.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,55 @@ def set_value_by_key(payload: dict, keys: list[JsonPathItem], value: Any) -> Non
Setter.set(payload, keys.copy(), value, None, None)


def delete_value_by_key(payload: dict, keys: list[JsonPathItem]) -> None:
"""
Delete value in payload by key path, matching the server's payload-delete
semantics (nested keys via dot notation, array indices and wildcards).

A key path that does not resolve to an existing value is a no-op, and
sibling values are preserved. This mirrors the json-path handling that
``set_value_by_key`` and ``value_by_key`` already use, so ``delete_payload``
honors the same paths as ``set_payload`` and filters.

Args:
payload: arbitrary json-like object
keys: list of json path items, e.g. the parse of "address.city",
"location[0].name" or "location[].name"
"""

def _delete(data: Any, k_list: list[JsonPathItem]) -> None:
if not k_list:
return

current_key = k_list.pop(0)

if len(k_list) == 0:
if isinstance(data, dict) and current_key.item_type == JsonPathItemType.KEY:
data.pop(current_key.key, None)
elif isinstance(data, list):
if current_key.item_type == JsonPathItemType.INDEX:
assert current_key.index is not None
if current_key.index < len(data):
del data[current_key.index]
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
elif current_key.item_type == JsonPathItemType.WILDCARD_INDEX:
data.clear()
return

if current_key.item_type == JsonPathItemType.KEY:
if isinstance(data, dict) and current_key.key in data:
_delete(data[current_key.key], k_list.copy())
elif current_key.item_type == JsonPathItemType.INDEX:
assert current_key.index is not None
if isinstance(data, list) and current_key.index < len(data):
_delete(data[current_key.index], k_list.copy())
elif current_key.item_type == JsonPathItemType.WILDCARD_INDEX:
if isinstance(data, list):
for item in data:
_delete(item, k_list.copy())

_delete(payload, keys.copy())


class Setter:
TYPE: Any
SETTERS: dict[JsonPathItemType, Type["Setter"]] = {}
Expand Down
37 changes: 37 additions & 0 deletions tests/congruence_tests/test_payload.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,43 @@ def test_delete_payload(prefer_grpc):
# endregion


@pytest.mark.parametrize("prefer_grpc", [True, False])
def test_delete_payload_with_nested_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 delete_keys(payload, keys):
for client in (local_client, remote_client):
client.upsert(
collection_name=COLLECTION_NAME,
points=[PointStruct(id=9999, payload=payload, vector=vector)],
wait=True,
)
client.delete_payload(
collection_name=COLLECTION_NAME, keys=keys, points=[9999], wait=True
)
compare_collections(local_client, remote_client, 1)

# nested dict key: only the leaf is removed, siblings preserved
delete_keys({"a": {"b": 1, "c": 2}, "top": 9}, ["a.b"])

# a non-existent nested path is a no-op
delete_keys({"a": {"c": 2}}, ["a.b"])

# field inside every element of a nested array
delete_keys({"loc": [{"x": 1, "y": 2}, {"x": 3, "y": 4}]}, ["loc[].x"])

# top-level and nested keys together
delete_keys({"a": {"b": 1}, "top": 9}, ["a.b", "top"])


@pytest.mark.parametrize("prefer_grpc", [True, False])
def test_clear_payload(prefer_grpc):
local_client: QdrantClient = init_local()
Expand Down
37 changes: 37 additions & 0 deletions tests/local/test_delete_value_by_key.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import pytest

from qdrant_client.local.json_path_parser import parse_json_path
from qdrant_client.local.payload_value_setter import delete_value_by_key


def _delete(payload: dict, key: str) -> dict:
delete_value_by_key(payload, parse_json_path(key))
return payload


@pytest.mark.parametrize(
("payload", "key", "expected"),
[
# top-level key
({"a": 1, "b": 2}, "a", {"b": 2}),
# nested dict path removes only the leaf, siblings preserved
({"a": {"b": 1, "c": 2}, "top": 9}, "a.b", {"a": {"c": 2}, "top": 9}),
# deeper path
({"a": {"b": {"c": 1, "d": 2}}}, "a.b.c", {"a": {"b": {"d": 2}}}),
# array index
({"loc": [{"x": 1}, {"x": 2}]}, "loc[0].x", {"loc": [{}, {"x": 2}]}),
# array wildcard removes the field from every element
(
{"loc": [{"x": 1, "y": 2}, {"x": 3, "y": 4}]},
"loc[].x",
{"loc": [{"y": 2}, {"y": 4}]},
),
# non-existent path is a no-op, nothing else touched
({"a": {"c": 2}}, "a.b", {"a": {"c": 2}}),
({"a": {"c": 2}}, "nope.nested", {"a": {"c": 2}}),
# path through a non-dict is a no-op
({"a": 5}, "a.b", {"a": 5}),
],
)
def test_delete_value_by_key(payload, key, expected):
assert _delete(payload, key) == expected