📖
EmbeddingGemmaでベクトル検索をやってみる
はじめに
EmbeddingGemma が発表されました。
テキストを固定長ベクトルに変換できる埋め込みモデルで、類似度検索やレコメンド、クラスタリングなどに向いています。詳細はブログ記事や公式の解説をご参照ください。
試してみること
EmbeddingGemmaを使って、Zennに投稿している自分の記事一覧をRSSで取得し、簡素なベクトル検索を試します。
機能概要
- 検索ワードを指定
- Zennの自分のアカウントから「タイトル/URL/サマリー」を取得
- タイトル60%+RSSサマリ40%の重みで一致度を計算
- 上位3件を表示
実行
Hugging FaceでEmbeddingGemmaを使うため、アクセストークン(API Key)を準備してください。
Google Colab で実行できるノートブックを用意しました。
コード
!pip install sentence-transformers faiss-cpu beautifulsoup4 feedparser tqdm
# hugging faceのAPIKEY
from huggingface_hub import login
# 自分のAPIKEYを入力(環境変数での設定を推奨)
login(token="xxxxxxxxxxxxxx")
print("HF login ok")
from __future__ import annotations
# ===== 依存・型 =====
from sentence_transformers import SentenceTransformer
import feedparser, requests, bs4, re, numpy as np, torch
from typing import List, Dict, Tuple
# ===== ユーザー設定 =====
USER = "hikaruy"
FEED_URL = f"https://zenn.dev/{USER}/feed"
QUERY = "causal impactについて"
TOP_K = 3 # 上位何件を返すか
WEIGHT_VALUE = 0.6 # 本文ベクトルの重み
WEIGHT_SUMMARY = 0.4 # サマリベクトルの重み
REQUEST_TIMEOUT = 20 # HTTPタイムアウト(秒)
USER_AGENT = ( # シンプルなUA(ブロック回避用)
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/124.0 Safari/537.36"
)
# ===== モデルのロード =====
# - GPUがあれば自動でCUDAを利用
# - SentenceTransformerでEmbeddingGemmaを読み込み
model_id = "google/embeddinggemma-300m"
device = "cuda" if torch.cuda.is_available() else "cpu"
model = SentenceTransformer(model_id).to(device)
print("Embedding dim:", model.get_sentence_embedding_dimension(), "| device:", device)
# ===== ユーティリティ群 =====
def _normalize(x: np.ndarray) -> np.ndarray:
"""L2正規化(数値安定性のための微小値付与)"""
norm = np.linalg.norm(x, axis=-1, keepdims=True) + 1e-12
return x / norm
def reweight_pairwise(w_value: float, w_summary: float) -> Tuple[float, float]:
"""2つの重みを和が1になるよう正規化(両方0ならvalue=1,summary=0)"""
s = max(w_value + w_summary, 0.0)
return (1.0, 0.0) if s == 0 else (w_value / s, w_summary / s)
def encode_texts(model: SentenceTransformer, texts: List[str]) -> np.ndarray:
"""テキスト群を埋め込みベクトル(float32, 正規化済み)へ変換"""
with torch.inference_mode():
emb = model.encode(
texts,
convert_to_numpy=True,
normalize_embeddings=True, # ここでL2正規化
show_progress_bar=False,
)
return emb.astype("float32")
def fetch_clean_text(url: str) -> str:
"""URLを取得→HTMLをプレーンテキストへ整形(script/style除去・空白圧縮)"""
r = requests.get(url, timeout=REQUEST_TIMEOUT, headers={"User-Agent": USER_AGENT})
r.raise_for_status()
soup = bs4.BeautifulSoup(r.text, "html.parser")
for bad in soup(["script", "style", "noscript"]):
bad.decompose()
text = soup.get_text("\n")
text = re.sub(r"\n{2,}", "\n", text)
text = re.sub(r"[ \t]{2,}", " ", text)
return text.strip()
def make_summary(text: str, limit: int = 200) -> str:
"""1行要約(全角・半角空白をまとめ、指定文字数で省略)"""
t = re.sub(r"\s+", " ", text or "").strip()
return (t[:limit] + "…") if len(t) > limit else t
# ===== データ生成 =====
def build_items_from_feed(entries) -> List[Dict[str, str]]:
"""
RSSエントリから検索対象レコードを作成
- base : タイトルとURL(表示用)
- value : 記事本文(HTML除去後のプレーンテキスト)
- summary: RSS要約(最大800文字)
"""
items: List[Dict[str, str]] = []
for e in entries:
title = getattr(e, "title", "").strip()
link = getattr(e, "link", "").strip()
summ = (getattr(e, "summary", "") or getattr(e, "description", "")).strip()
base = f"{title} | {link}".strip()
# 本文取得(失敗時はsummary→titleの順でフォールバック)
body = ""
if link:
try:
body = fetch_clean_text(link)
except Exception as ex:
print("skip:", link, ex)
if not body:
body = summ or title
items.append({"base": base, "value": body, "summary": summ[:800]})
return items
# ===== 検索本体 =====
def search_by_value_and_summary(
items: List[Dict[str, str]],
query: str,
model: SentenceTransformer,
top_k: int = 1,
w_value: float = 0.6,
w_summary: float = 0.4,
return_score: bool = True,
):
"""
value(本文)とsummary(RSS要約)を重み付きで合成した埋め込みで近似検索
- 手順
1) valueとsummaryを埋め込み
2) 正規化された重みで加算し、再度正規化
3) クエリ埋め込みと内積(コサイン類似度と等価)でランキング
- 返り値
return_score=Trueなら (base, value, summary, score) のタプルを上位top_k件
"""
bases = [str(it.get("base", "")) for it in items]
values = [str(it.get("value", "")) for it in items]
summaries = [str(it.get("summary", "")) for it in items]
wv, ws = reweight_pairwise(w_value, w_summary)
emb_value = encode_texts(model, values)
emb_summary = encode_texts(model, summaries)
emb_items = _normalize(wv * emb_value + ws * emb_summary).astype("float32")
emb_query = encode_texts(model, [query])[0]
scores = emb_items @ emb_query
order = np.argsort(-scores)[:max(1, top_k)]
results = []
for i in order:
tup = (bases[i], values[i], summaries[i])
results.append((*tup, float(scores[i])) if return_score else tup)
return results
# ===== 実行(エントリポイント) =====
# 実行時の流れ:
# 1) RSSを取得 → エントリ一覧を得る
# 2) 各記事の本文を取得してrecordsを構築
# 3) クエリに対して類似度検索
# 4) 上位K件を出力(要約は本文から生成)
if __name__ == "__main__":
# 1) RSS取得
feed = feedparser.parse(FEED_URL)
entries = getattr(feed, "entries", []) or []
print(f"記事数: {len(entries)}")
# 2) レコード構築
items = build_items_from_feed(entries)
print(f"作成レコード数: {len(items)}")
# 3) 重みの正規化 → 検索実行
W_VALUE, W_SUMMARY = reweight_pairwise(WEIGHT_VALUE, WEIGHT_SUMMARY)
hits = search_by_value_and_summary(
items,
QUERY,
model=model,
top_k=TOP_K,
w_value=W_VALUE,
w_summary=W_SUMMARY,
return_score=True,
)
# 4) 結果表示
print("=== Hits ===")
for rank, (base, value, summary, score) in enumerate(hits, 1):
print(f"{rank}番目")
print("Base :", base)
print("Score :", round(score, 3))
print("[Summary]", make_summary(value, 200))
print("--------")
検索ワード
QUERY = "causal impactについて"
結果
Embedding dim: 768 | device: cpu
記事数: 9
作成レコード数: 9
=== Hits ===
1番目
Base : Causal impactを調べてみた。 | https://zenn.dev/hikaruy/articles/4502ebadea0d09
Score : 0.408
[Summary] Causal impactを調べてみた。 Zenn ヤマシタヒカル 👿 Causal impactを調べてみた。 2024/10/27 に公開 2024/10/28 did ベイズ 因果推論 causalimact tech はじめに これまで手持ちデータでPythonライブラリでCausal imactを使っていましたが、モデルの中身を調べず使っていたので、今回、論文や参考サイトなどで調べてみま…
--------
2番目
Base : もう一度、強化学習を理解する | https://zenn.dev/hikaruy/articles/09cfeb1a166cd6
Score : 0.384
[Summary] もう一度、強化学習を理解する Zenn ヤマシタヒカル 🖖 もう一度、強化学習を理解する 2025/01/18 に公開 2025/01/26 強化学習 LLM ppo tech はじめに 強化学習はLLMのRLHFや、ロボット制御、自動運転技術などで活用されています。 以前、私が書いた DPOの記事 で触れたPPOの実装を試そうと思いましたが、強化学習は機械学習や深層学習と比べて抽象的で分かりづら…
--------
3番目
Base : Delta Learning Hypothesis(デルタ学習仮説)をやってみた | https://zenn.dev/hikaruy/articles/667d3dad4209bc
Score : 0.38
[Summary] Delta Learning Hypothesis(デルタ学習仮説)をやってみた Zenn ヤマシタヒカル 📌 Delta Learning Hypothesis(デルタ学習仮説)をやってみた 2025/07/25 に公開 2025/07/27 LLM dpo Gemma 3 orpo deltalearning tech はじめに 「デルタ学習仮説(Delta Learning Hypothes…
--------
まとめ
狙った用語で検索はできましたが、スコア差はそこまで大きくありませんでした。モデルが軽量なので、対象件数が少なければローカルでも試せます。重み付けや文分割(チャンク)の工夫、サマリー生成の質などを詰めるともう少し精度が上がりそうです。
おわりに
今回は Zenn の自分の記事一覧を取得して簡単な検索を行いました。スクレイピングやクローリングを禁止しているサイトもあります。robots.txt
や利用規約を必ず確認し、サーバーに負荷をかけないよう配慮しましょう。
Discussion