モデル別実行レシピ

韓国語文書検索にはどの埋め込みモデルを使う? Qwen3・BGE・EmbeddingGemma 2を比較

モデル名より、同じ質問で正しい段落を見つけるか測ります。

休暇規程PDFを共通のテスト文書にし、モデルごとの検索結果と索引・質問時間を記録します。CPU1でモデルを1つずつ実行する例とHit@5計算を紹介します。

実行条件と要点
  • PDF・段落・質問・正解IDを固定し、モデルだけを変更します。
  • 回答可能な質問のHit@5と索引・検索時間を別々に記録します。

同じ休暇規程PDFで比較します

Qwen3-Embedding、BGE-M3、EmbeddingGemma 2は、質問と文書を数値ベクトルに変換して関連する段落を探す埋め込みモデルです。同じ休暇規定のPDFと質問を使って、3モデルの検索結果を比較します。リランカーは検索候補と質問を再度照合して順序を調整し、最終回答は別のLLM2が作成します。

休暇規程PDFから「入社6か月の社員も夏季休暇を取れますか」という根拠を探します。抽出テキストを原本と照合し、質問ごとに正解段落IDを付けます。

文書が検索用の段落に分かれ、一部がローカルPCへ送られる様子
モデルを比較するには文書を同じ方法で分割します。

チェックポイントと入力形式をそろえます

Qwen3-Embedding-0.6Bは公式表で32K入力と1,024次元ベクトルに対応し、質問に検索指示を付ける方法を推奨します。BGE-M3は8,192トークン3とdense・sparse・multi-vectorに対応し、dense例では質問指示を追加しません。まずdense検索を比較します。

EmbeddingGemma 2はテキスト専用の270M構成を選べ、128・256・512・768次元に対応します。テキスト用のタスク指示を使い、まず標準次元で比較します。パラメーター数や次元数はディスク上の索引容量と同じではないため、別に測ります。

モデルカードに記載されたテキスト検索の条件
モデル標準ベクトル質問入力
Qwen3-Embedding-0.6B1,024次元・32K英語の検索指示
BGE-M31,024次元・8,192dense例に追加指示なし
EmbeddingGemma 2768次元・8,192SearchQuery接頭辞

CPUでモデルを1つずつ測定します

同じcorpus.jsonとquestions.jsonで3モデルを1つずつ比較します。ファイルがない場合はscriptが停止します。正解が1つの回答可能な質問では、Hit@5を上位5件に正解IDが含まれる割合として計算します。gold_ids: []は別に表示します。

モデル読み込み・文書埋め込み・質問検索の時間を出力します。.npy容量は数値vector配列だけで、vector DB索引やmetadataを含みません。このcodeは配列容量のみを表示します。

下の2つのJSON配列を同名のfileとして保存し、Python codeはqwen_retrieval_baseline.pyとして保存します。2文書は形式例のみです。Hit@5比較には大きな業務corpusを使います。

corpus.json
[
  {"id":"p-001","title":"여름휴가 자격","text":"입사 6개월 이상인 직원은 여름휴가를 신청할 수 있습니다."},
  {"id":"p-002","title":"신청 기한","text":"사용 희망일 14일 전까지 신청해야 합니다."}
]
同じfield形式で業務文書を追加します。
questions.json
[
  {"id":"q-001","text":"입사 6개월 차도 여름휴가를 신청할 수 있나요?","gold_ids":["p-001"]},
  {"id":"q-002","text":"퇴직 후 미사용 휴가를 현금으로 받나요?","gold_ids":[]}
]
回答がない質問では`gold_ids`を空配列にします。
環境を作成
python -m venv .venv
source .venv/bin/activate
python -m pip install -U pip
python -m pip install 'sentence-transformers>=5.0' numpy FlagEmbedding
python qwen_retrieval_baseline.py --model qwen
python qwen_retrieval_baseline.py --model bge
python qwen_retrieval_baseline.py --model gemma
モデルを1つずつ試す最小構成です。初回に重みをダウンロードします。
qwen_retrieval_baseline.py — 固定corpusで実行
import argparse, json, time
from pathlib import Path
import numpy as np
MODELS = {"qwen":"Qwen/Qwen3-Embedding-0.6B", "bge":"BAAI/bge-m3", "gemma":"google/embeddinggemma-2"}
INSTRUCTION = "Instruct: Given a Korean policy question, retrieve passages that answer the question\nQuery: "
p = argparse.ArgumentParser(); p.add_argument("--model", choices=MODELS, required=True); args=p.parse_args()
for name in ("corpus.json", "questions.json"):
    if not Path(name).is_file(): raise SystemExit(f"Missing {name}; save the JSON example first.")
