可為空欄位
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 的實體不會引發錯誤。
- 若有效向量數量少於請求的
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.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 使用方式,請參閱《預設值》。