Conception d'un modèle de données avec un tableau de structuresCompatible with Milvus 2.6.4+
Les applications modernes d'IA, en particulier dans l'Internet des objets (IoT) et la conduite autonome, raisonnent généralement sur des événements riches et structurés : une lecture de capteur avec son horodatage et son intégration vectorielle, un journal de diagnostic avec un code d'erreur et un extrait audio, ou un segment de voyage avec l'emplacement, la vitesse et le contexte de la scène. Pour cela, la base de données doit prendre en charge de manière native l'ingestion et la recherche de données imbriquées.
Au lieu de demander à l'utilisateur de convertir ses événements structurels atomiques en modèles de données plats, Milvus introduit le tableau de structures, où chaque structure du tableau peut contenir des scalaires et des vecteurs, tout en préservant l'intégrité sémantique.
Pourquoi des tableaux de structures ?
Les applications modernes d'IA, de la conduite autonome à la recherche multimodale, s'appuient de plus en plus sur des données imbriquées et hétérogènes. Les modèles de données plats traditionnels peinent à représenter des relations complexes telles que"un document avec de nombreux morceaux annotés" ou"une scène de conduite avec de multiples manœuvres observées". C'est là que le type de données Array of Structs de Milvus se distingue.
Un tableau de structures vous permet de stocker un ensemble ordonné d'éléments structurés, où chaque structure contient sa propre combinaison de champs scalaires et d'intégrations vectorielles. Ce type de données est donc idéal pour
lesdonnées hiérarchiques: Entités mères avec plusieurs enregistrements enfants, comme un livre avec de nombreux morceaux de texte, ou une vidéo avec de nombreuses images annotées.
Encastrements multimodaux: Chaque structure peut contenir plusieurs vecteurs, tels que l'intégration de textes et d'images, ainsi que des métadonnées.
Données temporelles ou séquentielles: Les structures d'un champ Array représentent naturellement des séries temporelles ou des événements progressifs.
Contrairement aux solutions traditionnelles qui stockent des blobs JSON ou répartissent les données sur plusieurs collections, le tableau de structures permet l'application native du schéma, l'indexation des vecteurs et un stockage efficace dans Milvus.
Directives de conception des schémas
En plus de toutes les directives discutées dans Conception du modèle de données pour la recherche, vous devez également prendre en compte les éléments suivants avant de commencer à utiliser un tableau de structures dans la conception de votre modèle de données.
Définir le schéma de la structure
Avant d'ajouter le champ Array à votre collection, définissez le schéma interne de la structure. Chaque champ de la structure doit être explicitement typé, scalaire(VARCHAR, INT, BOOLEAN, etc.) ou vectoriel(FLOAT_VECTOR).
Il est conseillé d'alléger le schéma de la structure en n'incluant que les champs que vous utiliserez pour l'extraction ou l'affichage. Évitez de gonfler le schéma avec des métadonnées inutilisées.
Définissez la capacité maximale de manière réfléchie
Chaque champ de tableau possède un attribut qui spécifie le nombre maximum d'éléments que le champ de tableau peut contenir pour chaque entité. Définissez cette capacité en fonction de la limite supérieure de votre cas d'utilisation. Par exemple, il y a 1 000 morceaux de texte par document, ou 100 manœuvres par scène de conduite.
Une valeur trop élevée entraîne un gaspillage de mémoire et vous devrez effectuer des calculs pour déterminer le nombre maximal de structures dans le champ Array.
Indexer les champs vectoriels dans les structures
L'indexation est obligatoire pour les champs vectoriels, y compris les champs vectoriels d'une collection et ceux définis dans une structure. Pour les champs vectoriels d'une structure, vous devez utiliser AUTOINDEX ou HNSW comme type d'index et la série MAX_SIM comme type de métrique.
Pour plus de détails sur toutes les limites applicables, reportez-vous aux limites.
Un exemple concret : Modélisation de l'ensemble de données CoVLA pour la conduite autonome
Le jeu de données CoVLA (Comprehensive Vision-Language-Action), présenté par Turing Motors et accepté à la Winter Conference on Applications of Computer Vision (WACV) 2025, constitue une base solide pour l'entraînement et l'évaluation de modèles Vision-Langage-Action (VLA) dans le domaine de la conduite autonome. Chaque point de données, qui est généralement un clip vidéo, contient non seulement des données visuelles brutes, mais aussi des légendes structurées décrivant :
Les comportements du véhicule ego (par exemple, "Fusionner à gauche en cédant le passage au trafic venant en sens inverse"),
les objets détectés présents (par exemple, les véhicules de tête, les piétons, les feux de signalisation), et
une légende de la scène au niveau de l'image.
Cette nature hiérarchique et multimodale en fait un candidat idéal pour la fonction "tableau de structures". Pour plus de détails sur l'ensemble de données CoVLA, consultez le site Web de l'ensemble de données CoVLA.
Etape 1 : Mapper l'ensemble de données dans un schéma de collection
L'ensemble de données CoVLA est un ensemble de données de conduite multimodales à grande échelle comprenant 10 000 clips vidéo, soit un total de plus de 80 heures de séquences. Il échantillonne les images à une fréquence de 20 Hz et annote chaque image avec des légendes détaillées en langage naturel ainsi que des informations sur l'état des véhicules et les coordonnées des objets détectés.
La structure de l'ensemble de données est la suivante :
├── video_1 (VIDEO) # video.mp4
│ ├── video_id (INT)
│ ├── video_url (STRING)
│ ├── frames (ARRAY)
│ │ ├── frame_1 (STRUCT)
│ │ │ ├── caption (STRUCT) # captions.jsonl
│ │ │ │ ├── plain_caption (STRING)
│ │ │ │ ├── rich_caption (STRING)
│ │ │ │ ├── risk (STRING)
│ │ │ │ ├── risk_correct (BOOL)
│ │ │ │ ├── risk_yes_rate (FLOAT)
│ │ │ │ ├── weather (STRING)
│ │ │ │ ├── weather_rate (FLOAT)
│ │ │ │ ├── road (STRING)
│ │ │ │ ├── road_rate (FLOAT)
│ │ │ │ ├── is_tunnel (BOOL)
│ │ │ │ ├── is_tunnel_yes_rate (FLOAT)
│ │ │ │ ├── is_highway (BOOL)
│ │ │ │ ├── is_highway_yes_rate (FLOAT)
│ │ │ │ ├── has_pedestrain (BOOL)
│ │ │ │ ├── has_pedestrain_yes_rate (FLOAT)
│ │ │ │ ├── has_carrier_car (BOOL)
│ │ │ ├── traffic_light (STRUCT) # traffic_lights.jsonl
│ │ │ │ ├── index (INT)
│ │ │ │ ├── class (STRING)
│ │ │ │ ├── bbox (LIST<FLOAT>)
│ │ │ ├── front_car (STRUCT) # front_cars.jsonl
│ │ │ │ ├── has_lead (BOOL)
│ │ │ │ ├── lead_prob (FLOAT)
│ │ │ │ ├── lead_x (FLOAT)
│ │ │ │ ├── lead_y (FLOAT)
│ │ │ │ ├── lead_speed_kmh (FLOAT)
│ │ │ │ ├── lead_a (FLOAT)
│ │ ├── frame_2 (STRUCT)
│ │ ├── ... (STRUCT)
│ │ ├── frame_n (STRUCT)
├── video_2
├── ...
├── video_n
Vous pouvez constater que la structure de l'ensemble de données CoVLA est très hiérarchique, divisant les données collectées en plusieurs fichiers .jsonl, ainsi que les clips vidéo au format .mp4.
Dans Milvus, vous pouvez utiliser un champ JSON ou un champ Array-of-Structs pour créer des structures imbriquées dans un schéma de collection. Lorsque les vector embeddings font partie du format imbriqué, seul un champ Tableau de structures est pris en charge. Toutefois, une structure à l'intérieur d'un tableau ne peut pas elle-même contenir d'autres structures imbriquées. Pour stocker l'ensemble de données CoVLA tout en conservant les relations essentielles, vous devez supprimer la hiérarchie inutile et aplatir les données de manière à ce qu'elles correspondent au schéma de la collection Milvus.
Le diagramme ci-dessous illustre la manière dont nous pouvons modéliser cet ensemble de données à l'aide du schéma illustré dans le schéma suivant :
Modèle d'ensemble de données
Le diagramme ci-dessus illustre la structure d'un clip vidéo, qui comprend les champs suivants :
video_idsert de clé primaire, qui accepte les entiers de type INT64.statesest un corps JSON brut qui contient l'état du véhicule ego dans chaque image de la vidéo en cours.captionsest un tableau de structures, chaque structure ayant les champs suivants :frame_ididentifie une image spécifique dans la vidéo en cours.plain_captionest une description de l'image actuelle sans l'environnement ambiant, tel que la météo, l'état de la route, etc., etplain_cap_vectorest le vecteur d'intégration correspondant.rich_captionest une description de l'image actuelle avec l'environnement ambiant, etrich_cap_vectorest le vecteur d'intégration correspondant.riskest une description du risque auquel le véhicule ego est confronté dans l'image actuelle, etrisk_vectorest son vecteur d'intégration correspondant, etTous les autres attributs de l'image, tels que
road,weather,is_tunnel,has_pedestrain, etc...
traffic_lightsest un corps JSON qui contient tous les signaux de feux de circulation identifiés dans la trame actuelle.front_carsest également un tableau de structures qui contient toutes les voitures de tête identifiées dans la trame actuelle.
Étape 2 : Initialisation des schémas
Pour commencer, nous devons initialiser les schémas d'une structure de légende, d'une structure de voitures de tête et de la collection.
Initialiser le schéma de la structure Caption.
client = MilvusClient("http://localhost:19530") # create the schema for the caption struct schema_for_caption = client.create_struct_field_schema() schema_for_caption.add_field( field_name="frame_id", datatype=DataType.INT64, description="ID of the frame to which the ego vehicle's behavior belongs" ) schema_for_caption.add_field( field_name="plain_caption", datatype=DataType.VARCHAR, max_length=1024, description="plain description of the ego vehicle's behaviors" ) schema_for_caption.add_field( field_name="plain_cap_vector", datatype=DataType.FLOAT_VECTOR, dim=768, description="vectors for the plain description of the ego vehicle's behaviors" ) schema_for_caption.add_field( field_name="rich_caption", datatype=DataType.VARCHAR, max_length=1024, description="rich description of the ego vehicle's behaviors" ) schema_for_caption.add_field( field_name="rich_cap_vector", datatype=DataType.FLOAT_VECTOR, dim=768, description="vectors for the rich description of the ego vehicle's behaviors" ) schema_for_caption.add_field( field_name="risk", datatype=DataType.VARCHAR, max_length=1024, description="description of the ego vehicle's risks" ) schema_for_caption.add_field( field_name="risk_vector", datatype=DataType.FLOAT_VECTOR, dim=768, description="vectors for the description of the ego vehicle's risks" ) schema_for_caption.add_field( field_name="risk_correct", datatype=DataType.BOOL, description="whether the risk assessment is correct" ) schema_for_caption.add_field( field_name="risk_yes_rate", datatype=DataType.FLOAT, description="probability/confidence of risk being present" ) schema_for_caption.add_field( field_name="weather", datatype=DataType.VARCHAR, max_length=50, description="weather condition" ) schema_for_caption.add_field( field_name="weather_rate", datatype=DataType.FLOAT, description="probability/confidence of the weather condition" ) schema_for_caption.add_field( field_name="road", datatype=DataType.VARCHAR, max_length=50, description="road type" ) schema_for_caption.add_field( field_name="road_rate", datatype=DataType.FLOAT, description="probability/confidence of the road type" ) schema_for_caption.add_field( field_name="is_tunnel", datatype=DataType.BOOL, description="whether the road is a tunnel" ) schema_for_caption.add_field( field_name="is_tunnel_yes_rate", datatype=DataType.FLOAT, description="probability/confidence of the road being a tunnel" ) schema_for_caption.add_field( field_name="is_highway", datatype=DataType.BOOL, description="whether the road is a highway" ) schema_for_caption.add_field( field_name="is_highway_yes_rate", datatype=DataType.FLOAT, description="probability/confidence of the road being a highway" ) schema_for_caption.add_field( field_name="has_pedestrian", datatype=DataType.BOOL, description="whether there is a pedestrian present" ) schema_for_caption.add_field( field_name="has_pedestrian_yes_rate", datatype=DataType.FLOAT, description="probability/confidence of pedestrian presence" ) schema_for_caption.add_field( field_name="has_carrier_car", datatype=DataType.BOOL, description="whether there is a carrier car present" )Initialiser le schéma de la structure des voitures avant
Bien qu'une voiture avant n'implique pas de vector embeddings, vous devez quand même l'inclure en tant que tableau de Struct car la taille des données dépasse la taille maximale d'un champ JSON.
schema_for_front_car = client.create_struct_field_schema() schema_for_front_car.add_field( field_name="frame_id", datatype=DataType.INT64, description="ID of the frame to which the ego vehicle's behavior belongs" ) schema_for_front_car.add_field( field_name="has_lead", datatype=DataType.BOOL, description="whether there is a leading vehicle" ) schema_for_front_car.add_field( field_name="lead_prob", datatype=DataType.FLOAT, description="probability/confidence of the leading vehicle's presence" ) schema_for_front_car.add_field( field_name="lead_x", datatype=DataType.FLOAT, description="x position of the leading vehicle relative to the ego vehicle" ) schema_for_front_car.add_field( field_name="lead_y", datatype=DataType.FLOAT, description="y position of the leading vehicle relative to the ego vehicle" ) schema_for_front_car.add_field( field_name="lead_speed_kmh", datatype=DataType.FLOAT, description="speed of the leading vehicle in km/h" ) schema_for_front_car.add_field( field_name="lead_a", datatype=DataType.FLOAT, description="acceleration of the leading vehicle" )Initialiser le schéma de la collection
schema = client.create_schema() schema.add_field( field_name="video_id", datatype=DataType.VARCHAR, description="primary key", max_length=16, is_primary=True, auto_id=False ) schema.add_field( field_name="video_url", datatype=DataType.VARCHAR, max_length=512, description="URL of the video" ) schema.add_field( field_name="captions", datatype=DataType.ARRAY, element_type=DataType.STRUCT, struct_schema=schema_for_caption, max_capacity=600, description="captions for the current video" ) schema.add_field( field_name="traffic_lights", datatype=DataType.JSON, description="frame-specific traffic lights identified in the current video" ) schema.add_field( field_name="front_cars", datatype=DataType.ARRAY, element_type=DataType.STRUCT, struct_schema=schema_for_front_car, max_capacity=600, description="frame-specific leading cars identified in the current video" )
Étape 3 : Définir les paramètres d'indexation
Tous les champs vectoriels doivent être indexés. Pour indexer les champs vectoriels dans un élément Struct, vous devez utiliser AUTOINDEX ou HNSW comme type d'index et le type de métrique de la série MAX_SIM pour mesurer les similitudes entre les listes d'inclusion.
index_params = client.prepare_index_params()
index_params.add_index(
field_name="captions[plain_cap_vector]",
index_type="AUTOINDEX",
metric_type="MAX_SIM_COSINE",
index_name="captions_plain_cap_vector_idx", # mandatory for now
index_params={"M": 16, "efConstruction": 200}
)
index_params.add_index(
field_name="captions[rich_cap_vector]",
index_type="AUTOINDEX",
metric_type="MAX_SIM_COSINE",
index_name="captions_rich_cap_vector_idx", # mandatory for now
index_params={"M": 16, "efConstruction": 200}
)
index_params.add_index(
field_name="captions[risk_vector]",
index_type="AUTOINDEX",
metric_type="MAX_SIM_COSINE",
index_name="captions_risk_vector_idx", # mandatory for now
index_params={"M": 16, "efConstruction": 200}
)
Il est conseillé d'activer le déchiquetage JSON pour les champs JSON afin d'accélérer le filtrage dans ces champs.
Étape 4 : Créer une collection
Une fois que les schémas et les index sont prêts, vous pouvez créer la collection cible comme suit :
client.create_collection(
collection_name="covla_dataset",
schema=schema,
index_params=index_params
)
Étape 5 : Insérer les données
Turing Motos organise l'ensemble de données CoVLA en plusieurs fichiers, y compris les clips vidéo bruts (.mp4), les états (states.jsonl), les légendes (captions.jsonl), les feux de circulation (traffic_lights.jsonl) et les voitures de tête (front_cars.jsonl).
Vous devez fusionner les données de chaque clip vidéo de ces fichiers et insérer les données. Le script suivant permet de fusionner les données d'un clip vidéo spécifique.
import json
from openai import OpenAI
openai_client = OpenAI(
api_key='YOUR_OPENAI_API_KEY',
)
video_id = "0a0fc7a5db365174" # represent a single video with 600 frames
# get all front car records in the specified video clip
entries = []
front_cars = []
with open('data/front_car/{}.jsonl'.format(video_id), 'r') as f:
for line in f:
entries.append(json.loads(line))
for entry in entries:
for key, value in entry.items():
value['frame_id'] = int(key)
front_cars.append(value)
# get all traffic lights identified in the specified video clip
entries = []
traffic_lights = []
frame_id = 0
with open('data/traffic_lights/{}.jsonl'.format(video_id), 'r') as f:
for line in f:
entries.append(json.loads(line))
for entry in entries:
for key, value in entry.items():
if not value or (value['index'] == 1 and key != '0'):
frame_id+=1
if value:
value['frame_id'] = frame_id
traffic_lights.append(value)
else:
value_dict = {}
value_dict['frame_id'] = frame_id
traffic_lights.append(value_dict)
# get all captions generated in the video clip and convert them into vector embeddings
entries = []
captions = []
with open('data/captions/{}.jsonl'.format(video_id), 'r') as f:
for line in f:
entries.append(json.loads(line))
def get_embedding(text, model="embeddinggemma:latest"):
response = openai_client.embeddings.create(input=text, model=model)
return response.data[0].embedding
# Add embeddings to each entry
for entry in entries:
# Each entry is a dict with a single key (e.g., '0', '1', ...)
for key, value in entry.items():
value['frame_id'] = int(key) # Convert key to integer and assign to frame_id
if "plain_caption" in value and value["plain_caption"]:
value["plain_cap_vector"] = get_embedding(value["plain_caption"])
if "rich_caption" in value and value["rich_caption"]:
value["rich_cap_vector"] = get_embedding(value["rich_caption"])
if "risk" in value and value["risk"]:
value["risk_vector"] = get_embedding(value["risk"])
captions.append(value)
data = {
"video_id": video_id,
"video_url": "https://your-storage.com/{}".format(video_id),
"captions": captions,
"traffic_lights": traffic_lights,
"front_cars": front_cars
}
Une fois que vous avez traité les données en conséquence, vous pouvez les insérer comme suit :
client.insert(
collection_name="covla_dataset",
data=[data]
)
# {'insert_count': 1, 'ids': ['0a0fc7a5db365174'], 'cost': 0}