埋め込み: 理論と実践
すべてのセマンティック検索システム、すべての RAG パイプライン、および言語で動作するすべての AI アプリケーション 自然には共通の基本的な要素があります。 埋め込み。私が翻訳者です 数字の意味を理解し、テキストの世界と数学の世界をつなぐ架け橋となります。埋め込みなしでは、 データベースは「犬」と「車」を区別できませんでした。埋め込みを使用すると、データベースは「犬」と「車」を認識します。 「トースター」というよりは「猫」に近いです。
このシリーズの最初の記事では、 ベクター そして節約することを学びました PostgreSQL のクエリ ベクトル。しかし、それらのベクトルはどこから来るのでしょうか?の埋め込みを生成する方法 品質?そして何よりも、数十ものモデルの中からどのモデルを選ぶべきでしょうか?この記事では答えます 数学理論から Python や PostgreSQL を使った実践まで、これらすべての疑問に答えます。
シリーズ概要
| # | アイテム | 集中 |
|---|---|---|
| 1 | ベクター | インストール、オペレーター、インデックス作成 |
| 2 | あなたはここにいます - 埋め込み | モデル、距離、世代 |
| 3 | PostgreSQL を使用した RAG | エンドツーエンドの RAG パイプライン |
| 4 | 高度な類似性検索 | ハイブリッド検索、フィルタリング |
| 5 | インデックス作成とパフォーマンス | HNSW、IVFFフラット、チューニング |
| 6 | 本番環境の RAG | モニタリング、スケーリング、CI/CD |
何を学ぶか
- エンベディングとは何ですか、そしてなぜそれが現代の AI の基礎となるのか
- 歴史的進化: ワンホット エンコーディングから Word2Vec、GloVe、BERT、Sentence Transformers まで
- 埋め込みの数学的特性: ベクトルの類似性とセマンティック クラスタリング
- 4 つの距離メトリクスと公式およびユースケース
- Python でエンベディングを生成する方法: ローカルおよび API 経由
- pgvector を使用して PostgreSQL で埋め込みを保存およびクエリする方法
- マルチモーダルな埋め込み: テキスト、画像、オーディオ、コード
- 埋め込みモデル (MTEB) の品質を評価する方法
- 数百万のドキュメントのコストとスケーリング戦略
1. 埋め込みとは何ですか
Un 埋め込み オブジェクト (単語、文、 文書、画像など)を次元数を減らした連続空間に配置します。実際には配列です そのオブジェクトの「意味」を捉える浮動小数点数。
# L'embedding della frase "Il gatto dorme sul divano"
# generato con text-embedding-3-small di OpenAI (1536 dimensioni)
embedding = [
0.0231, -0.0456, 0.0891, -0.0123, 0.0567, -0.0234,
0.0789, -0.0345, 0.0123, -0.0678, 0.0456, -0.0891,
# ... altri 1524 valori ...
]
print(f"Tipo: {type(embedding)}") # <class 'list'>
print(f"Dimensioni: {len(embedding)}") # 1536
重要な洞察は次のとおりです。よく訓練されたベクトル空間では、 幾何学的距離 2 つのベクトル間の 意味上の類似性 それらが表す概念の中で。フレーズ 同じような意味を持つ文は近いベクトルを持ち、異なる意味を持つ文は遠く離れたものになります。
埋め込みの特徴
| 財産 | 説明 | Esempio |
|---|---|---|
| 密集 | 各次元にはゼロ以外の値があります | [0.023、-0.045、0.089、...] |
| 続く | 離散値ではなく実数値 | 各コンポーネントは float32/float16 です。 |
| 固定次元 | 同じモデルは常に同じ長さのベクトルを生成します | 384、768、1536、または 3072 の寸法 |
| 意味的に重要な | ベクトル間の距離は意味の関係を反映する | sim("猫", "ネコ") > sim("猫", "車") |
埋め込み空間をマップとして考えると、同様の概念が「近隣」を形成します。 あるエリアでは動物、別のエリアでは乗り物、さらに別のエリアでは感情。しかし美しさは これらの関係はトレーニングから自動的に生まれるものであり、自然に生まれるものではないという事実にあります。 手動でプログラムされています。
2. 言葉からベクトルへ: 歴史的進化
エンベディングの歴史は、ますます洗練されたアイデアの進歩です。 これにより、以前の制限が解決されます。この進化を理解すると、その理由を理解するのに役立ちます 最新のモデルは非常にうまく機能します。
2.1 ワンホット エンコーディング (1990 年代)
最も単純なアプローチ: 各単語は 1 つだけの 1 とすべてのベクトルで表されます。 他は 0。語彙に V 個の単語がある場合、各ベクトルは V 次元を持ちます。
# Vocabolario: ["gatto", "cane", "pesce", "auto", "moto"]
# Dimensione vettore = dimensione vocabolario = 5
gatto = [1, 0, 0, 0, 0]
cane = [0, 1, 0, 0, 0]
pesce = [0, 0, 1, 0, 0]
auto = [0, 0, 0, 1, 0]
moto = [0, 0, 0, 0, 1]
# Problema 1: la distanza tra "gatto" e "cane" e uguale
# alla distanza tra "gatto" e "auto"
import numpy as np
dist_gatto_cane = np.linalg.norm(
np.array(gatto) - np.array(cane)
) # sqrt(2) = 1.414
dist_gatto_auto = np.linalg.norm(
np.array(gatto) - np.array(auto)
) # sqrt(2) = 1.414 -- identica!
# Problema 2: con un vocabolario di 100.000 parole,
# ogni vettore ha 100.000 dimensioni (sparso, inefficiente)
ワンホットエンコーディングの制限
爆発的な次元: 100K 単語の語彙の場合、各ベクトルの次元は 100K になります。 ほぼゼロです。 セマンティック情報がありません: すべてのベクトルは等距離です 彼らの間で。 「猫」は「地震」と同じように「猫」からも遠い。このアプローチ 単語間の意味関係は捕捉されません。
2.2 TF-IDF (用語頻度 - 逆文書頻度)
さらに一歩前進: 0/1 の代わりに、ベクトルの成分が単語の重要度を示します。 文書内のコーパス全体との比較。しかし、それぞれの文書は空間内に散在するベクトルになります。 語彙の次元。
from sklearn.feature_extraction.text import TfidfVectorizer
documenti = [
"il gatto dorme sul divano",
"il cane gioca nel giardino",
"l'automobile corre sulla strada",
"il felino riposa sulla poltrona",
]
vectorizer = TfidfVectorizer()
tfidf_matrix = vectorizer.fit_transform(documenti)
# Risultato: matrice sparsa (4 documenti x N termini)
print(f"Shape: {tfidf_matrix.shape}") # (4, 14)
print(f"Termini: {vectorizer.get_feature_names_out()}")
# Problema: "gatto dorme" e "felino riposa" sono lontani
# perchè usano parole diverse, anche se il significato e simile
from sklearn.metrics.pairwise import cosine_similarity
sim = cosine_similarity(tfidf_matrix[0:1], tfidf_matrix[3:4])
print(f"Similarità gatto-felino: {sim[0][0]:.3f}") # ~0.07 (bassa!)
TF-IDF は単語を重要度によって重み付けすることでワンホット エンコーディングを改善しますが、同じ問題が発生します。 根本的な問題: 彼は「猫」と「ネコ科の動物」が同義であることを理解していません。 字句が完全に一致する場合のみ。
2.3 Word2Vec: 革命 (2013)
2013 年、Tomas Mikolov と Google のチームは Word2Vec をリリースし、すべてを変えました。 素晴らしいアイデア: 単語はそれが出現する文脈によって定義されます。言葉 同様のコンテキストで出現するものは同様の表現になります。
Word2Vec は、浅いニューラル ネットワークを使用して密なベクトル (通常は 100 ~ 300 次元) を学習します。 大きなテキストコーパスから。 2 つのアーキテクチャ:
Word2Vec アーキテクチャ
| 建築 | 入力 | 出力 | 説明 |
|---|---|---|---|
| CBOW | 文脈上の単語 | 対象単語 | 周囲のコンテキストを考慮して中心となる単語を予測します |
| スキップグラム | 対象単語 | 文脈上の単語 | 中心となる単語から周囲の単語を予測します |
from gensim.models import Word2Vec
# Corpus di esempio (in produzione: milioni di frasi)
frasi = [
["il", "gatto", "dorme", "sul", "divano"],
["il", "cane", "gioca", "nel", "giardino"],
["il", "felino", "riposa", "sulla", "poltrona"],
["il", "cane", "corre", "nel", "parco"],
]
# Addestramento Word2Vec (Skip-gram)
model = Word2Vec(
sentences=frasi,
vector_size=100, # dimensionalità embedding
window=5, # contesto: 5 parole prima e dopo
min_count=1, # includi parole con almeno 1 occorrenza
sg=1, # 1 = Skip-gram, 0 = CBOW
epochs=100
)
# Ora "gatto" e "felino" sono vicini!
print(model.wv.most_similar("gatto", topn=3))
# [('felino', 0.92), ('cane', 0.85), ('dorme', 0.71)]
# Accesso al vettore
vettore_gatto = model.wv["gatto"]
print(f"Dimensioni: {vettore_gatto.shape}") # (100,)
print(f"Primi 5: {vettore_gatto[:5]}")
2.4 GloVe: グローバル ベクトル (2014)
スタンフォード大学は、別のアプローチで GloVe (Global Vectors for Word Representation) を開発しました。 GloVe はニューラル ネットワークの代わりに因数分解します。 共起行列 グローバルコーパス。グローバル統計手法 (LSA など) の利点と、 Word2Vec ローカル コンテキストの。
GloVe は、2 つのベクトル間のドット積が確実に得られるコスト関数を最小化します。 単語の数は、その共起確率の対数に比例します。
2.5 FastText: サブワード埋め込み (2016)
Facebook AI Research (FAIR) は Word2Vec を拡張しました ファストテキスト、それが表す 各単語は文字の N グラムのセットとして扱われます。これにより、次の 2 つの重大な問題が解決されます。
- まれな単語または語彙外 (OOV) の単語: FastText は、サブセグメント ベクトルを構成することで、これまでに見たことのない単語の埋め込みを生成できます
- 形態: 形態学的に関連する単語 (例: 「run」、「ran」、「runner」) は n グラムを共有するため、類似したベクトルを持ちます。
進化: 疎な表現から密な表現へ
| 方法 | Anno | タイプ | 一般的な寸法 | セマンティクス |
|---|---|---|---|---|
| ワンホット | - | 散らばっている | V(語彙) | なし |
| TF-IDF | 1972年 | 散らばっている | V(語彙) | 統計 |
| Word2古い | 2013年 | 密集 | 100-300 | ローカルコンテキスト |
| グローブ | 2014年 | 密集 | 50-300 | グローバル + ローカル |
| ファストテキスト | 2016年 | 密集 | 100-300 | サブワード + コンテキスト |
| バート | 2018年 | 密集 | 768 | 状況に応じたダイナミクス |
| センテンストランスフォーマー | 2019年 | 密集 | 384-1024 | 文全体 |
3. 埋め込みの数学的性質
Word2Vec の最も魅力的な発見の 1 つは、ベクトル空間が学習するということです。 概念間の代数的関係。ベクトルの算術演算 意味的に一貫した結果を生成します。
3.1 ベクトルの類似性
有名なたとえ: 王 - 男性 + 女性 = 女王。ベクトル用語で言えば、 「王」と「男」の違いが「王族」の概念を捉え、それを「女」に加える あなたは「女王」を獲得します。正式には:
import gensim.downloader as api
# Carica embeddings GloVe pre-addestrati
model = api.load("glove-wiki-gigaword-100")
# king - man + woman = ?
result = model.most_similar(
positive=["king", "woman"],
negative=["man"],
topn=3
)
print(result)
# [('queen', 0.7698), ('princess', 0.6450), ('monarch', 0.6345)]
# Altre analogie che funzionano:
# Parigi - Francia + Italia = Roma
result2 = model.most_similar(
positive=["paris", "italy"],
negative=["france"],
topn=1
)
print(result2) # [('rome', 0.8722)]
# buono - cattivo + triste = ?
result3 = model.most_similar(
positive=["good", "sad"],
negative=["bad"],
topn=1
)
print(result3) # [('happy', 0.6891)]
3.2 セマンティッククラスタリング
埋め込みはベクトル空間で自然にクラスターを形成します。投影すると 2D のベクトル (t-SNE または UMAP を使用) では、同じカテゴリの単語が存在することが観察されます。 それらは一緒にグループ化されます:動物の近くには動物、国の近くには国、近くには職業 職業に。
この特性は実際のアプリケーションにとって基礎的なものであり、類似性検索は機能します。 それはまさに、同様のトピックに関するドキュメントがベクトル空間に密接に埋め込まれているからです。
4. 最新の埋め込み: コンテキストに応じた埋め込み
Word2Vec と GloVe は、 単語の単一ベクトル、独立して コンテキスト。しかし、「学校」という言葉は、「学校の机」と「魚の群れ」では意味が異なります。ザ コンテキストに応じた埋め込み2018 年に BERT で導入されたこの問題は、次のように解決されます。 同じ単語でも文脈に応じて異なるベクトルを持ちます。
4.1 BERTの埋め込み
BERT (Bidirectional Encoder Representations from Transformers) は文全体を処理して出力します。 各トークンのベクトル。文全体を埋め込むには、通常、次を使用します。
- CLS トークン: 最初の特別なトークン [CLS] には文の集合表現が含まれます
- 平均プーリング: すべてのトークン ベクトルの平均 - 一般に、類似性検索により良い結果が得られます。
BERT は類似性検索には最適ではありません
オリジナルの BERT は、高品質の文埋め込みを生成するようにトレーニングされていませんでした。 CLSトークン 意味的な類似性ではなく、分類のために最適化されています。類似検索の場合は、 Sentence Transformers のような特殊なモデルが必要です。
4.2 文トランスフォーマー (SBERT)
2019 年に Reimers と Gurevych は Sentence-BERT を導入し、BERT を微調整しました。 意味のある文の埋め込みを生成するシャム。この類似性は革命をもたらしました 検索: 単純なコサイン距離を使用して文を比較することが初めて可能になりました。 高品質の結果が得られます。
4.3 埋め込みモデル: 包括的な比較
埋め込みモデルの比較 (2026)
| モデル | プロバイダー | 寸法 | MTEBスコア | コスト/100万トークン | 注意事項 |
|---|---|---|---|---|---|
| テキスト埋め込み-3-small | OpenAI | 1536年 | 62.3 | $0.02 | 優れた品質/価格比 |
| テキスト埋め込み-3-大 | OpenAI | 3072 | 64.6 | $0.13 | OpenAI の最高品質 |
| 埋め込みv3 | コヒア | 1024 | 64.5 | $0.10 | 100以上の言語をサポート |
| 航海-3 | 航海AI | 1024 | 67.1 | $0.06 | 検索用のトップ |
| all-MiniLM-L6-v2 | ハグ顔 | 384 | 56.3 | 無料 | 高速、ローカル、コンパクト |
| all-mpnet-base-v2 | ハグ顔 | 768 | 57.8 | 無料 | 最高の基本的なオープンソース モデル |
| gte-large-en-v1.5 | アリババ (HF) | 1024 | 65.4 | 無料 | 市販モデルとの競争力 |
| bge-large-en-v1.5 | バーアイ (HF) | 1024 | 64.2 | 無料 | RAGに最適 |
モデルの選び方
- プロトタイプ / 限られた予算: all-MiniLM-L6-v2 (無料、高速、384 ディム)
- 生産、低コスト: text-embedding-3-small (OpenAI、0.02 ドル/100 万トークン)
- 最高品質の取得: voyage-3 または gte-large-en-v1.5
- 多言語: Cohere embed-v3 (100 以上の言語)
- 自己ホスト型 / プライバシー: bge-large-en-v1.5 または gte-large-en-v1.5
5. ベクトル間の距離の測定
距離メトリックの選択は、類似性検索の品質に直接影響します。 4 つの主要な指標とその数式、強みを見てみましょう そしてそれらをいつ使用するか。
5.1 コサイン類似度
テキストの埋め込みに最も使用されるメトリック。 2 つのベクトル間の角度を測定します。 大きさ(長さ)は無視します。同じ方向を指す 2 つのベクトル コサイン類似度 1、直交度 0、反対度 -1 を持ちます。
pgvector では、演算子は <=> を計算します コサイン距離
(= 1 - コサイン類似度)。0 は同一を意味し、2 は反対を意味します。
5.2 ユークリッド距離(L2)
The "as the crow flies" distance between two points in space.それは両方を考慮します ベクトルの大きさよりも方向。
pgvector では、演算子は <-> 距離L2を計算します。
5.3 内積(ドット積)
内積は方向と大きさの両方を測定します。正規化されたベクトルの場合 (ノルム = 1)、ドット積はコサイン類似度に相当します。
pgvector では、演算子は <#> 負の内積を計算します (互換性のため)
ORDER BY ASC を使用)。
5.4 マンハッタンまでの距離(L1)
コンポーネントごとの絶対差の合計。外れ値に対する感度が低い ユークリッド距離に関して。
import numpy as np
from scipy.spatial.distance import cosine, euclidean, cityblock
# Due vettori di esempio (normalizzati)
a = np.array([0.5, 0.3, 0.8, 0.1, 0.6])
b = np.array([0.4, 0.35, 0.75, 0.15, 0.55])
# Normalizzazione L2
a_norm = a / np.linalg.norm(a)
b_norm = b / np.linalg.norm(b)
# 1. Cosine Similarity (1 - cosine distance)
cos_sim = 1 - cosine(a_norm, b_norm)
print(f"Cosine similarity: {cos_sim:.6f}") # ~0.999
# 2. Distanza Euclidea (L2)
l2_dist = euclidean(a_norm, b_norm)
print(f"Distanza L2: {l2_dist:.6f}") # ~0.042
# 3. Dot Product (per vettori normalizzati = cosine similarity)
dot = np.dot(a_norm, b_norm)
print(f"Dot product: {dot:.6f}") # ~0.999
# 4. Distanza Manhattan (L1)
l1_dist = cityblock(a_norm, b_norm)
print(f"Distanza L1: {l1_dist:.6f}") # ~0.072
# Relazione L2-Cosine per vettori normalizzati:
# d_L2^2 = 2 * (1 - cos_sim)
print(f"\nVerifica: L2^2 = {l2_dist**2:.6f}")
print(f"2*(1-cos) = {2*(1-cos_sim):.6f}") # uguale!
いつどのメトリックを使用するか
| メトリック | pgvector 演算子 | いつ使用するか | 避ける時期 |
|---|---|---|---|
| ささいなこと | <=> |
大きさが重要でない場合のテキストの埋め込み | 大きさが重要な空間データ |
| L2 (ユークリッド) | <-> |
大きさが重要な場合の画像、数値データ | コンポーネント間でスケールが異なるベクトル |
| 内積 | <#> |
ベクトルはすでに正規化されています (パフォーマンスがわずかに向上) | 正規化されていないベクトル (大きさによって歪んだ結果) |
| マンハッタン(L1) | pgvector の非ネイティブ | データがまばらで、外れ値に対する堅牢性 | 高密度埋め込みでの一般的な使用 |
実践的なルール
テキスト埋め込みの 95% のケースでは、次を使用します。 コサイン距離
(<=> pgvector 内)。最新の埋め込みモデルはベクトルを生成します
すでに正規化されているため、コサイン積とドット積は実質的に同等になります。距離
ユークリッドは、空間データ、またはベクトルの大きさが情報を伝える場合に意味を持ちます。
6. Python で埋め込みを生成する
次に、3 つの異なるアプローチでエンベディングを生成する方法を見てみましょう。 文トランスフォーマー、OpenAI API、HuggingFace Inference API。それぞれのアプローチには、 具体的な利点とトレードオフ。
6.1 文トランスフォーマー (ローカル)
最も柔軟でプライベートなアプローチ: モデルはデータなしでマシン上で実行されます。 ネットワーク外に出るため、API 呼び出しのコストはかかりません。
# pip install sentence-transformers
from sentence_transformers import SentenceTransformer
import numpy as np
# Carica il modello (scaricato automaticamente al primo uso)
model = SentenceTransformer("all-MiniLM-L6-v2")
# Embedding di una singola frase
frase = "PostgreSQL e un database relazionale open-source"
embedding = model.encode(frase)
print(f"Tipo: {type(embedding)}") # numpy.ndarray
print(f"Dimensioni: {embedding.shape}") # (384,)
# Embedding di più frasi (batch - molto più efficiente)
frasi = [
"PostgreSQL e un database relazionale open-source",
"pgvector aggiunge il supporto per vettori a PostgreSQL",
"Il machine learning richiede grandi quantità di dati",
"La pizza margherita e un piatto tipico napoletano",
]
embeddings = model.encode(
frasi,
batch_size=32, # processa 32 frasi alla volta
show_progress_bar=True, # mostra progresso per batch grandi
normalize_embeddings=True # normalizza a norma L2 = 1
)
print(f"Shape: {embeddings.shape}") # (4, 384)
# Calcola similarità tra tutte le coppie
from sentence_transformers.util import cos_sim
similarities = cos_sim(embeddings, embeddings)
print(f"\nMatrice di similarità:\n{similarities}")
# Le prime 2 frasi (su PostgreSQL) avranno alta similarità
# La frase sulla pizza sarà distante dalle altre
6.2 OpenAI 埋め込み API
OpenAI の API は、インフラストラクチャ管理なしで高品質のモデルを提供します。理想的な 中量生産向け。
# pip install openai
from openai import OpenAI
import os
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
def get_embeddings(
texts: list[str],
model: str = "text-embedding-3-small"
) -> list[list[float]]:
"""Genera embeddings per una lista di testi."""
response = client.embeddings.create(
input=texts,
model=model,
)
return [item.embedding for item in response.data]
# Singolo embedding
testo = "PostgreSQL come vector database per AI"
embedding = get_embeddings([testo])[0]
print(f"Dimensioni: {len(embedding)}") # 1536
# Batch di embeddings (fino a 2048 testi per chiamata)
testi = [
"Come installare pgvector su Docker",
"Tutorial per similarity search in PostgreSQL",
"Guida alla cottura della pasta al forno",
]
embeddings = get_embeddings(testi)
print(f"Embeddings generati: {len(embeddings)}") # 3
# Dimensione ridotta con text-embedding-3-small
# Puoi specificare dimensioni inferiori per risparmiare spazio
response = client.embeddings.create(
input=["Testo di esempio"],
model="text-embedding-3-small",
dimensions=512 # ridotto da 1536 a 512
)
emb_ridotto = response.data[0].embedding
print(f"Dimensioni ridotte: {len(emb_ridotto)}") # 512
6.3 HuggingFace 推論 API
ローカル モデルと商用 API の間の妥協点: 数千のモデルへのアクセス API 経由のオープンソースであり、豊富な無料枠が付いています。
# pip install huggingface_hub
from huggingface_hub import InferenceClient
import os
client = InferenceClient(
token=os.getenv("HF_TOKEN")
)
def get_hf_embeddings(
texts: list[str],
model: str = "BAAI/bge-large-en-v1.5"
) -> list[list[float]]:
"""Genera embeddings usando HuggingFace Inference API."""
result = client.feature_extraction(
text=texts,
model=model,
)
return result
# Genera embeddings
testi = [
"Vector search con PostgreSQL e pgvector",
"Come creare indici HNSW per ricerca veloce",
]
embeddings = get_hf_embeddings(testi)
print(f"Embeddings: {len(embeddings)}") # 2
print(f"Dimensioni: {len(embeddings[0])}") # 1024 (bge-large)
6.4 効率的なバッチ処理
数千または数百万のドキュメントの埋め込みを生成する必要がある場合、効率が向上します。 バッチ処理の重要性が増します。
import time
from typing import Generator
from sentence_transformers import SentenceTransformer
import numpy as np
def chunk_list(
lst: list, chunk_size: int
) -> Generator[list, None, None]:
"""Divide una lista in chunk di dimensione fissa."""
for i in range(0, len(lst), chunk_size):
yield lst[i:i + chunk_size]
def generate_embeddings_batch(
texts: list[str],
model_name: str = "all-MiniLM-L6-v2",
batch_size: int = 256,
device: str = "cpu" # "cuda" per GPU
) -> np.ndarray:
"""Genera embeddings in batch con progress tracking."""
model = SentenceTransformer(model_name, device=device)
all_embeddings = []
total_batches = (len(texts) + batch_size - 1) // batch_size
start = time.time()
for i, batch in enumerate(chunk_list(texts, batch_size)):
batch_emb = model.encode(
batch,
batch_size=batch_size,
normalize_embeddings=True,
show_progress_bar=False
)
all_embeddings.append(batch_emb)
elapsed = time.time() - start
rate = (i + 1) * batch_size / elapsed
print(
f"Batch {i+1}/{total_batches} - "
f"{rate:.0f} testi/sec"
)
return np.vstack(all_embeddings)
# Utilizzo
texts = [f"Documento numero {i}" for i in range(10_000)]
embeddings = generate_embeddings_batch(
texts,
batch_size=256,
device="cuda" # usa GPU se disponibile
)
print(f"Shape finale: {embeddings.shape}") # (10000, 384)
7. PostgreSQL での埋め込みの履歴化
埋め込みを生成する方法がわかったので、pgvector を使用して埋め込みを PostgreSQL に保存する方法を見てみましょう。 類似性検索クエリを実行します。これは記事 1 への実際的なリンクです シリーズの。
7.1 テーブルのレイアウト
-- Abilita pgvector
CREATE EXTENSION IF NOT EXISTS vector;
-- Tabella documenti con embedding
CREATE TABLE documents (
id BIGSERIAL PRIMARY KEY,
title VARCHAR(500) NOT NULL,
content TEXT NOT NULL,
source VARCHAR(255),
category VARCHAR(100),
embedding vector(384), -- dimensione del modello scelto
created_at TIMESTAMPTZ DEFAULT NOW(),
metadata JSONB DEFAULT '{}'::jsonb
);
-- Indice HNSW per ricerca veloce (cosine distance)
CREATE INDEX idx_documents_embedding
ON documents
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 200);
-- Indice su categoria per filtri combinati
CREATE INDEX idx_documents_category
ON documents (category);
7.2 Pythonからの挿入
import psycopg2
from psycopg2.extras import execute_values
from sentence_transformers import SentenceTransformer
import numpy as np
# Configurazione
DB_CONFIG = {
"host": "localhost",
"port": 5432,
"dbname": "vectordb",
"user": "admin",
"password": "secret_password",
}
# 1. Genera embeddings
model = SentenceTransformer("all-MiniLM-L6-v2")
documenti = [
{
"title": "Introduzione a pgvector",
"content": "pgvector e un'estensione PostgreSQL per vettori...",
"source": "blog",
"category": "database"
},
{
"title": "RAG con LangChain",
"content": "Retrieval Augmented Generation combina retrieval...",
"source": "tutorial",
"category": "ai"
},
{
"title": "Python per Data Science",
"content": "Python e il linguaggio più usato per data science...",
"source": "guide",
"category": "programming"
},
]
# Genera embeddings per i contenuti
testi = [d["content"] for d in documenti]
embeddings = model.encode(testi, normalize_embeddings=True)
# 2. Salva in PostgreSQL
conn = psycopg2.connect(**DB_CONFIG)
cur = conn.cursor()
# Prepara i dati per batch insert
values = []
for doc, emb in zip(documenti, embeddings):
values.append((
doc["title"],
doc["content"],
doc["source"],
doc["category"],
emb.tolist() # converti numpy array in lista Python
))
# Inserimento batch efficiente
execute_values(
cur,
"""INSERT INTO documents
(title, content, source, category, embedding)
VALUES %s""",
values,
template="(%s, %s, %s, %s, %s::vector)"
)
conn.commit()
print(f"Inseriti {len(values)} documenti con embeddings")
cur.close()
conn.close()
7.3 Pythonからの類似性検索
def similarity_search(
query: str,
top_k: int = 5,
category: str = None,
threshold: float = 0.3
) -> list[dict]:
"""Cerca documenti simili alla query."""
# Genera embedding della query
query_embedding = model.encode(
query, normalize_embeddings=True
).tolist()
conn = psycopg2.connect(**DB_CONFIG)
cur = conn.cursor()
# Query con filtro opzionale
if category:
cur.execute("""
SELECT id, title, content, category,
1 - (embedding <=> %s::vector) AS similarity
FROM documents
WHERE category = %s
AND 1 - (embedding <=> %s::vector) > %s
ORDER BY embedding <=> %s::vector
LIMIT %s
""", (
query_embedding, category,
query_embedding, threshold,
query_embedding, top_k
))
else:
cur.execute("""
SELECT id, title, content, category,
1 - (embedding <=> %s::vector) AS similarity
FROM documents
WHERE 1 - (embedding <=> %s::vector) > %s
ORDER BY embedding <=> %s::vector
LIMIT %s
""", (
query_embedding,
query_embedding, threshold,
query_embedding, top_k
))
results = []
for row in cur.fetchall():
results.append({
"id": row[0],
"title": row[1],
"content": row[2][:200], # troncato
"category": row[3],
"similarity": round(row[4], 4),
})
cur.close()
conn.close()
return results
# Esempio d'uso
risultati = similarity_search(
"come usare i vettori in un database",
top_k=3,
category="database"
)
for r in risultati:
print(f"[{r['similarity']}] {r['title']}")
7.4 インデックス作成: HNSW と IVFFlat
数千を超えるドキュメントを含むデータセットの場合、インデックスが不可欠です 許容できるパフォーマンス。 pgvector は 2 種類のインデックスを提供します。
HNSW vs IVFFlat
| 特性 | ニューサウスウェールズ州 | IVFフラット |
|---|---|---|
| クエリ速度 | 非常に速い | 速い |
| 想起 | 95-99% | 85-95% |
| ビルド時間 | 遅い (分) | 速い (秒) |
| メモリ | 高 (RAM 内のグラフ) | 低い(重心) |
| 挿入/更新 | 良好 (増分更新) | 定期的な再構築が必要 |
| こんな方におすすめ | 生産、高品質 | プロトタイピング、静的データセット |
-- HNSW (raccomandato per produzione)
-- m: connessioni per nodo (16-64, default 16)
-- ef_construction: qualità costruzione (64-512, default 64)
CREATE INDEX idx_hnsw_cosine
ON documents
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 200);
-- Per L2 distance
CREATE INDEX idx_hnsw_l2
ON documents
USING hnsw (embedding vector_l2_ops)
WITH (m = 16, ef_construction = 200);
-- IVFFlat (più veloce da costruire)
-- lists: numero di cluster (sqrt(N) come regola base)
CREATE INDEX idx_ivfflat_cosine
ON documents
USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100); -- per ~10K documenti
-- Parametri di query per controllare recall vs velocità
SET hnsw.ef_search = 100; -- default 40, alza per più recall
SET ivfflat.probes = 10; -- default 1, alza per più recall
8. さまざまな種類のデータの埋め込み
埋め込みはテキストに限定されません。最新のモデルは表現を生成できる 画像、オーディオ、ソース コード、さらにはマルチモーダル データのベクター グラフィックス。
マルチモーダル埋め込み: データ型モデル
| データ型 | モデル | 寸法 | 使用事例 |
|---|---|---|---|
| 文章 | all-MiniLM-L6-v2、text-embedding-3-small | 384-3072 | セマンティック検索、RAG、分類 |
| 画像 | CLIP (OpenAI)、SigLIP (Google) | 512-768 | 画像検索、視覚的分類 |
| オーディオ | ささやき、拍手 | 512-1280 | オーディオ検索、音楽分類 |
| コード | CodeBERT、StarCoder 埋め込み | 768 | コード検索、重複検出 |
| マルチモーダル | クリップ、イメージバインド (メタ) | 512-1024 | クロスモーダル検索 (テキストと画像) |
# pip install transformers pillow
from transformers import CLIPProcessor, CLIPModel
from PIL import Image
import torch
import numpy as np
# Carica CLIP
model = CLIPModel.from_pretrained("openai/clip-vit-base-patch32")
processor = CLIPProcessor.from_pretrained("openai/clip-vit-base-patch32")
# Embedding di un'immagine
image = Image.open("foto_gatto.jpg")
inputs = processor(images=image, return_tensors="pt")
with torch.no_grad():
image_embedding = model.get_image_features(**inputs)
image_emb = image_embedding[0].numpy()
print(f"Image embedding: {image_emb.shape}") # (512,)
# Embedding di testo (nello STESSO spazio!)
text_inputs = processor(
text=["un gatto che dorme", "un cane che gioca"],
return_tensors="pt",
padding=True
)
with torch.no_grad():
text_embeddings = model.get_text_features(**text_inputs)
text_embs = text_embeddings.numpy()
# Calcola similarità cross-modale
from numpy.linalg import norm
for i, text in enumerate(["un gatto che dorme", "un cane che gioca"]):
sim = np.dot(image_emb, text_embs[i]) / (
norm(image_emb) * norm(text_embs[i])
)
print(f"Similarità '{text}': {sim:.4f}")
# "un gatto che dorme" avra similarità più alta con foto_gatto.jpg
CLIP の強みは、テキストと画像がその中に存在することです。 同じベクトル空間。 テキスト クエリを使用して画像を検索したり、画像に関連するテキストを検索したりできます。これ PostgreSQL でのマルチモーダル検索などのシナリオを開きます: CLIP 埋め込みを保存します pgvector テーブルとテキスト クエリを含む円。
9. 埋め込みの品質を評価する
埋め込みモデルが「良い」かどうかはどうやって分かるのでしょうか?答えはタスクによって異なります 具体的だが標準化されたベンチマークと客観的な指標が存在します。
9.1 MTEB: 大規模テキスト埋め込みベンチマーク
MTEB は、埋め込みモデルを評価するための参照ベンチマークです。パフォーマンスを測定する 8 つのカテゴリにグループ化された 58 以上のタスク:
- 検索: クエリを指定して関連ドキュメントを検索する
- 意味的テキスト類似性 (STS): 2 つの文はどれくらい似ているか
- 分類: テキストをカテゴリに分類する
- クラスタリング: 類似したテキストをグループ化する
- ペアの分類: 2 つのテキストに関連性があるかどうかを判断する
- 再ランキング: 関連性によって結果を並べ替える
- まとめ: 要約の質
- バイテキストマイニング: 対訳を見つける
from sentence_transformers import SentenceTransformer
from sentence_transformers.evaluation import (
InformationRetrievalEvaluator
)
# Prepara dataset di valutazione
queries = {
"q1": "come installare pgvector",
"q2": "cos'è la similarity search",
"q3": "embedding di immagini con CLIP",
}
corpus = {
"d1": "Guida installazione pgvector su Ubuntu",
"d2": "pgvector per PostgreSQL: setup Docker",
"d3": "Ricerca per similarità vettoriale",
"d4": "CLIP: modello multimodale per immagini e testo",
"d5": "Ricetta pasta alla carbonara",
}
# Mappatura query -> documenti rilevanti
relevant = {
"q1": {"d1": 1, "d2": 1}, # d1 e d2 rilevanti per q1
"q2": {"d3": 1},
"q3": {"d4": 1},
}
# Valuta il modello
model = SentenceTransformer("all-MiniLM-L6-v2")
evaluator = InformationRetrievalEvaluator(
queries=queries,
corpus=corpus,
relevant_docs=relevant,
name="custom-eval"
)
results = evaluator(model)
print(f"NDCG@10: {results['custom-eval_ndcg@10']:.4f}")
print(f"MAP@10: {results['custom-eval_map@10']:.4f}")
9.2 内部評価と外部評価
評価に対する 2 つのアプローチ
| タイプ | 測定内容 | Esempio | いつ使用するか |
|---|---|---|---|
| 本質的 | ベクトル自体のプロパティ | アナロジー、クラスタリング、STS | モデル間の簡単な比較 |
| 外部的 | 最終タスクのパフォーマンス | RAG品質、研究精度 | 本番での最終決定 |
実践的なアドバイス
MTEB スコアだけに頼らないでください。モデルの MTEB スコアは高くなりますが、 特定のドメインでのパフォーマンスが低下します。 常にデータセットを評価します: ドメインから関連するクエリとドキュメントの小さなセットを作成し、測定します nDCGとMAP。これにより、実際のパフォーマンスのより信頼性の高い推定値が得られます。
10. 次元削減
高次元ベクトルは視覚化するのが難しく、コストがかかる可能性があります ストレージと計算の点で。次元削減手法 これらは視覚化と最適化の両方に役立ちます。
10.1 視覚化テクニック
次元縮小技術
| 技術 | 保存する | スピード | 一般的な使用方法 |
|---|---|---|---|
| PCA | グローバル分散 | 非常に速い | 保存用のサイズ縮小、前処理 |
| t-SNE | 局所構造 | 遅い | クラスターの 2D 視覚化 |
| UMAP | ローカル + グローバル構造 | 平均 | 2D 視覚化、インデックス作成前の削減にも対応 |
# pip install umap-learn matplotlib
import umap
import matplotlib.pyplot as plt
import numpy as np
from sentence_transformers import SentenceTransformer
model = SentenceTransformer("all-MiniLM-L6-v2")
# Testi con categorie diverse
testi = [
# Database
"PostgreSQL e un database relazionale",
"MongoDB e un database NoSQL",
"Redis e un database in-memory",
# AI/ML
"Il deep learning usa reti neurali profonde",
"GPT e un modello di linguaggio",
"La regressione lineare e un algoritmo semplice",
# Cucina
"La pizza si cuoce nel forno a legna",
"Il tiramisu e un dolce italiano",
"La pasta alla carbonara usa uova e guanciale",
]
categorie = ["DB"]*3 + ["AI"]*3 + ["Cucina"]*3
colori = ["blue"]*3 + ["red"]*3 + ["green"]*3
# Genera embeddings (384 dimensioni)
embeddings = model.encode(testi, normalize_embeddings=True)
# Riduci a 2D con UMAP
reducer = umap.UMAP(n_components=2, random_state=42)
emb_2d = reducer.fit_transform(embeddings)
# Visualizza
plt.figure(figsize=(10, 8))
for i, (x, y) in enumerate(emb_2d):
plt.scatter(x, y, c=colori[i], s=100, zorder=5)
plt.annotate(testi[i][:30], (x, y),
fontsize=8, ha='left')
plt.title("Embeddings in 2D (UMAP)")
plt.savefig("embeddings_umap.png", dpi=150)
plt.show()
10.2 マトリョーシカの埋め込み
最近の革新的な技術: i マトリョーシカ表現学習 (MRL) エンベディングは、ベクトルの最初の N コンポーネントがすでにエンベディングになるようにトレーニングされます。 有効です。良好な品質を維持しながら、ベクトルを 1536 次元から 512 または 256 次元に切り詰めることができます。
OpenAI text-embedding-3-small e text-embedding-3-large サポート
このテクニック: パラメータを指定できます dimensions ベクトルを取得するには
埋め込みを再計算することなく、よりコンパクトになります。
from openai import OpenAI
import numpy as np
import os
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
testo = "PostgreSQL come vector database per applicazioni AI"
# Genera lo stesso embedding a dimensioni diverse
for dim in [256, 512, 1024, 1536]:
response = client.embeddings.create(
input=[testo],
model="text-embedding-3-small",
dimensions=dim
)
emb = response.data[0].embedding
print(f"Dimensioni: {dim}, norma: {np.linalg.norm(emb):.4f}")
# In PostgreSQL: usa colonne con dimensione appropriata
# CREATE TABLE docs_compact (
# id BIGSERIAL PRIMARY KEY,
# content TEXT,
# embedding vector(256) -- più compatto, 6x meno storage
# );
11. コストと戦略の拡張
プロトタイプから本番環境に移行する際、 埋め込みは重要な要素になります。詳細な分析を見てみましょう。
11.1 100万ドキュメントあたりのコスト
コストの見積もり: 100 万ドキュメント (ドキュメントあたり平均 500 トークン)
| モデル | 発電コスト | ベクトルサイズ | ストレージ (float32) | 初期合計 |
|---|---|---|---|---|
| all-MiniLM-L6-v2 | $0 (現地) | 384 | ~1.4GB | GPU/CPU時間のみ |
| テキスト埋め込み-3-small | ~10ドル | 1536年 | ~5.7GB | ~10 ドル + ストレージ |
| テキスト埋め込み-3-大 | ~$65 | 3072 | ~11.4GB | ~65 ドル + ストレージ |
| 航海-3 | ~$30 | 1024 | ~3.8GB | ~30 ドル + ストレージ |
保存式: N ドキュメント * サイズ * 4 バイト (float32)。 例: キャリアのみの場合、1M * 1536 * 4 = 5.7 GB。
11.2 セルフホスト型と API: トレードオフ
セルフホスト型と API の比較
| 待ってます | 自己ホスト型 | API (OpenAI、Cohere) |
|---|---|---|
| 初期費用 | 高 (GPU 約 1 ~ 3 ドル/時間) | 低額 (従量制) |
| ボリュームあたりのコスト | 約 1,000 万件以上の最安ドキュメント | リニア、ボリュームに合わせてスケール |
| レイテンシ | 低 (ネットワークなし) | 通話あたり 50 ~ 200 ミリ秒 |
| プライバシー | データはオンプレミスに残ります | 第三者に送信されるデータ |
| メンテナンス | GPUの管理、アップデート、モニタリング | ゼロ |
| 品質 | 選択したモデルによって異なります | 全体的に高く、安定している |
11.3 キャッシュ戦略
キャッシュの埋め込みは、コストと遅延を削減するために不可欠です。同じなら テキストが何度もリクエストされると、埋め込みを再生成するのは意味がありません。
import hashlib
import json
import psycopg2
from typing import Optional
import numpy as np
class EmbeddingCache:
"""Cache embeddings in PostgreSQL per evitare ricalcoli."""
def __init__(self, db_config: dict):
self.db_config = db_config
self._init_table()
def _init_table(self):
conn = psycopg2.connect(**self.db_config)
cur = conn.cursor()
cur.execute("""
CREATE TABLE IF NOT EXISTS embedding_cache (
content_hash VARCHAR(64) PRIMARY KEY,
model_name VARCHAR(100) NOT NULL,
embedding vector(1536),
created_at TIMESTAMPTZ DEFAULT NOW()
)
""")
conn.commit()
cur.close()
conn.close()
def _hash(self, text: str, model: str) -> str:
"""Hash deterministico del contenuto + modello."""
key = f"{model}::{text}"
return hashlib.sha256(key.encode()).hexdigest()
def get(
self, text: str, model: str
) -> Optional[list[float]]:
"""Cerca embedding in cache."""
h = self._hash(text, model)
conn = psycopg2.connect(**self.db_config)
cur = conn.cursor()
cur.execute(
"SELECT embedding FROM embedding_cache "
"WHERE content_hash = %s",
(h,)
)
row = cur.fetchone()
cur.close()
conn.close()
return row[0] if row else None
def put(
self, text: str, model: str, embedding: list[float]
):
"""Salva embedding in cache."""
h = self._hash(text, model)
conn = psycopg2.connect(**self.db_config)
cur = conn.cursor()
cur.execute(
"INSERT INTO embedding_cache "
"(content_hash, model_name, embedding) "
"VALUES (%s, %s, %s::vector) "
"ON CONFLICT (content_hash) DO NOTHING",
(h, model, embedding)
)
conn.commit()
cur.close()
conn.close()
12. PostgreSQL AIシリーズとの連携
私たちが構築しているエコシステムに埋め込みがどのように統合されるかをまとめてみましょう このシリーズでは:
全体の流れ:第1条から第3条まで
| ステップ | アイテム | アクション |
|---|---|---|
| 1 | pgvector (第 1 条) | pgvector を使用して PostgreSQL を構成し、ベクター列を含むテーブルを作成する |
| 2 | 埋め込み (第 2 条 - 本) | テンプレートを選択し、埋め込みを生成し、pgvector に保存します。 |
| 3 | PostgreSQL を使用した RAG (条項 3) | pgvector による検索と LLM を組み合わせて質問に答えます |
記事 1 では、インフラストラクチャを準備しました: pgvector がインストールされた PostgreSQL、テーブル ベクトル列と HNSW インデックスが設定されています。この記事ではそのギャップを埋めました 基本: これらのベクトルはどこから来たのか、適切なモデルを選択する方法、およびそれらを生成する方法 効率的に。次の記事では、すべてを使用する完全な RAG パイプラインを構築します。 このスタック: pgvector でインデックス付けされたドキュメント、クエリに対してオンザフライで生成された埋め込み、 もう 1 つは、取得したコンテキストに基づいて応答を生成する LLM です。
13. 結論とチェックリスト
埋め込みは、自然言語と言語を結び付ける基本的なコンポーネントです。 ベクトルデータベースの数学。適切なモデル、距離メトリックの選択 適切かつ効率的なスケーリング戦略は、直接影響を与える決定です。 AI システムの品質とコスト。
チェックリスト: 適切な埋め込みモデルの選択
- タスクを定義します。 検索、分類、クラスタリング、STS?
- 言語を特定します。 英語のみ、多言語、またはドメイン固有ですか?
- 制約を評価します。 予算、プライバシー、遅延、利用可能なインフラストラクチャ
- 候補者を 2 ~ 3 人選ぶ モデルテーブルから (セクション 4.3)
- 評価セットを作成する ドメインから (関連ドキュメントを含む 50 ~ 100 のクエリ)
- nDCGとMAPを測定 各候補者のデータセット上で
- コストを計算する 予想される量に対して完全に動作する (セクション 11)
- 小さいサイズをテストします。 多くの場合、多くの場合、512 調光で十分です。
- キャッシュの実装 再生コストを削減するため
- モニターの品質 評価セットを使用して時間の経過とともに
次の記事: PostgreSQL を使用した RAG
シリーズの次の記事では、これを構築します。 RAG パイプラインが完了しました PostgreSQL + pgvector を知識ベースとして使用した (検索拡張生成)。 類似性検索とインテリジェント チャンキングを組み合わせる方法、統合する方法を見ていきます。 GPT-4 や Claude などの LLM、および生成された応答の品質を測定する方法。







