Campos nulos

Milvus admite campos nulos, lo que permite que el valor de un campo falte o se establezca explícitamente en NULL. La posibilidad de que un campo sea nulo se define a nivel de esquema y se aplica de forma coherente en las operaciones de ingesta de datos, indexación, búsqueda y consulta.

Utiliza campos nulos cuando:

  • Se importan datos desde sistemas externos que permiten valores ausentes.
  • Algunos metadatos son opcionales o solo están disponibles para una parte del conjunto de datos.
  • Las representaciones vectoriales se generan de forma asíncrona y se insertan posteriormente.

Limitaciones

  • Los campos vectoriales que admiten valores NULL no son compatibles con las expresiones de filtro « IS NULL » o « IS NOT NULL ». No es posible filtrar explícitamente entidades en función de si el valor de un campo vectorial es NULL.

  • A partir de Milvus 3.0.0, el campo StructArray principal puede ser nulo. Establece « nullable=True » en el campo StructArray principal, no en los subcampos individuales. El valor NULL se aplica a todo el campo StructArray, no a un elemento Struct individual, y Milvus propaga internamente la posibilidad de ser nulo del campo principal a sus subcampos. Un campo StructArray añadido a una colección existente debe ser nulo para que las entidades existentes puedan devolver NULL en el nuevo campo. Para obtener más detalles, consulta «Límites de StructArray».

  • El atributo de nulidad se define al crear un campo y no se puede modificar posteriormente. No es posible habilitar o deshabilitar la nulidad para un campo ya existente.

  • Los campos marcados como nulos no pueden utilizarse como claves de partición. Los campos de clave de partición deben contener siempre valores válidos y no nulos. Para obtener más información, consulta «Uso de la clave de partición».

¿Qué es un campo nulo?

En Milvus, la posibilidad de que un campo almacene un valor NULL se controla mediante un atributo de campo a nivel de esquema denominado « nullable ».

Cuando un campo se define con ` nullable=True`, Milvus permite que falte el valor del campo durante la ingesta de datos. En la práctica, Milvus trata las dos entradas siguientes como equivalentes y almacena el valor del campo como `NULL`:

  • El campo se omite en la entidad de entrada.
  • El campo se establece explícitamente en NULL (por ejemplo, ` None ` en Python).

Si un campo no se define como nulo (el comportamiento por defecto), cada entidad debe proporcionar un valor válido para ese campo. Omitir el campo o asignarle explícitamente un valor NULL provocará que la operación de inserción o importación falle.

El atributo «nullable» es compatible tanto con campos escalares como con campos vectoriales en un esquema de colección. A partir de Milvus 3.0.0, también es compatible con el campo padre StructArray. No configures los subcampos de Struct como «nullable» de forma independiente; define la «nullability» en el campo padre StructArray y Milvus propagará esa configuración a sus subcampos internamente.

La nulidad determina si puede faltar el valor de un campo; no define qué valor se utiliza cuando falta un campo.

  • Si se configura un campo nulo sin un valor por defecto, al omitir el campo se almacenará un valor NULL.
  • Si se configura un valor por defecto, Milvus puede almacenar dicho valor en su lugar. Para más detalles, consulta «Valores por defecto».

Definir un campo nulo en el esquema de la colección

Para utilizar campos nulos, debe habilitar el atributo «nullable» al definir el esquema de la colección.

En este ejemplo, el esquema de la colección define un campo vectorial denominado « embedding » con « nullable=True ». Esto permite que las entidades de la colección omitan el valor del vector o lo establezcan explícitamente en NULL durante la ingesta de datos.

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

En este esquema:

  • El campo « embedding » está marcado explícitamente como nulo.
  • Las entidades pueden omitir el campo « embedding » o asignarle un valor NULL durante la inserción.
  • La decisión de permitir valores NULL se establece en el momento de la creación de la colección.

Para mayor claridad, los siguientes ejemplos se centran en un campo vectorial nulo (embedding). La definición de campos escalares nulos es opcional y no es necesaria para seguir el resto de esta guía.

Opcional: Definir un campo escalar nulo

Los campos escalares también pueden definirse como nulos utilizando el mismo atributo nullable y siguen las mismas reglas durante la ingesta. Por ejemplo:

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 }

Comportamiento de inserción con valores ausentes o NULL

Una vez que un campo se define como nulo en el esquema de la colección, Milvus permite que el valor del campo falte o se establezca explícitamente como NULL durante la ingesta de datos.

