Champs pouvant prendre la valeur NULL

Milvus prend en charge les champs pouvant accepter la valeur NULL, ce qui permet qu'une valeur de champ soit manquante ou explicitement définie sur NULL. La possibilité d'accepter la valeur NULL est définie au niveau du schéma et s'applique de manière cohérente lors des opérations d'ingestion, d'indexation, de recherche et de requête.

Utilisez des champs pouvant accepter la valeur NULL lorsque :

  • Les données sont ingérées à partir de systèmes externes autorisant des valeurs manquantes.
  • Certaines métadonnées sont facultatives ou ne sont disponibles que pour une partie de l'ensemble de données.
  • Les représentations vectorielles sont générées de manière asynchrone et insérées ultérieurement.

Restrictions

  • Les champs vectoriels autorisant les valeurs NULL ne prennent pas en charge les expressions de filtrage de type « IS NULL » ou « IS NOT NULL ». Vous ne pouvez pas filtrer explicitement les entités en fonction du fait qu’une valeur de champ vectoriel soit NULL ou non.

  • À partir de Milvus 3.0.0, le champ StructArray parent peut être nullable. Définissez l’option « nullable=True » sur le champ StructArray parent, et non sur les sous-champs individuels. La valeur NULL s’applique à l’ensemble du champ StructArray, et non à un élément Struct individuel, et Milvus propage en interne la nullabilité du champ parent à ses sous-champs. Un champ StructArray ajouté à une collection existante doit être « nullable » afin que les entités existantes puissent renvoyer la valeur NULL pour ce nouveau champ. Pour plus de détails, reportez-vous à la section « Limites de StructArray ».

  • L’attribut « nullable » est défini lors de la création d’un champ et ne peut pas être modifié par la suite. Vous ne pouvez pas activer ou désactiver la nullabilité pour un champ existant.

  • Les champs marqués comme pouvant être nuls ne peuvent pas être utilisés comme clés de partition. Les champs de clé de partition doivent toujours contenir des valeurs valides et non nulles. Pour plus d’informations, reportez-vous à la section Utilisation d’une clé de partition.

Qu’est-ce qu’un champ pouvant accepter la valeur NULL ?

Dans Milvus, la possibilité pour un champ de stocker une valeur NULL est contrôlée par un attribut de champ au niveau du schéma nommé « nullable ».

Lorsqu’un champ est défini avec l’attribut ` nullable=True`, Milvus autorise l’absence de valeur pour ce champ lors de l’ingestion des données. En pratique, Milvus traite les deux entrées suivantes comme équivalentes et stocke la valeur du champ comme NULL :

  • Le champ est omis dans l’entité d’entrée.
  • Le champ est explicitement défini sur NULL (par exemple, ` None ` en Python).

Si un champ n’est pas défini comme pouvant être nul (comportement par défaut), chaque entité doit fournir une valeur valide pour ce champ. L’omission du champ ou l’attribution explicite d’une valeur NULL entraînera l’échec de l’opération d’insertion ou d’importation.

L’attribut « nullable » est pris en charge à la fois pour les champs scalaires et vectoriels dans un schéma de collection. À partir de Milvus 3.0.0, il est également pris en charge sur le champ parent StructArray. Ne configurez pas les sous-champs Struct comme pouvant être nuls de manière indépendante ; définissez la possibilité de valeur nulle sur le champ parent StructArray et Milvus propagera ce paramètre à ses sous-champs en interne.

La nullabilité détermine si la valeur d’un champ peut être manquante ; elle ne définit pas quelle valeur est utilisée lorsqu’un champ est manquant.

  • Si un champ pouvant être nul est configuré sans valeur par défaut, l’omission du champ entraîne le stockage d’une valeur NULL.
  • Si une valeur par défaut est configurée, Milvus peut stocker la valeur par défaut à la place. Pour plus de détails, consultez la section « Valeurs par défaut ».

Définir un champ pouvant être nul dans le schéma de collection

Pour utiliser des champs pouvant être nuls, vous devez activer l’attribut « nullable » lors de la définition du schéma de collection.

Dans cet exemple, le schéma de collection définit un champ vectoriel nommé « embedding » avec la valeur par défaut « nullable=True ». Cela permet aux entités de la collection d’omettre la valeur du vecteur ou de la définir explicitement sur NULL lors de l’ingestion des données.

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

Dans ce schéma :

  • Le champ ` embedding ` est explicitement marqué comme pouvant prendre la valeur NULL.
  • Les entités peuvent omettre le champ « embedding » ou lui attribuer une valeur NULL lors de l’insertion.
  • La décision d’autoriser les valeurs NULL est prise au moment de la création de la collection.

Par souci de clarté, les exemples suivants se concentrent sur un champ vectoriel pouvant prendre la valeur NULL (embedding). La définition de champs scalaires pouvant prendre la valeur NULL est facultative et n’est pas requise pour suivre la suite de ce guide.

Facultatif : définir un champ scalaire pouvant prendre la valeur NULL

Les champs scalaires peuvent également être définis comme pouvant prendre la valeur NULL à l’aide du même attribut ` nullable ` et suivent les mêmes règles lors de l’ingestion. Par exemple :

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 }

