Alibaba Cloud Milvus 2.6 は、書き込み時と検索時に直接モデル推論を実行する AI 関数を提供します。このチュートリアルでは、これらの関数を使用して自動運転のフレームを分析する方法を説明します。1 つのコレクションを作成し、それにいくつかの関数をアタッチしてから、テキストからフレームへの検索、画像からフレームへの検索、コーナーケースマイニングのための構造化フィルタリング、およびマルチモーダル再ランキングを実行します。最終的には、生のフレーム URL を検索可能なベクトルと構造化されたシーンデータに変換する、単一コレクションのパイプラインが完成します。
ソリューション概要
自動運転の開発パイプラインにおいて、車載カメラはデータの最初の入口です。テスト車両は通常、6 台から 12 台のサラウンドビューカメラを搭載し、20~30 FPS で映像を撮影するため、1 台の車両で 1 日の路上テストあたり数テラバイトのビデオが生成されます。このビデオは、認識モデルのトレーニング、事故の再現、計画および制御戦略の反復にとって中心的な資産ですが、巨大で非構造化されており、検索が困難です。
重要なのはビデオクリップそのものではなく、その中で何が起こっているかです。フレームレベルのシーン理解では、交通参加者 (歩行者、非電動車両、先行車)、交通制御要素 (信号機の状態、車線、制限速度標識)、道路環境 (交差点、高速道路、トンネル、工事区域)、および異常イベント (割り込み、信号無視、急ブレーキ、道路上の障害物) を識別する必要があります。これらの結果が構造化データとして永続化されて初めて、以下のシナリオがサポート可能になります。
データリプレイ — 実際の道路フレームをシミュレーションおよびトレーニングパイプラインにリプレイして、認識モデルを反復開発します。
コーナーケースマイニング — 「雨の夜のトンネル入口にある工事区域」のようなロングテールシナリオを、巨大なフレームセットから取得します。これらは、モデルが最も頻繁に失敗し、サンプルが最も少ないケースです。
自動ラベリング — 大量の人間によるバウンディングボックスアノテーションの代わりに、大規模モデルを使用してフレームに大まかなラベルを生成し、レビューと修正を人間に任せます。
路上テストレポート — シーンとイベントごとに統計を集計し、定量化された定期レポートを作成します。
Milvus 2.6 の AI 関数は、これらの機能をベクトルデータベース自体に統合します。別の外部パイプラインを維持する代わりに、モデル推論は Milvus がinsertおよびsearch中に内部的にトリガーする関数として実行されます。このチュートリアルでは、以下の関数を使用します。
| 機能 | コード内の関数タイプ | 目的 | フレーム分析での使用 |
AI_EMBEDDING | FunctionType.TEXTEMBEDDING | qwen3-vl-embedding を使用して、フレーム画像を 2,560 次元のベクトルに変換します。このマルチモーダルモデルは、テキストと画像を同じベクトル空間にマッピングします。 | テキストからフレームへの検索と画像からフレームへの検索をサポートし、「雨の中の工事区域」のようなセマンティックリクエストがベクトル検索によって一致するようにします。 |
AI_CLASSIFY | TEXTTRANSFORM, task="ai_classify" | 事前設定されたラベルセットから各フレームに最も一致するラベルを選択し、それを分類フィールドに書き込みます。 | 各フレームに、交差点、高速道路、トンネル、工事区域などのシーンラベルをタグ付けします。 |
AI_EXTRACT | TEXTTRANSFORM, task="ai_extract" | 指定されたラベルに基づいてフレームから要素を抽出し、それらを JSON として構造化フィールドに書き込みます。 | 正確なフィルタリングのために、信号機の状態、車線数、天候、異常イベントを抽出します。 |
AI_ENTITY_EXTRACT | TEXTTRANSFORM, task="ai_entity_extract" | フレーム内に明示的に表示される名前付きエンティティを認識します。 | 標識から地名、道路名、制限速度の値を抽出します。 |
AI_RERANK | FunctionType.RERANK | qwen3-vl-rerank を使用して、上位 N 件のベクトルリコール結果を再度スコアリングします (オプション)。 | 主題、アクション、シーンの一貫性によって結果を再ランキングし、コーナーケースマイニングのための詳細ランキングを改善します。 |
機能名 (例:AI_EMBEDDING) は概念的に関数を指し、コードでは表に示されている対応する function_type と task の値を使用します。最初の 4 つの関数は書き込み時に自動的に実行され、「書き込み時推論」パイプラインを形成します。AI_RERANK は検索段階に適用され、オプションの詳細ランキング機能強化です。
前提条件
Milvus 2.6 インスタンス。AI 関数には 2.6 カーネルが必要であり、インスタンス作成後に別途モデルサービスをバインドする必要はありません。
インターネット経由で接続する場合のパブリックネットワークアクセス。インスタンス詳細ページの [セキュリティ設定] タブで [パブリックネットワークアクセス] を有効にし、クライアントの送信元 IP アドレスをパブリックアクセスホワイトリストに追加します。
インストール済みの `pymilvus`。このチュートリアルの例は、pymilvus 3.0.0 で検証されています。
自分でフレームを抽出する場合にインストール済みの `ffmpeg`。
モデルがインターネット経由でアクセスできるアドレス (OSS など) にアップロードされたフレーム画像。
ステップ 2 で提供する、ご利用の Milvus インスタンスの接続エンドポイント (URI) とアクセストークン。
制限事項と考慮事項
プロシージャを実行する前に、以下の制約を確認してください。いずれかを見逃すと、失敗したり、結果が静かに不正になったりします。
接続ポート — RESTful インターフェイスと gRPC はポート 19530 を共有するため、ポートを明示的に指定する必要があります (例:
http://c-xxx.milvus.aliyuncs.com:19530)。ポートを省略すると、リクエストはデフォルトでポート 80 に送られ、接続がタイムアウトします。ベクトルディメンション — ベクトルフィールドの
dimは、埋め込み関数のパラメーターのdim(2560) と一致する必要があります。マルチモーダルフラグ — マルチモーダル関数には
"is_multimodal": "true"を含める必要があります。抽出ラベルのデフォルト値 — すべての抽出ラベルにデフォルト値を定義します。JSON フィールドが null の場合、
!=条件はその行に一致せず、フィルタリングされた結果からフレームが静かにドロップされます。埋め込みバッチサイズ —
qwen3-vl-embeddingのマルチモーダルバッチ制限は 10 です。制限を超えるバッチは、エラーimage batch size can should be [1, 10](サービスからそのまま返されます) を返します。フレームは 10 個以下のバッチで書き込みます。書き込み後のフラッシュ — 書き込み後、
flush()を呼び出します。そうしないと、直後の検索で空の結果が返される可能性があります。REST 再ランキング — REST 再ランキングインターフェイスの
documentsパラメーターは、フレーム URL 文字列の配列を受け取り、top_nをサポートしていないため、候補ごとに 1 つのスコアを返します。フレームあたりのモデル呼び出し — 取り込む各フレームは、書き込み時に自動的に実行される 4 つの関数呼び出し (埋め込み、分類、抽出、エンティティ認識) をトリガーします。大規模なフレームセットのキャパシティを計画する際には、この点を考慮してください。
操作手順
このプロシージャでは、パイプラインをエンドツーエンドで構築します。開始する前に、次の点に注意してください。
ステップ 2 では、ステップ 3 から 6 のすべてが依存する共有接続コードを準備します。最初に実行してください。
ステップ 1 は、フレーム画像がまだない場合にのみ必要です。
ステップ 6 (再ランキング) はオプションです。
ステップ 7 では、作成したリソースを削除します。
ステップ 1:ビデオからフレームを抽出
車載カメラは連続したビデオを撮影するため、まず ffmpeg を使用してフレームを抽出し、フレーム画像をモデルがアクセスできるアドレスにアップロードしてから、frame_url を取り込みコードに渡します。抽出中に、各フレームの clip_id と ts_ms (ビデオクリップ ID とフレームのタイムスタンプ) を記録して、検索ヒットを元のビデオにトレースできるようにします。
# 時間ベースの抽出:1 秒ごとに 1 フレームを取得し、幅を 960 にスケーリングします
ffmpeg -i clip001.mp4 -vf "fps=1,scale=960:-1" -q:v 3 frames/clip001_%04d.jpg
# キーフレームのみ (I フレーム):より多くの情報と少ない冗長性
ffmpeg -i clip001.mp4 -vf "select='eq(pict_type,I)',scale=960:-1" -fps_mode vfr -q:v 3 frames/clip001_key_%04d.jpg
# 特定の時点の単一フレーム (例:5.2 秒、ts_ms=5200 に対応)
ffmpeg -ss 5.2 -i clip001.mp4 -frames:v 1 -vf scale=960:-1 -q:v 3 frames/clip001_5200ms.jpg目的に応じて抽出戦略を選択します。
時間ベースの抽出 — トレーニングおよびリプレイデータセットを構築するための定期的なサンプリング。
キーフレームのみ (I フレーム) — より多くの情報と少ない冗長性。フレーム数を減らしてコストを制御できるため、大規模なコーナーケースマイニングに推奨されます。
特定の時間の単一フレーム — 特定のタイムスタンプでの事故の再現など、既知の瞬間を特定します。
scale=960:-1の-1は、元の縦横比から高さを自動的に計算します。I フレームを取得するには-fps_mode vfrを使用します。古い-vsync vfr構文も実行できますが、非推奨の警告が表示されます。
ステップ 2:共有接続コードの準備
以下のコードには、接続設定と TEXTTRANSFORM 関数タイプの互換性ラッパーが含まれています。MILVUS_URI と MILVUS_TOKEN を、ご利用のインスタンスの値に置き換えてください。
from __future__ import annotations
import json
from pymilvus import DataType, Function, FunctionType, MilvusClient
MILVUS_URI = "http://c-xxxx.milvus.aliyuncs.com:19530"
MILVUS_TOKEN = "root:xxx"
# Alibaba Cloud Milvus は、関数タイプの値が 9 のマネージド拡張機能として TEXTTRANSFORM を公開します。
# 一部の pymilvus バージョンでは、このメンバーが FunctionType enum に含まれていないため、以下のラッパーで追加します。
TEXTTRANSFORM_FUNCTION_TYPE = 9
def texttransform_function_type() -> FunctionType:
for type_name in ("TEXTTRANSFORM", "TEXT_TRANSFORM", "TextTransform"):
function_type = getattr(FunctionType, type_name, None)
if function_type is not None:
return function_type
existing = getattr(FunctionType, "_value2member_map_", {}).get(TEXTTRANSFORM_FUNCTION_TYPE)
if existing is not None:
return existing
extension = int.__new__(FunctionType, TEXTTRANSFORM_FUNCTION_TYPE)
extension._name_ = "TEXTTRANSFORM"
extension._value_ = TEXTTRANSFORM_FUNCTION_TYPE
FunctionType._value2member_map_[TEXTTRANSFORM_FUNCTION_TYPE] = extension
FunctionType._member_map_["TEXTTRANSFORM"] = extension
return extension
VECTOR_DIM = 2560
EMBED_MODEL = "qwen3-vl-embedding"
VLM_MODEL = "qwen3.7-plus" # マルチモーダル理解モデル (分類、抽出、エンティティ)
client = MilvusClient(uri=MILVUS_URI, token=MILVUS_TOKEN)AI_CLASSIFY、AI_EXTRACT、および AI_ENTITY_EXTRACT はすべて TEXTTRANSFORM タイプ (関数タイプの値 9) の関数であり、task パラメーターによって区別されます。一部の pymilvus バージョンでは、このメンバーが FunctionType enum に含まれていないため、互換性ラッパーが必要です。
EMBED_MODEL を変更すると、ベクトルディメンションも変更される可能性があります。既存のコレクションのインデックス再作成はコストの高い操作であるため、VECTOR_DIM とステップ 3 のスキーマを、選択したモデルと同期させてください。
この例では、デモ目的で認証情報をハードコーディングしていますが、本番環境では MILVUS_URI と MILVUS_TOKEN を環境変数やシークレットマネージャーからロードし、ソース管理にコミットせず、ルートアカウントの使用を避けてください。
ステップ 3:コレクションの作成と AI 関数のアタッチ
ベクトルフィールドはセマンティック検索に使用され、他の 3 つのフィールドは分類、構造化抽出、エンティティ認識の結果を保持します。4 つの関数はすべて frame_url を入力として受け取り、書き込み時に自動的に実行されます。
driving_frames という名前のコレクションが既に存在する場合、この例ではそのコレクションを削除し、そのデータを永久に削除します。既存のデータがある場合や、バックアップを先に行う場合は、一意のコレクション名を使用してください。この削除ブランチは、クリーンな状態からチュートリアルを再実行できるように含まれています。
# ===== コレクションの作成:単一の定義で 4 つの AI 関数をアタッチ =====
collection_name = "driving_frames"
if client.has_collection(collection_name):
client.drop_collection(collection_name)
schema = MilvusClient.create_schema(auto_id=True, enable_dynamic_field=False)
schema.add_field("id", DataType.INT64, is_primary=True)
schema.add_field("frame_url", DataType.VARCHAR, max_length=4096) # フレーム画像のアドレス
schema.add_field("clip_id", DataType.VARCHAR, max_length=128) # ソースビデオクリップの ID
schema.add_field("ts_ms", DataType.INT64) # フレームのタイムスタンプ (ms)
schema.add_field("embedding", DataType.FLOAT_VECTOR, dim=VECTOR_DIM)
schema.add_field("scene", DataType.VARCHAR, max_length=64) # AI_CLASSIFY の出力
schema.add_field("attributes", DataType.JSON) # AI_EXTRACT の出力
schema.add_field("entities", DataType.JSON) # AI_ENTITY_EXTRACT の出力
# 1) マルチモーダル埋め込み:フレーム画像 -> 2,560 次元のベクトル
schema.add_function(Function(
name="embed_frame", function_type=FunctionType.TEXTEMBEDDING,
input_field_names=["frame_url"], output_field_names=["embedding"],
params={"provider": "aliyun_milvus", "model_name": EMBED_MODEL,
"dim": VECTOR_DIM, "is_multimodal": "true"}))
# 2) シーン分類:交差点/高速道路/トンネル/工事区域/一般道
schema.add_function(Function(
name="classify_scene", function_type=texttransform_function_type(),
input_field_names=["frame_url"], output_field_names=["scene"],
params={"provider": "aliyun_milvus", "model_name": VLM_MODEL,
"task": "ai_classify", "media_type": "image",
"labels": "intersection,highway,tunnel,construction zone,ordinary road",
"prompt": "表示されている道路環境に基づいてフレームを分類してください。", "temperature": "0"}))
# 3) 構造化抽出:信号機/車線数/天候/異常イベント
# 注:モデルが null を返すのを防ぐため、すべてのラベルにデフォルト値を定義します (下記の説明を参照)
schema.add_function(Function(
name="extract_traffic", function_type=texttransform_function_type(),
input_field_names=["frame_url"], output_field_names=["attributes"],
params={"provider": "aliyun_milvus", "model_name": VLM_MODEL,
"task": "ai_extract", "media_type": "image",
"labels": "traffic_light,lane_count,weather,anomaly_event",
"prompt": "traffic_light は red/green/yellow/none のいずれかを取ります。"
"weather は sunny/rainy/cloudy/night/unknown のいずれかを取り、判断できない場合は unknown を使用します。"
"lane_count は整数を取り、判断できない場合は 0 を使用します。"
"anomaly_event は、割り込み、信号無視、事故などの異常イベントを記述し、何もない場合は none を使用します。",
"temperature": "0"}))
# 4) 名前付きエンティティ:道路名/地名/制限速度の値
schema.add_function(Function(
name="extract_entities", function_type=texttransform_function_type(),
input_field_names=["frame_url"], output_field_names=["entities"],
params={"provider": "aliyun_milvus", "model_name": VLM_MODEL,
"task": "ai_entity_extract", "media_type": "image",
"entity_types": "LOCATION,PRODUCT",
"prompt": "フレーム内の交通標識に明示的に表示されている地名、道路名、制限速度の値のみを抽出してください。見た目から推測しないでください。",
"temperature": "0"}))
index_params = client.prepare_index_params()
index_params.add_index(field_name="embedding", index_type="AUTOINDEX", metric_type="COSINE")
client.create_collection(collection_name=collection_name, schema=schema,
index_params=index_params)主な関数パラメーターは次のとおりです。
provider— モデルプロバイダー。このチュートリアルではaliyun_milvusを使用します。model_name— 関数を支えるモデル。task—TEXTTRANSFORM関数の操作を選択します:ai_classify、ai_extract、またはai_entity_extract。media_type— 入力がフレーム画像であるため、imageに設定します。labels— 分類または抽出のための、カンマ区切りのラベルセット。entity_types— 認識するカンマ区切りのエンティティタイプ。この例では、地名と道路名をLOCATIONに、制限速度の値をPRODUCTにマッピングします。temperature— 決定論的な出力を得るために0に設定します。
SDK では、is_multimodalとdim— 「制限事項と考慮事項」をご参照ください。マルチモーダル関数はis_multimodalを設定する必要があり、ベクトルフィールドのdimは関数のdim(2560) と一致する必要があります。paramsの値は文字列として渡されます (例:"is_multimodal": "true"、"temperature": "0")。ステップ 6 で使用される REST ペイロードでは、paramsの値はネイティブ JSON 型を使用します (例:"is_multimodal": true)。
すべての抽出ラベルにデフォルト値を定義します。 上記のプロンプトでは、モデルが null を返すのを避けるために、各ラベルにデフォルト値を割り当てています。これは、null の JSON フィールドは != 条件に一致せず、フィルタリングされた結果からフレームが静かにドロップされるため重要です。デフォルト値は欠損値を減らしますが、必ずしも排除するわけではありません。ステップ 4 の取り込み結果が示すように、トンネルのフレームにはまだ weather の値がありません。フィルターを設計する際には欠損値を考慮し、「制限事項と考慮事項」をご参照ください。
ステップ 4 での取り込みが成功する (フィールドが自動的に入力される) ことで、コレクションとその 4 つの関数が正しくアタッチされていることが確認されます。
ステップ 4:書き込み時推論によるフレームの取り込み
frame_url、clip_id、ts_ms の 3 つのフィールドのみを書き込みます。AI 関数は、手動アノテーションなしで他の 4 つのフィールドを自動的に入力します。
# ===== フレームの取り込み:書き込み時推論により embedding/scene/attributes/entities が自動的に入力されます =====
# frame_url を、モデルがインターネット経由でアクセスできるご自身の画像アドレスに置き換えてください。
# clip_id と ts_ms は、フレームのソースクリップとタイムスタンプを記録するため、ヒットを元のビデオにトレースできます。
frames = [
{"frame_url": "https://<your-bucket>.oss-cn-hangzhou.aliyuncs.com/frames/clip_dashcam_5000.jpg",
"clip_id": "clip_dashcam", "ts_ms": 5000},
{"frame_url": "https://<your-bucket>.oss-cn-hangzhou.aliyuncs.com/frames/clip_dashcam_12000.jpg",
"clip_id": "clip_dashcam", "ts_ms": 12000},
{"frame_url": "https://<your-bucket>.oss-cn-hangzhou.aliyuncs.com/frames/clip_highway_3000.jpg",
"clip_id": "clip_highway", "ts_ms": 3000},
{"frame_url": "https://<your-bucket>.oss-cn-hangzhou.aliyuncs.com/frames/clip_highway_9000.jpg",
"clip_id": "clip_highway", "ts_ms": 9000},
{"frame_url": "https://<your-bucket>.oss-cn-hangzhou.aliyuncs.com/frames/clip_urban_2000.jpg",
"clip_id": "clip_urban", "ts_ms": 2000},
{"frame_url": "https://<your-bucket>.oss-cn-hangzhou.aliyuncs.com/frames/clip_urban_8000.jpg",
"clip_id": "clip_urban", "ts_ms": 8000},
]
# 各バッチをマルチモーダルバッチ制限の 10 以下に保ちます (「制限事項と考慮事項」を参照)。
_BATCH = 8
for _i in range(0, len(frames), _BATCH):
client.insert(collection_name, frames[_i:_i + _BATCH])
client.flush(collection_name)
client.load_collection(collection_name)
# 書き込みが完了したら、構造化された結果を直接クエリして検証します
ingested_rows = client.query(
collection_name, filter="",
output_fields=["frame_url", "clip_id", "ts_ms", "scene", "attributes", "entities"],
limit=100)
for row in ingested_rows:
print(f"{row['clip_id']}@{row['ts_ms']}ms scene={row['scene']} "
f"attributes={json.dumps(row['attributes'], ensure_ascii=False)} "
f"entities={json.dumps(row['entities'], ensure_ascii=False)}")書き込みループは各バッチを 8 に保ち、マルチモーダルバッチ制限の 10 以内に収めます。書き込み後、flush() を呼び出す必要があります。そうしないと、直後の検索で空の結果が返される可能性があります。検証クエリは limit=100 を使用するため、スポットチェックとして最大 100 行を返します。より大きなフレームセットの場合は、結果をページングするか、カウントを実行して完全な書き込みを確認してください。
テストでは、6 フレームの書き込みに加えてフラッシュとロードに約 12 秒かかりました。次の表は、サンプルフレームセットのリファレンス出力を示しています。正確な値はご自身のフレームに依存するため、各フィールドが自動入力されているか、分類が画像と一致しているかに注目してください。
| フレーム | scene | attributes | entities |
| dashcam@5000ms | intersection | traffic_light=green, lane_count=4, weather=sunny, anomaly_event=none | [] |
| dashcam@12000ms | construction zone | traffic_light=none, lane_count=3, weather=rainy, anomaly_event=none | [] |
| highway@3000ms | highway | traffic_light=none, lane_count=4, weather=sunny, anomaly_event=none | [{"text":"EXIT 111","type":"LOCATION"}] |
| highway@9000ms | tunnel | traffic_light=none, lane_count=2, anomaly_event=none | [] |
| urban@2000ms | ordinary road | weather=cloudy, anomaly_event=pedestrians and animals on the road | [] |
| urban@8000ms | intersection | traffic_light=red, lane_count=4, weather=sunny, anomaly_event=none | [] |
6 つのフレームすべてのシーン分類は画像の内容と一致し、抽出された信号機の状態、天候、車線数は、それらのフィールドが入力されたフレームの画像と一致していました。一部のフレームでは、特定のフィールドがまだ欠落していました (例:トンネルフレームの weather、一般道フレームの traffic_light/lane_count)。これが、フィルタリング時に欠損値を考慮する必要がある理由です。名前付きエンティティは、読み取り可能な標識テキストを含むフレームに対してのみ抽出され、モデルは見た目から地名を推測しませんでした。これは、プロンプトの「見た目から推測しない」という制約と一致します。
ステップ 5:フレームの検索
書き込み段階で生成されたベクトルと構造化フィールドは、検索段階で組み合わされます。ベクトルはセマンティック検索を処理し、構造化フィールドは正確なフィルタリングを処理します。
テキストで検索するには
クエリテキストは、同じマルチモーダルモデルによって画像ベクトル空間にマッピングされ、直接検索されます。
query = "construction zone in the rain, with traffic cones and construction signs on the road"
text_search = client.search(
collection_name=collection_name, data=[query], anns_field="embedding",
limit=10, output_fields=["frame_url", "scene", "attributes"])
for hit in text_search[0]:
print(f"score={hit['distance']:.4f} scene={hit['entity']['scene']}")ベクトル検索と構造化フィルタリングを組み合わせるには (コーナーケースマイニング)
構造化フィールドは、書き込み時に AI_EXTRACT と AI_CLASSIFY から取得され、filter で直接使用できます。
corner_query = "abnormal event on the road at night"
corner_search = client.search(
collection_name=collection_name, data=[corner_query], anns_field="embedding",
limit=10, filter='attributes["anomaly_event"] != "none"',
output_fields=["frame_url", "scene", "attributes"])
print(f"Matched {len(corner_search[0])} long-tail scenes")構造化フィルタリングは、文字列の等価性および数値比較をサポートします。たとえば、attributes["lane_count"] >= 4 は、4 車線以上のフレームを選択します。条件を組み合わせる場合、!= 条件は JSON フィールド値が null の行に一致せず、ロングテールサンプルが静かにドロップされる可能性があることを覚えておいてください。組み合わせた条件は、フレームライブラリの実際の内容と一致する必要があります。scene == "tunnel" and attributes["anomaly_event"] != "none" は、トンネルシーンと異常イベントの両方を必要とします。そのようなフレームが存在しない場合、クエリは 0 行を返しますが、これは故障ではなく、有効なコーナーケースマイニングの結論です。デバッグ中は、まず単一の条件でパイプラインが機能することを確認し、次に条件を段階的に追加して範囲を絞り込んでください。
画像で検索するには
data を画像 URL に置き換え、他はすべて変更しません。
image_query_url = frames[0]["frame_url"]
image_search = client.search(
collection_name=collection_name, data=[image_query_url],
anns_field="embedding", limit=10, output_fields=["frame_url", "scene"])
for hit in image_search[0]:
print(f"score={hit['distance']:.4f} scene={hit['entity']['scene']}")次の表は、サンプルフレームセットのリファレンス結果を示しています。絶対的なスコアではなく、相対的な順序に注目してください。スコアはご自身のフレームに依存します。
| 検索方法 | クエリ | 結果 |
| テキストからフレームへの検索 | 雨の中の工事区域、道路上に交通コーンと工事標識がある | 工事区域のフレームが 0.5904 で 1 位にランク付けされ、次点は 0.1797 で、明確な差がありました。 |
| 画像からフレームへの検索 | クエリとして使用された交差点フレームの URL | クエリ画像自体は 1.0000 のスコア、類似の交差点フレームは 0.5534、最も関連性の低いフレームは 0.1162 のスコアでした。 |
| 構造化フィルタリング | attributes["anomaly_event"] != "none" | 実際に異常イベントを含むフレームに一致しました。 |
画像からフレームへの検索では、クエリ画像は自身に対して 1.0000 のスコアになります。これは、画像エンコーディングの一貫性を検証するために使用できる再現可能なチェックです。
ステップ 6:再ランキング関数による結果の再ランキング (オプション)
ベクトル類似度はセマンティックな近さを測定しますが、これはクエリの意図への関連性と完全に同等ではありません。qwen3-vl-rerank を使用して、検索されたフレームを詳細にランキングし、ロングテールシナリオのランキング品質を向上させることができます。コードを読む前に、2 つのパターンのいずれかを選択してください。
searchでランカーをアタッチする — 1 回の呼び出しでリコールと詳細ランキングを行いたい場合に使用します。REST 再ランキングインターフェイスを呼び出す — 既に候補フレームリストがあり、それを再ランキングしたいだけの場合に使用します。
どちらのパターンも同じフレームに対して同一のスコアを生成するため、エンジニアリングのニーズに応じてどちらかを選択できます。
パターン 1 は search でランカーをアタッチするため、qwen3-vl-rerank は上位 N 件のベクトルリコール結果を詳細にランキングします。
# ===== AI_RERANK によるマルチモーダル再ランキング (オプション) =====
QUERY = "cut-in behavior at a highway ramp"
reranker = Function(
name="rerank_frames", function_type=FunctionType.RERANK,
input_field_names=["frame_url"],
params={"reranker": "model", "provider": "aliyun_milvus",
"model_name": "qwen3-vl-rerank", "queries": [QUERY],
"is_multimodal": "true",
"instruct": "クエリへの関連性に基づいて候補フレームをランク付けし、"
"主題、アクション、シーン、および詳細な視覚的ディテールを優先します。",
"timeout_sec": 10})
rerank_search = client.search(
collection_name=collection_name, data=[QUERY], anns_field="embedding",
limit=20, output_fields=["frame_url", "scene"], ranker=reranker)
for hit in rerank_search[0]:
print(f"rerank_score={hit['distance']:.4f} scene={hit['entity']['scene']}")パターン 2 は、REST エンドポイント /v2/vectordb/ai/rerank を介して、既に検索されたフレーム URL のセットのみを再ランキングします。以下のヘルパーは JSON リクエストを POST します。REST 呼び出しの前に定義してください。
from typing import Any
from urllib.error import HTTPError
from urllib.request import Request, urlopen
def post_json(path: str, body: dict[str, Any], timeout: int = 120) -> tuple[int, dict[str, Any]]:
request = Request(
f"{MILVUS_URI.rstrip('/')}{path}",
data=json.dumps(body, ensure_ascii=False).encode("utf-8"),
headers={"Authorization": f"Bearer {MILVUS_TOKEN}", "Content-Type": "application/json"},
method="POST",
)
try:
with urlopen(request, timeout=timeout) as response:
return response.status, json.loads(response.read().decode("utf-8"))
except HTTPError as exc:
return exc.code, json.loads(exc.read().decode("utf-8"))
rest_docs = [f["frame_url"] for f in frames[:3]]
status, data = post_json("/v2/vectordb/ai/rerank", {
"model_name": "qwen3-vl-rerank", "query": QUERY,
"documents": rest_docs,
"params": {"is_multimodal": True,
"instruct": "クエリへの関連性に基づいて候補フレームをランク付けし、"
"主題、アクション、シーン、および詳細な視覚的ディテールを優先します。",
"timeout_sec": 10}})
assert status == 200 and data.get("code") == 0, data
for item in sorted(data["data"]["output"]["results"],
key=lambda x: x["relevance_score"], reverse=True):
print(f"index={item['index']} relevance_score={item['relevance_score']:.4f}")assert 文は例専用のチェックです。本番環境では、status とレスポンスの code を検査する明示的なエラー処理に置き換え、レスポンスをログに記録して例外を発生させないようにしてください。
マルチモーダル再ランキングには params に is_multimodal が必要であり、instruct を使用して、主題、アクション、シーン、詳細な視覚的ディテールを優先するなど、ランキングの焦点を指定できます。REST の documents パラメーターはフレーム URL 文字列の配列を受け取り、top_n をサポートしていないため、候補ごとに 1 つのスコアを返します。
次の表は、「高速道路のランプでの割り込み行為」というテストクエリのリファレンス再ランキング結果を示しています。絶対的なスコアではなく、相対的な順序に注目してください。
| ランク | フレームシーン | 再ランキングスコア |
| 1 | highway | 0.5898 |
| 2 | construction zone | 0.5018 |
| 3 | tunnel | 0.4858 |
| 4 | intersection | 0.4830 |
| 5 | intersection | 0.4089 |
| 6 | ordinary road | 0.3819 |
高速道路のフレームが 1 位にランク付けされ、クエリのセマンティクスと一致しました。
ステップ 7:リソースのクリーンアップ
チュートリアルが完了したら、不要な課金を避けるために作成したリソースを削除します。
コレクションを削除します。
client.drop_collection(collection_name)コレクションが削除されたことを確認します。次のコマンドは
Falseを返すはずです。print(client.has_collection(collection_name))チュートリアルのために OSS にアップロードしたフレーム画像が不要になった場合は、削除します。
(オプション) このチュートリアルのためだけにパブリックネットワークアクセスを有効にした場合は、インスタンス詳細ページの [セキュリティ設定] タブで [パブリックネットワークアクセス] を無効にします。
トラブルシューティング
一般的な問題を診断するには、次の表を使用してください。これらの症状の背後にある制約については、「制限事項と考慮事項」で説明しています。
| 症状 | 原因 | ソリューション |
| 書き込み直後の検索で空の結果が返される。 | flush() が呼び出されなかった。 | 書き込み後に flush() を呼び出してから検索する。 |
| 接続がタイムアウトする。 | ポートが省略されたため、リクエストがポート 80 に送られた。 | URI でポート 19530 を明示的に指定する。 |
| 組み合わせたフィルターが 0 行を返す。 | ライブラリ内にすべての条件に一致するフレームがない。 | これはエラーではなく、有効なコーナーケースマイニングの結論である可能性があります。単一の条件でパイプラインを確認し、徐々に条件を追加する。 |
バッチ書き込みで image batch size can should be [1, 10] が返される。 | バッチがマルチモーダル制限の 10 を超えている。 | フレームを 10 個以下のバッチで書き込む。 |
ソリューションの価値
次の表は、従来の自己構築ソリューションと Milvus AI 関数アプローチを比較したものです。
| 側面 | 従来の自己構築ソリューション | Milvus AI 関数 |
| 開発サイクル | フレーム抽出、推論、ベクトルデータベース、メタデータデータベースにまたがる共同デバッグ | コレクション作成時に関数をアタッチし、直接書き込みと検索を行う |
| 推論オペレーション | スケーリングと障害復旧が必要な自己構築の GPU 推論クラスター | Milvus によって管理される呼び出しで、運用する推論クラスターは不要 |
| データ移動 | オブジェクトストレージ、推論クラスター、ベクトルデータベース間でフレームが繰り返し移動する | 書き込み時推論で、データは Milvus インスタンス内に留まる |
| マルチモーダル検索 | テキストと画像のベクトル空間の自己構築によるアライメントが必要 | qwen3-vl-embedding は、テキストからフレームへの検索と画像からフレームへの検索をネイティブにサポート |
| アノテーション基準 | 手動アノテーションは遅く高価で、基準も様々 | prompt が判断ルールを統一するため、ラベル基準は一貫性があり再現可能 |
自動運転チームにとって、このアプローチは次のことを実現します。
コーナーケースマイニングの高速化 — テキストからフレームへの検索と構造化フィルタリングを、マルチモーダル再ランキングと組み合わせることで、単一の自然言語文からロングテールシナリオを取得します。
自動ラベリングコストの削減 —
AI_CLASSIFY、AI_EXTRACT、AI_ENTITY_EXTRACTは、書き込み時に統一された基準でラベルを生成し、レビューと修正を人間に任せます。パイプラインの集約 — 推論はベクトルデータベース内で実行されます。書き込みはベクトルと構造化された理解を生成し、検索はマルチモーダルなセマンティック検索を実行し、運転データは Milvus インスタンス内に留まります。