可为空字段

Milvus 支持可为空字段,允许字段值为空或显式设置为 NULL。可为空性在 Schema 级别定义,并在数据摄入、索引、搜索和查询操作中始终如一地应用。

在以下情况下使用可空字段:

  • 从允许缺失值的外部系统摄取数据时。
  • 部分元数据为可选,或仅在数据集的部分数据中可用。
  • 向量Embeddings是异步生成的,并在稍后插入。

限制

  • 允许 NULL 值的向量字段不支持 `IS NULL ` 或 `IS NOT NULL ` 过滤表达式。您无法根据向量字段值是否为 NULL 来显式过滤实体。

  • 从 Milvus 3.0.0 开始,父级StructArray字段可以是可为空的。请在父级 StructArray 字段上设置 `nullable=True `,而非在单个子字段上设置。NULL 适用于整个 StructArray 字段,而非单个 Struct 元素,且 Milvus 会在内部将父级字段的可为空性传播至其子字段。 添加到现有 Collection 中的 StructArray 字段必须为可空,以便现有实体能够为该新字段返回 NULL。详情请参阅《StructArray 限制》。

  • 可空属性在字段创建时定义,此后无法修改。您无法对现有字段启用或禁用可空性。

  • 标记为可空的字段不能用作Partition Key。Partition Key字段必须始终包含有效的、非空的值。有关更多信息,请参阅《使用Partition Key》。

什么是可空字段?

在 Milvus 中,字段是否允许存储 NULL 值由一个名为 `nullable` 的 Schema 级字段属性控制。

当字段被定义为nullable=True 时,Milvus 允许在数据摄入过程中该字段值缺失。实际上,Milvus 将以下两种输入视为等效,并将字段值存储为 NULL:

  • 输入实体中省略了该字段。
  • 该字段被显式设置为 NULL(例如,在 Python 中使用 `None `)。

如果字段未定义为可为空(默认行为),则每个实体都必须为该字段提供有效值。省略该字段或显式赋值为 NULL 都会导致插入或导入操作失败。

Schema 模式中的标量字段和向量字段均支持 nullable 属性。从 Milvus 3.0.0 开始,父级 StructArray 字段也支持该属性。请勿单独将 Struct 子字段配置为可为空;应在 StructArray 父级上定义可为空性,Milvus 会内部将该设置传播到其子字段。

可空性决定字段值是否可以为空;它并不定义字段为空时使用的具体值。

  • 如果配置了可空字段但未指定默认值,则省略该字段时,系统将存储 NULL 值。
  • 如果配置了默认值,Milvus 可能会存储该默认值。有关详细信息,请参阅“默认值”

在Collection Schema中定义可空字段

要使用可空字段,您必须在定义 Collection Schema 时启用 nullable 属性。

在此示例中,Collection Schema定义了一个名为embedding 的向量字段,其nullable=True 。这允许Collection中的实体在数据摄入过程中省略向量值,或将其显式设置为NULL。

from pymilvus import MilvusClient, DataType

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

# Define schema fields
schema = client.create_schema()
schema.add_field("id", DataType.INT64, is_primary=True)  # Primary field
schema.add_field(
    field_name="embedding",
    datatype=DataType.FLOAT_VECTOR,
    dim=4,
    nullable=True,  # Enable the nullable attribute; defaults to False
)

client.create_collection(
    collection_name="my_collection",
    schema=schema,
)
import io.milvus.v2.client.ConnectConfig;
import io.milvus.v2.client.MilvusClientV2;
import io.milvus.v2.common.DataType;
import io.milvus.v2.service.collection.request.AddFieldReq;
import io.milvus.v2.service.collection.request.CreateCollectionReq;

MilvusClientV2 client = new MilvusClientV2(ConnectConfig.builder()
        .uri("http://localhost:19530")
        .token("root:Milvus")
        .build());

CreateCollectionReq.CollectionSchema schema = CreateCollectionReq.CollectionSchema.builder()
        .build();

