Нулевые поля

Milvus поддерживает поля с нулевым значением, которые позволяют значению поля отсутствовать или явно задавать значение NULL. Нулевые поля определяются на уровне схемы и последовательно применяются при вводе данных, индексировании, поиске и запросах.

Используйте поля с нулевым значением, когда:

  • Данные поступают из внешних систем, допускающих пропущенные значения.
  • Некоторые метаданные являются необязательными или доступны только для части набора данных.
  • Векторные вложения генерируются асинхронно и вставляются позже.

Ограничения

  • Векторные поля, допускающие значения NULL, не поддерживают выражения фильтрации IS NULL или IS NOT NULL. Вы не можете явно фильтровать сущности на основе того, является ли значение векторного поля NULL.

  • ПоляArray of Structs не поддерживают значения NULL. Вы не можете пометить поле Array of Structs или любое поле, вложенное в него, как nullable.

  • Атрибут nullable определяется при создании поля и не может быть изменен впоследствии. Вы не можете включить или отключить возможность использования нуля для существующего поля.

  • Поля, помеченные как nullable, нельзя использовать в качестве ключей разделов. Поля ключей разделов всегда должны содержать правильные, не нулевые значения. Дополнительные сведения см. в разделе Использование ключа раздела.

Что такое поле с нулевым значением?

В Milvus разрешено ли полю хранить NULL-значение, контролируется атрибутом поля на уровне схемы под названием nullable.

Если поле определено с помощью nullable=True, Milvus позволяет значению поля отсутствовать при вводе данных. На практике Milvus рассматривает следующие два входа как эквивалентные и сохраняет значение поля как NULL:

  • Поле опущено во входной сущности.
  • Поле явно установлено в NULL (например, None в Python).

Если поле не определено как nullable (поведение по умолчанию), каждая сущность должна предоставить корректное значение для этого поля. Пропуск поля или явное присвоение ему значения NULL приведет к неудаче операции вставки или импорта.

Атрибут nullable поддерживается как для скалярных, так и для векторных полей в схеме коллекции. Однако поля Array of Structs не поддерживают атрибут nullable.

Атрибут nullable определяет, может ли значение поля отсутствовать; он не определяет, какое значение будет использоваться, когда поле отсутствует.

  • Если поле с возможностью обнуления сконфигурировано без значения по умолчанию, его отсутствие приводит к сохранению значения 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 явно помечено как nullable.
  • Сущности могут опускать поле embedding или присваивать ему значение NULL при вставке.
  • Решение о разрешении NULL-значений принимается при создании коллекции.

Для наглядности в следующих примерах рассматривается векторное поле с нулевым значением (embedding). Определение нулевых скалярных полей является необязательным и не требуется для выполнения остальной части этого руководства.

Необязательно: Определите нулируемое скалярное поле

Скалярные поля также могут быть определены как 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.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.

Поведение индексов для полей с нулевым значением

После вставки данных вы можете построить индекс для нулевого поля, как обычно. Ключевое различие заключается в том, как 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"}'

На этом этапе:

  • Сущности с действительными значениями вложений индексируются и готовы к поиску.
  • Сущности, чьи вложения имеют значение 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.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 считаются кандидатами.
  • Сущности с NULL-значениями для embedding исключаются из оценки.
  • Количество возвращаемых результатов зависит от того, сколько валидных векторов существует в коллекции.

Последствия запросов и фильтрации

Предыдущие примеры посвящены векторным полям. В этом разделе описывается поведение значений 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, исключаются из результатов.

Нулевые поля и значения по умолчанию

Когда для поля настроены оба параметра 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 для значений по умолчанию см. в разделе Значения по умолчанию.