可空欄位

Milvus 支援 nullable 欄位,允許欄位值遺失或明確設定為 NULL。可空欄位在模式層級定義,並一致地應用於資料擷取、索引、搜尋和查詢作業。

在下列情況下使用可空欄位

  • 資料從允許缺失值的外部系統擷取。
  • 某些元資料是可選的,或只適用於部分資料集。
  • 向量嵌入以非同步方式產生,並在稍後插入。

限制

  • 允許 NULL 值的向量欄位不支援IS NULL 或IS NOT NULL 過濾表達式。您無法根據向量欄位值是否為 NULL 來明確過濾實體。

  • Structs 陣列欄位不支援 NULL 值。您無法將 Array of Structs 欄位或嵌套在其內的任何欄位標記為 nullable。

  • nullable 屬性是在欄位建立時定義的,之後就無法修改。您無法啟用或停用現有欄位的 nullability。

  • 標記為 nullable 的欄位不能用作分割鍵。分区键字段必须始终包含有效的非空值。如需詳細資訊,請參閱使用分割區金鑰。

什麼是可空欄位?

在 Milvus 中,一個欄位是否允許儲存 NULL 值,是由一個名為nullable 的模式層級欄位屬性所控制。

當一個欄位被定義為nullable=True 時,Milvus 允許在資料擷取過程中,欄位的值是缺失的。實際上,Milvus 將以下兩種輸入視為等同,並將欄位值儲存為 NULL:

  • 欄位從輸入實體中省略。
  • 欄位被明確設定為 NULL (例如,Python 中的None )。

如果欄位未定義為 nullable (預設行為),每個實體都必須提供該欄位的有效值。遺漏欄位或明確指定 NULL 值會導致插入或匯入作業失敗。

集合模式中的標量欄位和向量欄位都支援 nullable 屬性。但是,Array of Structs 欄位不支援 nullable 屬性。

Nullability 決定欄位值是否可能遺失;它並未定義當欄位遺失時使用的值。

  • 如果配置的 nullable 欄位沒有預設值,遺漏欄位會導致儲存 NULL 值。
  • 如果配置了預設值,Milvus 可能會儲存預設值。詳情請參閱預設值。

在集合模式中定義可空字段

要使用可空欄位,必須在定義集合模式時啟用可空屬性。

在這個範例中,集合模式定義了一個向量欄位,名稱為embedding ,其nullable=True 。這允許集合中的實體在資料擷取過程中省略向量值或明確地將其設定為 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;

import java.util.Collections;

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

CreateCollectionReq.CollectionSchema schema = client.createSchema();
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());

CreateCollectionReq requestCreate = CreateCollectionReq.builder()
        .collectionName("my_collection")
        .collectionSchema(schema)
        .indexParams(Collections.emptyList())
        .build();
client.createCollection(requestCreate);
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" \
  -d "{
    \"collectionName\": \"my_collection\",
    \"schema\": {
      \"fields\": [
        $pkField,
        $embeddingField
      ]
    }
  }"

在此模式中:

  • embedding 欄位明確地標記為 nullable。
  • 實體可省略embedding 欄位,或在插入時為其指定 NULL 值。
  • 允許 NULL 值的決定是在集合建立時固定的。

為了清楚起見,以下的範例著重於可宣告無效的向量欄位 (embedding)。定義 nullable 標量欄位是可選的,不需要遵循本指南的其他部分。

可選:定義空標量欄位

標量欄位也可以使用相同的nullable 屬性定義為 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 值的插入行為

一旦在集合模式中將欄位定義為 nullable,Milvus 允許在資料擷取過程中,欄位值會遺失或明確設定為 NULL。

以下範例將三個實體插入在集合模式中定義可為空欄位所建立的集合中,展示這些不同的情況。

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.JsonObject;
import io.milvus.v2.service.vector.request.InsertReq;
import io.milvus.v2.service.vector.response.InsertResp;

import java.util.ArrayList;
import java.util.List;

List<JsonObject> rows = new ArrayList<>();
Gson gson = new Gson();
rows.add(gson.fromJson("{\"id\": 1, \"embedding\": [0.1, 0.2, 0.3, 0.4]}", JsonObject.class));
rows.add(gson.fromJson("{\"id\": 2, \"embedding\": null}", JsonObject.class));
rows.add(gson.fromJson("{\"id\": 3}", JsonObject.class));