schema.addField(AddFieldReq.builder()
        .fieldName("id")
        .dataType(DataType.Int64)
        .isPrimaryKey(true)
        .build());
schema.addField(AddFieldReq.builder()
        .fieldName("embedding")
        .dataType(DataType.FloatVector)
        .dimension(4)
        .isNullable(true)
        .build());

client.createCollection(CreateCollectionReq.builder()
        .collectionName("my_collection")
        .collectionSchema(schema)
        .build());
import { MilvusClient, DataType } from "@zilliz/milvus2-sdk-node";

const client = new MilvusClient({
  address: "http://localhost:19530",
  token: "root:Milvus",
});

await client.createCollection({
  collection_name: "my_collection",
  fields: [
    {
      name: "id",
      data_type: DataType.Int64,
      is_primary_key: true,
      autoID: false,
    },
    {
      name: "embedding",
      data_type: DataType.FloatVector,
      dim: 4,
      nullable: true,
    },
  ],
});
import (
    "context"
    "fmt"

    "github.com/milvus-io/milvus/client/v2/entity"
    "github.com/milvus-io/milvus/client/v2/milvusclient"
)

ctx, cancel := context.WithCancel(context.Background())
defer cancel()

client, err := milvusclient.New(ctx, &milvusclient.ClientConfig{
    Address: "localhost:19530",
})
if err != nil {
    fmt.Println(err.Error())
    // handle error
}
defer client.Close(ctx)

schema := entity.NewSchema()
schema.WithField(entity.NewField().
    WithName("id").
    WithDataType(entity.FieldTypeInt64).
    WithIsPrimaryKey(true),
).WithField(entity.NewField().
    WithName("embedding").
    WithDataType(entity.FieldTypeFloatVector).
    WithDim(4).
    WithNullable(true),
)

err = client.CreateCollection(ctx,
    milvusclient.NewCreateCollectionOption("my_collection", schema))
if err != nil {
    fmt.Println(err.Error())
    // handle error
}
export TOKEN="root:Milvus"
export CLUSTER_ENDPOINT="http://localhost:19530"

export pkField='{
  "fieldName": "id",
  "dataType": "Int64",
  "isPrimary": true
}'

export embeddingField='{
  "fieldName": "embedding",
  "dataType": "FloatVector",
  "typeParams": {"dim": "4"},
  "nullable": true
}'

curl --request POST \
  --url "${CLUSTER_ENDPOINT}/v2/vectordb/collections/create" \
  --header "Authorization: Bearer ${TOKEN}" \
  --header "Content-Type: application/json" \
  --header "Request-Timeout: 10" \
  -d "{
    \"collectionName\": \"my_collection\",
    \"schema\": {
      \"fields\": [
        $pkField,
        $embeddingField
      ]
    }
  }"

在此Schema中:

  • embedding 字段被显式标记为可为空。
  • 实体在插入时可以省略embedding 字段,或将其赋值为 NULL。
  • 是否允许 NULL 值的决定在 Collection 创建时即已确定。

为清晰起见,以下示例将重点放在可为空的向量字段(embedding )上。定义可为空的标量字段是可选的,且无需遵循本指南的其余内容。

可选:定义可空标量字段

标量字段也可以使用相同的 `nullable ` 属性定义为可为空,并在数据摄入过程中遵循相同的规则。例如:

schema.add_field(
    field_name="age",
    datatype=DataType.INT64,
    nullable=True,
)
schema.addField(AddFieldReq.builder()
        .fieldName("age")
        .dataType(DataType.Int64)
        .isNullable(true)
        .build());
// Add to the fields array when calling createCollection:
// { name: "age", data_type: DataType.Int64, nullable: true },
schema.WithField(entity.NewField().
    WithName("age").
    WithDataType(entity.FieldTypeInt64).
    WithNullable(true),
)
# Add another field object to the schema "fields" array, for example:
# { "fieldName": "age", "dataType": "Int64", "nullable": true }

