LESSON 40分

ストーリー

佐藤CTO
エンベディングモデルが決まった。次はベクトルを格納するDBの選定だ
あなた
Pinecone、Weaviate、Qdrant、pgvector…選択肢が多すぎます
佐藤CTO
選択肢が多いからこそ、明確な選定基準が必要だ。我々のユースケースに合うものを選ぶ。専用ベクトルDB vs 既存DBの拡張、マネージド vs セルフホスト。それぞれのトレードオフを理解しよう

ベクトルDBの分類

3つのカテゴリ

graph TD
    Title["ベクトルDB の分類"]
    Title --- A & B & C

    A["専用ベクトルDB<br/>(Purpose-built)"]
    AList["Pinecone, Qdrant, Weaviate,<br/>Milvus, ChromaDB"]
    A --- AList

    B["既存DBの拡張<br/>(Extension)"]
    BList["pgvector(PostgreSQL),<br/>MongoDB Atlas Vector Search,<br/>Elasticsearch kNN"]
    B --- BList

    C["インメモリ/<br/>ライブラリ"]
    CList["FAISS, Annoy, hnswlib<br/>(DB機能なし、検索ライブラリのみ)"]
    C --- CList

    classDef category fill:#e0f2fe,stroke:#0284c7,font-weight:bold
    classDef items fill:#f0fdf4,stroke:#22c55e
    classDef title fill:#1e40af,stroke:#1e40af,color:#fff,font-weight:bold
    class A,B,C category
    class AList,BList,CList items
    class Title title

主要ベクトルDBの詳細比較

Pinecone