El ejemplo siguiente inserta tres entidades en la colección creada en «Definir un campo nulo en el esquema de la colección», lo que ilustra estos diferentes casos.

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

En este ejemplo:

  • La entidad id = 1 proporciona un valor vectorial válido.
  • La entidad con id = 2 asigna explícitamente un valor NULL al campo « embedding ».
  • La entidad con id = 3 omite por completo el campo « embedding »; Milvus lo almacena como NULL.

Comportamiento del índice en campos que admiten valores nulos

Tras insertar los datos, puedes crear un índice en un campo nulo como de costumbre. La diferencia clave radica en cómo gestiona Milvus los valores NULL durante la creación del índice:

  • Solo se añaden al índice las entidades con valores distintos de NULL.
  • Las entidades con valores NULL se omiten y no participan en la creación del índice.

En el caso de un campo vectorial nulo, esto significa que solo las entidades con vectores válidos pueden buscarse por similitud vectorial.

# 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"}'

En este punto:

  • Las entidades con valores de incrustación válidos se indexan y quedan listas para la búsqueda.
  • Las entidades cuya incrustación es NULL permanecen en la colección, pero no se incluyen en el índice vectorial.

Comportamiento de la búsqueda con campos que admiten valores nulos

Cuando se realizan operaciones de búsqueda en un campo que admite valores nulos, Milvus evalúa únicamente las entidades con valores distintos de nulo para el campo utilizado en la búsqueda. Las entidades cuyo campo vectorial es NULL se omiten automáticamente.

En el caso de un campo vectorial nulo, como « embedding » en este ejemplo:

  • Solo se evalúan y clasifican las entidades con valores vectoriales válidos.
  • Las entidades con vectores NULL no provocan errores.
  • Si el número de vectores válidos es menor que el solicitado topK (limit), Milvus puede devolver menos resultados que limit.

El siguiente ejemplo realiza una búsqueda vectorial en el campo vectorial nulo 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"]
  }'

En esta búsqueda:

  • Solo se consideran candidatas las entidades con valores no nulos para embedding.
  • Las entidades con valores NULL para ` embedding ` quedan excluidas de la evaluación.
  • El número de resultados devueltos depende del número de vectores válidos que existan en la colección.

Implicaciones de las consultas y el filtrado

Los ejemplos anteriores se centran en los campos vectoriales. En esta sección se describe cómo se comportan los valores NULL en las expresiones de filtro escalares.

Los campos escalares pueden definirse con ` nullable=True ` y siguen las mismas reglas de ingestión que los campos vectoriales. Sin embargo, los valores escalares `NULL` siempre se evalúan como «false» en las expresiones de filtro.

Por ejemplo, dado un campo escalar que admite valores NULL age, el siguiente filtro selecciona las entidades cuya edad es superior a 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"

Las entidades en las que age es NULL quedan excluidas de los resultados, ya que un valor NULL no cumple la condición del filtro.

Del mismo modo, las comprobaciones de igualdad no coinciden con los valores NULL. Por ejemplo:

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

Las entidades en las que « status » es NULL se excluyen de los resultados.

Campos nulos y valores predeterminados

Cuando se configuran tanto ` nullable ` como ` default_value ` para un campo, las siguientes reglas determinan cómo gestiona Milvus las entradas NULL o los valores de campo que faltan durante la inserción.

Nullable habilitadoValor por defectoEntrada del usuario (NULL u omitida)Resultado
Sí (no NULL)NULL u omitidoUtiliza el valor por defecto
NoNULL u omitidoSe almacena como NULL
NoSí (no NULL)NULL u omitidoUtiliza el valor por defecto
NoNoNULL u omitidoGenera un error
NoSí (NULL por defecto)NULL u omitidoGenera un error

Puntos clave:

  • Cuando un campo tiene un valor por defecto distinto de NULL, se utiliza ese valor independientemente de si está habilitada la opció nullable.
  • Cuando se establece « nullable=True » pero no se ha definido ningún valor por defecto, el campo almacena NULL.
  • Cuando se habilita la opción « nullable=False » y no se establece ningún valor predeterminado, la inserción falla y se produce un error.
  • Establecer un valor por defecto NULL en un campo no nulo no es válido y provoca un error.

Para ver ejemplos completos y el uso de la API para los valores por defecto, consulta Valores por defecto.