4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Databricks × Qwen3-Embedding-0.6Bで日英クロスリンガルRAGを試す

4
Posted at

2026年2月、DatabricksのFoundation Model APIsにQwen3-Embedding-0.6BがPublic Previewとして追加されました。これまでDatabricksで利用できるホステッドembeddingモデルはGTE Large(英語のみ)とBGE Large(英語のみ)に限られていたため、日本語を扱うRAGシステムでは外部モデルやカスタムエンドポイントが必要でした。

Qwen3-Embedding-0.6Bは100以上の言語をサポートするマルチリンガルモデルです。本記事では、日本語と英語が混在するドキュメントに対して言語を超えた検索(クロスリンガル検索)がどの程度機能するか、既存のGTE Largeと比較しながら検証します。

Qwen3-Embedding-0.6Bとは

Alibaba CloudのQwenチームが開発したテキストembeddingモデルで、Qwen3基盤モデルの上にembeddingタスク向けのfine-tuningが施されています。

項目 仕様
パラメータ数 0.6B
Embedding次元数 1024(MRLで32〜1024まで調整可)
コンテキスト長 8,192トークン
多言語対応 100以上の言語
Instruction-aware あり(タスク指示で精度向上)
ライセンス Apache 2.0

0.6B/4B/8Bの3サイズが公開されていますが、Databricksがホストしているのは最軽量の0.6Bです。8BモデルはMTEBマルチリンガルリーダーボードで1位(2025年6月時点、スコア70.58)を獲得しており、0.6Bでもそのアーキテクチャの恩恵を受けています。

検証シナリオ

日本企業でよくある「技術仕様書は英語、業務マニュアルは日本語」という混在環境を想定します。

ドキュメント(8件)

ID 言語 カテゴリ 内容
doc_en_01 EN API仕様 OAuth 2.0 + PKCE認証フロー
doc_en_02 EN アーキテクチャ Kafka → Spark Streaming → Delta Lakeパイプライン
doc_en_03 EN API仕様 レート制限(1000 req/min)とリトライ戦略
doc_en_04 EN セキュリティ AES-256暗号化、TLS 1.3、RBAC
doc_ja_01 JA 業務フロー 3段階承認ワークフロー
doc_ja_02 JA 運用手順 障害発生時のインシデント対応手順
doc_ja_03 JA データガバナンス 個人情報の取り扱い規定
doc_ja_04 JA 運用手順 データパイプラインの監視方法

テストクエリ(7件)

言語をまたぐケース(JA→EN, EN→JA)と同一言語ケース(JA→JA, EN→EN)を混ぜて評価します。

クエリ 検索タイプ 期待するドキュメント
認証の仕組みを教えて JA→EN doc_en_01
How does the approval workflow work? EN→JA doc_ja_01
障害が起きたらどうすればいい? JA→JA doc_ja_02
What is the data encryption policy? EN→EN doc_en_04
APIの利用制限について JA→EN doc_en_03
How is personal data handled? EN→JA doc_ja_03
データパイプラインの構成 JA→EN doc_en_02

実装

Databricks Foundation Model APIsのpay-per-tokenエンドポイントを使います。追加ライブラリのインストールは不要で、Databricks SDKだけで完結します。

Embeddingの取得

from databricks.sdk import WorkspaceClient

w = WorkspaceClient()

QWEN3_ENDPOINT = "databricks-qwen3-embedding-0-6b"
GTE_ENDPOINT = "databricks-gte-large-en"

def get_embeddings(texts: list[str], endpoint: str) -> list[list[float]]:
    response = w.serving_endpoints.query(
        name=endpoint,
        input=texts,
    )
    return [item.embedding for item in response.data]

検索関数

コサイン類似度で最も近いドキュメントを返すシンプルな実装です。

import numpy as np

def cosine_similarity(a, b):
    a, b = np.array(a), np.array(b)
    return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))

def search(query, doc_embeddings, endpoint, top_k=3):
    query_emb = get_embeddings([query], endpoint)[0]
    scores = [
        (i, cosine_similarity(query_emb, emb))
        for i, emb in enumerate(doc_embeddings)
    ]
    scores.sort(key=lambda x: x[1], reverse=True)
    return scores[:top_k]

