構造体の配列によるデータモデル設計Compatible with Milvus 2.6.4+
現代のAIアプリケーション、特にモノのインターネット(IoT)や自律走行では、リッチで構造化されたイベントを推論するのが一般的だ。例えば、タイムスタンプとベクトル埋め込みを含むセンサーの読み取り値、エラーコードとオーディオスニペットを含む診断ログ、あるいは位置、速度、シーンコンテキストを含むトリップセグメントなどである。これらは、データベースがネスト化されたデータの取り込みと検索をネイティブにサポートする必要があります。
Milvusは、ユーザーにアトミックな構造イベントをフラットなデータモデルに変換するよう求める代わりに、Array of Structsを導入します。Array内の各Structは、セマンティックインテグリティを保持したまま、スカラーやベクトルを保持することができます。
なぜArray of Structsなのか?
自律走行からマルチモーダル検索に至るまで、現代のAIアプリケーションは、ネスト化された異種データにますます依存しています。従来のフラットなデータモデルでは、「1つのドキュメントに多数の注釈が付けられたチャンク」や「1つの運転シーンに複数の観察された操作」といった複雑な関係を表現するのに苦労する。そこでMilvusのArray of Structsデータ型が威力を発揮します。
Array of Structsでは、構造化された要素の順序付きセットを格納することができ、各Structはスカラーフィールドとベクトル埋め込みを独自に組み合わせて格納します。そのため、以下のような用途に最適です:
階層データ:階層的データ:複数の子レコードを持つ親エンティティ。例えば、多くのテキストチャンクを持つ書籍や、多くのアノテーションフレームを持つ動画など。
マルチモーダル埋め込み:各構造体は、メタデータとともに、テキスト埋め込みと画像埋め込みなど、複数のベクトルを保持できる。
時系列データまたはシーケンシャルデータ:ArrayフィールドのStructは、時系列またはステップバイステップのイベントを自然に表現します。
JSON blobを保存したり、複数のコレクションにデータを分割する従来の回避策とは異なり、Array of Structsはスキーマのネイティブなエンフォースメント、ベクトルインデックス、Milvus内での効率的なストレージを提供します。
スキーマ設計ガイドライン
検索のためのデータモデル設計で説明したすべてのガイドラインに加え、データモデル設計でArray of Structsの使用を開始する前に以下のことも考慮する必要があります。
Struct スキーマの定義
コレクションに Array フィールドを追加する前に、内部の Struct スキーマを定義します。構造体の各フィールドは、スカラー(VARCHAR、INT、BOOLEAN など)またはベクトル(FLOAT_VECTOR)で明示的に型付けする必要があります。
Struct スキーマには、検索や表示に使用するフィールドのみを含めるようにして、無駄のないスキーマを維持することをお勧めします。未使用のメタデータで肥大化しないようにしましょう。
最大容量は慎重に設定する
各Arrayフィールドには、各エンティティに対してArrayフィールドが保持できる要素の最大数を指定する属性があります。これは、ユースケースの上限に基づいて設定します。たとえば、ドキュメントごとに1,000個のテキストチャンクがあるとか、運転シーンごとに100個のマニューバーがあるとします。
値が高すぎるとメモリを浪費するので、Array フィールドの Structs の最大数を決定するための計算が必要になります。
ベクトルフィールドのインデックス
コレクション内のベクトルフィールドと Struct 内で定義されたベクトルフィールドの両方を含め、ベクトルフィールドにはインデクシングが必須です。Struct 内のベクトル・フィールドでは、AUTOINDEX またはHNSW をインデックス・タイプとして、MAX_SIM 系列をメトリック・タイプとして使用する必要があります。
適用可能なすべてのリミットの詳細については、リミットを参照してください。
実際の例自律走行のためのCoVLAデータセットのモデリング
Turing Motorsによって導入され、Winter Conference on Applications of Computer Vision (WACV) 2025で採択されたComprehensive Vision-Language-Action (CoVLA)データセットは、自律走行におけるVision-Language-Action (VLA)モデルの訓練と評価のための豊富な基盤を提供する。各データポイント(通常はビデオクリップ)には、生の視覚入力だけでなく、構造化されたキャプションも含まれています:
自車両の行動(例:「対向車に譲りながら左へ合流」)、
検出されたオブジェクト(先行車、歩行者、信号機など)。
シーンのフレームレベルのキャプション。
この階層的でマルチモーダルな性質は、Array of Structs機能の理想的な候補となる。CoVLAデータセットの詳細については、CoVLAデータセットウェブサイトを参照。
ステップ1:データセットをコレクションスキーマにマップする
CoVLAデータセットは、10,000のビデオクリップ、合計80時間以上の映像からなる大規模なマルチモーダル運転データセットである。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データセットの構造は高度に階層化されており、収集されたデータは、.mp4 形式のビデオクリップとともに、複数の.jsonl ファイルに分割されていることがわかる。
Milvusでは、JSONフィールドまたはArray-of-Structsフィールドを使用して、コレクションスキーマ内に入れ子構造を作成することができる。ベクトル埋め込みが入れ子形式の一部である場合、Array-of-Structsフィールドのみがサポートされます。ただし、Array 内の Struct は、それ自体にさらなる入れ子構造を含めることはできない。CoVLAデータセットを本質的な関係を保持したまま保存するためには、不要な階層構造を取り除き、Milvusコレクションスキーマに適合するようにデータを平坦化する必要があります。
下図は、以下のスキーマを使用して、このデータセットをどのようにモデル化できるかを示している:
データセット・モデル
上図はビデオクリップの構造を示しており、以下のフィールドで構成される:
video_idはプライマリキーで、INT64 型の整数を受け入れます。statesは生のJSONボディで、現在の動画の各フレームにおける自車両の状態を含む。captionsは Struct の配列で、各 Struct は以下のフィールドを持つ:frame_idは、現在のビデオ内の特定のフレームを識別します。plain_captionは、天候や道路状況などの周囲環境を含まない現在のフレームの説明であり、plain_cap_vectorはその対応するベクトル埋め込み。rich_captionは周囲環境を含む現在のフレームの説明であり、rich_cap_vectorはその対応するベクトル埋め込みである。riskは現在のフレームで自車両が直面するリスクの記述であり、risk_vectorはその対応するベクトル埋め込みである。road,weather,is_tunnel,has_pedestrain, etc...のようなフレームの他のすべての属性。
traffic_lightsは、現在のフレームで識別されたすべての信号機を含むJSONボディです。front_carsはまた、現在のフレームで識別されたすべての先頭車両を含むArray of Structsです。
ステップ2:スキーマの初期化
手始めに、caption Struct、front_cars Struct、コレクションのスキーマを初期化する必要がある。
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" )Front Car Structのスキーマを初期化する。
フロントカーはベクトル埋め込みを伴いませんが、データサイズが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フィールドの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}