LESSON 40分

ストーリー

佐藤CTO
RAGの設計が固まった。次はベクトルDBの構築だ
佐藤CTO
しかし、ベクトルDBの前にまず考えるべきことがある。ベクトルの元になるエンベディングモデルの選定だ。検索品質の基盤はここで決まる
あなた
エンベディングモデルはどれも同じではないのですか?
佐藤CTO
全く違う。次元数、多言語対応、ドメイン適応性、速度。全てが異なる。そして一度選んだモデルを後から変更するのは、全インデックスの再構築を意味する。最初の選定が極めて重要だ

エンベディングとは

ベクトル空間への変換

エンベディングとは、テキストを数値のベクトル(数値の配列)に変換することです。意味的に近いテキストは、ベクトル空間上で近い位置にマッピングされます。

// エンベディングの概念
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-largeOpenAI30728191対応$0.13
text-embedding-3-smallOpenAI15368191対応$0.02
embed-multilingual-v3.0Cohere1024512100+言語$0.10
Voyage-3Voyage AI102432000対応$0.06
Titan Embeddings v2AWS Bedrock10248192対応$0.02

オープンソースモデル

モデル次元数最大トークン多言語MTEBスコア
multilingual-e5-large1024512100+言語
bge-m310248192100+言語
gte-Qwen2-7B-instruct358432768多言語最高クラス
nomic-embed-text-v1.57688192英語中心中-高

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分