パートナーシップ
TwelveLabsとVespaを活用したマルチベクトル動画検索

ジェームズ・リー
開発者は、Twelve LabsのEmbed APIとVespaを統合し、Marengoでマルチモーダル埋め込みを生成、それをVespa Cloudにインデックスし、BM25レキシカル検索とベクトル類似度を組み合わせたハイブリッドランキングアプローチを用いてクエリを実行することで、セマンティックビデオ検索アプリケーションを構築できます。
開発者は、Twelve LabsのEmbed APIとVespaを統合し、Marengoでマルチモーダル埋め込みを生成、それをVespa Cloudにインデックスし、BM25レキシカル検索とベクトル類似度を組み合わせたハイブリッドランキングアプローチを用いてクエリを実行することで、セマンティックビデオ検索アプリケーションを構築できます。

この記事の内容
ニュースレターに登録する
ニュースレターに登録する
ビデオ理解に関する最新の技術進歩、チュートリアル、業界の動向をお届けします
ビデオ理解に関する最新の技術進歩、チュートリアル、業界の動向をお届けします
AIを活用してビデオを検索、分析、探索します。
2025/03/03
9分
記事へのリンクをコピー
これを私たちと共同で開発してくれた Zohar Nissare-Houssen 氏と Andreas Eriksen 氏に深く感謝いたします!
はじめに
マルチモーダル・コンテンツの時代において、動画データから有意義なインサイトを抽出するには、テキスト、音声、視覚情報などの複数のモーダリティを処理・解釈できる高度なツールが必要です。TwelveLabsの Embed API を使用すると、開発者は視覚表現、発話内容、文脈上のインタラクションを含む、動画コンテンツの本質を凝縮した豊かでマルチモーダルな埋め込み(embeddings)を生成できます。これらの埋め込みは、動画の統一されたベクトル表現を提供することで、セマンティック動画検索のような高度なアプリケーションを実現します。
一方で、 Vespaは、大規模なデータセットに対する低レイテンシの計算向けに設計されたプラットフォームであり、構造化データやベクトルデータのインデックス作成およびクエリ実行に優れています。近似最近傍(ANN)検索のサポートとハイブリッドランキング機能を備えたVespaは、スケーラブルな動画検索ソリューションを展開するための理想的なパートナーです。
このチュートリアルでは、TwelveLabsの Embed API と Vespa を統合して、セマンティック動画検索アプリケーションを構築する方法を説明します。両プラットフォームの強みを組み合わせることで、強力なハイブリッド検索機能を実現しながら、動画の埋め込みとメタデータを効率的にインデックス登録できます。
ステップ 1: セットアップと設定
このセクションでは、TwelveLabsの Embed API と Vespa Cloud を使用してセマンティック動画検索アプリケーションを構築するために必要な環境と設定をセットアップします。セットアッププロセスをステップバイステップで進めていきましょう。
前提条件
始める前に、以下を用意していることを確認してください:
Python 3.7 以上がインストールされていること
APIキーを持つTwelveLabsのアカウント
Vespa Cloudのトライアルアカウント
環境セットアップ
まず、必要な Python パッケージをインストールしましょう:
!pip3 install pyvespa vespacli twelvelabs pandas
次に、必要なパッケージをすべてインポートします:
import os import hashlib import json from vespa.package import ( ApplicationPackage, Field, Schema, Document, HNSW, RankProfile, FieldSet, SecondPhaseRanking, Function, ) from vespa.deployment import VespaCloud from vespa.io import VespaResponse, VespaQueryResponse from twelvelabs import TwelveLabs from twelvelabs.models.embed import EmbeddingsTask import pandas as pd from datetime import datetime
APIの設定
TwelveLabs Embed API を使用するには、API キーを設定する必要があります:
まだ登録していない場合は、 https://auth.twelvelabs.io/u/signup でサインアップしてください
https://playground.twelvelabs.io/dashboard/api-key に移動して、APIキーを取得します
APIキーを設定します:
TL_API_KEY = os.getenv("TL_API_KEY") or input("Enter your TL_API key: ")
注意: 無料(Free)プランには600分間の動画インデックス作成が含まれており、このチュートリアルを進めるには十分です。
Vespa Cloudのセットアップ
Vespa Cloudをセットアップするには:
https://vespa.ai/free-trial でVespa Cloudトライアルアカウントを作成します
console.vespa-cloud.com にログインし、テナントを作成します
アプリケーションの設定を構成します:
# Replace with your tenant name from the Vespa Cloud Console tenant_name = "vespa-team" # Replace with your application name (does not need to exist yet) application = "videosearch"
接続の確認
TwelveLabsクライアントを初期化して、セットアップが正しくできているかを確認しましょう:
# Initialize Twelve Labs client client = TwelveLabs(api_key=TL_API_KEY) # Test the connection try: client.tasks.list() print("Successfully connected to Twelve Labs API") except Exception as e: print(f"Error connecting to Twelve Labs API: {e}")
これらの設定が完了したら、次のセクションでサンプル動画用の埋め込み(embeddings)生成に進みましょう。
重要: APIキーは安全に保護し、コード内に直接コミットしないようにしてください。本番環境では環境変数や安全なシークレット管理ソリューションの使用を検討してください。
ステップ 2: 属性と埋め込みの生成
このセクションでは、Twelve Labs の Generate API および Embed API を使用して、サンプル動画のマルチモーダルな埋め込みと属性を生成します。ワークフローを説明するために、Internet Archiveから3つの動画を処理します。
動画処理の初期化
まず、動画ソースを設定し、インデックスを作成します:
VIDEO_URLs = [ "https://ia801503.us.archive.org/27/items/hide-and-seek-with-giant-jenny/HnVideoEditor_2022_10_29_205557707.ia.mp4", "https://ia601401.us.archive.org/1/items/twas-the-night-before-christmas-1974-full-movie-freedownloadvideo.net/twas-the-night-before-christmas-1974-full-movie-freedownloadvideo.net.mp4", "https://dn720401.ca.archive.org/0/items/mr-bean-the-animated-series-holiday-for-teddy/S2E12.ia.mp4", ] # Initialize client and create index client = TwelveLabs(api_key=TL_API_KEY) timestamp = int(datetime.now().timestamp()) index_name = "Vespa_" + str(timestamp) # Create Index with Pegasus 1.2 model index = client.index.create( name=index_name, models=[{"name": "pegasus1.2", "options": ["visual", "audio"]}], addons=["thumbnail"] )
動画のアップロードと処理
それでは、動画をアップロードして処理しましょう:
def on_task_update(task: EmbeddingsTask): print(f" Status={task.status}") for video_url in VIDEO_URLs: task = client.task.create(index_id=index.id, url=video_url) status = task.wait_for_done(sleep_interval=10, callback=on_task_update) if task.status != "ready": raise RuntimeError(f"Indexing failed with status {task.status}")

動画属性の生成
各動画の要約とキーワードを生成します:
summaries = [] keywords_array = [] titles = [ "Mr. Bean the Animated Series Holiday for Teddy", "Twas the night before Christmas", "Hide and Seek with Giant Jenny", ] videos = client.index.video.list(index_id) for video in videos: # Generate summary res = client.generate.summarize( video_id=video.id, type="summary", prompt="Generate an abstract of the video serving as metadata on the video, up to five sentences." ) summaries.append(res.summary) # Generate keywords keywords = client.generate.text( video_id=video.id, prompt="Based on this video, I want to generate five keywords for SEO. Provide just the keywords as a comma delimited list." ) keywords_array.append(keywords.data)
マルチモーダル埋め込みの生成

Marengo検索モデルを使用して埋め込みタスクを作成します:
task_ids = [] for url in VIDEO_URLs: task = client.embed.task.create(model_name="Marengo-retrieval-2.7", video_url=url) task_ids.append(str(task.id)) status = task.wait_for_done(sleep_interval=10, callback=on_task_update) if task.status != "ready": raise RuntimeError(f"Embedding failed with status {task.status}")
埋め込みの取得とデータ処理
最後に、生成された埋め込みを取得します:
tasks = [] for task_id in task_ids: task = client.embed.task.retrieve(task_id) tasks.append(task)
生成された埋め込み出力には、以下のような特徴があります:
各動画は6秒間のチャンクに分割されます
各セグメントには1024次元の埋め込みベクトルが含まれます
動画の長さに応じて、1本の動画から37〜242個のセグメントが生成されます
各セグメントには開始・終了オフセットのタイムスタンプが含まれます
時間的文脈をとらえるため、埋め込みスコープは "clip" に設定されます