Comportement d’insertion en cas de valeurs manquantes ou NULL

Une fois qu’un champ est défini comme pouvant accepter la valeur NULL dans le schéma de collection, Milvus autorise que la valeur du champ soit manquante ou explicitement définie sur NULL lors de l’ingestion des données.

L’exemple ci-dessous insère trois entités dans la collection créée dans la section « Définir un champ pouvant accepter la valeur NULL » du schéma de collection, illustrant ainsi ces différents cas.

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

Dans cet exemple :

  • L'entité id = 1 fournit une valeur vectorielle valide.
  • L'entité id = 2 attribue explicitement une valeur NULL au champ « embedding ».
  • L'entité id = 3 omet complètement le champ « embedding » ; Milvus le stocke alors comme NULL.

Comportement de l'index sur les champs pouvant prendre la valeur NULL

Après l'insertion des données, vous pouvez créer un index sur un champ pouvant contenir des valeurs NULL comme d'habitude. La principale différence réside dans la manière dont Milvus gère les valeurs NULL lors de la création de l'index :

  • Seules les entités dont les valeurs ne sont pas NULL sont ajoutées à l’index.
  • Les entités comportant des valeurs NULL sont ignorées et ne participent pas à la création de l’index.

Pour un champ vectoriel pouvant prendre la valeur NULL, cela signifie que seules les entités comportant des vecteurs valides peuvent être recherchées par similarité vectorielle.

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

À ce stade :

  • Les entités dont les valeurs d'embeddings sont valides sont indexées et prêtes à être recherchées.
  • Les entités dont l'embedding est NULL restent dans la collection, mais elles ne sont pas incluses dans l'index vectoriel.

Comportement de recherche avec des champs pouvant prendre la valeur NULL

Lorsque vous effectuez des opérations de recherche sur un champ pouvant prendre la valeur NULL, Milvus n'évalue que les entités dont la valeur du champ utilisé dans la recherche n'est pas NULL. Les entités dont le champ vectoriel est NULL sont automatiquement ignorées.

Pour un champ vectoriel pouvant prendre la valeur NULL, tel que ` embedding ` dans cet exemple :

  • Seules les entités présentant des valeurs vectorielles valides sont évaluées et classées.
  • Les entités dont le vecteur est NULL ne provoquent pas d’erreurs.
  • Si le nombre de vecteurs valides est inférieur au nombre d’entités requis ( topK (limit), Milvus peut renvoyer moins de résultats que limit.

L'exemple suivant effectue une recherche vectorielle sur le champ vectoriel pouvant contenir des valeurs 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"]
  }'

Dans cette recherche :

  • Seules les entités dont les valeurs embedding ne sont pas nulles sont considérées comme candidates.
  • Les entités dont la valeur « embedding » est nulle sont exclues de l’évaluation.
  • Le nombre de résultats renvoyés dépend du nombre de vecteurs valides présents dans la collection.

Implications en matière de requêtes et de filtrage

Les exemples précédents se concentrent sur les champs vectoriels. Cette section décrit le comportement des valeurs NULL dans les expressions de filtrage scalaires.

Les champs scalaires peuvent être définis avec nullable=True et suivent les mêmes règles d’ingestion que les champs vectoriels. Cependant, les valeurs scalaires NULL sont toujours évaluées à faux dans les expressions de filtrage.

Par exemple, pour un champ scalaire pouvant prendre la valeur NULL age, le filtre suivant sélectionne les entités dont l’âge est supérieur à 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"

Les entités pour lesquelles age est NULL sont exclues des résultats, car une valeur NULL ne satisfait pas à la condition de filtrage.

De même, les comparaisons d’égalité ne correspondent pas aux valeurs NULL. Par exemple :

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

Les entités pour lesquelles « status » est NULL sont exclues des résultats.

Champs pouvant prendre la valeur NULL et valeurs par défaut

Lorsque les paramètres « nullable » et « default_value » sont tous deux configurés pour un champ, les règles suivantes déterminent la manière dont Milvus gère les entrées NULL ou les valeurs de champ manquantes lors de l’insertion.

Valeurs NULL autoriséesValeur par défautSaisie utilisateur (NULL ou omise)Résultat
OuiOui (non NULL)NULL ou omisUtilise la valeur par défaut
OuiNonNULL ou omisEnregistré comme NULL
NonOui (non NULL)NULL ou omisUtilise la valeur par défaut
NonNonNULL ou omisGénère une erreur
NonOui (NULL par défaut)NULL ou omisGénère une erreur

Points clés :

  • Lorsqu'un champ possède une valeur par défaut non NULL, cette valeur est utilisée, que l'option « nullable » soit activée ou non.
  • Lorsque l'nullable=True est activée mais qu'aucune valeur par défaut n'est définie, le champ stocke la valeur NULL.
  • Lorsqu'nullable=False est activé mais qu'aucune valeur par défaut n'est définie, l'insertion échoue et génère une erreur.
  • Définir une valeur par défaut NULL sur un champ non nullable n’est pas valide et provoque une erreur.

Pour des exemples complets et l’utilisation de l’API concernant les valeurs par défaut, consultez la section « Valeurs par défaut ».