구조 배열을 사용한 데이터 모델 설계Compatible with Milvus 2.6.4+

특히 사물 인터넷(IoT) 및 자율 주행 분야의 최신 AI 애플리케이션은 일반적으로 타임스탬프와 벡터가 포함된 센서 판독값, 오류 코드 및 오디오 스니펫이 포함된 진단 로그, 위치, 속도 및 장면 컨텍스트가 포함된 여행 세그먼트 등 풍부하고 구조화된 이벤트를 통해 추론합니다. 이러한 데이터는 데이터베이스가 중첩된 데이터의 수집 및 검색을 기본적으로 지원해야 합니다.

밀버스는 사용자에게 원자 구조 이벤트를 플랫 데이터 모델로 변환하도록 요청하는 대신, 배열의 각 구조체가 스칼라와 벡터를 보유하여 의미적 무결성을 유지할 수 있는 구조체 배열을 도입했습니다.

구조체 배열이 필요한 이유

자율 주행부터 멀티모달 검색에 이르기까지 최신 AI 애플리케이션은 점점 더 중첩된 이기종 데이터에 의존하고 있습니다. 기존의 평면 데이터 모델로는'주석이 달린 청크가 많은 하나의 문서' 또는'여러 개의 주행 장면이 관찰되는 하나의 주행 장면'과 같은 복잡한 관계를 표현하는 데 어려움을 겪습니다. 바로 이 부분에서 Milvus의 구조체 배열 데이터 유형이 빛을 발합니다.

구조체 배열을 사용하면 각 구조체에 스칼라 필드와 벡터 임베딩의 고유한 조합이 포함된 구조화된 요소의 정렬된 집합을 저장할 수 있습니다. 따라서 다음과 같은 경우에 이상적입니다:

  • 계층적 데이터: 텍스트 청크가 많은 책이나 주석이 달린 프레임이 많은 동영상과 같이 여러 개의 자식 레코드가 있는 상위 엔티티.

  • 멀티모달 임베딩: 각 구조체는 메타데이터와 함께 텍스트 임베딩과 이미지 임베딩과 같은 여러 벡터를 포함할 수 있습니다.

  • 일시적 또는 순차적 데이터: 배열 필드의 구조체는 시계열 또는 단계별 이벤트를 자연스럽게 나타냅니다.

JSON 블롭을 저장하거나 여러 컬렉션에 걸쳐 데이터를 분할하는 기존의 해결 방법과 달리, 구조체 배열은 Milvus 내에서 기본 스키마 적용, 벡터 인덱싱 및 효율적인 스토리지를 제공합니다.

스키마 설계 가이드라인

검색을 위한 데이터 모델 디자인에서 설명한 모든 가이드라인 외에도 데이터 모델 디자인에서 구조체 배열을 사용하기 전에 다음 사항도 고려해야 합니다.

구조체 스키마 정의

컬렉션에 배열 필드를 추가하기 전에 내부 구조체 스키마를 정의합니다. 구조체의 각 필드는 명시적으로 스칼라(VARCHAR, INT, BOOLEAN 등) 또는 벡터(FLOAT_VECTOR) 유형으로 입력해야 합니다.

검색 또는 표시에 사용할 필드만 포함시켜 구조체 스키마를 간결하게 유지하는 것이 좋습니다. 사용하지 않는 메타데이터로 인해 부풀어 오르지 않도록 하세요.

최대 용량을 신중하게 설정하세요.

각 배열 필드에는 각 엔티티에 대해 배열 필드가 담을 수 있는 최대 요소 수를 지정하는 속성이 있습니다. 사용 사례의 상한을 기준으로 이 값을 설정하세요. 예를 들어 문서당 1,000개의 텍스트 청크 또는 운전 장면당 100개의 기동이 있습니다.

값이 지나치게 높으면 메모리가 낭비되므로 배열 필드에서 최대 구조체 수를 결정하기 위해 몇 가지 계산을 수행해야 합니다.

구조체에서 벡터 필드 인덱싱

컬렉션의 벡터 필드와 구조체에 정의된 벡터 필드를 모두 포함하여 벡터 필드에는 인덱싱이 필수입니다. 구조체의 벡터 필드의 경우 색인 유형으로 AUTOINDEX 또는 HNSW 을 사용하고 메트릭 유형으로 MAX_SIM 시리즈를 사용해야 합니다.

적용 가능한 모든 제한에 대한 자세한 내용은 제한을 참조하세요.

실제 예제 자율 주행을 위한 CoVLA 데이터 세트 모델링하기

튜링 모터스가 도입하고 2025년 컴퓨터 비전 응용 동계 컨퍼런스(WACV)에서 채택된 종합 비전-언어-행동(CoVLA) 데이터 세트는 자율 주행에서 비전-언어-행동(VLA) 모델을 훈련하고 평가하기 위한 풍부한 토대를 제공합니다. 일반적으로 비디오 클립인 각 데이터 포인트에는 원시 시각적 입력뿐만 아니라 이를 설명하는 구조화된 캡션도 포함됩니다:

  • 자아 차량의 행동 (예: "마주 오는 차량에 양보하면서 좌회전"),

  • 존재하는 감지된 물체 (예: 선행 차량, 보행자, 신호등) 및

  • 장면의 프레임 수준 캡션.

이러한 계층적, 다중 모달 특성은 구조 배열 기능에 이상적인 후보입니다. CoVLA 데이터 세트에 대한 자세한 내용은 CoVLA 데이터 세트 웹사이트를 참조하세요.

1단계: 데이터 집합을 컬렉션 스키마에 매핑하기

CoVLA 데이터 세트는 총 80시간이 넘는 10,000개의 비디오 클립으로 구성된 대규모의 멀티모달 주행 데이터 세트입니다. 20Hz의 속도로 프레임을 샘플링하고 각 프레임에 차량 상태 및 감지된 물체의 좌표 정보와 함께 상세한 자연어 캡션으로 주석을 달았습니다.

데이터 세트 구조는 다음과 같습니다:

├── 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 필드 또는 구조 배열 필드를 사용하여 컬렉션 스키마 내에서 중첩된 구조를 만들 수 있습니다. 벡터 임베딩이 중첩된 형식의 일부인 경우, 구조체 배열 필드만 지원됩니다. 그러나 배열 내부의 구조체 자체는 중첩된 구조를 더 포함할 수 없습니다. 필수 관계를 유지하면서 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단계: 스키마 초기화

시작하려면 캡션 구조체, front_cars 구조체 및 컬렉션에 대한 스키마를 초기화해야 합니다.

  • 캡션 구조체에 대한 스키마를 초기화합니다.

    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"
    )
    
  • 프론트 카 구조체에 대한 스키마 초기화하기

    앞차는 벡터 임베딩을 포함하지 않지만 데이터 크기가 JSON 필드의 최대 크기를 초과하므로 Struct의 배열로 포함해야 합니다.

    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는 원시 비디오 클립(.mp4), 주(states.jsonl), 캡션(captions.jsonl), 신호등(traffic_lights.jsonl), 앞차(front_cars.jsonl) 등 여러 파일로 CoVLA 데이터 세트를 구성합니다.

이러한 파일에서 각 비디오 클립의 데이터 조각을 병합하고 데이터를 삽입해야 합니다. 다음은 특정 동영상 클립의 데이터 조각을 병합하는 스크립트입니다.

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}