Проектирование модели данных с помощью массива структурCompatible with Milvus 2.6.4+

Современные приложения искусственного интеллекта, особенно в сфере Интернета вещей (IoT) и автономного вождения, обычно работают с богатыми структурированными событиями: показания датчика с временной меткой и векторным вложением, диагностический журнал с кодом ошибки и аудиофрагментом или фрагмент поездки с местоположением, скоростью и контекстом сцены. Все это требует от базы данных встроенной поддержки приема и поиска вложенных данных.

Вместо того чтобы просить пользователя преобразовывать атомарные структурные события в плоские модели данных, Milvus представляет массив структур, где каждая структура в массиве может содержать скаляры и векторы, сохраняя семантическую целостность.

Почему именно массив структур

Современные приложения искусственного интеллекта, от автономного вождения до мультимодального поиска, все чаще опираются на вложенные разнородные данные. Традиционные плоские модели данных с трудом справляются с представлением сложных взаимосвязей, таких как"один документ с множеством аннотированных фрагментов" или"одна сцена вождения с множеством наблюдаемых маневров". Именно в этом случае тип данных Array of Structs в Milvus является идеальным.

Массив структур позволяет хранить упорядоченный набор структурированных элементов, где каждая структура содержит свою собственную комбинацию скалярных полей и векторных вкраплений. Это делает его идеальным для:

  • Иерархических данных: Родительские сущности с множеством дочерних записей, например, книга с большим количеством текстовых фрагментов или видео с большим количеством аннотированных кадров.

  • Мультимодальные вкрапления: Каждая структура может содержать несколько векторов, например, текстовые вкрапления плюс вкрапления изображений, а также метаданные.

  • Временные или последовательные данные: Структуры в поле Array естественным образом представляют временные серии или пошаговые события.

В отличие от традиционных обходных путей, которые хранят JSON блобы или разбивают данные по нескольким коллекциям, массив структур обеспечивает собственное применение схемы, векторную индексацию и эффективное хранение в Milvus.

Рекомендации по проектированию схем

В дополнение ко всем рекомендациям, рассмотренным в разделе "Проектирование модели данных для поиска", перед началом использования массива структур в вашей модели данных вам следует учесть следующие моменты.

Определите схему структуры

Прежде чем добавлять поле Array в коллекцию, определите внутреннюю схему Struct. Каждое поле в структуре должно иметь явный тип, скалярный(VARCHAR, INT, BOOLEAN и т. д.) или векторный(FLOAT_VECTOR).

Рекомендуется сохранять схему Struct компактной, включая в нее только те поля, которые вы будете использовать для поиска или отображения. Избегайте раздувания неиспользуемых метаданных.

Вдумчиво устанавливайте максимальную емкость

Каждое поле Array имеет атрибут, определяющий максимальное количество элементов, которое поле Array может содержать для каждой сущности. Установите это значение, исходя из верхней границы вашего сценария использования. Например, в одном документе может быть 1000 текстовых фрагментов или 100 маневров в одной сцене вождения.

Слишком большое значение приводит к расточительству памяти, и вам придется произвести некоторые вычисления, чтобы определить максимальное количество структур в поле Array.

Индексировать векторные поля в структурах

Индексация обязательна для векторных полей, включая как векторные поля в коллекции, так и поля, определенные в структурах. Для векторных полей в структурах в качестве типа индекса следует использовать AUTOINDEX или HNSW, а в качестве типа метрики - серию MAX_SIM.

Подробные сведения о всех применимых ограничениях см. в разделе Ограничения.

Пример из реального мира: Моделирование набора данных CoVLA для автономного вождения

Набор данных Comprehensive Vision-Language-Action (CoVLA), представленный компанией Turing Motors и принятый на Зимней конференции по применению компьютерного зрения (WACV) 2025 года, представляет собой богатую основу для обучения и оценки моделей Vision-Language-Action (VLA) в автономном вождении. Каждая точка данных, обычно представляющая собой видеоклип, содержит не только необработанные визуальные данные, но и структурированные подписи, описывающие:

  • Поведение эгомобиля (например, "Поворот налево, уступая дорогу встречному транспорту"),

  • обнаруженные объекты (например, ведущие транспортные средства, пешеходы, светофоры) и

  • Надпись на уровне кадра сцены.