graph TD
    PC["Pinecone
マネージドSaaS(専用ベクトルDB)"] PC --> F["特徴"] F --- F1["フルマネージド
インフラ管理不要"] F --- F2["サーバーレスプランで
小規模開始可能"] F --- F3["メタデータフィルタリング対応"] F --- F4["Namespace で
マルチテナント対応"] PC --> P["長所"] P --- P1["運用負荷ゼロ"] P --- P2["スケーラビリティが高い"] P --- P3["高可用性
99.95% SLA"] PC --> C["短所"] C --- C1["ベンダーロックイン"] C --- C2["データが外部に保存される"] C --- C3["大規模ではコスト高"] PC --> PR["価格: Serverless
$0.33/1M reads, $2/1M writes"] style PC fill:#dbeafe,stroke:#2563eb,stroke-width:2px,color:#1e40af style F fill:#d1fae5,stroke:#059669,color:#065f46 style P fill:#d1fae5,stroke:#059669,color:#065f46 style C fill:#fee2e2,stroke:#dc2626,color:#991b1b style PR fill:#fef3c7,stroke:#d97706,stroke-width:2px,color:#92400e style F1,F2,F3,F4,P1,P2,P3,C1,C2,C3 fill:#f3f4f6,stroke:#9ca3af,color:#374151

Qdrant

graph TD
    QD["Qdrant
オープンソース(専用ベクトルDB)"] QD --> F["特徴"] F --- F1["Rust製で高速"] F --- F2["豊富なフィルタリング機能"] F --- F3["ペイロード(メタデータ)に対する
高度なクエリ"] F --- F4["Qdrant Cloud(マネージド)
も利用可能"] QD --> P["長所"] P --- P1["高い検索パフォーマンス"] P --- P2["セルフホスト可能
データ管理自由"] P --- P3["gRPC & REST API"] P --- P4["多彩な距離メトリクス"] QD --> C["短所"] C --- C1["セルフホスト時の運用負荷"] C --- C2["エコシステムは
Pineconeほど成熟していない"] QD --> PR["価格: OSS無料
Cloud $0.045/hr〜"] style QD fill:#dbeafe,stroke:#2563eb,stroke-width:2px,color:#1e40af style F fill:#d1fae5,stroke:#059669,color:#065f46 style P fill:#d1fae5,stroke:#059669,color:#065f46 style C fill:#fee2e2,stroke:#dc2626,color:#991b1b style PR fill:#fef3c7,stroke:#d97706,stroke-width:2px,color:#92400e style F1,F2,F3,F4,P1,P2,P3,P4,C1,C2 fill:#f3f4f6,stroke:#9ca3af,color:#374151

Weaviate

graph TD
    WV["Weaviate
オープンソース(専用ベクトルDB)"] WV --> F["特徴"] F --- F1["GraphQL APIでのクエリ"] F --- F2["内蔵のベクタライズモジュール"] F --- F3["ハイブリッド検索
BM25 + ベクトル のネイティブサポート"] F --- F4["マルチモーダル対応"] WV --> P["長所"] P --- P1["ハイブリッド検索が
簡単に使える"] P --- P2["スキーマ定義による
データ管理"] P --- P3["活発なコミュニティ"] P --- P4["モジュラーアーキテクチャ"] WV --> C["短所"] C --- C1["メモリ使用量が大きい"] C --- C2["GraphQL の学習コスト"] WV --> PR["価格: OSS無料
Cloud $25/mo〜"] style WV fill:#dbeafe,stroke:#2563eb,stroke-width:2px,color:#1e40af style F fill:#d1fae5,stroke:#059669,color:#065f46 style P fill:#d1fae5,stroke:#059669,color:#065f46 style C fill:#fee2e2,stroke:#dc2626,color:#991b1b style PR fill:#fef3c7,stroke:#d97706,stroke-width:2px,color:#92400e style F1,F2,F3,F4,P1,P2,P3,P4,C1,C2 fill:#f3f4f6,stroke:#9ca3af,color:#374151

pgvector (PostgreSQL)

graph TD
    PG["pgvector
PostgreSQL拡張"] PG --> F["特徴"] F --- F1["既存のPostgreSQLに追加可能"] F --- F2["SQLでベクトル検索が可能"] F --- F3["HNSW & IVFFlat インデックス"] F --- F4["トランザクション対応"] PG --> P["長所"] P --- P1["既存インフラの活用"] P --- P2["SQLの知識がそのまま使える"] P --- P3["メタデータとの結合が容易"] P --- P4["ACID トランザクション"] P --- P5["低い運用学習コスト"] PG --> C["短所"] C --- C1["大規模データでの
スケーラビリティ"] C --- C2["専用DBほどの
検索パフォーマンスは出ない"] C --- C3["分散構成が難しい"] PG --> PR["価格: PostgreSQLと同じ
拡張は無料"] style PG fill:#dbeafe,stroke:#2563eb,stroke-width:2px,color:#1e40af style F fill:#d1fae5,stroke:#059669,color:#065f46 style P fill:#d1fae5,stroke:#059669,color:#065f46 style C fill:#fee2e2,stroke:#dc2626,color:#991b1b style PR fill:#fef3c7,stroke:#d97706,stroke-width:2px,color:#92400e style F1,F2,F3,F4,P1,P2,P3,P4,P5,C1,C2,C3 fill:#f3f4f6,stroke:#9ca3af,color:#374151

選定マトリクス

ユースケース別推奨

ユースケース推奨理由
MVP/PoCChromaDB / pgvector最速でプロトタイプ構築
既にPostgreSQL運用中pgvectorインフラ追加不要
本番RAG (〜100万ベクトル)pgvector / Qdrantコスパ良好
本番RAG (100万〜1億)Qdrant / Pinecone / Weaviateスケーラビリティ必要
ハイブリッド検索重視Weaviateネイティブサポート
運用負荷最小Pineconeフルマネージド
データ主権が最重要Qdrant (self-hosted) / pgvector完全自社管理

機能比較表

機能PineconeQdrantWeaviatepgvector
ハイブリッド検索Sparse vectorBM25プラグインネイティブ別途全文検索
メタデータフィルタ対応高度GraphQLSQL WHERE
マルチテナントNamespaceコレクションテナントスキーマ/RLS
バックアップ自動スナップショットバックアップpg_dump
レプリケーション自動Raft自動PostgreSQL方式
最大ベクトル数10億+数十億数億数千万(推奨)

TypeScript実装例

pgvector を使った実装

import { Pool } from 'pg';

class PgVectorStore implements VectorStore {
  constructor(private readonly pool: Pool) {}

  async initialize(): Promise<void> {
    await this.pool.query('CREATE EXTENSION IF NOT EXISTS vector');
    await this.pool.query(`
      CREATE TABLE IF NOT EXISTS embeddings (
        id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
        content TEXT NOT NULL,
        embedding vector(1536),
        metadata JSONB DEFAULT '{}',
        created_at TIMESTAMPTZ DEFAULT NOW()
      )
    `);
    // HNSW インデックス作成
    await this.pool.query(`
      CREATE INDEX IF NOT EXISTS embeddings_hnsw_idx
      ON embeddings USING hnsw (embedding vector_cosine_ops)
      WITH (m = 16, ef_construction = 200)
    `);
  }

  async upsert(chunks: Chunk[]): Promise<void> {
    const query = `
      INSERT INTO embeddings (id, content, embedding, metadata)
      VALUES ($1, $2, $3::vector, $4::jsonb)
      ON CONFLICT (id) DO UPDATE SET
        content = EXCLUDED.content,
        embedding = EXCLUDED.embedding,
        metadata = EXCLUDED.metadata
    `;

    for (const chunk of chunks) {
      await this.pool.query(query, [
        chunk.id,
        chunk.content,
        `[${chunk.embedding!.join(',')}]`,
        JSON.stringify(chunk.metadata),
      ]);
    }
  }

  async search(
    queryEmbedding: number[],
    topK: number,
    filter?: Record<string, unknown>,
  ): Promise<RetrievedContext[]> {
    let whereClause = '';
    const params: unknown[] = [
      `[${queryEmbedding.join(',')}]`,
      topK,
    ];

    if (filter) {
      const conditions: string[] = [];
      let paramIndex = 3;
      for (const [key, value] of Object.entries(filter)) {
        conditions.push(`metadata->>'${key}' = $${paramIndex}`);
        params.push(value);
        paramIndex++;
      }
      if (conditions.length > 0) {
        whereClause = `WHERE ${conditions.join(' AND ')}`;
      }
    }

    const result = await this.pool.query(
      `SELECT id, content, metadata,
              1 - (embedding <=> $1::vector) AS score
       FROM embeddings
       ${whereClause}
       ORDER BY embedding <=> $1::vector
       LIMIT $2`,
      params,
    );

    return result.rows.map(row => ({
      chunk: {
        id: row.id,
        documentId: row.metadata.documentId,
        content: row.content,
        metadata: row.metadata,
      },
      score: row.score,
    }));
  }
}

Qdrant を使った実装

import { QdrantClient } from '@qdrant/js-client-rest';

class QdrantVectorStore implements VectorStore {
  private client: QdrantClient;
  private collectionName: string;

  constructor(url: string, collectionName: string) {
    this.client = new QdrantClient({ url });
    this.collectionName = collectionName;
  }

  async initialize(): Promise<void> {
    await this.client.createCollection(this.collectionName, {
      vectors: {
        size: 1536,
        distance: 'Cosine',
      },
      optimizers_config: {
        default_segment_number: 2,
      },
      replication_factor: 1,
    });
  }

  async upsert(chunks: Chunk[]): Promise<void> {
    const points = chunks.map(chunk => ({
      id: chunk.id,
      vector: chunk.embedding!,
      payload: {
        content: chunk.content,
        ...chunk.metadata,
      },
    }));

    await this.client.upsert(this.collectionName, {
      wait: true,
      points,
    });
  }

  async search(
    queryEmbedding: number[],
    topK: number,
    filter?: Record<string, unknown>,
  ): Promise<RetrievedContext[]> {
    const qdrantFilter = filter ? this.buildFilter(filter) : undefined;

    const results = await this.client.search(this.collectionName, {
      vector: queryEmbedding,
      limit: topK,
      filter: qdrantFilter,
      with_payload: true,
    });

    return results.map(result => ({
      chunk: {
        id: String(result.id),
        documentId: String(result.payload?.documentId ?? ''),
        content: String(result.payload?.content ?? ''),
        metadata: result.payload as Record<string, unknown>,
      },
      score: result.score,
    }));
  }

  private buildFilter(filter: Record<string, unknown>) {
    const must = Object.entries(filter).map(([key, value]) => ({
      key,
      match: { value },
    }));
    return { must };
  }
}
VectorStoreのインターフェース定義
interface VectorStore {
  initialize(): Promise<void>;
  upsert(chunks: Chunk[]): Promise<void>;
  search(
    queryEmbedding: number[],
    topK: number,
    filter?: Record<string, unknown>,
  ): Promise<RetrievedContext[]>;
  delete(ids: string[]): Promise<void>;
}

// ファクトリパターンで切り替え可能にする
function createVectorStore(config: VectorStoreConfig): VectorStore {
  switch (config.type) {
    case 'pgvector':
      return new PgVectorStore(new Pool(config.connection));
    case 'qdrant':
      return new QdrantVectorStore(config.url, config.collection);
    case 'pinecone':
      return new PineconeVectorStore(config.apiKey, config.index);
    default:
      throw new Error(`Unknown vector store type: ${config.type}`);
  }
}

まとめ

ポイント内容
3つのカテゴリ専用ベクトルDB / 既存DB拡張 / インメモリライブラリ
pgvector既存PostgreSQL環境なら最も低コスト。数千万ベクトルまで
Qdrant高性能セルフホスト。データ主権が必要な場合に最適
Pineconeフルマネージドで運用負荷最小。コストは高め
選定基準データ量、運用負荷、データ主権、既存インフラとの親和性

チェックリスト

  • ベクトルDBの3つのカテゴリを理解した
  • 主要ベクトルDB(Pinecone/Qdrant/Weaviate/pgvector)の特徴を把握した
  • ユースケース別の選定基準を理解した
  • TypeScriptでの実装パターンを理解した

次のステップへ

ベクトルDBの選定基準を学びました。次のセクションでは、ベクトルDBの検索性能を左右するインデックス戦略について深く掘り下げます。

ツールの選定は、要件を明確にしてから。技術の流行ではなく、ユースケースで判断すること。


推定読了時間: 40分