跳到主要内容
版本:v2.6.x

更新实体

upsert 操作符为在 Collection 中插入或更新实体提供了一种便捷的方法。

操作概述

根据 upsert 请求中提供的主键是否存在于 Collection 中,您可以使用upsert 插入新实体或更新现有实体。如果找不到主键,则进行插入操作。否则,将执行更新操作。

在 Milvus 中,upsert 可在覆盖合并模式下工作。

覆盖模式下的 upsert

覆盖模式下的上载请求结合了插入和删除操作。当收到针对现有实体的upsert 请求时,Milvus 会插入请求有效载荷中携带的数据,并同时删除数据中指定原始主键的现有实体。

Upsert In Override Mode覆盖模式下的 upsert

如果目标 Collection 的主字段已启用autoID ,则upsert 请求仍必须包括目标实体的主键。Milvus 使用所提供的主键来定位要替换的实体,并在插入请求有效载荷中携带的数据前生成一个新的主键。

对于已启用nullable 的字段,如果不需要更新,可以在upsert 请求中省略。

在合并模式下向 upsertCompatible with Milvus v2.6.2+

您还可以使用partial_update 标志,使上载请求以合并模式运行。这样就可以在请求有效载荷中只包含需要更新的字段。

Upsert In Merge Mode合并模式下的 upsert

要执行合并,请在upsert 请求中将partial_update 设置为True ,并将主键和需要更新的字段设置为新值。

收到这样的请求后,Milvus 会执行强一致性查询以检索实体,根据请求中的数据更新字段值,插入修改后的数据,然后用请求中携带的原始主键删除现有实体。

对于ARRAY 字段,合并模式支持两种操作符:ARRAY_APPENDARRAY_REMOVE 。这些操作符可让您在现有ARRAY 字段中追加元素或删除匹配元素,而无需先查询实体以获取其当前值。有关详细信息,请参阅使用部分更新操作符的 Upsert ARRAY 字段

Upsert 行为:特别注意事项

在使用合并功能之前,有几个特别注意事项需要考虑。以下情况假定您有一个 Collection,其中有两个标量字段,分别名为titleissue ,以及一个主键id 和一个向量字段vector

  • 启用 nullable 的向 upsert 字段

    假设issue 字段可以为空。在倒插这些字段时,请注意以下几点:

    • 如果在upsert 请求中省略issue 字段并禁用partial_updateissue 字段将更新为null ,而不是保留其原始值。

    • 要保留issue 字段的原始值,要么启用partial_update 并省略issue 字段,要么在upsert 请求中包含issue 字段及其原始值。

  • 在动态字段中倒插键

    假设在示例 Collection 中启用了动态键,实体动态字段中的键值对与{"author": "John", "year": 2020, "tags": ["fiction"]} 类似。

    当你向 upsert 实体的键,如author,year, 或tags ,或添加其他键时,请注意:

    • 如果上载partial_update 时禁用,默认行为是覆盖。这意味着动态字段的值将被请求中包含的所有非 Schema 定义的字段及其值覆盖。

      例如,如果请求中包含的数据是{"author": "Jane", "genre": "fantasy"} ,目标实体动态字段中的键值对将更新为该值。

    • 如果在启用partial_update 的情况下进行 upsert,默认行为是合并。这意味着动态字段的值将与请求中包含的所有非 Schema 定义的字段及其值合并。

      例如,如果请求中包含的数据是{"author": "John", "year": 2020, "tags": ["fiction"]} ,则目标实体动态字段中的键值对在 upsert 后将变成{"author": "John", "year": 2020, "tags": ["fiction"], "genre": "fantasy"}

  • 倒插一个 JSON 字段。

    假设示例 Collection 有一个名为extras 的 Schema 定义 JSON 字段,实体的此 JSON 字段中的键值对类似于{"author": "John", "year": 2020, "tags": ["fiction"]}

    当您使用修改后的 JSON 数据向 upsert 实体的extras 字段时,请注意该 JSON 字段被视为一个整体,您不能有选择地更新单个键。换句话说,JSON 字段支持合并模式下的倒插。

  • 倒插 ARRAY 字段。

    在合并模式下,ARRAY 字段支持ARRAY_APPENDARRAY_REMOVE 部分更新操作符。当您想在现有ARRAY 字段中添加元素或移除匹配元素而不替换整个数组值时,请使用这些操作符。