存在缺失值或 NULL 值时的插入行为

一旦在 Schema 中将字段定义为可空,Milvus 便允许在数据摄入过程中该字段值为缺失或显式设置为 NULL。

下面的示例将三个实体插入到“在Schema中定义可为空字段”中创建的Collection中,演示了这些不同情况。

data = [
    {
        "id": 1,
        "embedding": [0.1, 0.2, 0.3, 0.4],
    },
    {
        "id": 2,
        "embedding": None,  # Explicitly set to NULL
    },
    {
        "id": 3,  # Field omitted → stored as NULL
    },
]

client.insert(
    collection_name="my_collection",
    data=data,
)
import com.google.gson.Gson;
import com.google.gson.JsonNull;
import com.google.gson.JsonObject;
import io.milvus.v2.service.vector.request.InsertReq;

import java.util.Arrays;
import java.util.List;

Gson gson = new Gson();

JsonObject row1 = new JsonObject();
row1.addProperty("id", 1);
row1.add("embedding", gson.toJsonTree(Arrays.asList(0.1f, 0.2f, 0.3f, 0.4f)));

JsonObject row2 = new JsonObject();
row2.addProperty("id", 2);
row2.add("embedding", JsonNull.INSTANCE); // Explicitly set to NULL

JsonObject row3 = new JsonObject();
row3.addProperty("id", 3); // Field omitted; stored as NULL

List<JsonObject> data = Arrays.asList(row1, row2, row3);

client.insert(InsertReq.builder()
        .collectionName("my_collection")
        .data(data)
        .build());
const data = [
  { id: 1, embedding: [0.1, 0.2, 0.3, 0.4] },
  { id: 2, embedding: null },
  { id: 3 },
];

await client.insert({
  collection_name: "my_collection",
  data: data,
});
import (
    "context"
    "fmt"

    "github.com/milvus-io/milvus/client/v2/milvusclient"
)

// Assumes `client` is the Milvus client from the Go schema example above.
ctx := context.Background()

rows := []any{
    map[string]any{"id": int64(1), "embedding": []float32{0.1, 0.2, 0.3, 0.4}},
    map[string]any{"id": int64(2), "embedding": nil},
    map[string]any{"id": int64(3)},
}

_, err := client.Insert(ctx, milvusclient.NewRowBasedInsertOption("my_collection", rows...))
if err != nil {
    fmt.Println(err.Error())
}
curl --request POST \
  --url "${CLUSTER_ENDPOINT}/v2/vectordb/entities/insert" \
  --header "Authorization: Bearer ${TOKEN}" \
  --header "Content-Type: application/json" \
  --header "Request-Timeout: 10" \
  -d '{
    "collectionName": "my_collection",
    "data": [
      {"id": 1, "embedding": [0.1, 0.2, 0.3, 0.4]},
      {"id": 2, "embedding": null},
      {"id": 3}
    ]
  }'

在此示例中:

  • 实体id = 1提供了有效的向量值。
  • 实体id = 2将 NULL 值显式赋值给 `embedding ` 字段。
  • 实体id = 3完全省略了 `embedding ` 字段;Milvus 将其存储为 NULL。

可为空字段的索引行为

插入数据后,您可以照常在可为空字段上构建索引。主要区别在于 Milvus 在构建索引时如何处理 NULL 值:

  • 只有具有非空值的实体才会被添加到索引中。
  • 具有 NULL 值的实体将被跳过,不参与索引构建。

对于可为空的向量字段,这意味着只有具有有效向量的实体才能通过向量相似度进行搜索。

# Set index parameters
index_params = client.prepare_index_params()
index_params.add_index(
    field_name="embedding",
    index_type="AUTOINDEX",
    metric_type="COSINE",
)

# Create index
client.create_index(
    collection_name="my_collection",
    index_params=index_params,
)

