「犬」と「猫」は近く、「犬」と「車」は遠いと、コンピュータはどうやって知るのか?
社内ドキュメント検索に生成 AI を組み込もうとして、embedding という言葉に行き当たったことはないだろうか。OpenAI などの embedding API に文章を投げると、意味不明な数字の列が返ってくるだけに見える。あの数字の羅列のどこに「意味」が入っていて、なぜそこから文章同士の「近い/遠い」を計算できるのだろうか。
- embedding は文章や単語の意味を、固定長の数値ベクトル(OpenAI の場合は 1536〜3072 個の数字の並び)として表現したものだ
- 二つのベクトルが向いている方向の近さを表すコサイン類似度が、そのまま「意味の近さ」の代理指標として使われる
- 検索対象が数百万件になると全件比較は遅くなるため、実際のベクトル DB は HNSW のような近似最近傍探索(ANN)で候補を絞り込み、これが RAG の検索ステップの土台になっている
embedding とは何か
キーワード検索は文字列が完全一致するかどうかで文書を拾う。だから「犬」で検索しても「イヌ」や「dog」を含む文書は原則として拾えない。ここで使われるのがembedding(埋め込み)で、文章や単語をモデルに通して得られる、固定長の実数値ベクトルのことだ。OpenAI の embedding モデルでいえば text-embedding-3-small は 1536 個、text-embedding-3-large は 3072 個の数字の並びを 1 つの文章に対して返す(どちらも最大入力は 8192 トークン)。
個々の数字が具体的に「何の意味」を表しているかは、人間には直接読み取れない。モデルが大量のテキストを学習する過程で、意味の近い言葉ほど近いベクトルになるよう内部的に見つけ出した抽象的な軸だからだ。重要なのは中身の解釈ではなく、この空間の中で「意味が近い文章ほど近い場所に置かれる」という配置そのものにある。
色を「赤」「青」という名前ではなく、RGB の 3 つの数値(例えば赤は 255, 0, 0)で表すと、二つの色がどれくらい似ているかを計算で求められるようになる。embedding はこの発想を 1536 個の軸まで拡張したようなものだと考えると近い。色の場合は 3 つの数字が「赤みの強さ」「青みの強さ」だと人間にも分かるが、embedding の 1536 個の数字が個別に何を表すかは読み取れない。それでも「近い意味ほど数字の並びが似る」という構造は同じだ。
なぜベクトルの「近さ」を意味の近さとして計算できるのか
二つのベクトルの近さを測る方法として、OpenAI のドキュメントはコサイン類似度(cosine similarity)を推奨している。これは二つのベクトルが原点からどれだけ同じ方向を向いているか、つまり成す角度を -1 から 1 の数値にしたものだ。角度が 0(完全に同じ方向)ならコサイン類似度は 1、90 度なら 0、正反対なら -1 になる。
テキスト embedding(数値の列) "犬" ──▶ [0.91, -0.12, 0.30, …] ┐ "猫" ──▶ [0.88, -0.15, 0.27, …] ├ 近い(cos大) "車" ──▶ [-0.20, 0.65, -0.10,…] ┘ 遠い(cos小)
OpenAI の embedding は長さ 1 に揃える L2 正規化済みで返ってくるため、コサイン類似度は各成分を掛けて足すだけの内積で計算でき、しかも正規化済みならコサイン類似度と直線距離(ユークリッド距離)は同じ順位になる、と公式ドキュメントは述べている。だから実装側は単純な掛け算と足し算の組み合わせだけで「意味の近さ」のランキングを作れる。ここで見落としやすいのは、この数値が測っているのはあくまで学習データの中での配置の近さであり、内容が事実として正しいかどうかとは別問題だという点だ(詳しくは後述の誤解を参照)。
意味が近い文章をどう高速に探すのか
素朴に実装するなら、クエリの embedding と保存済みの全文書の embedding それぞれとのコサイン類似度を計算し、上位を返せばよい。しかし文書数が数百万件規模になると、毎回全件と内積を取る線形スキャンは遅くなる。そこで使われるのが HNSW(Hierarchical Navigable Small World)のような近似最近傍探索(ANN, Approximate Nearest Neighbor)アルゴリズムだ。
HNSW はベクトルを層状のグラフとして保持する。上位の層はノードが少なく長距離のリンクでまばらに結ばれているため大まかな方向へすばやく移動でき、下位の層に降りるほどノードが密になり近傍を細かく絞り込める。全件比較よりずっと少ない比較回数で候補を見つけられる代わりに、返ってくるのは「厳密に最も近い」ではなく「だいたい近い」候補になる——これが ANN の「近似(Approximate)」の意味で、探索の速さと精度(再現率)はトレードオフの関係にある。pgvector や Pinecone のような「ベクトル DB」と呼ばれる製品は、embedding の保存とこの ANN 探索をまとめて提供するミドルウェアだ。
RAG での使われ方
ここまでの仕組みを組み合わせたのが、LLM の文脈でよく聞く RAG(Retrieval-Augmented Generation)だ。文書を取り込み(ingestion)、embedding に変換し、クエリと近い文書を検索(retrieval)し、その内容をプロンプトに追加(augmentation)してから LLM に生成(generation)させる、という一連のパイプラインを指す。学習データに含まれていない社内ドキュメントのような情報をもとに答えさせたいときに使う手法だ。
実装で最初につまずきやすいのが、文書をどの粒度に分割するか(チャンク分割)だ。文書をまるごと 1 つの embedding にすると内容が薄まり、特定の質問に対応する箇所を拾いにくくなる。逆に 1 文単位まで細かく割ると、そのチャンク単独では前後の文脈が失われて意味的なまとまりを失う。だから段落程度の適度な粒度に切り分けてからそれぞれを個別に embedding 化する、という前処理が挟まる。検索で上位に来たチャンクをそのままプロンプトに埋め込み、その内容をもとに答えるよう LLM に指示するのが RAG の最後のステップになる。
社内ドキュメント検索やサポートチャットボットに RAG を組み込むとき、あるいは Postgres の pgvector 拡張で embedding 列を作りベクトル検索のクエリを書くときに、この仕組みに直接触れることになる。「関係ない文書が検索結果に混じる」ときは、コサイン類似度のスコアそのものを確認し、閾値の設定やチャンク分割の粒度を疑ってみるとよい。
本物の embedding モデルを使わなくても、コサイン類似度の計算そのものは手作りのトイベクトルで体感できる。ここでは「犬」「猫」「車」を表す 3 次元のダミーベクトル(実際のモデルが出す embedding ではない)を用意し、コサイン類似度を素の Python で計算する。
mkdir -p /tmp/embedding-demo && cd /tmp/embedding-demo
cat > cosine_demo.py <<'EOF'
import math
def cosine_similarity(a, b):
dot = sum(x * y for x, y in zip(a, b))
norm_a = math.sqrt(sum(x * x for x in a))
norm_b = math.sqrt(sum(y * y for y in b))
return dot / (norm_a * norm_b)
# 説明用に手作りした3次元のトイベクトル(実際のembeddingではない)
inu = [0.9, 0.1, 0.0]
neko = [0.8, 0.2, 0.1]
kuruma = [0.0, 0.1, 0.95]
print("犬 と 猫:", cosine_similarity(inu, neko))
print("犬 と 車:", cosine_similarity(inu, kuruma))
EOF
python3 cosine_demo.py
「犬」と「猫」は 0.98 前後、「犬」と「車」は 0.01 前後になるはずだ。ベクトルの中身は説明のために手で作った数字にすぎないが、「向いている方向が近いほど 1 に近づき、直交に近いほど 0 に近づく」という計算そのものは、実際の embedding モデルが返す 1536 次元のベクトルでも変わらない。
- コサイン類似度が高い結果は、内容として正しい/信頼できる — コサイン類似度が表すのは学習データの分布の中で近い文脈に出現しやすかったという配置の近さであり、事実として正確かどうかは保証しない。
- ベクトル検索を導入すれば、キーワード検索はもう要らない — 型番・エラーコード・固有名詞のような完全一致が必要な語は、embedding モデルが学習時に十分な頻度で見ていない限り近いベクトルになりにくい。そのため多くの実システムは BM25 のようなキーワード検索とベクトル検索を組み合わせるハイブリッド検索を採用している。
- RAG を使えば LLM の hallucination(事実と異なる内容の生成)は無くなる — 検索した文書を土台にしても、それをもとに文章を組み立てる生成のステップ自体は変わらず確率的な予測であり、誤った情報を混ぜる可能性は残る。
- embedding(埋め込み)
- 文章や単語の意味を固定長の数値ベクトルとして表現したもの。
- コサイン類似度
- 二つのベクトルが向いている方向の近さを -1〜1 の数値で表した指標。
- ベクトル DB
- embedding の保存と近傍探索をまとめて提供するデータベース・ミドルウェア。
- ANN(近似最近傍探索)
- 厳密な最近傍ではなく「だいたい近い」候補を高速に見つけるアルゴリズムの総称。HNSW はその代表例。
- チャンク分割
- 検索対象の文書を embedding 化しやすい適度な粒度に分割する前処理。
- RAG(Retrieval-Augmented Generation)
- 検索で見つけた外部情報をプロンプトに加えたうえで LLM に生成させる手法。
- Vector embeddings - OpenAI API — embedding の次元・L2 正規化・コサイン類似度の推奨について述べた公式リファレンス。
- Understanding Hierarchical Navigable Small Worlds (HNSW) - Zilliz Learn — 近似最近傍探索の層状グラフ構造を図解した解説記事。
- Hybrid Search Explained - Weaviate — ベクトル検索とキーワード検索(BM25)を組み合わせる理由と仕組みの解説。