限制和约束

根据上述内容,有几个限制和约束需要遵循:

  • upsert 请求必须始终包含目标实体的主键,即使启用了autoID 。对于autoID Collection,请求中的主键标识了要替换的现有实体。Milvus 会为插入的替换实体生成新的主键。

  • 目标 Collection 必须已加载并可供查询。

  • 请求中指定的所有字段必须存在于目标 Collection 的 Schema 中。

  • 请求中指定的所有字段的值必须与 Schema 中定义的数据类型相匹配。

  • 对于使用函数从另一个字段派生出来的任何字段,Milvus 将在倒插过程中删除派生字段,以便重新计算。

倒插 Collection 中的实体

在本节中,我们将把实体上载到名为my_collection 的 Collection 中。该 Collection 只有两个字段,分别名为id,vector,titleissueid 字段是主字段,而titleissue 字段是标量字段。

这三个实体如果存在于 Collection 中,将被包含 upsert 请求的实体覆盖。

from pymilvus import MilvusClient

client = MilvusClient(
uri="http://localhost:19530",
token="root:Milvus"
)

data=[
{
"id": 0,
"vector": [-0.619954382375778, 0.4479436794798608, -0.17493894838751745, -0.4248030059917294, -0.8648452746018911],
"title": "Artificial Intelligence in Real Life",
"issue": "vol.12"
}, {
"id": 1,
"vector": [0.4762662251462588, -0.6942502138717026, -0.4490002642657902, -0.628696575798281, 0.9660395877041965],
"title": "Hollow Man",
"issue": "vol.19"
}, {
"id": 2,
"vector": [-0.8864122635045097, 0.9260170474445351, 0.801326976181461, 0.6383943392381306, 0.7563037341572827],
"title": "Treasure Hunt in Missouri",
"issue": "vol.12"
}
]

res = client.upsert(
collection_name='my_collection',
data=data
)

print(res)

# Output
# {'upsert_count': 3}

向 upsertPartition 中的实体

您还可以将实体上载到指定的 Partition 中。以下代码片段假定你的 Collection 中有一个名为PartitionA的 Partition。

如果 Partition 中存在三个实体,它们将被请求中包含的实体覆盖。

data=[
{
"id": 10,
"vector": [0.06998888224297328, 0.8582816610326578, -0.9657938677934292, 0.6527905683627726, -0.8668460657158576],
"title": "Layour Design Reference",
"issue": "vol.34"
},
{
"id": 11,
"vector": [0.6060703043917468, -0.3765080534566074, -0.7710758854987239, 0.36993888322346136, 0.5507513364206531],
"title": "Doraemon and His Friends",
"issue": "vol.2"
},
{
"id": 12,
"vector": [-0.9041813104515337, -0.9610546012461163, 0.20033003106083358, 0.11842506351635174, 0.8327356724591011],
"title": "Pikkachu and Pokemon",
"issue": "vol.12"
},
]

res = client.upsert(
collection_name="my_collection",
data=data,
partition_name="partitionA"
)

print(res)

# Output
# {'upsert_count': 3}

在合并模式下倒插实体 Compatible with Milvus v2.6.2+

下面的代码示例演示了如何通过部分更新来倒插实体。只提供需要更新的字段及其新值,以及显式部分更新标记。

在下面的示例中,upsert 请求中指定的实体的issue 字段将更新为请求中包含的值。

说明

在合并模式下执行 upsert 时,请确保请求中涉及的实体具有相同的字段集。假设有两个或更多实体要进行 upsert,如以下代码片段所示,它们必须包含相同的字段,以防止出现错误并保持数据完整性。

data=[
{
"id": 1,
"issue": "vol.14"
},
{
"id": 2,
"issue": "vol.7"
}
]

res = client.upsert(
collection_name="my_collection",
data=data,
partial_update=True
)

print(res)

# Output
# {'upsert_count': 2}

使用部分更新操作符倒插 ARRAY 字段 Compatible with Milvus v2.6.17+