InsertResp insertR = client.insert(InsertReq.builder()
        .collectionName("my_collection")
        .data(rows)
        .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" \
  -d '{
    "collectionName": "my_collection",
    "data": [
      {"id": 1, "embedding": [0.1, 0.2, 0.3, 0.4]},
      {"id": 2, "embedding": null},
      {"id": 3}
    ]
  }'

在這個範例中

  • 實體id = 1提供一個有效的向量值。
  • Entityid = 2明確地指定一個 NULL 值給embedding 欄位。
  • Entityid = 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.ArrayList;
import java.util.List;

List<IndexParam> indexes = new ArrayList<>();
indexes.add(IndexParam.builder()
        .fieldName("embedding")
        .indexName("embedding_idx")
        .indexType(IndexParam.IndexType.AUTOINDEX)
        .metricType(IndexParam.MetricType.COSINE)
        .build());

client.createIndex(CreateIndexReq.builder()
        .collectionName("my_collection")
        .indexParams(indexes)
        .build());

LoadCollectionReq loadReq = LoadCollectionReq.builder()
        .collectionName("my_collection")
        .build();
client.loadCollection(loadReq);
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" \
  -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" \
  -d '{"collectionName": "my_collection"}'

此時

  • 具有有效 embedding 值的實體已建立索引並可供搜尋。
  • 嵌入值為 NULL 的實體仍保留在集合中,但它們不會包含在向量索引中。

可為空欄位的搜尋行為

當您在可空欄位執行搜尋作業時,Milvus 只會評估搜尋中使用的欄位具有非空值的實體。向量欄位為 NULL 的實體會自動跳過。

對於可為空的向量欄位,例如本範例中的embedding :

  • 只有具有有效向量值的實體才會被評估和排序。
  • 具有 NULL 向量的實體不會造成錯誤。
  • 如果有效向量的數量小於要求的topK (limit),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.Collections;
import java.util.HashMap;
import java.util.Map;

Map<String, Object> searchParams = new HashMap<>();
searchParams.put("metric_type", "COSINE");

SearchResp resp = client.search(SearchReq.builder()
        .collectionName("my_collection")
        .annsField("embedding")
        .data(Collections.singletonList(new FloatVec(new float[]{0.1f, 0.2f, 0.3f, 0.4f})))
        .topK(3)
        .searchParams(searchParams)
        .outputFields(Collections.singletonList("embedding"))
        .build());

System.out.println(resp.getSearchResults());
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" \
  -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 值的實體將被排除在評估之外。
  • 返回結果的數量取決於集合中存在多少個有效向量。

查詢和篩選的影響

前面的範例著重於向量欄位。本節說明 NULL 值在標量篩選表達式中的表現。

標量欄位可以使用nullable=True 定義,並遵循與向量欄位相同的擷取規則。然而,NULL 標量值在篩選表達式中總是求值為 false。

例如,給定一個可為空的標量欄位age ,以下的篩選器會選擇年齡大於 18 歲的實體:

expr = "age > 18"
String expr = "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 expr = "status == \"active\"";
const expr = 'status == "active"';
filter := `status == "active"`
# "filter": "status == \"active\""

status 為 NULL 的實體會從結果中剔除。

可歸空欄位及預設值

當nullable 和default_value 都設定為欄位時,以下規則決定 Milvus 在插入時如何處理 NULL 輸入或遺漏的欄位值。

Nullable 已啟用預設值使用者輸入 (NULL 或省略)結果
是是 (非 NULL)NULL 或省略使用預設值
是無NULL 或省略儲存為 NULL
無是(非 NULL)NULL 或省略使用預設值
無無NULL 或省略產生錯誤
無是 (NULL 預設)NULL 或省略產生錯誤

主要啟示:

  • 當欄位具有非 NULL 預設值時,不論是否啟用nullable ,都會使用該值。
  • 當nullable=True 但未設定預設值時,欄位會儲存 NULL。
  • 當nullable=False 但未設定預設值時,插入會出錯失敗。
  • 在非空欄位上設定 NULL 預設值是無效的,並會造成錯誤。

有關預設值的完整範例和 API 使用,請參閱預設值。