評価

7つのテストケースそれぞれで、期待されるドキュメントが検索結果の何位に来るかを確認し、Hit@1(1位に来るか)とHit@3(3位以内に来るか)を集計します。

test_cases = [
    {"query": "認証の仕組みを教えて",                    "expected_id": "doc_en_01", "type": "JA→EN"},
    {"query": "How does the approval workflow work?",    "expected_id": "doc_ja_01", "type": "EN→JA"},
    {"query": "障害が起きたらどうすればいい?",            "expected_id": "doc_ja_02", "type": "JA→JA"},
    {"query": "What is the data encryption policy?",     "expected_id": "doc_en_04", "type": "EN→EN"},
    {"query": "APIの利用制限について",                    "expected_id": "doc_en_03", "type": "JA→EN"},
    {"query": "How is personal data handled?",           "expected_id": "doc_ja_03", "type": "EN→JA"},
    {"query": "データパイプラインの構成",                  "expected_id": "doc_en_02", "type": "JA→EN"},
]

結果

モデル Hit@1 Hit@3 平均スコア
Qwen3-Embedding-0.6B 71.4% 100.0% 0.6005
GTE-Large-En 28.6% 42.9% 0.5137

Qwen3-Embedding-0.6BがHit@1で2.5倍、Hit@3では100%対42.9%と大きな差をつけました。

可視化

グラフで見るとさらに差が明確です。ノートブックには以下の3つの可視化を含めています。

Hit@1 / Hit@3 比較

import plotly.graph_objects as go

models = ["Qwen3-Embedding-0.6B", "GTE-Large-En"]
hit1 = [71.4, 28.6]
hit3 = [100.0, 42.9]

fig = go.Figure()
fig.add_trace(go.Bar(name="Hit@1", x=models, y=hit1, marker_color="#2563EB",
                     text=[f"{v:.1f}%" for v in hit1], textposition="outside"))
fig.add_trace(go.Bar(name="Hit@3", x=models, y=hit3, marker_color="#60A5FA",
                     text=[f"{v:.1f}%" for v in hit3], textposition="outside"))
fig.update_layout(
    title="クロスリンガル検索精度の比較",
    yaxis=dict(title="正解率 (%)", range=[0, 115]),
    barmode="group", template="plotly_white", height=450,
)
fig.show()

newplot.png

このグラフでは、Hit@3でQwen3が100%(全問正解)を達成している一方、GTE Largeは半分以下にとどまっていることが一目でわかります。

テストケース別スコア比較

7つのテストケースそれぞれのコサイン類似度を並べたグラフも用意しています。クロスリンガルのケース(黄色背景)でGTE Largeのスコアが大きく落ち込むのに対し、Qwen3は安定して高いスコアを維持しています。

newplot (1).png

クロスリンガル vs 同一言語

テストケースを「クロスリンガル(JA↔EN)」と「同一言語(JA→JA / EN→EN)」に分けて平均スコアを比較すると、最も差が出るのはクロスリンガルのケースです。GTE Largeは同一言語ではそこそこ機能するものの、言語をまたいだ瞬間にスコアが大幅に下がることが確認できます。

newplot (2).png

何が起きているのか

GTE Largeは英語のテキストに対して最適化されたモデルです。英語同士(EN→EN)の検索ではそれなりに機能しますが、日本語クエリで英語ドキュメントを検索するケース(JA→EN)や、英語クエリで日本語ドキュメントを検索するケース(EN→JA)では、言語の壁を超えられず関係のないドキュメントが上位に来てしまいます。

一方、Qwen3-Embedding-0.6Bは多言語で学習されているため、「認証の仕組みを教えて」と「OAuth 2.0 with PKCE flow」が意味的に近いことを理解でき、言語に関係なく適切なドキュメントを返します。Hit@3が100%ということは、7テストケース全てで正解が3位以内に入っているということです。

0.6Bの軽量さ