これらの埋め込みは、映像要素、音声、時間的な関係など、動画のマルチモーダルな側面を捉えています。これらを利用して、Vespaでセマンティック検索を実現します。
ステップ 3: Vespaアプリケーションのデプロイ
このステップでは、VespaアプリケーションパッケージをVespa Cloudにデプロイし、属性や埋め込みを含む処理済みの動画データをアプリケーションにフィード(投入)します。

アプリケーションパッケージの作成
Vespaアプリケーションパッケージは、アプリケーションのスキーマと構成を定義します。これには、Twelve Labs Embed API によって生成されたフィールド(動画属性や埋め込みなど)が含まれます。ここでは、 pyvespa を使ってスキーマを定義する方法を示します:
# Define the schema for "videos" videos_schema = Schema( name="videos", document=Document( fields=[ Field(name="video_url", type="string", indexing=["summary"]), Field(name="title", type="string", indexing=["index", "summary"], match=["text"], index="enable-bm25"), Field(name="keywords", type="string", indexing=["index", "summary"], match=["text"], index="enable-bm25"), Field(name="video_summary", type="string", indexing=["index", "summary"], match=["text"], index="enable-bm25"), Field(name="embedding_scope", type="string", indexing=["attribute", "summary"]), Field(name="start_offset_sec", type="array<float>", indexing=["attribute", "summary"]), Field(name="end_offset_sec", type="array<float>", indexing=["attribute", "summary"]), Field( name="embeddings", type="tensor<float>(p{},x[1024])", indexing=["index", "attribute"], ann=HNSW(distance_metric="angular"), ), ] ), ) # Add fieldsets for search fieldsets = [ FieldSet(name="default", fields=["title", "keywords", "video_summary"]), ] # Define ranking functions mapfunctions = [ Function( name="similarities", expression="""sum(query(q) * attribute(embeddings), x)""", ), Function( name="bm25_score", expression="bm25(title) + bm25(keywords) + bm25(video_summary)", ), ] # Define a hybrid rank profile semantic_rankprofile = RankProfile( name="hybrid", inputs=[("query(q)", "tensor<float>(x[1024])")], first_phase="bm25_score", second_phase=SecondPhaseRanking(expression="closeness(field, embeddings)", rerank_count=10), match_features=["closest(embeddings)"], summary_features=["similarities"], functions=mapfunctions, ) # Add rank profile to schema videos_schema.add_rank_profile(semantic_rankprofile) # Create the application package package = ApplicationPackage(name=application, schema=[videos_schema])
このスキーマは、動画データ(属性と埋め込み)がVespaでどのように格納、インデックス登録、およびクエリ検索されるかを定義しています。
アプリケーションパッケージのデプロイ
アプリケーションパッケージをVespa Cloudにデプロイするには、 VespaCloud を使用して接続を作成し、定義されたパッケージをデプロイします。
from vespa.deployment import VespaCloud # Deploy the application package to Vespa Cloud vespa_cloud = VespaCloud( tenant=tenant_name, application=application, application_package=package, key_content=os.getenv("VESPA_TEAM_API_KEY"), # Replace with your API key content ) app = vespa_cloud.deploy() print("Deployment complete!")

デプロイが完了すると、上記のようなデプロイログが表示されます。アプリケーションは現在稼働しており、データを受け取る準備ができています。
Vespaへのデータ投入(フィード)
次に、 pyvespa を使用して動画データ(属性と埋め込み)をVespaに投入します。各ドキュメントは、URLとセグメントインデックスから派生した一意のIDによって識別され、動画セグメントに対応しています。
フィード用データの準備
import hashlib # Initialize a list to store Vespa feed documents vespa_feed = [] # Reverse VIDEO_URLs since attributes were generated in reverse order VIDEO_URLs.reverse() for i, task in enumerate(tasks): video_url = VIDEO_URLs[i] title = titles[i] keywords = keywords_array[i] summary = summaries[i] start_offsets = [] end_offsets = [] embeddings = {} for index, segment in enumerate(task.video_embedding.segments): start_offsets.append(float(segment.start_offset_sec)) end_offsets.append(float(segment.end_offset_sec)) embeddings[str(index)] = list(map(float, segment.embeddings_float)) # Create unique ID for each segment id_hash = hashlib.md5(f"{video_url}_{index}".encode()).hexdigest() document = { "id": id_hash, "fields": { "video_url": video_url, "title": title, "keywords": keywords, "video_summary": summary, "embedding_scope": segment.embedding_scope, "start_offset_sec": start_offsets, "end_offset_sec": end_offsets, "embeddings": embeddings, }, } vespa_feed.append(document)
Vespaへのデータ投入(フィード)
from vespa.io import VespaResponse def callback(response: VespaResponse, id: str): if not response.is_successful(): print(f"Failed to feed document {id} with status code {response.status_code}: {response.get_json()}") # Feed data synchronously app.feed_iterable(vespa_feed, schema="videos", callback=callback) print("Data feeding complete!")
フィード作業により、すべての動画データがVespaのコンテンツクラスターにインデックス登録されます。ログを確認するか、Vespaインスタンスに問い合わせることで、これを確認できます。
概要
アプリケーションパッケージ: 動画属性と埋め込みを格納するためのスキーマを定義しました。
デプロイ: パッケージを Vespa Cloud にデプロイしました。
データ投入(フィード): デプロイされたアプリケーションに処理済みの動画データをインデックス登録しました。
これでVespaアプリケーションがセマンティック検索に対応できるようになりました!

ステップ 4: セマンティック検索の実行
このステップでは、Vespaに保存されている動画埋め込みに対してセマンティック検索を実行します。検索には、キーワードによるテキスト検索(語彙検索)とベクトル類似度検索を組み合わせたハイブリッドランキングを使用し、最も関連性の高い動画セグメントを取得します。
ハイブリッド検索の実行
クエリの埋め込みの作成
「そりに乗ったサンタクロース(Santa Claus on his sleigh)」というクエリに該当する動画セグメントを検索するために、まずは動画の埋め込みに使用したのと同じモデル(Marengo-retrieval-2.7)を使ってクエリの埋め込みを生成します:
client = TwelveLabs(api_key=TL_API_KEY) user_query = "Santa Claus on his sleigh" # Generate embedding for the query res = client.embed.create( model_name="Marengo-retrieval-2.7", text=user_query, ) print("Created a text embedding") print(f" Model: {res.model_name}") if res.text_embedding is not None and res.text_embedding.segments is not None: q_embedding = res.text_embedding.segments[0].embeddings_float print(f" Embedding Dimension: {len(q_embedding)}") print(f" Sample 5 values from array: {q_embedding[:5]}")
この手法により 1024 次元のクエリ埋め込みが出力されます。これを使用して、Vespa で近傍探索用のクエリを実行します。
Vespaでのハイブリッド検索の実行
Vespa の近似最近傍(ANN)検索機能を使用し、テキスト検索(BM25)とベクトル類似度ランキングを組み合わせます。このクエリにより、ハイブリッドランキングに基づいて最も関連性の高い結果が取得されます:
with app.syncio(connections=1) as session: response: VespaQueryResponse = session.query( yql="select * from videos where userQuery() OR ({targetHits:100}nearestNeighbor(embeddings,q))", query=user_query, ranking="hybrid", hits=1, body={"input.query(q)": q_embedding}, ) assert response.is_successful() # Print the top hit for hit in response.hits: print(json.dumps(hit, indent=4)) # Get full response JSON response.get_json()
ハイブリッドランキングは次のように機能します:
第一段階(First Phase): 動画タイトル、キーワード、要約に基づいたBM25語彙ランキング。
第二段階(Second Phase): クエリの埋め込みと動画の埋め込み間のベクトル類似度を用いたリランキング。
出力ログに示されているように、最も関連度の高い結果(Top Hit)は、これらを組み合わせたスコアが最も高かったセグメントに対応します。
結果をDataFrameに変換して処理する
結果を使いやすくするために、pandas の DataFrame を使って、類似度スコアに基づいて上位 N 個のセグメントを抽出して並べ替えます:
def get_top_n_similarity_matches(data, N=5): """ Extracts top N similarity scores and their corresponding offsets. Args: - data (dict): Input JSON-like structure containing similarities and offsets. - N (int): Number of top similarity scores to return. Returns: - pd.DataFrame: A DataFrame with top N similarity scores and offsets. """ # Extract relevant fields similarities = data["fields"]["summaryfeatures"]["similarities"]["cells"] start_offset_sec = data["fields"]["start_offset_sec"] end_offset_sec = data["fields"]["end_offset_sec"] # Sort by similarity score sorted_similarities = sorted(similarities.items(), key=lambda x: x[1], reverse=True) # Prepare results for top N matches results = [] for index_str, score in sorted_similarities[:N]: index = int(index_str) if index < len(start_offset_sec): results.append({ "index": index, "similarity_score": score, "start_offset_sec": start_offset_sec[index], "end_offset_sec": end_offset_sec[index], }) return pd.DataFrame(results) # Get top 10 matches df_result = get_top_n_similarity_matches(response.hits[0], N=10) print(df_result)
出力される DataFrame には以下が含まれます:
Index: 動画内でのセグメントを示すインデックス。
Similarity Score: ベクトル類似度に基づく関連性スコア。
Start/End Offsets: 秒単位でのセグメント開始・終了時間境界。
以下の表は、クエリから得られた上位10件の類似検索結果の例です:

このフォーマットにより、詳細な分析や動画再生において、最も関連性の高い複数のセグメントを素早く特定しやすくなります。
概要
TwelveLabs の Embed API を使用してクエリの埋め込みを生成しました。
Vespa でテキスト検索(BM25)とベクトル類似度に基づいたハイブリッド検索を実行しました。
上位の検索結果を分析しやすいように pandas の DataFrame に変換して処理しました。
このワークフローは、TwelveLabsのマルチモーダル埋め込みとVespaの高度な検索機能を用いて、関連度が高い該当セグメントを効率的に見つけ出す方法を実証しています。
ステップ 5: 結果の確認
このステップでは、関連する動画の区間をグループ化し、整理することで、セマンティック検索の結果を確認します。その後、ノートブックに埋め込まれた動画プレーヤーを使ってビジュアルに結果を再生し、その関連性を評価します。
隣接する類似セグメントの統合
結果をより簡単に確認できるように、同一箇所の連続した動画セグメント(各セグメントの開始・終了時刻に基づく)を一つの範囲として統合する必要があります。また、切り替わりがスムーズになるように、各セグメントの境界に 3 秒間ののりしろ(オーバーラップ)を設けます。以下の関数は、この統合を行い、読みやすさを考慮して時間オフセットを MM:SS 形式に変換します。
例えば、上位の検索結果に重複する時間帯や隣接する時間帯が含まれている場合、この関数はそれらを単一の範囲にマージします。その出力は、以下のようになります:
Consolidated Segments (MM:SS): [('20:15', '20:27'), ('20:39', '21:21'), ('22:51', '23:15'
動画プレーヤーを使用した結果の視覚化
特定されたセグメントを実際に確認するために、ノートブック上に簡単な動画プレーヤーを埋め込みます。統合された時間範囲に手動で進めることも、指定した時間にジャンプするようにプログラムを設定することもできます。
動画プレーヤーの埋め込み
from IPython.display import HTML # Define video URL (replace with your video URL) video_url = "https://ia601401.us.archive.org/1/items/twas-the-night-before-christmas-1974-full-movie-freedownloadvideo.net/twas-the-night-before-christmas-1974-full-movie-freedownloadvideo.net.mp4" # Define middle point of a segment for playback preview (e.g., first consolidated segment) middle_point = (1272 + 1278) / 2 # Example from top match # Generate HTML for video player video_player = f""" <video id="myVideo" width="640" height="480" controls> <source src="{video_url}" type="video/mp4"> Your browser does not support the video tag. </video """ # Display the video player HTML(video_player)
このコードは、指定された時間(middle_point)から再生を開始するインタラクティブな動画プレーヤーを生成します。興味のある別のポイントに基づいて middle_point の数値を変更して確認できます。

検索結果の評価
プレーヤーが埋め込まれたら、統合された時間範囲を手動またはシークバーで移動しながら、各ビジュアルを視覚的に評価できます。先ほど生成された segments のリストを利用し、クエリとの高い関連が確実視される一致箇所を見つけて再生してください。
例えば:
セグメント:
'20:21' - '21:18'該当シーンの確認: このセグメントには、そりに乗ったサンタクロースがはっきりと映っており、本ハイブリッド探索が関連性の高いコンテンツを正確に認識・抽出できていることが証明されます。
終わりに
TwelveLabsの Embed API と Vespa を組み合わせることで、高度なセマンティック動画検索アプリケーションの実用可能な可能性が大きく広がります。 Embed API は、ビデオの時間軸や文脈的なニュアンスをとらえる豊富なマルチモーダル埋め込みを提供し、Vespa の強力なインデックス登録およびハイブリッド検索機能によって、求める該当箇所を非常に高速で特定できます。
この統合がもたらす主な利点は以下の通りです:
マルチモーダルな理解: TwelveLabs が生成する統一された埋め込みにより、複数のメディア要素にまたがる動画の内容を包括的に表現できます。
優れたスケーラビリティ: 大規模なデータセットでも処理能力が高く、大量のデータが存在するスケール時にも低レイテンシで処理・検索を実行できます。
洗練されたハイブリッド検索: キーワードベースの(BM25)テキスト検索に、ベクトル的な意味類似度(ANN)を掛け合わせることで、目的の箇所の高い識別精度を維持できます。
高度な柔軟性: 開発者は、要件に応じたスキーマ設計、独自のランク構成、様々なクエリロジックを必要に合わせて自由にカスタマイズできます。
本チュートリアルを参照することで、ユーザー体験を飛躍的に向上させ、動画アーカイブなどのコンテンツ検索における新たなアイデアを形にする、スケーラブルで知的な動画検索システムを構築できるようになります。
これを私たちと共同で開発してくれた Zohar Nissare-Houssen 氏と Andreas Eriksen 氏に深く感謝いたします!
はじめに
マルチモーダル・コンテンツの時代において、動画データから有意義なインサイトを抽出するには、テキスト、音声、視覚情報などの複数のモーダリティを処理・解釈できる高度なツールが必要です。TwelveLabsの Embed API を使用すると、開発者は視覚表現、発話内容、文脈上のインタラクションを含む、動画コンテンツの本質を凝縮した豊かでマルチモーダルな埋め込み(embeddings)を生成できます。これらの埋め込みは、動画の統一されたベクトル表現を提供することで、セマンティック動画検索のような高度なアプリケーションを実現します。
一方で、 Vespaは、大規模なデータセットに対する低レイテンシの計算向けに設計されたプラットフォームであり、構造化データやベクトルデータのインデックス作成およびクエリ実行に優れています。近似最近傍(ANN)検索のサポートとハイブリッドランキング機能を備えたVespaは、スケーラブルな動画検索ソリューションを展開するための理想的なパートナーです。
このチュートリアルでは、TwelveLabsの Embed API と Vespa を統合して、セマンティック動画検索アプリケーションを構築する方法を説明します。両プラットフォームの強みを組み合わせることで、強力なハイブリッド検索機能を実現しながら、動画の埋め込みとメタデータを効率的にインデックス登録できます。
ステップ 1: セットアップと設定
このセクションでは、TwelveLabsの Embed API と Vespa Cloud を使用してセマンティック動画検索アプリケーションを構築するために必要な環境と設定をセットアップします。セットアッププロセスをステップバイステップで進めていきましょう。
前提条件
始める前に、以下を用意していることを確認してください:
Python 3.7 以上がインストールされていること
APIキーを持つTwelveLabsのアカウント
Vespa Cloudのトライアルアカウント
環境セットアップ
まず、必要な Python パッケージをインストールしましょう:
!pip3 install pyvespa vespacli twelvelabs pandas
次に、必要なパッケージをすべてインポートします:
import os import hashlib import json from vespa.package import ( ApplicationPackage, Field, Schema, Document, HNSW, RankProfile, FieldSet, SecondPhaseRanking, Function, ) from vespa.deployment import VespaCloud from vespa.io import VespaResponse, VespaQueryResponse from twelvelabs import TwelveLabs from twelvelabs.models.embed import EmbeddingsTask import pandas as pd from datetime import datetime
APIの設定
TwelveLabs Embed API を使用するには、API キーを設定する必要があります:
まだ登録していない場合は、 https://auth.twelvelabs.io/u/signup でサインアップしてください
https://playground.twelvelabs.io/dashboard/api-key に移動して、APIキーを取得します
APIキーを設定します:
TL_API_KEY = os.getenv("TL_API_KEY") or input("Enter your TL_API key: ")
注意: 無料(Free)プランには600分間の動画インデックス作成が含まれており、このチュートリアルを進めるには十分です。
Vespa Cloudのセットアップ
Vespa Cloudをセットアップするには:
https://vespa.ai/free-trial でVespa Cloudトライアルアカウントを作成します
console.vespa-cloud.com にログインし、テナントを作成します
アプリケーションの設定を構成します:
# Replace with your tenant name from the Vespa Cloud Console tenant_name = "vespa-team" # Replace with your application name (does not need to exist yet) application = "videosearch"
接続の確認
TwelveLabsクライアントを初期化して、セットアップが正しくできているかを確認しましょう:
# Initialize Twelve Labs client client = TwelveLabs(api_key=TL_API_KEY) # Test the connection try: client.tasks.list() print("Successfully connected to Twelve Labs API") except Exception as e: print(f"Error connecting to Twelve Labs API: {e}")
これらの設定が完了したら、次のセクションでサンプル動画用の埋め込み(embeddings)生成に進みましょう。
重要: APIキーは安全に保護し、コード内に直接コミットしないようにしてください。本番環境では環境変数や安全なシークレット管理ソリューションの使用を検討してください。
ステップ 2: 属性と埋め込みの生成
このセクションでは、Twelve Labs の Generate API および Embed API を使用して、サンプル動画のマルチモーダルな埋め込みと属性を生成します。ワークフローを説明するために、Internet Archiveから3つの動画を処理します。
動画処理の初期化
まず、動画ソースを設定し、インデックスを作成します:
VIDEO_URLs = [ "https://ia801503.us.archive.org/27/items/hide-and-seek-with-giant-jenny/HnVideoEditor_2022_10_29_205557707.ia.mp4", "https://ia601401.us.archive.org/1/items/twas-the-night-before-christmas-1974-full-movie-freedownloadvideo.net/twas-the-night-before-christmas-1974-full-movie-freedownloadvideo.net.mp4", "https://dn720401.ca.archive.org/0/items/mr-bean-the-animated-series-holiday-for-teddy/S2E12.ia.mp4", ] # Initialize client and create index client = TwelveLabs(api_key=TL_API_KEY) timestamp = int(datetime.now().timestamp()) index_name = "Vespa_" + str(timestamp) # Create Index with Pegasus 1.2 model index = client.index.create( name=index_name, models=[{"name": "pegasus1.2", "options": ["visual", "audio"]}], addons=["thumbnail"] )
動画のアップロードと処理
それでは、動画をアップロードして処理しましょう:
def on_task_update(task: EmbeddingsTask): print(f" Status={task.status}") for video_url in VIDEO_URLs: task = client.task.create(index_id=index.id, url=video_url) status = task.wait_for_done(sleep_interval=10, callback=on_task_update) if task.status != "ready": raise RuntimeError(f"Indexing failed with status {task.status}")

動画属性の生成
各動画の要約とキーワードを生成します:
summaries = [] keywords_array = [] titles = [ "Mr. Bean the Animated Series Holiday for Teddy", "Twas the night before Christmas", "Hide and Seek with Giant Jenny", ] videos = client.index.video.list(index_id) for video in videos: # Generate summary res = client.generate.summarize( video_id=video.id, type="summary", prompt="Generate an abstract of the video serving as metadata on the video, up to five sentences." ) summaries.append(res.summary) # Generate keywords keywords = client.generate.text( video_id=video.id, prompt="Based on this video, I want to generate five keywords for SEO. Provide just the keywords as a comma delimited list." ) keywords_array.append(keywords.data)
マルチモーダル埋め込みの生成

Marengo検索モデルを使用して埋め込みタスクを作成します:
task_ids = [] for url in VIDEO_URLs: task = client.embed.task.create(model_name="Marengo-retrieval-2.7", video_url=url) task_ids.append(str(task.id)) status = task.wait_for_done(sleep_interval=10, callback=on_task_update) if task.status != "ready": raise RuntimeError(f"Embedding failed with status {task.status}")
埋め込みの取得とデータ処理
最後に、生成された埋め込みを取得します:
tasks = [] for task_id in task_ids: task = client.embed.task.retrieve(task_id) tasks.append(task)
生成された埋め込み出力には、以下のような特徴があります:
各動画は6秒間のチャンクに分割されます
各セグメントには1024次元の埋め込みベクトルが含まれます
動画の長さに応じて、1本の動画から37〜242個のセグメントが生成されます
各セグメントには開始・終了オフセットのタイムスタンプが含まれます
時間的文脈をとらえるため、埋め込みスコープは "clip" に設定されます

これらの埋め込みは、映像要素、音声、時間的な関係など、動画のマルチモーダルな側面を捉えています。これらを利用して、Vespaでセマンティック検索を実現します。
ステップ 3: Vespaアプリケーションのデプロイ
このステップでは、VespaアプリケーションパッケージをVespa Cloudにデプロイし、属性や埋め込みを含む処理済みの動画データをアプリケーションにフィード(投入)します。

アプリケーションパッケージの作成
Vespaアプリケーションパッケージは、アプリケーションのスキーマと構成を定義します。これには、Twelve Labs Embed API によって生成されたフィールド(動画属性や埋め込みなど)が含まれます。ここでは、 pyvespa を使ってスキーマを定義する方法を示します:
# Define the schema for "videos" videos_schema = Schema( name="videos", document=Document( fields=[ Field(name="video_url", type="string", indexing=["summary"]), Field(name="title", type="string", indexing=["index", "summary"], match=["text"], index="enable-bm25"), Field(name="keywords", type="string", indexing=["index", "summary"], match=["text"], index="enable-bm25"), Field(name="video_summary", type="string", indexing=["index", "summary"], match=["text"], index="enable-bm25"), Field(name="embedding_scope", type="string", indexing=["attribute", "summary"]), Field(name="start_offset_sec", type="array<float>", indexing=["attribute", "summary"]), Field(name="end_offset_sec", type="array<float>", indexing=["attribute", "summary"]), Field( name="embeddings", type="tensor<float>(p{},x[1024])", indexing=["index", "attribute"], ann=HNSW(distance_metric="angular"), ), ] ), ) # Add fieldsets for search fieldsets = [ FieldSet(name="default", fields=["title", "keywords", "video_summary"]), ] # Define ranking functions mapfunctions = [ Function( name="similarities", expression="""sum(query(q) * attribute(embeddings), x)""", ), Function( name="bm25_score", expression="bm25(title) + bm25(keywords) + bm25(video_summary)", ), ] # Define a hybrid rank profile semantic_rankprofile = RankProfile( name="hybrid", inputs=[("query(q)", "tensor<float>(x[1024])")], first_phase="bm25_score", second_phase=SecondPhaseRanking(expression="closeness(field, embeddings)", rerank_count=10), match_features=["closest(embeddings)"], summary_features=["similarities"], functions=mapfunctions, ) # Add rank profile to schema videos_schema.add_rank_profile(semantic_rankprofile) # Create the application package package = ApplicationPackage(name=application, schema=[videos_schema])
このスキーマは、動画データ(属性と埋め込み)がVespaでどのように格納、インデックス登録、およびクエリ検索されるかを定義しています。
アプリケーションパッケージのデプロイ
アプリケーションパッケージをVespa Cloudにデプロイするには、 VespaCloud を使用して接続を作成し、定義されたパッケージをデプロイします。
from vespa.deployment import VespaCloud # Deploy the application package to Vespa Cloud vespa_cloud = VespaCloud( tenant=tenant_name, application=application, application_package=package, key_content=os.getenv("VESPA_TEAM_API_KEY"), # Replace with your API key content ) app = vespa_cloud.deploy() print("Deployment complete!")

デプロイが完了すると、上記のようなデプロイログが表示されます。アプリケーションは現在稼働しており、データを受け取る準備ができています。
Vespaへのデータ投入(フィード)
次に、 pyvespa を使用して動画データ(属性と埋め込み)をVespaに投入します。各ドキュメントは、URLとセグメントインデックスから派生した一意のIDによって識別され、動画セグメントに対応しています。
フィード用データの準備
import hashlib # Initialize a list to store Vespa feed documents vespa_feed = [] # Reverse VIDEO_URLs since attributes were generated in reverse order VIDEO_URLs.reverse() for i, task in enumerate(tasks): video_url = VIDEO_URLs[i] title = titles[i] keywords = keywords_array[i] summary = summaries[i] start_offsets = [] end_offsets = [] embeddings = {} for index, segment in enumerate(task.video_embedding.segments): start_offsets.append(float(segment.start_offset_sec)) end_offsets.append(float(segment.end_offset_sec)) embeddings[str(index)] = list(map(float, segment.embeddings_float)) # Create unique ID for each segment id_hash = hashlib.md5(f"{video_url}_{index}".encode()).hexdigest() document = { "id": id_hash, "fields": { "video_url": video_url, "title": title, "keywords": keywords, "video_summary": summary, "embedding_scope": segment.embedding_scope, "start_offset_sec": start_offsets, "end_offset_sec": end_offsets, "embeddings": embeddings, }, } vespa_feed.append(document)
Vespaへのデータ投入(フィード)
from vespa.io import VespaResponse def callback(response: VespaResponse, id: str): if not response.is_successful(): print(f"Failed to feed document {id} with status code {response.status_code}: {response.get_json()}") # Feed data synchronously app.feed_iterable(vespa_feed, schema="videos", callback=callback) print("Data feeding complete!")
フィード作業により、すべての動画データがVespaのコンテンツクラスターにインデックス登録されます。ログを確認するか、Vespaインスタンスに問い合わせることで、これを確認できます。
概要
アプリケーションパッケージ: 動画属性と埋め込みを格納するためのスキーマを定義しました。
デプロイ: パッケージを Vespa Cloud にデプロイしました。
データ投入(フィード): デプロイされたアプリケーションに処理済みの動画データをインデックス登録しました。
これでVespaアプリケーションがセマンティック検索に対応できるようになりました!

ステップ 4: セマンティック検索の実行
このステップでは、Vespaに保存されている動画埋め込みに対してセマンティック検索を実行します。検索には、キーワードによるテキスト検索(語彙検索)とベクトル類似度検索を組み合わせたハイブリッドランキングを使用し、最も関連性の高い動画セグメントを取得します。
ハイブリッド検索の実行
クエリの埋め込みの作成
「そりに乗ったサンタクロース(Santa Claus on his sleigh)」というクエリに該当する動画セグメントを検索するために、まずは動画の埋め込みに使用したのと同じモデル(Marengo-retrieval-2.7)を使ってクエリの埋め込みを生成します:
client = TwelveLabs(api_key=TL_API_KEY) user_query = "Santa Claus on his sleigh" # Generate embedding for the query res = client.embed.create( model_name="Marengo-retrieval-2.7", text=user_query, ) print("Created a text embedding") print(f" Model: {res.model_name}") if res.text_embedding is not None and res.text_embedding.segments is not None: q_embedding = res.text_embedding.segments[0].embeddings_float print(f" Embedding Dimension: {len(q_embedding)}") print(f" Sample 5 values from array: {q_embedding[:5]}")
この手法により 1024 次元のクエリ埋め込みが出力されます。これを使用して、Vespa で近傍探索用のクエリを実行します。
Vespaでのハイブリッド検索の実行
Vespa の近似最近傍(ANN)検索機能を使用し、テキスト検索(BM25)とベクトル類似度ランキングを組み合わせます。このクエリにより、ハイブリッドランキングに基づいて最も関連性の高い結果が取得されます:
with app.syncio(connections=1) as session: response: VespaQueryResponse = session.query( yql="select * from videos where userQuery() OR ({targetHits:100}nearestNeighbor(embeddings,q))", query=user_query, ranking="hybrid", hits=1, body={"input.query(q)": q_embedding}, ) assert response.is_successful() # Print the top hit for hit in response.hits: print(json.dumps(hit, indent=4)) # Get full response JSON response.get_json()
ハイブリッドランキングは次のように機能します:
第一段階(First Phase): 動画タイトル、キーワード、要約に基づいたBM25語彙ランキング。
第二段階(Second Phase): クエリの埋め込みと動画の埋め込み間のベクトル類似度を用いたリランキング。
出力ログに示されているように、最も関連度の高い結果(Top Hit)は、これらを組み合わせたスコアが最も高かったセグメントに対応します。
結果をDataFrameに変換して処理する
結果を使いやすくするために、pandas の DataFrame を使って、類似度スコアに基づいて上位 N 個のセグメントを抽出して並べ替えます:
def get_top_n_similarity_matches(data, N=5): """ Extracts top N similarity scores and their corresponding offsets. Args: - data (dict): Input JSON-like structure containing similarities and offsets. - N (int): Number of top similarity scores to return. Returns: - pd.DataFrame: A DataFrame with top N similarity scores and offsets. """ # Extract relevant fields similarities = data["fields"]["summaryfeatures"]["similarities"]["cells"] start_offset_sec = data["fields"]["start_offset_sec"] end_offset_sec = data["fields"]["end_offset_sec"] # Sort by similarity score sorted_similarities = sorted(similarities.items(), key=lambda x: x[1], reverse=True) # Prepare results for top N matches results = [] for index_str, score in sorted_similarities[:N]: index = int(index_str) if index < len(start_offset_sec): results.append({ "index": index, "similarity_score": score, "start_offset_sec": start_offset_sec[index], "end_offset_sec": end_offset_sec[index], }) return pd.DataFrame(results) # Get top 10 matches df_result = get_top_n_similarity_matches(response.hits[0], N=10) print(df_result)
出力される DataFrame には以下が含まれます:
Index: 動画内でのセグメントを示すインデックス。
Similarity Score: ベクトル類似度に基づく関連性スコア。
Start/End Offsets: 秒単位でのセグメント開始・終了時間境界。
以下の表は、クエリから得られた上位10件の類似検索結果の例です:

このフォーマットにより、詳細な分析や動画再生において、最も関連性の高い複数のセグメントを素早く特定しやすくなります。
概要
TwelveLabs の Embed API を使用してクエリの埋め込みを生成しました。
Vespa でテキスト検索(BM25)とベクトル類似度に基づいたハイブリッド検索を実行しました。
上位の検索結果を分析しやすいように pandas の DataFrame に変換して処理しました。
このワークフローは、TwelveLabsのマルチモーダル埋め込みとVespaの高度な検索機能を用いて、関連度が高い該当セグメントを効率的に見つけ出す方法を実証しています。
ステップ 5: 結果の確認
このステップでは、関連する動画の区間をグループ化し、整理することで、セマンティック検索の結果を確認します。その後、ノートブックに埋め込まれた動画プレーヤーを使ってビジュアルに結果を再生し、その関連性を評価します。
隣接する類似セグメントの統合
結果をより簡単に確認できるように、同一箇所の連続した動画セグメント(各セグメントの開始・終了時刻に基づく)を一つの範囲として統合する必要があります。また、切り替わりがスムーズになるように、各セグメントの境界に 3 秒間ののりしろ(オーバーラップ)を設けます。以下の関数は、この統合を行い、読みやすさを考慮して時間オフセットを MM:SS 形式に変換します。
例えば、上位の検索結果に重複する時間帯や隣接する時間帯が含まれている場合、この関数はそれらを単一の範囲にマージします。その出力は、以下のようになります:
Consolidated Segments (MM:SS): [('20:15', '20:27'), ('20:39', '21:21'), ('22:51', '23:15'
動画プレーヤーを使用した結果の視覚化
特定されたセグメントを実際に確認するために、ノートブック上に簡単な動画プレーヤーを埋め込みます。統合された時間範囲に手動で進めることも、指定した時間にジャンプするようにプログラムを設定することもできます。
動画プレーヤーの埋め込み
from IPython.display import HTML # Define video URL (replace with your video URL) video_url = "https://ia601401.us.archive.org/1/items/twas-the-night-before-christmas-1974-full-movie-freedownloadvideo.net/twas-the-night-before-christmas-1974-full-movie-freedownloadvideo.net.mp4" # Define middle point of a segment for playback preview (e.g., first consolidated segment) middle_point = (1272 + 1278) / 2 # Example from top match # Generate HTML for video player video_player = f""" <video id="myVideo" width="640" height="480" controls> <source src="{video_url}" type="video/mp4"> Your browser does not support the video tag. </video """ # Display the video player HTML(video_player)
このコードは、指定された時間(middle_point)から再生を開始するインタラクティブな動画プレーヤーを生成します。興味のある別のポイントに基づいて middle_point の数値を変更して確認できます。

検索結果の評価
プレーヤーが埋め込まれたら、統合された時間範囲を手動またはシークバーで移動しながら、各ビジュアルを視覚的に評価できます。先ほど生成された segments のリストを利用し、クエリとの高い関連が確実視される一致箇所を見つけて再生してください。
例えば:
セグメント:
'20:21' - '21:18'該当シーンの確認: このセグメントには、そりに乗ったサンタクロースがはっきりと映っており、本ハイブリッド探索が関連性の高いコンテンツを正確に認識・抽出できていることが証明されます。
終わりに
TwelveLabsの Embed API と Vespa を組み合わせることで、高度なセマンティック動画検索アプリケーションの実用可能な可能性が大きく広がります。 Embed API は、ビデオの時間軸や文脈的なニュアンスをとらえる豊富なマルチモーダル埋め込みを提供し、Vespa の強力なインデックス登録およびハイブリッド検索機能によって、求める該当箇所を非常に高速で特定できます。
この統合がもたらす主な利点は以下の通りです:
マルチモーダルな理解: TwelveLabs が生成する統一された埋め込みにより、複数のメディア要素にまたがる動画の内容を包括的に表現できます。
優れたスケーラビリティ: 大規模なデータセットでも処理能力が高く、大量のデータが存在するスケール時にも低レイテンシで処理・検索を実行できます。
洗練されたハイブリッド検索: キーワードベースの(BM25)テキスト検索に、ベクトル的な意味類似度(ANN)を掛け合わせることで、目的の箇所の高い識別精度を維持できます。
高度な柔軟性: 開発者は、要件に応じたスキーマ設計、独自のランク構成、様々なクエリロジックを必要に合わせて自由にカスタマイズできます。
本チュートリアルを参照することで、ユーザー体験を飛躍的に向上させ、動画アーカイブなどのコンテンツ検索における新たなアイデアを形にする、スケーラブルで知的な動画検索システムを構築できるようになります。
これを私たちと共同で開発してくれた Zohar Nissare-Houssen 氏と Andreas Eriksen 氏に深く感謝いたします!
はじめに
マルチモーダル・コンテンツの時代において、動画データから有意義なインサイトを抽出するには、テキスト、音声、視覚情報などの複数のモーダリティを処理・解釈できる高度なツールが必要です。TwelveLabsの Embed API を使用すると、開発者は視覚表現、発話内容、文脈上のインタラクションを含む、動画コンテンツの本質を凝縮した豊かでマルチモーダルな埋め込み(embeddings)を生成できます。これらの埋め込みは、動画の統一されたベクトル表現を提供することで、セマンティック動画検索のような高度なアプリケーションを実現します。
一方で、 Vespaは、大規模なデータセットに対する低レイテンシの計算向けに設計されたプラットフォームであり、構造化データやベクトルデータのインデックス作成およびクエリ実行に優れています。近似最近傍(ANN)検索のサポートとハイブリッドランキング機能を備えたVespaは、スケーラブルな動画検索ソリューションを展開するための理想的なパートナーです。
このチュートリアルでは、TwelveLabsの Embed API と Vespa を統合して、セマンティック動画検索アプリケーションを構築する方法を説明します。両プラットフォームの強みを組み合わせることで、強力なハイブリッド検索機能を実現しながら、動画の埋め込みとメタデータを効率的にインデックス登録できます。
ステップ 1: セットアップと設定
このセクションでは、TwelveLabsの Embed API と Vespa Cloud を使用してセマンティック動画検索アプリケーションを構築するために必要な環境と設定をセットアップします。セットアッププロセスをステップバイステップで進めていきましょう。
前提条件
始める前に、以下を用意していることを確認してください:
Python 3.7 以上がインストールされていること
APIキーを持つTwelveLabsのアカウント
Vespa Cloudのトライアルアカウント
環境セットアップ
まず、必要な Python パッケージをインストールしましょう:
!pip3 install pyvespa vespacli twelvelabs pandas
次に、必要なパッケージをすべてインポートします:
import os import hashlib import json from vespa.package import ( ApplicationPackage, Field, Schema, Document, HNSW, RankProfile, FieldSet, SecondPhaseRanking, Function, ) from vespa.deployment import VespaCloud from vespa.io import VespaResponse, VespaQueryResponse from twelvelabs import TwelveLabs from twelvelabs.models.embed import EmbeddingsTask import pandas as pd from datetime import datetime
APIの設定
TwelveLabs Embed API を使用するには、API キーを設定する必要があります:
まだ登録していない場合は、 https://auth.twelvelabs.io/u/signup でサインアップしてください
https://playground.twelvelabs.io/dashboard/api-key に移動して、APIキーを取得します
APIキーを設定します:
TL_API_KEY = os.getenv("TL_API_KEY") or input("Enter your TL_API key: ")
注意: 無料(Free)プランには600分間の動画インデックス作成が含まれており、このチュートリアルを進めるには十分です。
Vespa Cloudのセットアップ
Vespa Cloudをセットアップするには:
https://vespa.ai/free-trial でVespa Cloudトライアルアカウントを作成します
console.vespa-cloud.com にログインし、テナントを作成します
アプリケーションの設定を構成します:
# Replace with your tenant name from the Vespa Cloud Console tenant_name = "vespa-team" # Replace with your application name (does not need to exist yet) application = "videosearch"
接続の確認
TwelveLabsクライアントを初期化して、セットアップが正しくできているかを確認しましょう:
# Initialize Twelve Labs client client = TwelveLabs(api_key=TL_API_KEY) # Test the connection try: client.tasks.list() print("Successfully connected to Twelve Labs API") except Exception as e: print(f"Error connecting to Twelve Labs API: {e}")
これらの設定が完了したら、次のセクションでサンプル動画用の埋め込み(embeddings)生成に進みましょう。
重要: APIキーは安全に保護し、コード内に直接コミットしないようにしてください。本番環境では環境変数や安全なシークレット管理ソリューションの使用を検討してください。
ステップ 2: 属性と埋め込みの生成
このセクションでは、Twelve Labs の Generate API および Embed API を使用して、サンプル動画のマルチモーダルな埋め込みと属性を生成します。ワークフローを説明するために、Internet Archiveから3つの動画を処理します。
動画処理の初期化
まず、動画ソースを設定し、インデックスを作成します:
VIDEO_URLs = [ "https://ia801503.us.archive.org/27/items/hide-and-seek-with-giant-jenny/HnVideoEditor_2022_10_29_205557707.ia.mp4", "https://ia601401.us.archive.org/1/items/twas-the-night-before-christmas-1974-full-movie-freedownloadvideo.net/twas-the-night-before-christmas-1974-full-movie-freedownloadvideo.net.mp4", "https://dn720401.ca.archive.org/0/items/mr-bean-the-animated-series-holiday-for-teddy/S2E12.ia.mp4", ] # Initialize client and create index client = TwelveLabs(api_key=TL_API_KEY) timestamp = int(datetime.now().timestamp()) index_name = "Vespa_" + str(timestamp) # Create Index with Pegasus 1.2 model index = client.index.create( name=index_name, models=[{"name": "pegasus1.2", "options": ["visual", "audio"]}], addons=["thumbnail"] )
動画のアップロードと処理
それでは、動画をアップロードして処理しましょう:
def on_task_update(task: EmbeddingsTask): print(f" Status={task.status}") for video_url in VIDEO_URLs: task = client.task.create(index_id=index.id, url=video_url) status = task.wait_for_done(sleep_interval=10, callback=on_task_update) if task.status != "ready": raise RuntimeError(f"Indexing failed with status {task.status}")

動画属性の生成
各動画の要約とキーワードを生成します:
summaries = [] keywords_array = [] titles = [ "Mr. Bean the Animated Series Holiday for Teddy", "Twas the night before Christmas", "Hide and Seek with Giant Jenny", ] videos = client.index.video.list(index_id) for video in videos: # Generate summary res = client.generate.summarize( video_id=video.id, type="summary", prompt="Generate an abstract of the video serving as metadata on the video, up to five sentences." ) summaries.append(res.summary) # Generate keywords keywords = client.generate.text( video_id=video.id, prompt="Based on this video, I want to generate five keywords for SEO. Provide just the keywords as a comma delimited list." ) keywords_array.append(keywords.data)
マルチモーダル埋め込みの生成

Marengo検索モデルを使用して埋め込みタスクを作成します:
task_ids = [] for url in VIDEO_URLs: task = client.embed.task.create(model_name="Marengo-retrieval-2.7", video_url=url) task_ids.append(str(task.id)) status = task.wait_for_done(sleep_interval=10, callback=on_task_update) if task.status != "ready": raise RuntimeError(f"Embedding failed with status {task.status}")
埋め込みの取得とデータ処理
最後に、生成された埋め込みを取得します:
tasks = [] for task_id in task_ids: task = client.embed.task.retrieve(task_id) tasks.append(task)
生成された埋め込み出力には、以下のような特徴があります:
各動画は6秒間のチャンクに分割されます
各セグメントには1024次元の埋め込みベクトルが含まれます
動画の長さに応じて、1本の動画から37〜242個のセグメントが生成されます
各セグメントには開始・終了オフセットのタイムスタンプが含まれます
時間的文脈をとらえるため、埋め込みスコープは "clip" に設定されます

これらの埋め込みは、映像要素、音声、時間的な関係など、動画のマルチモーダルな側面を捉えています。これらを利用して、Vespaでセマンティック検索を実現します。
ステップ 3: Vespaアプリケーションのデプロイ
このステップでは、VespaアプリケーションパッケージをVespa Cloudにデプロイし、属性や埋め込みを含む処理済みの動画データをアプリケーションにフィード(投入)します。

アプリケーションパッケージの作成
Vespaアプリケーションパッケージは、アプリケーションのスキーマと構成を定義します。これには、Twelve Labs Embed API によって生成されたフィールド(動画属性や埋め込みなど)が含まれます。ここでは、 pyvespa を使ってスキーマを定義する方法を示します:
# Define the schema for "videos" videos_schema = Schema( name="videos", document=Document( fields=[ Field(name="video_url", type="string", indexing=["summary"]), Field(name="title", type="string", indexing=["index", "summary"], match=["text"], index="enable-bm25"), Field(name="keywords", type="string", indexing=["index", "summary"], match=["text"], index="enable-bm25"), Field(name="video_summary", type="string", indexing=["index", "summary"], match=["text"], index="enable-bm25"), Field(name="embedding_scope", type="string", indexing=["attribute", "summary"]), Field(name="start_offset_sec", type="array<float>", indexing=["attribute", "summary"]), Field(name="end_offset_sec", type="array<float>", indexing=["attribute", "summary"]), Field( name="embeddings", type="tensor<float>(p{},x[1024])", indexing=["index", "attribute"], ann=HNSW(distance_metric="angular"), ), ] ), ) # Add fieldsets for search fieldsets = [ FieldSet(name="default", fields=["title", "keywords", "video_summary"]), ] # Define ranking functions mapfunctions = [ Function( name="similarities", expression="""sum(query(q) * attribute(embeddings), x)""", ), Function( name="bm25_score", expression="bm25(title) + bm25(keywords) + bm25(video_summary)", ), ] # Define a hybrid rank profile semantic_rankprofile = RankProfile( name="hybrid", inputs=[("query(q)", "tensor<float>(x[1024])")], first_phase="bm25_score", second_phase=SecondPhaseRanking(expression="closeness(field, embeddings)", rerank_count=10), match_features=["closest(embeddings)"], summary_features=["similarities"], functions=mapfunctions, ) # Add rank profile to schema videos_schema.add_rank_profile(semantic_rankprofile) # Create the application package package = ApplicationPackage(name=application, schema=[videos_schema])
このスキーマは、動画データ(属性と埋め込み)がVespaでどのように格納、インデックス登録、およびクエリ検索されるかを定義しています。
アプリケーションパッケージのデプロイ
アプリケーションパッケージをVespa Cloudにデプロイするには、 VespaCloud を使用して接続を作成し、定義されたパッケージをデプロイします。
from vespa.deployment import VespaCloud # Deploy the application package to Vespa Cloud vespa_cloud = VespaCloud( tenant=tenant_name, application=application, application_package=package, key_content=os.getenv("VESPA_TEAM_API_KEY"), # Replace with your API key content ) app = vespa_cloud.deploy() print("Deployment complete!")

デプロイが完了すると、上記のようなデプロイログが表示されます。アプリケーションは現在稼働しており、データを受け取る準備ができています。
Vespaへのデータ投入(フィード)
次に、 pyvespa を使用して動画データ(属性と埋め込み)をVespaに投入します。各ドキュメントは、URLとセグメントインデックスから派生した一意のIDによって識別され、動画セグメントに対応しています。
フィード用データの準備
import hashlib # Initialize a list to store Vespa feed documents vespa_feed = [] # Reverse VIDEO_URLs since attributes were generated in reverse order VIDEO_URLs.reverse() for i, task in enumerate(tasks): video_url = VIDEO_URLs[i] title = titles[i] keywords = keywords_array[i] summary = summaries[i] start_offsets = [] end_offsets = [] embeddings = {} for index, segment in enumerate(task.video_embedding.segments): start_offsets.append(float(segment.start_offset_sec)) end_offsets.append(float(segment.end_offset_sec)) embeddings[str(index)] = list(map(float, segment.embeddings_float)) # Create unique ID for each segment id_hash = hashlib.md5(f"{video_url}_{index}".encode()).hexdigest() document = { "id": id_hash, "fields": { "video_url": video_url, "title": title, "keywords": keywords, "video_summary": summary, "embedding_scope": segment.embedding_scope, "start_offset_sec": start_offsets, "end_offset_sec": end_offsets, "embeddings": embeddings, }, } vespa_feed.append(document)
Vespaへのデータ投入(フィード)
from vespa.io import VespaResponse def callback(response: VespaResponse, id: str): if not response.is_successful(): print(f"Failed to feed document {id} with status code {response.status_code}: {response.get_json()}") # Feed data synchronously app.feed_iterable(vespa_feed, schema="videos", callback=callback) print("Data feeding complete!")
フィード作業により、すべての動画データがVespaのコンテンツクラスターにインデックス登録されます。ログを確認するか、Vespaインスタンスに問い合わせることで、これを確認できます。
概要
アプリケーションパッケージ: 動画属性と埋め込みを格納するためのスキーマを定義しました。
デプロイ: パッケージを Vespa Cloud にデプロイしました。
データ投入(フィード): デプロイされたアプリケーションに処理済みの動画データをインデックス登録しました。
これでVespaアプリケーションがセマンティック検索に対応できるようになりました!

ステップ 4: セマンティック検索の実行
このステップでは、Vespaに保存されている動画埋め込みに対してセマンティック検索を実行します。検索には、キーワードによるテキスト検索(語彙検索)とベクトル類似度検索を組み合わせたハイブリッドランキングを使用し、最も関連性の高い動画セグメントを取得します。
ハイブリッド検索の実行
クエリの埋め込みの作成
「そりに乗ったサンタクロース(Santa Claus on his sleigh)」というクエリに該当する動画セグメントを検索するために、まずは動画の埋め込みに使用したのと同じモデル(Marengo-retrieval-2.7)を使ってクエリの埋め込みを生成します:
client = TwelveLabs(api_key=TL_API_KEY) user_query = "Santa Claus on his sleigh" # Generate embedding for the query res = client.embed.create( model_name="Marengo-retrieval-2.7", text=user_query, ) print("Created a text embedding") print(f" Model: {res.model_name}") if res.text_embedding is not None and res.text_embedding.segments is not None: q_embedding = res.text_embedding.segments[0].embeddings_float print(f" Embedding Dimension: {len(q_embedding)}") print(f" Sample 5 values from array: {q_embedding[:5]}")
この手法により 1024 次元のクエリ埋め込みが出力されます。これを使用して、Vespa で近傍探索用のクエリを実行します。
Vespaでのハイブリッド検索の実行
Vespa の近似最近傍(ANN)検索機能を使用し、テキスト検索(BM25)とベクトル類似度ランキングを組み合わせます。このクエリにより、ハイブリッドランキングに基づいて最も関連性の高い結果が取得されます:
with app.syncio(connections=1) as session: response: VespaQueryResponse = session.query( yql="select * from videos where userQuery() OR ({targetHits:100}nearestNeighbor(embeddings,q))", query=user_query, ranking="hybrid", hits=1, body={"input.query(q)": q_embedding}, ) assert response.is_successful() # Print the top hit for hit in response.hits: print(json.dumps(hit, indent=4)) # Get full response JSON response.get_json()
ハイブリッドランキングは次のように機能します:
第一段階(First Phase): 動画タイトル、キーワード、要約に基づいたBM25語彙ランキング。
第二段階(Second Phase): クエリの埋め込みと動画の埋め込み間のベクトル類似度を用いたリランキング。
出力ログに示されているように、最も関連度の高い結果(Top Hit)は、これらを組み合わせたスコアが最も高かったセグメントに対応します。
結果をDataFrameに変換して処理する
結果を使いやすくするために、pandas の DataFrame を使って、類似度スコアに基づいて上位 N 個のセグメントを抽出して並べ替えます:
def get_top_n_similarity_matches(data, N=5): """ Extracts top N similarity scores and their corresponding offsets. Args: - data (dict): Input JSON-like structure containing similarities and offsets. - N (int): Number of top similarity scores to return. Returns: - pd.DataFrame: A DataFrame with top N similarity scores and offsets. """ # Extract relevant fields similarities = data["fields"]["summaryfeatures"]["similarities"]["cells"] start_offset_sec = data["fields"]["start_offset_sec"] end_offset_sec = data["fields"]["end_offset_sec"] # Sort by similarity score sorted_similarities = sorted(similarities.items(), key=lambda x: x[1], reverse=True) # Prepare results for top N matches results = [] for index_str, score in sorted_similarities[:N]: index = int(index_str) if index < len(start_offset_sec): results.append({ "index": index, "similarity_score": score, "start_offset_sec": start_offset_sec[index], "end_offset_sec": end_offset_sec[index], }) return pd.DataFrame(results) # Get top 10 matches df_result = get_top_n_similarity_matches(response.hits[0], N=10) print(df_result)
出力される DataFrame には以下が含まれます:
Index: 動画内でのセグメントを示すインデックス。
Similarity Score: ベクトル類似度に基づく関連性スコア。
Start/End Offsets: 秒単位でのセグメント開始・終了時間境界。
以下の表は、クエリから得られた上位10件の類似検索結果の例です:

このフォーマットにより、詳細な分析や動画再生において、最も関連性の高い複数のセグメントを素早く特定しやすくなります。
概要
TwelveLabs の Embed API を使用してクエリの埋め込みを生成しました。
Vespa でテキスト検索(BM25)とベクトル類似度に基づいたハイブリッド検索を実行しました。
上位の検索結果を分析しやすいように pandas の DataFrame に変換して処理しました。
このワークフローは、TwelveLabsのマルチモーダル埋め込みとVespaの高度な検索機能を用いて、関連度が高い該当セグメントを効率的に見つけ出す方法を実証しています。
ステップ 5: 結果の確認
このステップでは、関連する動画の区間をグループ化し、整理することで、セマンティック検索の結果を確認します。その後、ノートブックに埋め込まれた動画プレーヤーを使ってビジュアルに結果を再生し、その関連性を評価します。
隣接する類似セグメントの統合
結果をより簡単に確認できるように、同一箇所の連続した動画セグメント(各セグメントの開始・終了時刻に基づく)を一つの範囲として統合する必要があります。また、切り替わりがスムーズになるように、各セグメントの境界に 3 秒間ののりしろ(オーバーラップ)を設けます。以下の関数は、この統合を行い、読みやすさを考慮して時間オフセットを MM:SS 形式に変換します。
例えば、上位の検索結果に重複する時間帯や隣接する時間帯が含まれている場合、この関数はそれらを単一の範囲にマージします。その出力は、以下のようになります:
Consolidated Segments (MM:SS): [('20:15', '20:27'), ('20:39', '21:21'), ('22:51', '23:15'
動画プレーヤーを使用した結果の視覚化
特定されたセグメントを実際に確認するために、ノートブック上に簡単な動画プレーヤーを埋め込みます。統合された時間範囲に手動で進めることも、指定した時間にジャンプするようにプログラムを設定することもできます。
動画プレーヤーの埋め込み
from IPython.display import HTML # Define video URL (replace with your video URL) video_url = "https://ia601401.us.archive.org/1/items/twas-the-night-before-christmas-1974-full-movie-freedownloadvideo.net/twas-the-night-before-christmas-1974-full-movie-freedownloadvideo.net.mp4" # Define middle point of a segment for playback preview (e.g., first consolidated segment) middle_point = (1272 + 1278) / 2 # Example from top match # Generate HTML for video player video_player = f""" <video id="myVideo" width="640" height="480" controls> <source src="{video_url}" type="video/mp4"> Your browser does not support the video tag. </video """ # Display the video player HTML(video_player)
このコードは、指定された時間(middle_point)から再生を開始するインタラクティブな動画プレーヤーを生成します。興味のある別のポイントに基づいて middle_point の数値を変更して確認できます。

検索結果の評価
プレーヤーが埋め込まれたら、統合された時間範囲を手動またはシークバーで移動しながら、各ビジュアルを視覚的に評価できます。先ほど生成された segments のリストを利用し、クエリとの高い関連が確実視される一致箇所を見つけて再生してください。
例えば:
セグメント:
'20:21' - '21:18'該当シーンの確認: このセグメントには、そりに乗ったサンタクロースがはっきりと映っており、本ハイブリッド探索が関連性の高いコンテンツを正確に認識・抽出できていることが証明されます。
終わりに
TwelveLabsの Embed API と Vespa を組み合わせることで、高度なセマンティック動画検索アプリケーションの実用可能な可能性が大きく広がります。 Embed API は、ビデオの時間軸や文脈的なニュアンスをとらえる豊富なマルチモーダル埋め込みを提供し、Vespa の強力なインデックス登録およびハイブリッド検索機能によって、求める該当箇所を非常に高速で特定できます。
この統合がもたらす主な利点は以下の通りです:
マルチモーダルな理解: TwelveLabs が生成する統一された埋め込みにより、複数のメディア要素にまたがる動画の内容を包括的に表現できます。
優れたスケーラビリティ: 大規模なデータセットでも処理能力が高く、大量のデータが存在するスケール時にも低レイテンシで処理・検索を実行できます。
洗練されたハイブリッド検索: キーワードベースの(BM25)テキスト検索に、ベクトル的な意味類似度(ANN)を掛け合わせることで、目的の箇所の高い識別精度を維持できます。
高度な柔軟性: 開発者は、要件に応じたスキーマ設計、独自のランク構成、様々なクエリロジックを必要に合わせて自由にカスタマイズできます。
本チュートリアルを参照することで、ユーザー体験を飛躍的に向上させ、動画アーカイブなどのコンテンツ検索における新たなアイデアを形にする、スケーラブルで知的な動画検索システムを構築できるようになります。




