% cd ..

Embedding で関連記事を自動リンクする仕組みを追加

Embedding で関連記事を自動リンクする仕組みを追加

関連記事をどうするか

ブログの記事末尾に「関連記事」を表示するのはよくある機能です。
WordPress なら YARPP(Yet Another Related Posts Plugin)のような定番プラグインがあって、手動でリンクを管理する必要はありません。

YARPP のソースコードは公開されていますが、スコアリングの詳細は公式にドキュメント化されていません。ざっくり言うと、タイトル・本文の単語の共通性、カテゴリー・タグの一致をそれぞれスコア化し、合計が閾値を超えた記事を「関連」として表示する仕組みのようです。要素ごとに「検討しない」「検討する」「検討する(重要視)」と重み付けでき、シンプルだけど実績のあるアプローチですね。

このブログでは、翻訳品質の検証に使っている embedding(テキストのベクトル化)をそのまま流用して、別のアプローチで関連記事を自動検出する仕組みを作りました。

YARPP(単語一致型)とこのブログ(embedding 型)の違い

YARPP(単語一致型)このブログ(embedding 型)
判定基準タイトル・本文の単語一致、カテゴリー・タグの一致テキスト全体の意味ベクトルの cosine similarity
強み同じキーワードが出てくる記事同士の関連付け同じ単語がなくても意味が近い記事を拾える(はず)
調整項目各要素の重み付け + 閾値閾値のみ
実績WordPress の定番。長年の運用実績あり実験中。記事が増えてからの検証が必要

embedding 型のメリットは「意味的な近さ」で判定できること。例えば「翻訳パイプライン」と「往復翻訳」は単語が違っても概念として近いので、embedding なら関連として拾えるはずです。一方で、記事が少ない段階では効果の検証が難しく、YARPP のような枯れた手法と比べてどこまで優位性があるかは未知数です。

仕組み

embedding で記事同士の「近さ」を測る

このブログにはもともと、往復翻訳の品質スコアを算出するために embedding の仕組みがあります(詳しくはこちら)。日本語の原文をベクトル化して、Supabase の pgvector に保存しています。

この embedding を関連記事検索にも使います。記事 A と記事 B の embedding の cosine similarity が高ければ「内容が近い」と判断できます。

post_embeddings テーブル

翻訳結果を保存する translations テーブルとは別に、記事単位で embedding を保持する post_embeddings テーブルを作りました。

create table post_embeddings (
  post_id text primary key,
  embedding vector(768),
  embedding_model text not null default 'gemini-embedding-001',
  updated_at timestamptz default now()
);

翻訳パイプライン(translate.ts)で翻訳するときに、ついでにこのテーブルにも embedding を保存します。source_embedding はもともと計算しているので、API コールの追加コストはゼロです。

静的サイト生成との相性

Next.js の静的サイト生成(SSG)では、ビルド時に全ページを生成します。つまり:

  1. 新しい記事を push
  2. Vercel が全ページを再ビルド
  3. 古い記事のページにも、新しい関連記事のリンクが自動で出る

手動リンクを管理するのと比べると、ここが決定的な違いです。一度仕組みを作れば、あとは記事を書くだけ。

この点では WordPress でも YARPP を使えば同じことができますが、サーバーの負荷という点ではこちらのほうが優位かもしれません。最近では内蔵キャッシュやデータベースの活用でだいぶマシにはなっていますが、YARPP といえばかつては WordPress の中でも「重いプラグイン」の筆頭で、一部の高速ホスティングサービス(WP Engine など)で禁止されていた時期もありました。

他のマッチングサービスの手法から学ぶ

ここまで「記事同士の近さ」を embedding で測る話をしてきましたが、「似たものを見つけて提示する」という問題は、世の中のマッチングサービスが長年取り組んできたテーマでもあります。

就活サイトの「あなたにおすすめの求人」、EC サイトの「この商品を見た人はこちらも」。裏側で動いているロジックは、大きく3つに分類できます。

手法やっていること代表例
属性一致(ルールベース)タグ・カテゴリ・スキルなど離散的な属性の一致度を見るYARPP、求人サイトのスキルフィルタ
協調フィルタリング「似た行動をした人が好んだもの」を推薦Amazon の「この商品を買った人はこちらも」
embedding 類似度テキストや画像をベクトル化し、意味的な近さを測る職務経歴書 vs 求人票のマッチング

現代の主要サービスはこの3つを組み合わせたハイブリッド型が主流です。LinkedIn なら「スキル一致(属性)+ 似た経歴の人が応募した求人(協調)+ 職務経歴書の意味的近さ(embedding)」を全部混ぜてスコアを出しています。

このブログはどこにいて、どこへ向かうか

このブログの関連記事は、3つのうち「embedding 類似度」だけで動いています。個人ブログには協調フィルタリングに必要な行動データ(誰が何を読んだか)がなく、属性一致(タグ)よりも embedding の方が「意味的な近さ」を拾えるはず、という仮説で選んだ構成です。

ただ、embedding だけでは限界が出てくる場面も想像できます。例えば、技術的に近いけどジャンルが全く違う記事(AI の話と映画の話で両方 embedding を使っている、など)が関連として出てしまうケース。こういう「意味は近いけど読者の期待とズレる」問題は、タグによる属性一致を組み合わせることで緩和できるはずです。

現実的な次の一手としては:

  • embedding + タグのハイブリッド化 — embedding スコアにタグの一致度を加味して、意味的な近さとジャンルの一致を両立させる。データも仕組みも揃っているので、すぐに試せる
  • 協調フィルタリングは見送り — アクセス解析で「この記事を読んだ人が次に読んだ記事」のデータを溜めれば技術的には可能だが、個人ブログの規模では当面データが足りない

翻訳パイプラインで embedding の計算がすでに動いているので、関連記事のための追加コストはゼロ。マッチングビジネスが大規模なインフラの上で回している仕組みを、Supabase の無料枠で小さく試せるのは個人ブログならではかな、と思う次第。

現状と今後

まだ記事が数本の段階なので、embedding 型の関連記事がうまく機能しているかは正直わかりません。現状は cosine similarity 0.8 以上・最大3件という暫定的な閾値で動かしていますが、記事数が少なすぎてまともに検証できる状態ではありません。YARPP のような単語一致型と比べてどこが優れているか(あるいは劣っているか)は、記事が増えてジャンルが多様化してからの評価になります。

今後検証したいこと:

  • 記事数が増えた時に、意味的に近いけどキーワードが異なる記事同士をちゃんと拾えるか
  • 閾値(現状 0.8)の最適値は記事数やジャンルによって変わるか
  • embedding 単体の限界が見えてきたら、タグ一致とのハイブリッド化でどのくらい改善するか

実データが溜まったら、スコア分布やヒット率を分析して行きたいと思います。