docs_data=json.loads(Path("corpus.json").read_text(encoding="utf-8")); questions=json.loads(Path("questions.json").read_text(encoding="utf-8"))
if not docs_data or not questions: raise SystemExit("Both JSON files need records.")
start=time.perf_counter()
if args.model=="bge":
    from FlagEmbedding import BGEM3FlagModel
    model=BGEM3FlagModel(MODELS[args.model], use_fp16=False, devices=["cpu"])
else:
    from sentence_transformers import SentenceTransformer
    kw={"device":"cpu"}
    if args.model=="gemma": kw["config_kwargs"]={"vision_config":None,"audio_config":None}
    model=SentenceTransformer(MODELS[args.model], **kw)
load_s=time.perf_counter()-start
docs=[f"title: {d.get('title','none')} | text: {d['text']}" for d in docs_data]
start=time.perf_counter()
if args.model=="bge": vectors=model.encode(docs,batch_size=2,max_length=8192)["dense_vecs"]
else: vectors=model.encode(docs,batch_size=2,normalize_embeddings=True,convert_to_numpy=True)
vectors=np.asarray(vectors,dtype=np.float32); index_s=time.perf_counter()-start
out=Path(f"{args.model}-vectors.npy"); np.save(out,vectors)
hits=answerable=0; no_answer=[]; rows=[]; start=time.perf_counter()
for q in questions:
    gold=q.get("gold_ids",[])
    if not gold: no_answer.append(q["id"]); continue
    if len(gold)!=1: raise ValueError(f"Exactly one gold ID required: {q['id']}")
    text=q["text"]
    if args.model=="qwen": text=INSTRUCTION+text
    if args.model=="gemma": text="task: search result | query: "+text
    if args.model=="bge": qv=model.encode([text],batch_size=1,max_length=8192)["dense_vecs"][0]
    else: qv=model.encode([text],normalize_embeddings=True,convert_to_numpy=True)[0]
    top=np.argsort(vectors@np.asarray(qv,dtype=np.float32))[-5:][::-1]; ids=[docs_data[i]["id"] for i in top]
    hit=gold[0] in ids; hits+=hit; answerable+=1; rows.append({"query_id":q["id"],"hit_at_5":bool(hit),"top5_ids":ids})
search_s=time.perf_counter()-start
print(json.dumps({"model":args.model,"model_load_s":round(load_s,3),"document_embedding_s":round(index_s,3),"query_search_s":round(search_s,3),"answerable_queries":answerable,"hit_at_5":hits/answerable if answerable else None,"no_answer_query_ids":no_answer,"raw_vector_array_bytes_in_ram":int(vectors.nbytes),"raw_npy_file_bytes_only":out.stat().st_size,"top5":rows},ensure_ascii=False,indent=2))
`qwen_retrieval_baseline.py`として保存して下のcommandを実行します。
質問ごとの正解根拠をカードに整理した様子
正解段落を指定すると、検索が条件を見落としたか確認できます。

同じ候補集合でリランカーを比較します

このcodeはdense初段検索までです。rerankingを試す場合、同じ質問で取得した候補IDを保存し、共通候補をBAAI/bge-reranker-v2-m3またはQwen/Qwen3-Reranker-0.6Bに別途渡します。

候補生成とrerankerの時間を分け、再順位付け前後の正解順位とHit@5を比べると後処理コストを評価できます。現行codeにreranker実行は含みません。

2台のPCで文書の索引作成と検索結果を比較する様子
品質と時間を一緒に記録し、業務への影響を見ます。

用語の注釈

  1. CPU — OSや一般的なプログラム命令を実行する中央処理装置です。AIでは前処理やデータ転送などの汎用処理も担います。

    本文に戻る
  2. 大規模言語モデル — 大量のテキストデータから言語パターンを学び、文脈に沿ったテキストを処理・生成するモデルです。

    本文に戻る
  3. トークン — モデルが入力や出力を分けて処理する単位です。

    本文に戻る

よくある質問

この記事で最初に確認すべきことは何ですか?
休暇規程PDFを共通のテスト文書にし、モデルごとの検索結果と索引・質問時間を記録します。CPUでモデルを1つずつ実行する例とHit@5計算を紹介します。