モデル別実行レシピ
最終更新
韓国語文書検索にはどの埋め込みモデルを使う? Qwen3・BGE・EmbeddingGemma 2を比較
モデル名より、同じ質問で正しい段落を見つけるか測ります。
休暇規程PDFを共通のテスト文書にし、モデルごとの検索結果と索引・質問時間を記録します。CPU1でモデルを1つずつ実行する例とHit@5計算を紹介します。
同じ休暇規程PDFで比較します
Qwen3-Embedding、BGE-M3、EmbeddingGemma 2は、質問と文書を数値ベクトルに変換して関連する段落を探す埋め込みモデルです。同じ休暇規定のPDFと質問を使って、3モデルの検索結果を比較します。リランカーは検索候補と質問を再度照合して順序を調整し、最終回答は別のLLM2が作成します。
休暇規程PDFから「入社6か月の社員も夏季休暇を取れますか」という根拠を探します。抽出テキストを原本と照合し、質問ごとに正解段落IDを付けます。

チェックポイントと入力形式をそろえます
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.6B | 1,024次元・32K | 英語の検索指示 |
| BGE-M3 | 1,024次元・8,192 | dense例に追加指示なし |
| EmbeddingGemma 2 | 768次元・8,192 | SearchQuery接頭辞 |
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を使います。
[
{"id":"p-001","title":"여름휴가 자격","text":"입사 6개월 이상인 직원은 여름휴가를 신청할 수 있습니다."},
{"id":"p-002","title":"신청 기한","text":"사용 희망일 14일 전까지 신청해야 합니다."}
][
{"id":"q-001","text":"입사 6개월 차도 여름휴가를 신청할 수 있나요?","gold_ids":["p-001"]},
{"id":"q-002","text":"퇴직 후 미사용 휴가를 현금으로 받나요?","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 gemmaimport 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))
同じ候補集合でリランカーを比較します
このcodeはdense初段検索までです。rerankingを試す場合、同じ質問で取得した候補IDを保存し、共通候補をBAAI/bge-reranker-v2-m3またはQwen/Qwen3-Reranker-0.6Bに別途渡します。
候補生成とrerankerの時間を分け、再順位付け前後の正解順位とHit@5を比べると後処理コストを評価できます。現行codeにreranker実行は含みません。

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