この精度が0.6Bパラメータ(GTE Largeよりも小さい)で実現されている点も注目です。Databricksのpay-per-tokenエンドポイントとして提供されているため、GPU環境の構築やモデルのデプロイなしに即座に利用できます。

ノートブック全体

本記事で使用したノートブックの全体を掲載します。Databricksワークスペースにインポートしてそのまま実行できます。

エンドポイント名databricks-qwen3-embedding-0-6bはPublic Preview時点のものです。ワークスペースのServingページで実際のエンドポイント名を確認してください。

ノートブック全体(クリックで展開)
# Databricks notebook source
# MAGIC %pip install databricks-sdk --upgrade -q
# MAGIC dbutils.library.restartPython()

# COMMAND ----------

import json
import numpy as np
import pandas as pd
from databricks.sdk import WorkspaceClient

w = WorkspaceClient()

# COMMAND ----------

# サンプルドキュメント(日英混在)
documents = [
    {"id": "doc_en_01", "lang": "en", "category": "API Specification",
     "text": "The authentication system uses OAuth 2.0 with PKCE flow. Users must first obtain an authorization code by redirecting to the /authorize endpoint, then exchange it for an access token via the /token endpoint. Tokens expire after 3600 seconds."},
    {"id": "doc_en_02", "lang": "en", "category": "Architecture",
     "text": "The data pipeline processes incoming events through Apache Kafka, transforms them using Apache Spark Structured Streaming, and stores the results in Delta Lake tables. The pipeline supports exactly-once semantics and handles late-arriving data with watermarks."},
    {"id": "doc_en_03", "lang": "en", "category": "API Specification",
     "text": "Rate limiting is enforced at 1000 requests per minute per API key. When the limit is exceeded, the API returns HTTP 429 with a Retry-After header. Clients should implement exponential backoff with jitter for retry logic."},
    {"id": "doc_en_04", "lang": "en", "category": "Security",
     "text": "All data at rest is encrypted using AES-256. Data in transit is protected with TLS 1.3. Access control is managed through Role-Based Access Control (RBAC) with predefined roles: Admin, Editor, and Viewer."},
    {"id": "doc_ja_01", "lang": "ja", "category": "業務フロー",
     "text": "承認ワークフローは3段階で構成されています。まず申請者がフォームを提出し、次に直属の上長が内容を確認して承認または差し戻しを行います。最終的に部門長が最終承認を行い、システムに反映されます。"},
    {"id": "doc_ja_02", "lang": "ja", "category": "運用手順",
     "text": "障害発生時の対応手順です。まずSlackの#incident-responseチャンネルに報告し、インシデントコマンダーを指名します。次に影響範囲の特定とステークホルダーへの連絡を行い、復旧対応を開始します。"},
    {"id": "doc_ja_03", "lang": "ja", "category": "データガバナンス",
     "text": "個人情報を含むデータの取り扱いについての規定です。収集時には利用目的を明示し、本人の同意を得る必要があります。保管期間は原則3年とし、期間経過後は速やかに削除します。"},
    {"id": "doc_ja_04", "lang": "ja", "category": "運用手順",
     "text": "データパイプラインの監視方法について説明します。Databricksのジョブ画面で各パイプラインの実行状況を確認し、エラー発生時にはログを確認して原因を特定します。日次バッチは毎朝6時に実行されます。"},
]

# COMMAND ----------

QWEN3_ENDPOINT = "databricks-qwen3-embedding-0-6b"
GTE_ENDPOINT = "databricks-gte-large-en"

def get_embeddings(texts, endpoint):
    response = w.serving_endpoints.query(name=endpoint, input=texts)
    return [item.embedding for item in response.data]

def cosine_similarity(a, b):
    a, b = np.array(a), np.array(b)
    return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))

def search(query, doc_embeddings, endpoint, top_k=3):
    query_emb = get_embeddings([query], endpoint)[0]
    scores = [(i, cosine_similarity(query_emb, e)) for i, e in enumerate(doc_embeddings)]
    scores.sort(key=lambda x: x[1], reverse=True)
    results = []
    for idx, score in scores[:top_k]:
        doc = documents[idx]
        results.append({
            "rank": len(results) + 1, "score": round(float(score), 4),
            "id": doc["id"], "lang": doc["lang"], "category": doc["category"],
            "text": doc["text"][:80] + "...",
        })
    return pd.DataFrame(results)