# Load collection for future search operations
client.load_collection(collection_name="my_collection")
import io.milvus.v2.common.IndexParam;
import io.milvus.v2.service.collection.request.LoadCollectionReq;
import io.milvus.v2.service.index.request.CreateIndexReq;

import java.util.Collections;

IndexParam indexParam = IndexParam.builder()
        .fieldName("embedding")
        .indexName("embedding_index")
        .indexType(IndexParam.IndexType.AUTOINDEX)
        .metricType(IndexParam.MetricType.COSINE)
        .build();

client.createIndex(CreateIndexReq.builder()
        .collectionName("my_collection")
        .indexParams(Collections.singletonList(indexParam))
        .build());

client.loadCollection(LoadCollectionReq.builder()
        .collectionName("my_collection")
        .build());
await client.createIndex({
  collection_name: "my_collection",
  field_name: "embedding",
  index_name: "embedding_idx",
  index_type: "AUTOINDEX",
  metric_type: "COSINE",
});

await client.loadCollection({
  collection_name: "my_collection",
});
import (
    "context"
    "fmt"

    "github.com/milvus-io/milvus/client/v2/entity"
    "github.com/milvus-io/milvus/client/v2/index"
    "github.com/milvus-io/milvus/client/v2/milvusclient"
)

// Assumes `client` is the Milvus client from the Go schema example above.
ctx := context.Background()

indexOption := milvusclient.NewCreateIndexOption("my_collection", "embedding",
    index.NewAutoIndex(entity.COSINE))

_, err := client.CreateIndex(ctx, indexOption)
if err != nil {
    fmt.Println(err.Error())
}

_, err = client.LoadCollection(ctx, milvusclient.NewLoadCollectionOption("my_collection"))
if err != nil {
    fmt.Println(err.Error())
}
curl --request POST \
  --url "${CLUSTER_ENDPOINT}/v2/vectordb/indexes/create" \
  --header "Authorization: Bearer ${TOKEN}" \
  --header "Content-Type: application/json" \
  --header "Request-Timeout: 10" \
  -d '{
    "collectionName": "my_collection",
    "indexParams": [
      {
        "fieldName": "embedding",
        "metricType": "COSINE",
        "indexType": "AUTOINDEX"
      }
    ]
  }'

curl --request POST \
  --url "${CLUSTER_ENDPOINT}/v2/vectordb/collections/load" \
  --header "Authorization: Bearer ${TOKEN}" \
  --header "Content-Type: application/json" \
  --header "Request-Timeout: 10" \
  -d '{"collectionName": "my_collection"}'

此时:

  • 具有有效Embeddings值的实体已被索引,并可供搜索。
  • 嵌入值为NULL的实体仍保留在Collection中,但不会被纳入向量索引。

可为空字段的搜索行为

当您对可为空字段执行搜索操作时,Milvus 仅评估该字段值不为空的实体。向量字段为 NULL 的实体将被自动跳过。

以本例中的embedding 等可为空的向量字段为例:

  • 只有具有有效向量值的实体才会被评估和排序。
  • 向量值为NULL的实体不会引发错误。
  • 如果有效向量的数量少于请求的topKlimit ),Milvus返回的结果可能少于limit

以下示例对可为空的向量字段embedding 执行向量搜索:

res = client.search(
    collection_name="my_collection",
    data=[[0.1, 0.2, 0.3, 0.4]],
    anns_field="embedding",
    limit=3,
    search_params={"metric_type": "COSINE"},
    output_fields=["embedding"],
)

print(res)
import io.milvus.v2.service.vector.request.SearchReq;
import io.milvus.v2.service.vector.request.data.FloatVec;
import io.milvus.v2.service.vector.response.SearchResp;

import java.util.Arrays;
import java.util.Collections;

SearchResp res = client.search(SearchReq.builder()
        .collectionName("my_collection")
        .data(Collections.singletonList(new FloatVec(Arrays.asList(0.1f, 0.2f, 0.3f, 0.4f))))
        .annsField("embedding")
        .limit(3)
        .outputFields(Collections.singletonList("embedding"))
        .build());

