ストーリー
エンベディングとは
ベクトル空間への変換
エンベディングとは、テキストを数値のベクトル(数値の配列)に変換することです。意味的に近いテキストは、ベクトル空間上で近い位置にマッピングされます。
// エンベディングの概念
const embedding = await embed("TypeScriptの型システム");
// → [0.023, -0.156, 0.089, ..., 0.042] // 1536次元のベクトル
const similar = await embed("TypeScriptの型定義");
// → [0.025, -0.148, 0.091, ..., 0.039] // 非常に近いベクトル
const different = await embed("今日の天気は晴れです");
// → [-0.112, 0.234, -0.067, ..., 0.178] // 遠いベクトル
類似度の計算
// コサイン類似度
function cosineSimilarity(a: number[], b: number[]): number {
const dotProduct = a.reduce((sum, val, i) => sum + val * b[i], 0);
const normA = Math.sqrt(a.reduce((sum, val) => sum + val * val, 0));
const normB = Math.sqrt(b.reduce((sum, val) => sum + val * val, 0));
return dotProduct / (normA * normB);
}
// 類似度の解釈
// 1.0: 完全に同一の意味
// 0.8-0.9: 非常に類似
// 0.6-0.8: 関連性あり
// 0.3-0.6: 弱い関連性
// 0.0-0.3: ほぼ無関係
主要エンベディングモデルの比較
商用API
| モデル | 提供者 | 次元数 | 最大トークン | 多言語 | 価格 ($/1M tokens) |
|---|---|---|---|---|---|
| text-embedding-3-large | OpenAI | 3072 | 8191 | 対応 | $0.13 |
| text-embedding-3-small | OpenAI | 1536 | 8191 | 対応 | $0.02 |
| embed-multilingual-v3.0 | Cohere | 1024 | 512 | 100+言語 | $0.10 |
| Voyage-3 | Voyage AI | 1024 | 32000 | 対応 | $0.06 |
| Titan Embeddings v2 | AWS Bedrock | 1024 | 8192 | 対応 | $0.02 |
オープンソースモデル
| モデル | 次元数 | 最大トークン | 多言語 | MTEBスコア |
|---|---|---|---|---|
| multilingual-e5-large | 1024 | 512 | 100+言語 | 高 |
| bge-m3 | 1024 | 8192 | 100+言語 | 高 |
| gte-Qwen2-7B-instruct | 3584 | 32768 | 多言語 | 最高クラス |
| nomic-embed-text-v1.5 | 768 | 8192 | 英語中心 | 中-高 |
MTEB ベンチマーク
MTEB(Massive Text Embedding Benchmark)は、エンベディングモデルの品質を測定する業界標準ベンチマークです。
graph TD
MTEB["MTEB の評価カテゴリ"]
MTEB --> R["Retrieval: 検索タスクでの精度"]
MTEB --> S["STS: 文の類似度判定"]
MTEB --> CL["Classification: テキスト分類"]
MTEB --> CLU["Clustering: テキストクラスタリング"]
MTEB --> RE["Reranking: 関連度の順序付け"]
MTEB --> PC["PairClassification: ペア分類"]
MTEB --> SU["Summarization: 要約の品質"]
NOTE["RAGシステムでは特に
Retrieval スコアが重要"]
style MTEB fill:#dbeafe,stroke:#2563eb,stroke-width:2px,color:#1e40af
style R fill:#d1fae5,stroke:#059669,color:#065f46
style S,CL,CLU,RE,PC,SU fill:#f3f4f6,stroke:#9ca3af,color:#374151
style NOTE fill:#fef3c7,stroke:#d97706,stroke-width:2px,color:#92400e
エンベディングモデルの選定基準
評価マトリクス
interface EmbeddingModelEvaluation {
model: string;
quality: {
mtebRetrieval: number; // MTEB Retrievalスコア
japaneseQuality: number; // 日本語での検索品質 (1-10)
domainAdaptation: number; // 専門用語への対応力 (1-10)
};
performance: {
dimensions: number; // 次元数
maxTokens: number; // 最大入力トークン数
latencyMs: number; // 推論レイテンシ (ms)
throughput: number; // tokens/sec
};
operational: {
costPer1M: number; // $/1M tokens
selfHostable: boolean; // セルフホスティング可能か
apiStability: number; // API安定性 (1-10)
};
}
次元数のトレードオフ
| 次元数 | ストレージ | 検索速度 | 表現力 | 用途 |
|---|---|---|---|---|
| 256-512 | 小 | 高速 | 低 | 大量データ、コスト重視 |
| 768-1024 | 中 | 中速 | 中-高 | 一般的な用途(推奨) |
| 1536-3072 | 大 | 低速 | 高 | 高精度要求、少量データ |
Matryoshka Embedding
OpenAI text-embedding-3 系では、生成された高次元ベクトルの先頭N次元を切り出して使う「Matryoshka Embedding」がサポートされています。
import { OpenAI } from 'openai';
const openai = new OpenAI();
// 次元数を指定してエンベディングを取得
async function getEmbedding(text: string, dimensions: number = 1536): Promise<number[]> {
const response = await openai.embeddings.create({
model: 'text-embedding-3-small',
input: text,
dimensions, // 256, 512, 1536 など指定可能
});
return response.data[0].embedding;
}
// コスト最適化: 検索精度が許容範囲なら低次元で運用
// 512次元: ストレージ1/3、検索3倍速、精度は95%以上維持
多言語対応の考慮事項
日本語エンベディングの課題
graph TD
JA["日本語テキストの特殊性"]
JA --> T1["トークナイゼーション:
英語と異なる分割ルール"]
JA --> T2["漢字/ひらがな/カタカナ:
同じ意味の多様な表記"]
JA --> T3["専門用語:
技術用語が英語混在する"]
JA --> T4["文脈依存性:
同じ語が文脈で意味が変わる"]
EX["例: サーバー = サーバ = server
同じ意味を近いベクトルにする必要がある"]
style JA fill:#dbeafe,stroke:#2563eb,stroke-width:2px,color:#1e40af
style T1,T2,T3,T4 fill:#f3f4f6,stroke:#9ca3af,color:#374151
style EX fill:#fef3c7,stroke:#d97706,stroke-width:2px,color:#92400e
多言語モデルの評価方法
// 日本語検索品質の評価セット例
const japaneseBenchmark = [
{
query: "Kubernetesでポッドがクラッシュループする原因",
expectedDocs: ["k8s-troubleshooting-crashloop.md"],
},
{
query: "マイクロサービスの認証パターン",
expectedDocs: ["auth-patterns.md", "microservice-security.md"],
},
{
query: "データベースのデッドロック解消方法",
expectedDocs: ["db-deadlock-resolution.md"],
},
];
// 各モデルでの検索精度を比較
async function benchmarkEmbeddingModels(
models: EmbeddingService[],
vectorStores: VectorStore[],
benchmark: typeof japaneseBenchmark,
): Promise<Record<string, number>> {
const results: Record<string, number> = {};
for (let i = 0; i < models.length; i++) {
let totalRecall = 0;
for (const sample of benchmark) {
const queryEmbedding = await models[i].embed(sample.query);
const searchResults = await vectorStores[i].search(queryEmbedding, 5);
const retrievedIds = searchResults.map(r => r.chunk.metadata.source);
const recall = sample.expectedDocs.filter(id =>
retrievedIds.includes(id)
).length / sample.expectedDocs.length;
totalRecall += recall;
}
results[models[i].modelName] = totalRecall / benchmark.length;
}
return results;
}
エンベディングの前処理テクニック
テキストの前処理で検索品質を向上
function preprocessForEmbedding(text: string): string {
// 1. 不要な空白・改行の正規化
let processed = text.replace(/\s+/g, ' ').trim();
// 2. コードブロックの処理(コード内容は保持するが装飾を除去)
processed = processed.replace(/```[\s\S]*?```/g, (match) => {
return match.replace(/```\w*\n?/, '').replace(/```/, '').trim();
});
// 3. URL を簡略化
processed = processed.replace(
/https?:\/\/[^\s]+/g,
'[URL]'
);
// 4. 特殊文字の除去
processed = processed.replace(/[│├└┌┐─═╔╗╚╝║]/g, '');
return processed;
}
// ドキュメントのタイトル/メタ情報を先頭に付加(検索精度向上)
function enrichChunkForEmbedding(chunk: Chunk): string {
const title = chunk.metadata.documentTitle ?? '';
const section = chunk.metadata.sectionTitle ?? '';
const prefix = [title, section].filter(Boolean).join(' > ');
return prefix ? `${prefix}\n\n${chunk.content}` : chunk.content;
}
実装パターン
エンベディングサービスの抽象化
interface EmbeddingService {
readonly modelName: string;
readonly dimensions: number;
embed(text: string): Promise<number[]>;
embedBatch(texts: string[]): Promise<number[][]>;
}
class OpenAIEmbedding implements EmbeddingService {
readonly modelName = 'text-embedding-3-small';
readonly dimensions: number;
constructor(
private readonly client: OpenAI,
dimensions: number = 1536,
) {
this.dimensions = dimensions;
}
async embed(text: string): Promise<number[]> {
const response = await this.client.embeddings.create({
model: this.modelName,
input: text,
dimensions: this.dimensions,
});
return response.data[0].embedding;
}
async embedBatch(texts: string[]): Promise<number[][]> {
// OpenAI APIは最大2048入力をバッチ処理可能
const batchSize = 2048;
const allEmbeddings: number[][] = [];
for (let i = 0; i < texts.length; i += batchSize) {
const batch = texts.slice(i, i + batchSize);
const response = await this.client.embeddings.create({
model: this.modelName,
input: batch,
dimensions: this.dimensions,
});
allEmbeddings.push(...response.data.map(d => d.embedding));
}
return allEmbeddings;
}
}
まとめ
| ポイント | 内容 |
|---|---|
| エンベディングの基礎 | テキストを数値ベクトルに変換し、類似度で検索する |
| モデル選定 | MTEB Retrievalスコア、多言語対応、次元数、コストで評価 |
| 次元数のトレードオフ | 768-1024次元が一般的な推奨。Matryoshkaで柔軟に調整可能 |
| 日本語対応 | 多言語モデルの使用と、自社データでのベンチマークが必須 |
チェックリスト
- エンベディングとベクトル類似度の基本を理解した
- 主要エンベディングモデルの特徴を把握した
- MTEB ベンチマークの位置づけを理解した
- 多言語・日本語対応の考慮事項を理解した
次のステップへ
エンベディングモデルの選定基準を学びました。次のセクションでは、生成したベクトルを保存・検索するベクトルDBの比較と選定を行います。
エンベディングの品質がRAGの検索精度の天井を決める。基盤の選定を疎かにしないこと。
推定読了時間: 40分