# COMMAND ----------

# Embedding生成
doc_texts = [doc["text"] for doc in documents]
doc_embeddings_qwen3 = get_embeddings(doc_texts, QWEN3_ENDPOINT)
doc_embeddings_gte = get_embeddings(doc_texts, GTE_ENDPOINT)

# COMMAND ----------

# クロスリンガル検索テスト
for q in ["認証の仕組みを教えて", "How does the approval workflow work?"]:
    print(f"\n🔍 クエリ: {q}")
    print("\n📊 Qwen3-Embedding-0.6B")
    display(search(q, doc_embeddings_qwen3, QWEN3_ENDPOINT))
    print("\n📊 GTE Large (English only)")
    display(search(q, doc_embeddings_gte, GTE_ENDPOINT))

# COMMAND ----------

# 定量評価
test_cases = [
    {"query": "認証の仕組みを教えて", "expected_id": "doc_en_01", "type": "JA→EN"},
    {"query": "How does the approval workflow work?", "expected_id": "doc_ja_01", "type": "EN→JA"},
    {"query": "障害が起きたらどうすればいい?", "expected_id": "doc_ja_02", "type": "JA→JA"},
    {"query": "What is the data encryption policy?", "expected_id": "doc_en_04", "type": "EN→EN"},
    {"query": "APIの利用制限について", "expected_id": "doc_en_03", "type": "JA→EN"},
    {"query": "How is personal data handled?", "expected_id": "doc_ja_03", "type": "EN→JA"},
    {"query": "データパイプラインの構成", "expected_id": "doc_en_02", "type": "JA→EN"},
]

def evaluate_model(doc_embeddings, endpoint, model_name):
    results = []
    for tc in test_cases:
        query_emb = get_embeddings([tc["query"]], endpoint)[0]
        scores = [(i, cosine_similarity(query_emb, e)) for i, e in enumerate(doc_embeddings)]
        scores.sort(key=lambda x: x[1], reverse=True)
        expected_idx = next(i for i, d in enumerate(documents) if d["id"] == tc["expected_id"])
        rank = next(r + 1 for r, (idx, _) in enumerate(scores) if idx == expected_idx)
        expected_score = next(s for idx, s in scores if idx == expected_idx)
        results.append({
            "model": model_name, "type": tc["type"],
            "query": tc["query"][:25] + "...", "expected_doc": tc["expected_id"],
            "rank": rank, "hit@1": "" if rank == 1 else "",
            "hit@3": "" if rank <= 3 else "", "score": round(float(expected_score), 4),
        })
    return results

all_results = pd.DataFrame(
    evaluate_model(doc_embeddings_qwen3, QWEN3_ENDPOINT, "Qwen3-Embedding-0.6B") +
    evaluate_model(doc_embeddings_gte, GTE_ENDPOINT, "GTE-Large-En")
)
display(all_results)

# COMMAND ----------

# サマリー
summary = all_results.groupby("model").agg(
    hit_at_1=("hit@1", lambda x: (x == "").sum()),
    hit_at_3=("hit@3", lambda x: (x == "").sum()),
    total=("hit@1", "count"), avg_score=("score", "mean"),
).reset_index()
summary["hit@1_rate"] = (summary["hit_at_1"] / summary["total"] * 100).round(1).astype(str) + "%"
summary["hit@3_rate"] = (summary["hit_at_3"] / summary["total"] * 100).round(1).astype(str) + "%"
summary["avg_score"] = summary["avg_score"].round(4)
display(summary[["model", "hit@1_rate", "hit@3_rate", "avg_score"]])

# COMMAND ----------

# 可視化
import plotly.graph_objects as go

