Entwurf eines Datenmodells mit einem Array von StructsCompatible with Milvus 2.6.4+

Moderne KI-Anwendungen, insbesondere im Internet der Dinge (IoT) und beim autonomen Fahren, verarbeiten in der Regel umfangreiche, strukturierte Ereignisse: einen Sensormesswert mit Zeitstempel und Vektoreinbettung, ein Diagnoseprotokoll mit Fehlercode und Audioschnipsel oder einen Fahrtabschnitt mit Standort, Geschwindigkeit und Szenenkontext. Diese erfordern, dass die Datenbank von Haus aus die Aufnahme und Suche von verschachtelten Daten unterstützt.

Anstatt den Benutzer aufzufordern, seine atomaren Strukturereignisse in flache Datenmodelle umzuwandeln, führt Milvus das Array of Structs ein, in dem jedes Struct im Array Skalare und Vektoren enthalten kann, wobei die semantische Integrität erhalten bleibt.

Warum Array of Structs

Moderne KI-Anwendungen, vom autonomen Fahren bis zur multimodalen Suche, stützen sich zunehmend auf verschachtelte, heterogene Daten. Herkömmliche flache Datenmodelle haben Schwierigkeiten, komplexe Beziehungen wie"ein Dokument mit vielen kommentierten Chunks" oder"eine Fahrszene mit mehreren beobachteten Manövern" darzustellen. Hier kommt der Datentyp Array of Structs in Milvus ins Spiel.

Ein Array of Structs ermöglicht es Ihnen, eine geordnete Menge strukturierter Elemente zu speichern, wobei jede Struct ihre eigene Kombination aus skalaren Feldern und Vektoreinbettungen enthält. Dies macht ihn ideal für:

  • Hierarchische Daten: Übergeordnete Entitäten mit mehreren untergeordneten Datensätzen, z. B. ein Buch mit vielen Textabschnitten oder ein Video mit vielen kommentierten Einzelbildern.

  • Multimodale Einbettungen: Jede Struktur kann mehrere Vektoren enthalten, wie z. B. eine Texteinbettung plus eine Bildeinbettung, zusammen mit Metadaten.

  • Zeitliche oder sequenzielle Daten: Strukturen in einem Array-Feld stellen natürlich Zeitserien oder schrittweise Ereignisse dar.

Im Gegensatz zu herkömmlichen Lösungen, die JSON-Blobs speichern oder Daten auf mehrere Sammlungen aufteilen, bietet das Array of Structs eine native Schemaerzwingung, Vektorindizierung und effiziente Speicherung in Milvus.

Richtlinien für das Schema-Design

Zusätzlich zu den Richtlinien, die in Datenmodelldesign für die Suche besprochen wurden, sollten Sie auch die folgenden Dinge berücksichtigen, bevor Sie ein Array of Structs in Ihrem Datenmodelldesign verwenden.

Definieren Sie das Struct-Schema

Bevor Sie das Array-Feld zu Ihrer Sammlung hinzufügen, definieren Sie das innere Struct-Schema. Jedes Feld in der Struct muss explizit typisiert sein, skalar(VARCHAR, INT, BOOLEAN, etc.) oder vektoriell(FLOAT_VECTOR).

Wir empfehlen Ihnen, das Struct-Schema schlank zu halten, indem Sie nur Felder aufnehmen, die Sie für den Abruf oder die Anzeige verwenden. Vermeiden Sie das Aufblähen mit ungenutzten Metadaten.

Legen Sie die maximale Kapazität mit Bedacht fest

Jedes Array-Feld hat ein Attribut, das die maximale Anzahl von Elementen angibt, die das Array-Feld für jede Entität enthalten kann. Legen Sie dies auf der Grundlage der Obergrenze Ihres Anwendungsfalls fest. Zum Beispiel gibt es 1.000 Textbausteine pro Dokument oder 100 Fahrmanöver pro Fahrszene.

Ein zu hoher Wert verschwendet Speicher, und Sie müssen einige Berechnungen durchführen, um die maximale Anzahl von Structs im Feld Array zu bestimmen.

Vektorfelder in Structs indizieren

Die Indizierung ist für Vektorfelder obligatorisch, und zwar sowohl für die Vektorfelder in einer Sammlung als auch für die in einer Struktur definierten Felder. Für Vektorfelder in einer Struktur sollten Sie AUTOINDEX oder HNSW als Index-Typ und MAX_SIM series als metrischen Typ verwenden.

Einzelheiten zu allen anwendbaren Grenzwerten finden Sie in den Grenzwerten.

Ein Beispiel aus der Praxis: Modellierung des CoVLA-Datensatzes für autonomes Fahren

Der von Turing Motors vorgestellte und auf der Winter Conference on Applications of Computer Vision (WACV) 2025 angenommene Comprehensive Vision-Language-Action (CoVLA)-Datensatz bietet eine reichhaltige Grundlage für das Training und die Evaluierung von Vision-Language-Action (VLA)-Modellen beim autonomen Fahren. Jeder Datenpunkt, in der Regel ein Videoclip, enthält nicht nur rohe visuelle Eingaben, sondern auch strukturierte Beschriftungen, die das Verhalten des Ego-Fahrzeugs beschreiben:

  • Das Verhalten des Ego-Fahrzeugs (z. B. "Links abbiegen und dabei dem Gegenverkehr ausweichen"),

  • die erkannten Objekte (z. B. vorausfahrende Fahrzeuge, Fußgänger, Ampeln) und

  • eine Beschriftung der Szene auf Frame-Ebene.