在引入部分更新操作符之前,更新ARRAY 字段的部分内容需要客户端读取-修改-写入流程:查询现有数组,在应用代码中进行修改,然后向 upsert 完整的替换值。部分更新操作符可以让你只发送要追加或移除的元素,从而减少了客户端逻辑,避免了上载前的额外读取。

假设主键为1 的实体已经有tags = ["new", "trial"] 。如果没有部分更新操作符,向数组中添加"premium" 就需要向 upsert 完整的替换数组:

client.upsert(
collection_name="users",
data=[{"pk": 1, "tags": ["new", "trial", "premium"]}],
partial_update=True,
)

使用ARRAY_APPEND 时,只需发送要添加的元素:

from pymilvus import FieldOp

client.upsert(
collection_name="users",
data=[{"pk": 1, "tags": ["premium"]}],
field_ops={"tags": FieldOp.array_append()},
)
说明

通过field_ops 将任一操作符附加到字段,都会隐式启用部分更新语义。因此,您无需在传递partial_update=True 的同时传递field_ops

限制

  • 有效负载值必须与ARRAY 目标字段的element_type 匹配。例如,如果目标字段是ARRAY<VARCHAR> ,则有效载荷必须包含字符串值。
  • ARRAY_APPENDARRAY_REMOVE 支持ARRAY 字段,其element_typeBOOL,INT8,INT16,INT32,INT64,FLOAT,DOUBLE, 或VARCHAR
  • 进行ARRAY_APPEND 操作后,生成的数组长度不得超过字段的max_capacity
  • 对同一实体的并发上载在不同请求之间不是原子的。如果两个请求同时更新同一个ARRAY 字段,后一个写入会覆盖前一个写入。如果需要保留所有并发更改,请使用应用程序级协调。

示例

下面的示例使用了一个小型users Collection,其中有一个主键pk 、一个tags 类型的字段ARRAY<VARCHAR> 和一个embedding 向量字段。它首先插入两个具有初始tags 值的实体,然后使用ARRAY_APPENDARRAY_REMOVE 来展示每个操作符如何更改存储的数组。

from pymilvus import DataType, FieldOp, MilvusClient

client = MilvusClient(
uri="http://localhost:19530",
token="root:Milvus"
)

# 1. Create a collection with an ARRAY<VARCHAR> field
schema = client.create_schema(enable_dynamic_field=False)
schema.add_field("pk", DataType.INT64, is_primary=True)
schema.add_field("embedding", DataType.FLOAT_VECTOR, dim=5)
schema.add_field(
"tags",
DataType.ARRAY,
element_type=DataType.VARCHAR,
max_capacity=8,
max_length=32,
)

index_params = client.prepare_index_params()
index_params.add_index(
field_name="embedding",
index_type="AUTOINDEX",
metric_type="L2",
)

client.create_collection(
collection_name="users",
schema=schema,
index_params=index_params
)

# 2. Seed two entities
client.insert(
collection_name="users",
data=[
{"pk": 1, "embedding": [0.1, 0.2, 0.3, 0.4, 0.5], "tags": ["new"]},
{"pk": 2, "embedding": [0.6, 0.7, 0.8, 0.9, 1.0], "tags": ["new", "trial"]},
],
)

# 3. Append tags without reading the existing ARRAY values
client.upsert(
collection_name="users",
data=[
{"pk": 1, "tags": ["premium", "vip"]},
{"pk": 2, "tags": ["premium"]},
],
field_ops={"tags": FieldOp.array_append()},
)

res = client.query(
collection_name="users",
filter="pk in [1, 2]",
output_fields=["pk", "tags"],
)
print(res)

# Example output:
# data: [
# "{'pk': 1, 'tags': ['new', 'premium', 'vip']}",
# "{'pk': 2, 'tags': ['new', 'trial', 'premium']}"
# ]

# 4. Remove matching tags without replacing the full ARRAY field
client.upsert(
collection_name="users",
data=[
{"pk": 1, "tags": ["new"]},
{"pk": 2, "tags": ["trial"]},
],
field_ops={"tags": FieldOp.array_remove()},
)

res = client.query(
collection_name="users",
filter="pk in [1, 2]",
output_fields=["pk", "tags"],
)
print(res)

# Example output:
# data: [
# "{'pk': 1, 'tags': ['premium', 'vip']}",
# "{'pk': 2, 'tags': ['new', 'premium']}"
# ]