널러블 필드
Milvus는 필드 값을 누락하거나 명시적으로 NULL로 설정할 수 있는 nullable 필드를 지원합니다. 무효화 가능성은 스키마 수준에서 정의되며 데이터 수집, 인덱싱, 검색 및 쿼리 작업 전반에 걸쳐 일관되게 적용됩니다.
다음과 같은 경우에 무효화 가능한 필드를 사용합니다:
- 누락된 값을 허용하는 외부 시스템에서 데이터를 수집하는 경우.
- 일부 메타데이터는 선택 사항이거나 데이터 세트의 일부에만 사용할 수 있습니다.
- 벡터 임베딩은 비동기적으로 생성되어 나중에 삽입됩니다.
제한 사항
NULL 값을 허용하는 벡터 필드는
IS NULL또는IS NOT NULL필터 표현식을 지원하지 않습니다. 벡터 필드 값이 NULL인지 여부에 따라 엔티티를 명시적으로 필터링할 수 없습니다.구조체 배열 필드는 NULL 값을 지원하지 않습니다. 구조체 배열 필드 또는 그 안에 중첩된 필드는 null 가능으로 표시할 수 없습니다.
null 가능 속성은 필드를 생성할 때 정의되며 이후에는 수정할 수 없습니다. 기존 필드에 대해서는 무효화 가능성을 활성화 또는 비활성화할 수 없습니다.
무효화 가능으로 표시된 필드는 파티션 키로 사용할 수 없습니다. 파티션 키 필드에는 항상 유효한 null이 아닌 값이 포함되어야 합니다. 자세한 내용은 파티션 키 사용을 참조하세요.
Null 가능 필드란 무엇인가요?
Milvus에서 필드가 NULL 값을 저장할 수 있는지 여부는 nullable 이라는 스키마 수준 필드 속성으로 제어됩니다.
필드가 nullable=True 로 정의된 경우 Milvus는 데이터 수집 중에 필드 값이 누락될 수 있도록 허용합니다. 실제로 Milvus는 다음 두 입력을 동등한 것으로 취급하고 필드 값을 NULL로 저장합니다:
- 입력 엔티티에서 필드가 생략된 경우.
- 필드가 명시적으로 NULL로 설정된 경우(예: Python의 경우
None).
필드가 null 가능으로 정의되지 않은 경우(기본 동작), 모든 엔터티는 해당 필드에 유효한 값을 제공해야 합니다. 필드를 생략하거나 명시적으로 NULL 값을 할당하면 삽입 또는 가져오기 작업이 실패합니다.
null 가능 속성은 컬렉션 스키마의 스칼라 및 벡터 필드 모두에 대해 지원됩니다. 그러나 구조체의 배열 필드는 null 가능 속성을 지원하지 않습니다.
무효화 가능성은 필드 값의 누락 여부를 결정하지만, 필드 누락 시 어떤 값이 사용되는지는 정의하지 않습니다.
- 기본값 없이 null 가능 필드가 구성된 경우 필드를 생략하면 저장된 NULL 값이 생성됩니다.
- 기본값이 구성된 경우 Milvus는 기본값을 대신 저장할 수 있습니다. 자세한 내용은 기본값을 참조하십시오.
컬렉션 스키마에서 null 가능 필드 정의하기
nullable 필드를 사용하려면 컬렉션 스키마를 정의할 때 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필드는 명시적으로 null 가능으로 표시되어 있습니다.- 엔티티는 삽입 중에
embedding필드를 생략하거나 NULL 값을 할당할 수 있습니다. - NULL 값을 허용할지 여부는 컬렉션 생성 시점에 결정됩니다.
명확성을 위해 다음 예제에서는 null 가능한 벡터 필드(embedding)에 초점을 맞춥니다. null 가능 스칼라 필드를 정의하는 것은 선택 사항이며 이 가이드의 나머지 부분을 따르기 위해 반드시 필요한 것은 아닙니다.
선택 사항입니다: 널러블 스칼라 필드 정의하기
스칼라 필드도 동일한 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로 설정될 수 있습니다.
아래 예제에서는 컬렉션 스키마에서 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는
embedding필드에 명시적으로 NULL 값을 할당합니다. - 엔티티 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인 엔티티는 컬렉션에 남아 있지만 벡터 인덱스에는 포함되지 않습니다.
null 가능한 필드를 사용한 검색 동작
null 가능한 필드에서 검색 작업을 수행하면 Milvus는 검색에 사용된 필드에 대해 null이 아닌 값을 가진 엔티티만 평가합니다. 벡터 필드가 NULL인 엔티티는 자동으로 건너뜁니다.
이 예제에서 embedding 와 같은 null 가능한 벡터 필드의 경우:
- 유효한 벡터 값을 가진 엔티티만 평가되고 순위가 매겨집니다.
- NULL 벡터를 가진 엔티티는 오류를 일으키지 않습니다.
- 유효한 벡터의 수가 요청된
topK(limit)보다 작은 경우 Milvus는limit보다 적은 수의 결과를 반환할 수 있습니다.
다음 예제는 null 가능한 벡터 필드 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"]
}'
이 검색에서:
- null이 아닌
embedding값을 가진 엔티티만 후보로 간주됩니다. embedding값이 NULL인 엔티티는 평가에서 제외됩니다.- 반환되는 결과의 수는 컬렉션에 존재하는 유효한 벡터의 수에 따라 달라집니다.
쿼리 및 필터링의 의미
이전 예제에서는 벡터 필드에 중점을 두었습니다. 이 섹션에서는 스칼라 필터 표현식에서 NULL 값이 어떻게 작동하는지 설명합니다.
스칼라 필드는 nullable=True 로 정의할 수 있으며 벡터 필드와 동일한 수집 규칙을 따릅니다. 그러나 필터 표현식에서 NULL 스칼라 값은 항상 거짓으로 평가됩니다.
예를 들어, 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"
age 이 NULL인 엔티티는 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 또는 생략 | 오류를 발생시킵니다. |
| No | 예(기본값은 NULL) | NULL 또는 생략 | 오류를 발생시킵니다. |
핵심 사항:
- 필드에 NULL이 아닌 기본값이 있는 경우
nullable활성화 여부에 관계없이 해당 값이 사용됩니다. nullable=True이지만 기본값이 설정되지 않은 경우 필드에 NULL이 저장됩니다.nullable=False에 기본값이 설정되어 있지 않은 경우 오류와 함께 삽입이 실패합니다.- 널로 설정할 수 없는 필드에 기본값을 NULL로 설정하면 유효하지 않으며 오류가 발생합니다.
기본값에 대한 전체 예제 및 API 사용법은 기본값을 참조하세요.