Diese hierarchische, multimodale Natur macht sie zu einem idealen Kandidaten für das Merkmal Array of Structs. Einzelheiten zum CoVLA-Datensatz finden Sie auf der CoVLA-Datensatz-Website.

Schritt 1: Zuordnen des Datensatzes zu einem Sammelschema

Der CoVLA-Datensatz ist ein großer, multimodaler Fahrdatensatz, der 10.000 Videoclips mit insgesamt über 80 Stunden Filmmaterial umfasst. Die Bilder werden mit einer Rate von 20 Hz abgetastet und jedes Bild wird mit detaillierten Beschriftungen in natürlicher Sprache sowie mit Informationen über den Fahrzeugzustand und die Koordinaten der erkannten Objekte versehen.

Die Struktur des Datensatzes ist wie folgt:

├── 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

Die Struktur des CoVLA-Datensatzes ist sehr hierarchisch und unterteilt die gesammelten Daten in mehrere .jsonl Dateien, zusammen mit den Videoclips im .mp4 Format.

In Milvus können Sie entweder ein JSON-Feld oder ein Array-of-Structs-Feld verwenden, um verschachtelte Strukturen innerhalb eines Sammelschemas zu erstellen. Wenn Vektoreinbettungen Teil des verschachtelten Formats sind, wird nur ein Array-of-Structs-Feld unterstützt. Eine Struct innerhalb eines Arrays kann jedoch selbst keine weiteren verschachtelten Strukturen enthalten. Um den CoVLA-Datensatz unter Beibehaltung der wesentlichen Beziehungen zu speichern, müssen Sie unnötige Hierarchien entfernen und die Daten abflachen, damit sie in das Milvus-Sammlungsschema passen.

Das folgende Diagramm veranschaulicht, wie wir diesen Datensatz mit dem im folgenden Schema dargestellten Schema modellieren können:

Dataset Model Dataset-Modell

Das obige Diagramm veranschaulicht die Struktur eines Videoclips, der die folgenden Felder umfasst:

  • video_id dient als Primärschlüssel, der Ganzzahlen vom Typ INT64 akzeptiert.

  • states ist ein roher JSON-Body, der den Zustand des Ego-Fahrzeugs in jedem Frame des aktuellen Videos enthält.

  • captions ist ein Array von Structs, wobei jede Struct die folgenden Felder enthält:

    • frame_id identifiziert ein bestimmtes Bild im aktuellen Video.

    • plain_caption ist eine Beschreibung des aktuellen Frames ohne die Umgebung, wie z.B. Wetter, Straßenzustand, usw., und plain_cap_vector ist die entsprechende Vektoreinbettung.

    • rich_caption ist eine Beschreibung des aktuellen Frames mit der Umgebungsumgebung, und rich_cap_vector ist die entsprechende Vektoreinbettung.

    • risk ist eine Beschreibung des Risikos, dem das Ego-Fahrzeug im aktuellen Frame ausgesetzt ist, und risk_vector ist die entsprechende Vektoreinbettung, und

    • Alle anderen Attribute des Frames, wie road, weather, is_tunnel, has_pedestrain, usw...

  • traffic_lights ist ein JSON-Body, der alle im aktuellen Frame identifizierten Ampelsignale enthält.

  • front_cars ist ebenfalls ein Array von Structs, das alle im aktuellen Frame identifizierten führenden Autos enthält.

Schritt 2: Initialisierung der Schemata

Zu Beginn müssen wir das Schema für eine Caption Struct, eine Front_cars Struct und die Collection initialisieren.

  • Initialisieren Sie das Schema für die Caption Struct.

    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"
    )
    
  • Initialisieren Sie das Schema für die Front Car Struct

    Obwohl ein Front Car keine Vektoreinbettungen enthält, müssen Sie es dennoch als Array von Struct einschließen, da die Datengröße das Maximum für ein JSON-Feld überschreitet.

    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"
    )
    
  • Initialisieren des Schemas für die Sammlung

    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"
    )
    

Schritt 3: Index Parameter setzen

Alle Vektorfelder müssen indiziert werden. Um die Vektorfelder in einem Element Struct zu indizieren, müssen Sie AUTOINDEX oder HNSW als Indextyp und den Metrik-Typ MAX_SIM series verwenden, um die Ähnlichkeiten zwischen Einbettungslisten zu messen.

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}
)

Es wird empfohlen, JSON Shredding für JSON-Felder zu aktivieren, um die Filterung innerhalb dieser Felder zu beschleunigen.

Schritt 4: Erstellen einer Sammlung

Sobald die Schemata und Indizes fertig sind, können Sie die Zielsammlung wie folgt erstellen:

client.create_collection(
    collection_name="covla_dataset",
    schema=schema,
    index_params=index_params
)

Schritt 5: Einfügen der Daten

Turing Motos organisiert den CoVLA-Datensatz in mehreren Dateien, darunter Rohvideoclips (.mp4), Zustände (states.jsonl), Beschriftungen (captions.jsonl), Ampeln (traffic_lights.jsonl) und Vorderwagen (front_cars.jsonl).

Sie müssen die Datenstücke für jeden Videoclip aus diesen Dateien zusammenführen und die Daten einfügen. Im Folgenden finden Sie das Skript zum Zusammenführen der Daten für einen bestimmten 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
}

Nachdem Sie die Daten entsprechend verarbeitet haben, können Sie sie wie folgt einfügen:

client.insert(
    collection_name="covla_dataset",
    data=[data]
)

# {'insert_count': 1, 'ids': ['0a0fc7a5db365174'], 'cost': 0}