System.out.println(res);
const res = await client.search({
  collection_name: "my_collection",
  data: [[0.1, 0.2, 0.3, 0.4]],
  anns_field: "embedding",
  limit: 3,
  search_params: { metric_type: "COSINE" },
  output_fields: ["embedding"],
});

console.log(res);
import (
    "context"
    "fmt"

    "github.com/milvus-io/milvus/client/v2/entity"
    "github.com/milvus-io/milvus/client/v2/milvusclient"
)

// Assumes `client` is the Milvus client from the Go schema example above.
ctx := context.Background()

query := []float32{0.1, 0.2, 0.3, 0.4}
resultSets, err := client.Search(ctx, milvusclient.NewSearchOption(
    "my_collection",
    3,
    []entity.Vector{entity.FloatVector(query)},
).WithANNSField("embedding").
    WithOutputFields("embedding"))
if err != nil {
    fmt.Println(err.Error())
}
fmt.Println(resultSets)
curl --request POST \
  --url "${CLUSTER_ENDPOINT}/v2/vectordb/entities/search" \
  --header "Authorization: Bearer ${TOKEN}" \
  --header "Content-Type: application/json" \
  --header "Request-Timeout: 10" \
  -d '{
    "collectionName": "my_collection",
    "data": [[0.1, 0.2, 0.3, 0.4]],
    "annsField": "embedding",
    "limit": 3,
    "searchParams": {"metricType": "COSINE"},
    "outputFields": ["embedding"]
  }'

在此搜索中:

  • 仅将embedding 值不为空的实体视为候选对象。
  • embedding 值为 NULL 的实体将被排除在评估之外。
  • 返回的结果数量取决于Collection中存在多少个有效的向量。

查询与过滤的注意事项

前面的示例侧重于向量字段。本节将说明 NULL 值在标量过滤表达式中的行为。

标量字段可以使用nullable=True 进行定义,并遵循与向量字段相同的摄入规则。但是,在过滤表达式中NULL 标量值始终被评估为 false

例如,假设有一个可为空的标量字段age ,以下过滤器将筛选出年龄大于 18 岁的实体:

expr = "age > 18"
String filter = "age > 18";
const expr = "age > 18";
filter := "age > 18"
# Use in query/search filter parameter, for example:
# "filter": "age > 18"

age 为 NULL 的实体将被排除在结果之外,因为 NULL 值不符合过滤条件。

同样地,等值检查也不会匹配 NULL 值。例如:

expr = 'status == "active"'
String filter = "status == \"active\"";
const expr = 'status == "active"';
filter := `status == "active"`
# "filter": "status == \"active\""

status 为 NULL 的实体将被排除在结果之外。

可为空字段与默认值

当某个字段同时配置了nullabledefault_value 时,以下规则将决定Milvus在插入过程中如何处理NULL输入或缺失的字段值。

启用可为空默认值用户输入(NULL 或省略)结果
是(非NULL)NULL 或省略使用默认值
NULL 或省略存储为 NULL
是(非NULL)NULL 或省略使用默认值
NULL 或省略抛出错误
是(默认值为NULL)NULL 或省略会引发错误

要点:

  • 当字段具有非NULL默认值时,无论是否启用了nullable ,系统都会使用该默认值。
  • 当启用nullable=True 但未设置默认值时,该字段将存储NULL。
  • 当启用nullable=False 但未设置默认值时,插入操作将失败并抛出错误。
  • 在不可为空字段上设置 NULL 默认值是无效的,并将引发错误。

有关默认值的完整示例和 API 使用方法,请参阅“默认值”

翻译自DeepL

想要更快、更简单、更好用的 Milvus SaaS服务 ?

Zilliz Cloud是基于Milvus的全托管向量数据库,拥有更高性能,更易扩展,以及卓越性价比

免费试用 Zilliz Cloud
反馈

此页对您是否有帮助?