可為空欄位

Milvus 支援可為空欄位,這意味著欄位值可以為空,或明確設定為 NULL。可為空性是在資料模型層級定義的,並會一致地套用至資料匯入、建立索引、搜尋及查詢等操作中。

在以下情況下應使用可為空欄位:

  • 從允許缺失值的外部系統匯入資料時。
  • 部分元資料為可選項,或僅適用於資料集的一部分。
  • 向量嵌入是異步產生並於稍後插入。

限制

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

  • 自 Milvus 3.0.0 起,父級StructArray欄位可設定為可為 NULL。請在父級 StructArray 欄位上設定nullable=True ,而非在個別子欄位上設定。NULL 適用於整個 StructArray 欄位,而非個別的 Struct 元素,且 Milvus 會內部將父級的可為 NULL 屬性傳遞至其子欄位。 新增至現有集合的 StructArray 欄位必須為可為 NULL,以便現有實體能針對該新欄位返回 NULL。詳細資訊請參閱StructArray 限制

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

  • 標記為可為空的欄位無法用作分區鍵。分區鍵欄位必須始終包含有效且非空的值。如需更多資訊,請參閱《使用分區鍵》。

何謂可為空欄位?

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

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

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

若未將欄位定義為可為空(此為預設行為),則每個實體都必須為該欄位提供有效值。省略該欄位或明確賦予 NULL 值,將導致插入或匯入操作失敗。

在集合模式中,標量欄位和向量欄位均支援 nullable 屬性。自 Milvus 3.0.0 起,此屬性亦支援於父級 StructArray 欄位。請勿獨立將 Struct 子欄位設定為可為 NULL;應在 StructArray 父級定義可為 NULL 屬性,Milvus 會內部將此設定傳播至其子欄位。

可為空性決定欄位值是否可以缺失;它並未定義當欄位缺失時會使用何種值。

  • 若可為空欄位未設定預設值,則省略該欄位將導致儲存為 NULL 值。
  • 若已設定預設值,Milvus 可能會改為儲存該預設值。詳細資訊請參閱「預設值」。

在集合模式中定義可為空欄位

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

在此範例中,集合模式定義了一個名為 `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;

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
      ]
    }
  }"

在此模式中:

  • embedding 欄位已明確標記為可為空。
  • 實體在插入時可省略embedding 欄位,或將其賦予 NULL 值。
  • 是否允許 NULL 值的設定,是在建立集合時即已確定。

為求清晰起見,以下範例將聚焦於可為空的向量欄位(embedding )。定義可為空的標量欄位屬可選操作,且無須遵循本指南其餘內容。

可選:定義可為 NULL 的標量欄位

標量欄位亦可透過相同的 `nullable ` 屬性定義為可為 NULL,並在資料導入過程中遵循相同的規則。例如:

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 時的插入行為

一旦在集合模式中將欄位定義為可為空,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.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。

可為 NULL 欄位的索引行為

插入資料後,您可以照常在可為 NULL 的欄位上建立索引。主要差異在於 Milvus 在建立索引時如何處理 NULL 值:

  • 僅會將具有非 NULL 值的實體加入索引。
  • 具有 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"}'

此時:

  • 具有有效嵌入向量值的實體已建立索引,並可進行搜尋。
  • 嵌入值為 NULL 的實體仍保留在集合中,但不會被納入向量索引。

可為空欄位的搜尋行為

當您對可為空欄位執行搜尋操作時,Milvus 僅會評估該搜尋欄位值不為空的實體。向量欄位為 NULL 的實體會自動被跳過。

以本例中的embedding 為例,此為可為 NULL 的向量欄位:

  • 僅會評估並對具有有效向量值的實體進行排序。
  • 向量為 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 的實體將被排除在評估之外。
  • 回傳結果的數量取決於集合中存在多少個有效的向量。

查詢與篩選的注意事項

前面的範例主要聚焦於向量欄位。本節將說明 NULL 值在標量篩選表達式中的行為。

標量欄位可以定義為nullable=True ,並遵循與向量欄位相同的導入規則。然而,在篩選表達式中,NULL 標量值始終被評估為 false

例如,假設有一個可為 NULL 的標量欄位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"

由於 NULL 值不符合篩選條件,因此age 為 NULL 的實體將被排除在結果之外。

同樣地,等值檢查也不會與 NULL 值匹配。例如:

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

status 為 NULL 的實體將從結果中排除。

可為 NULL 的欄位與預設值

當某個欄位同時配置了 `nullable ` 與 `default_value ` 時,以下規則將決定 Milvus 在插入資料時如何處理 `NULL` 輸入或缺失的欄位值。

啟用可為 NULL 設定預設值使用者輸入(NULL 或省略)結果
是(非 NULL)NULL 或省略使用預設值
NULL 或省略儲存為 NULL
是(非 NULL)NULL 或省略使用預設值
NULL 或省略拋出錯誤
是(預設為 NULL)NULL 或省略會拋出錯誤

重點摘要:

  • 當欄位設有非 NULL 預設值時,無論是否啟用 `nullable `,系統都會使用該預設值。
  • 當啟用「nullable=True 」但未設定預設值時,該欄位將儲存 NULL。
  • 當啟用「nullable=False 」但未設定預設值時,插入操作會失敗並產生錯誤。
  • 在不可為 NULL 的欄位上設定 NULL 預設值是無效的,並會引發錯誤。

有關預設值的完整範例和 API 使用方式,請參閱《預設值》