Такая иерархическая и мультимодальная природа делает ее идеальным кандидатом для функции Array of Structs. Подробные сведения о наборе данных CoVLA см. на веб-сайте набора данных CoVLA.

Шаг 1: Сопоставление набора данных со схемой коллекции

Набор данных CoVLA - это крупномасштабный мультимодальный набор данных о вождении, состоящий из 10 000 видеоклипов общей продолжительностью более 80 часов. Кадры сэмплируются с частотой 20 Гц, каждый кадр аннотируется подробными подписями на естественном языке, а также информацией о состоянии автомобиля и координатах обнаруженных объектов.

Структура набора данных выглядит следующим образом:

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

Можно заметить, что структура набора данных CoVLA сильно иерархизирована: собранные данные разделены на множество файлов .jsonl, а также видеоклипы в формате .mp4.

В Milvus для создания вложенных структур в схеме коллекции можно использовать либо поле JSON, либо поле Array-of-Structs. Когда векторные вложения являются частью вложенного формата, поддерживается только поле Array-of-Structs. Однако структура внутри массива не может сама содержать другие вложенные структуры. Чтобы хранить набор данных CoVLA, сохраняя важные взаимосвязи, необходимо удалить ненужную иерархию и уплостить данные, чтобы они соответствовали схеме коллекции Milvus.

На диаграмме ниже показано, как мы можем смоделировать этот набор данных, используя схему, приведенную ниже:

Dataset Model Модель набора данных

На приведенной выше диаграмме показана структура видеоклипа, состоящая из следующих полей:

  • video_id служит первичным ключом, который принимает целые числа типа INT64.

  • states необработанное JSON-тело, содержащее состояние эгомобиля в каждом кадре текущего видео.

  • captions Массив структур, каждая из которых имеет следующие поля:

    • frame_id идентифицирует конкретный кадр в текущем видео.

    • plain_caption описание текущего кадра без учета окружающей обстановки, такой как погода, состояние дороги и т. д., а plain_cap_vector - соответствующий вектор вкраплений.

    • rich_caption описание текущего кадра с окружающей средой, а rich_cap_vector - соответствующий вектор вкраплений.

    • risk описание риска, которому подвергается эго-транспортное средство в текущем кадре, и risk_vector - соответствующие векторные вложения, и

    • Все остальные атрибуты кадра, такие как road, weather, is_tunnel, has_pedestrain и т. д..

  • traffic_lights это JSON-тело, содержащее все сигналы светофора, идентифицированные в текущем кадре.

  • front_cars также является массивом структур, который содержит все ведущие автомобили, идентифицированные в текущем кадре.

Шаг 2: Инициализация схем

Для начала нам нужно инициализировать схему для caption Struct, front_cars Struct и коллекции.

  • Инициализируйте схему для структуры 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"
    )
    
  • Инициализируйте схему для структуры Front Car.

    Хотя передний автомобиль не использует векторные вложения, его все равно нужно включить в массив Struct, поскольку размер данных превышает максимальный для поля 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"
    )
    
  • Инициализация схемы для коллекции

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

Шаг 3: Задайте параметры индекса

Все векторные поля должны быть проиндексированы. Чтобы проиндексировать векторные поля в элементе Struct, необходимо использовать AUTOINDEX или HNSW в качестве типа индекса и метрический тип серии MAX_SIM для измерения сходства между списками встраивания.

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

Рекомендуется включить измельчение JSON для полей JSON, чтобы ускорить фильтрацию в этих полях.

Шаг 4: Создание коллекции

После того как схемы и индексы готовы, можно создать целевую коллекцию следующим образом:

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

Шаг 5: Вставка данных

Turing Motos организует набор данных CoVLA в нескольких файлах, включая необработанные видеоклипы (.mp4), штаты (states.jsonl), подписи (captions.jsonl), светофоры (traffic_lights.jsonl) и передние автомобили (front_cars.jsonl).

Необходимо объединить фрагменты данных для каждого видеоклипа из этих файлов и вставить данные. Ниже приведен скрипт для объединения данных для конкретного видеоклипа.

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
}

После соответствующей обработки данных их можно вставить следующим образом:

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

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