# Hit@1 / Hit@3 比較
models = ["Qwen3-Embedding-0.6B", "GTE-Large-En"]
hit1 = [71.4, 28.6]
hit3 = [100.0, 42.9]
fig = go.Figure()
fig.add_trace(go.Bar(name="Hit@1", x=models, y=hit1, marker_color="#2563EB",
                     text=[f"{v:.1f}%" for v in hit1], textposition="outside"))
fig.add_trace(go.Bar(name="Hit@3", x=models, y=hit3, marker_color="#60A5FA",
                     text=[f"{v:.1f}%" for v in hit3], textposition="outside"))
fig.update_layout(title="クロスリンガル検索精度の比較",
                  yaxis=dict(title="正解率 (%)", range=[0, 115]),
                  barmode="group", template="plotly_white", height=450)
fig.show()

# COMMAND ----------

# テストケース別スコア比較
df_qwen3 = all_results[all_results["model"] == "Qwen3-Embedding-0.6B"].copy()
df_gte = all_results[all_results["model"] == "GTE-Large-En"].copy()
labels = [f'{q}<br>({t})' for q, t in zip(df_qwen3["query"].values, df_qwen3["type"].values)]
shapes = [
    dict(type="rect", x0=i - 0.45, x1=i + 0.45, y0=0, y1=1,
         fillcolor="#FEF3C7", opacity=0.4, layer="below", line_width=0)
    for i, t in enumerate(df_qwen3["type"].values)
    if "" in t and t.split("")[0] != t.split("")[1]
]
fig = go.Figure()
fig.add_trace(go.Bar(name="Qwen3-Embedding-0.6B", x=labels, y=df_qwen3["score"].values, marker_color="#2563EB"))
fig.add_trace(go.Bar(name="GTE-Large-En", x=labels, y=df_gte["score"].values, marker_color="#F87171"))
fig.update_layout(title="テストケース別の検索スコア比較(黄色背景 = クロスリンガル)",
                  yaxis=dict(title="コサイン類似度", range=[0, 1.0]),
                  barmode="group", template="plotly_white", shapes=shapes, height=500)
fig.show()

# COMMAND ----------

# クロスリンガル vs 同一言語
all_results["cross_lingual"] = all_results["type"].apply(
    lambda t: "クロスリンガル (JA↔EN)" if t.split("")[0] != t.split("")[1] else "同一言語 (JA→JA / EN→EN)"
)
groups = all_results.groupby(["cross_lingual", "model"])["score"].mean().reset_index()
fig = go.Figure()
for model, color in [("Qwen3-Embedding-0.6B", "#2563EB"), ("GTE-Large-En", "#F87171")]:
    df = groups[groups["model"] == model]
    fig.add_trace(go.Bar(name=model, x=df["cross_lingual"], y=df["score"], marker_color=color,
                         text=[f"{v:.3f}" for v in df["score"]], textposition="outside"))
fig.update_layout(title="クロスリンガル vs 同一言語: 平均スコア比較",
                  yaxis=dict(title="平均コサイン類似度", range=[0, 0.9]),
                  barmode="group", template="plotly_white", height=450)
fig.show()

次のステップ

このデモでは最もシンプルな構成でクロスリンガル検索の効果を確認しました。実用に向けてはいくつかの発展が考えられます。

Vector Search連携: Databricks Vector Searchのインデックス作成時にembeddingモデルとしてQwen3-Embedding-0.6Bを指定すれば、Delta Lakeテーブルの更新に連動して自動的にインデックスが更新されるマネージドなベクトル検索が構築できます。

MRLによるコスト最適化: Qwen3-EmbeddingはMatryoshka Representation Learning(MRL)に対応しており、embedding次元数を1024から512や256に削減できます。次元数を半分にすればVector Searchのストレージコストも半分になるため、精度とコストのトレードオフを検証する価値があります。

RAGチャットボット構築: Qwen3-Embedding-0.6Bで検索し、Qwen3-Next 80B A3B Instructなどの LLMで回答を生成する構成にすれば、日英どちらの言語で質問しても適切に回答できるRAGチャットボットが構築できます。

参考リンク

はじめてのDatabricks

はじめてのDatabricks

Databricks無料トライアル

Databricks無料トライアル

4
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?