Progettazione del modello di dati con un array di struttureCompatible with Milvus 2.6.4+
Le moderne applicazioni di intelligenza artificiale, soprattutto nell'Internet delle cose (IoT) e nella guida autonoma, ragionano tipicamente su eventi ricchi e strutturati: la lettura di un sensore con il suo timestamp e il suo embedding vettoriale, un log diagnostico con un codice di errore e un frammento audio, o un segmento di viaggio con posizione, velocità e contesto della scena. Per questo è necessario che il database supporti in modo nativo l'inserimento e la ricerca di dati annidati.
Invece di chiedere all'utente di convertire gli eventi strutturali atomici in modelli di dati piatti, Milvus introduce l'array di strutture, dove ogni struttura dell'array può contenere scalari e vettori, preservando l'integrità semantica.
Perché gli array di strutture
Le moderne applicazioni di intelligenza artificiale, dalla guida autonoma al recupero multimodale, si basano sempre più su dati eterogenei e annidati. I modelli di dati piatti tradizionali faticano a rappresentare relazioni complesse come"un documento con molti pezzi annotati" o"una scena di guida con più manovre osservate". È qui che il tipo di dati Array of Structs di Milvus si fa notare.
Un array di strutture consente di memorizzare un insieme ordinato di elementi strutturati, dove ogni struttura contiene la propria combinazione di campi scalari e incorporazioni vettoriali. Questo lo rende ideale per:
Dati gerarchici: Entità genitore con più record figlio, come un libro con molti pezzi di testo o un video con molti fotogrammi annotati.
Incorporazione multimodale: Ogni struttura può contenere più vettori, come l'incorporazione di testo e l'incorporazione di immagini, oltre ai metadati.
Dati temporali o sequenziali: Le strutture in un campo Array rappresentano naturalmente serie temporali o eventi graduali.
A differenza delle soluzioni tradizionali che memorizzano blob JSON o dividono i dati in più raccolte, l'Array di strutture offre l'applicazione nativa dello schema, l'indicizzazione dei vettori e l'archiviazione efficiente all'interno di Milvus.
Linee guida per la progettazione degli schemi
Oltre a tutte le linee guida discusse in Progettazione del modello di dati per la ricerca, prima di iniziare a usare un Array di strutture nel modello di dati è necessario considerare anche i seguenti aspetti.
Definire lo schema della struttura
Prima di aggiungere il campo Array alla collezione, occorre definire lo schema interno della struct. Ogni campo della struct deve essere esplicitamente tipizzato, scalare(VARCHAR, INT, BOOLEAN, ecc.) o vettoriale(FLOAT_VECTOR).
Si consiglia di mantenere lo schema Struct snello, includendo solo i campi che si useranno per il recupero o la visualizzazione. Evitare di gonfiare i metadati inutilizzati.
Impostare la capacità massima in modo ponderato
Ogni campo Array ha un attributo che specifica il numero massimo di elementi che il campo Array può contenere per ogni entità. Impostare questo attributo in base al limite superiore del caso d'uso. Ad esempio, ci sono 1.000 pezzi di testo per documento o 100 manovre per scena di guida.
Un valore troppo alto spreca memoria e sarà necessario fare alcuni calcoli per determinare il numero massimo di strutture nel campo Array.
Indicizzare i campi vettoriali nelle strutture
L'indicizzazione è obbligatoria per i campi vettoriali, compresi quelli di una collezione e quelli definiti in una struttura. Per i campi vettoriali in una struttura, si deve usare AUTOINDEX o HNSW come tipo di indice e la serie MAX_SIM come tipo di metrica.
Per i dettagli su tutti i limiti applicabili, fare riferimento ai limiti.
Un esempio reale: Modellare il set di dati CoVLA per la guida autonoma
Il set di dati Comprehensive Vision-Language-Action (CoVLA), introdotto da Turing Motors e accettato alla Winter Conference on Applications of Computer Vision (WACV) 2025, fornisce una ricca base per l'addestramento e la valutazione dei modelli Vision-Language-Action (VLA) nella guida autonoma. Ogni punto di dati, di solito un video clip, contiene non solo input visivi grezzi ma anche didascalie strutturate che descrivono:
Il comportamento del veicolo ego (ad esempio, "accodarsi a sinistra mentre si accosta al traffico in arrivo"),
Gli oggetti rilevati presenti (ad esempio, veicoli in testa, pedoni, semafori).
una didascalia della scena a livello di fotogramma.
Questa natura gerarchica e multimodale lo rende un candidato ideale per la funzione Array of Structs. Per maggiori dettagli sul set di dati CoVLA, consultare il sito web del set di dati CoVLA.
Passo 1: mappare il dataset in uno schema di raccolta
Il dataset CoVLA è un dataset di guida multimodale su larga scala che comprende 10.000 videoclip, per un totale di oltre 80 ore di filmati. Campiona i fotogrammi a una frequenza di 20Hz e annota ogni fotogramma con didascalie dettagliate in linguaggio naturale, oltre a informazioni sullo stato del veicolo e sulle coordinate degli oggetti rilevati.
La struttura del dataset è la seguente:
├── 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
Si può notare che la struttura del dataset CoVLA è altamente gerarchica, dividendo i dati raccolti in più file .jsonl, insieme ai video clip nel formato .mp4.
In Milvus, è possibile utilizzare un campo JSON o un campo Array-of-Structs per creare strutture annidate all'interno di uno schema di raccolta. Quando le incorporazioni vettoriali fanno parte del formato annidato, è supportato solo un campo Array-of-Structs. Tuttavia, una struttura all'interno di un array non può a sua volta contenere altre strutture annidate. Per memorizzare il set di dati CoVLA mantenendo le relazioni essenziali, è necessario rimuovere la gerarchia non necessaria e appiattire i dati in modo che si adattino allo schema della raccolta Milvus.
Il diagramma seguente illustra come possiamo modellare questo set di dati utilizzando lo schema illustrato nello schema seguente:
Modello del set di dati
Il diagramma precedente illustra la struttura di un videoclip, che comprende i seguenti campi:
video_idserve come chiave primaria, che accetta numeri interi di tipo INT64.statesè un corpo JSON grezzo che contiene lo stato del veicolo ego in ogni fotogramma del video corrente.captionsè un array di strutture e ogni struttura ha i seguenti campi:frame_ididentifica un fotogramma specifico all'interno del video corrente.plain_captionè una descrizione del fotogramma corrente senza l'ambiente circostante, come il tempo, le condizioni della strada, ecc. eplain_cap_vectorè il suo corrispondente embedding vettoriale.rich_captionè una descrizione del fotogramma corrente con l'ambiente circostante erich_cap_vectorè il suo corrispondente embedding vettoriale.riskè una descrizione del rischio che il veicolo ego affronta nel fotogramma corrente, erisk_vectorè il suo corrispondente vettore embeddings, eTutti gli altri attributi del frame, come
road,weather,is_tunnel,has_pedestrain, ecc...
traffic_lightsè un corpo JSON che contiene tutti i segnali semaforici identificati nel frame corrente.front_carsè anche un array di strutture che contiene tutte le auto in testa identificate nel frame corrente.
Passo 2: inizializzare gli schemi
Per iniziare, occorre inizializzare gli schemi per una struttura Caption, una struttura Front_cars e la collezione.
Inizializzare lo schema per la struttura 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" )Inizializzare lo schema per la struttura Front Car
Anche se un'auto anteriore non comporta incorporazioni vettoriali, è comunque necessario includerla come array di Struct, perché la dimensione dei dati supera il massimo per un campo 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" )Inizializzare lo schema per la collezione
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" )
Passo 3: Impostare i parametri dell'indice
Tutti i campi vettoriali devono essere indicizzati. Per indicizzare i campi vettoriali in un elemento Struct, è necessario utilizzare AUTOINDEX o HNSW come tipo di indice e il tipo di metrica della serie MAX_SIM per misurare le somiglianze tra gli elenchi di incorporazioni.
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}
)
Si consiglia di abilitare il JSON shredding per i campi JSON, per accelerare il filtraggio all'interno di questi campi.
Passo 4: Creare una raccolta
Una volta che gli schemi e gli indici sono pronti, è possibile creare la raccolta di destinazione come segue:
client.create_collection(
collection_name="covla_dataset",
schema=schema,
index_params=index_params
)
Passo 5: Inserire i dati
Turing Motos organizza il set di dati CoVLA in più file, tra cui i video grezzi (.mp4), gli stati (states.jsonl), le didascalie (captions.jsonl), i semafori (traffic_lights.jsonl) e le auto frontali (front_cars.jsonl).
È necessario unire i dati di ciascun video clip da questi file e inserire i dati. Di seguito è riportato lo script per unire i dati di uno specifico videoclip.
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
}
Una volta elaborati i dati, è possibile inserirli come segue:
client.insert(
collection_name="covla_dataset",
data=[data]
)
# {'insert_count': 1, 'ids': ['0a0fc7a5db365174'], 'cost': 0}