記事データをベクトル化して、関連記事(メモ)を表示できるか試してみた、というメモ。
できあがった関連メモは以下のキャプチャのような感じ。これは本:マクロ金融危機入門の記事の関連メモ。「本」というカテゴリでは揃っているものの、関連性があるようなないような微妙なライン。データがもっと増えれば、それっぽい結果になってくるのかなあ。

実際にやったことは、Markdownファイルから取得した記事の本文にベクトル化処理をかけて、
import { pipeline } from "@xenova/transformers";
const embedder = await pipeline(
"feature-extraction",
"Xenova/paraphrase-multilingual-MiniLM-L12-v2"
);
const embeddings = await Promise.all(
contents.map(async (content) => {
return await embedder(content, { pooling: "mean", normalize: true });
})
);
コサイン類似度でスコアを出し、高い順に4件まで抽出してJSONに保存。
// 類似度関数
function cosine(a: any, b: any) {
let dot = 0,
normA = 0,
normB = 0;
for (let i = 0; i < a.length; i++) {
dot += a[i] * b[i];
normA += a[i] ** 2;
normB += b[i] ** 2;
}
return dot / (Math.sqrt(normA) * Math.sqrt(normB));
}
// 関連記事計算
const related: any = {};
for (let i = 0; i < docs.length; i++) {
related[docs[i].id] = docs
.map((_, j) => {
return {
...docs[j],
score: cosine(embeddings[i].data, embeddings[j].data),
};
})
.filter((s) => s.id !== docs[i].id)
.sort((a, b) => b.score - a.score)
.slice(0, 4);
}
fs.writeFileSync(outputPath, JSON.stringify(related, null, 2), "utf-8");
このJSONをAstro側で読み込んで表示する、という割とシンプルな感じです。
ベクトルDBを使わなくても、JSON保存で十分機能するので、このくらいのブログ規模なら手軽でちょうどいい感じ。ただ、全記事のベクトル化に体感で20〜30秒ほどかかっており、そこそこ重めの処理にはなってます。
ちなみに実装のほとんどはChatGPT任せで、実装よりもむしろスタイル調整の方に時間を使ってる感じがある。