DockerでのQdrantデプロイ:RAGとセマンティック検索における本番運用6ヶ月の振り返り

Database tutorial - IT technology blog
Database tutorial - IT technology blog

導入の背景:ベクトルワークロードをRustへ移行した理由

6ヶ月前、私たちのチームはセマンティック検索基盤のパフォーマンスの限界に直面していました。OpenAIの1536次元モデルで埋め込まれた2,000万件のチャンクを対象に、ユーザー向けのRAGワークフローを提供していましたが、AWSのr6i.4xlargeインスタンス上で稼働するPostgreSQLとpgvectorという既存構成が高負荷に耐えきれなくなっていたのです。一括再インデックス処理の実行中には、p99検索レイテンシが45msから140ms以上へと急増。さらに深刻なことに、Postgresのメモリ消費量は160GBを超え、OOM(メモリ不足)クラッシュの危機に頻繁に瀕していました。

構造化されたトランザクションデータの処理において、リレーショナルデータベースやドキュメントストアは依然として優れた選択肢です。しかし、高次元ベクトルの検索には、専用のメモリレイアウトとハードウェアアクセラレーションが不可欠です。私たちはMilvus、Pinecone、Qdrantの比較検証を行い、最終的にQdrantを標準採用することを決定しました。

Rustで実装されたQdrantはガベージコレクションによる一時停止(GCポーズ)を排除し、SIMDアクセラレーション(AVX-512)によってx86チップ上の計算スループットを最大化しました。移行後、450 QPSの負荷下におけるp99クエリレイテンシは11.8msまで低下し、スカラー量子化を有効化したことでRAM使用量も68%削減されました。

インストール:本番環境向けDocker & Docker Composeセットアップ

アドホックに起動した単一コンテナは、本番環境ではすぐに破綻します。Docker Composeを通じて、予測可能なディスク永続化、明示的なリソース制限、そして自動ヘルスチェックを設定する必要があります。

プロジェクトのディレクトリ構成

mkdir -p /opt/qdrant/{data,config,snapshots}
cd /opt/qdrant

Docker Composeの設定

安定版リリースであるv1.8.4を対象に、堅牢なdocker-compose.ymlファイルを作成します:

version: '3.8'

services:
  qdrant:
    image: qdrant/qdrant:v1.8.4
    container_name: qdrant_production
    restart: unless-stopped
    ports:
      - "6333:6333" # REST API & Web UI
      - "6334:6334" # gRPC API
    environment:
      - QDRANT__SERVICE__HTTP_PORT=6333
      - QDRANT__SERVICE__GRPC_PORT=6334
      - QDRANT__SERVICE__ENABLE_CORS=true
      - QDRANT__SERVICE__API_KEY=${QDRANT_API_KEY}
    volumes:
      - ./data:/qdrant/storage
      - ./config/config.yaml:/qdrant/config/production.yaml:ro
      - ./snapshots:/qdrant/snapshots
    ulimits:
      nofile:
        soft: 65535
        hard: 65535
    deploy:
      resources:
        limits:
          cpus: '8.0'
          memory: 32G
        reservations:
          cpus: '4.0'
          memory: 16G
    healthcheck:
      test: ["CMD-SHELL", "curl -sf http://localhost:6333/healthz || exit 1"]
      interval: 15s
      timeout: 5s
      retries: 3
      start_period: 10s

コンテナデーモンを起動する前に、256ビットのAPIキーを生成して.envに書き込みます:

echo "QDRANT_API_KEY=$(openssl rand -hex 32)" > .env
docker compose up -d

設定:ストレージ、HNSW、量子化の最適化

デフォルト設定はインデックス作成のスループットよりもローカルでの迅速なプロトタイピングを優先しています。数千万件規模のベクトルを保持するコレクションでは、インデックス作成の閾値を細かく調整し、JSONペイロードをNVMeディスクへオフロードすることが重要です。

本番用 config/config.yaml

storage:
  storage_path: /qdrant/storage
  snapshots_path: /qdrant/snapshots
  on_disk_payload: true

service:
  max_request_size_mb: 32
  max_workers: 8

hnsw_index:
  m: 16
  ef_construct: 128
  full_scan_threshold: 10000
  max_indexing_threads: 4
  on_disk: false

メモリ効率向上のためのスカラー量子化設定

生(Raw)の32ビット浮動小数点ベクトルは、規模が拡大するにつれてメモリコストが跳ね上がります。1536次元のベクトル2,000万件では、ベクトルデータだけで純粋に約122.8 GBのRAM(20,000,000 * 1536 * 4バイト)が必要になります。1バイトのスカラー量子化(int8)を有効にすると、このフットプリントを約30.7 GBまで圧縮できます。10,000件のテストクエリを用いた再現率評価では、精度の低下はわずか0.8%にとどまり、スループットは2倍に向上しました。

以下は、公式Pythonクライアントを使用した本番コレクションのプロビジョニングスクリプトです:

import os
from qdrant_client import QdrantClient
from qdrant_client.http import models

client = QdrantClient(
    url="http://localhost:6333",
    api_key=os.getenv("QDRANT_API_KEY"),
    prefer_grpc=True
)

# スカラー量子化を有効にしてコレクションを初期化
client.create_collection(
    collection_name="knowledge_base",
    vectors_config=models.VectorParams(
        size=1536,
        distance=models.Distance.COSINE,
        on_disk=False  # 最速の探索を実現するためグラフをRAM上に保持
    ),
    optimizers_config=models.OptimizersConfigDiff(
        indexing_threshold=20000,
        memmap_threshold=50000
    ),
    quantization_config=models.ScalarQuantization(
        scalar=models.ScalarQuantizationConfig(
            type=models.ScalarType.INT8,
            quantile=0.99,
            always_ram=True
        )
    )
)

# 高速なブール/完全一致フィルタリング用にメタデータフィールドのインデックスを作成
client.create_payload_index(
    collection_name="knowledge_base",
    field_name="tenant_id",
    field_schema=models.PayloadSchemaType.KEYWORD
)
client.create_payload_index(
    collection_name="knowledge_base",
    field_name="created_at",
    field_schema=models.PayloadSchemaType.INTEGER
)

検証と監視:ヘルスチェックと本番メトリクス

クラスタを立ち上げたら、実際のエンドユーザートラフィックを流す前に、データ取り込みとクエリの仕組みを検証します。

1. バッチUpsertとフィルタリング検索のテスト

Pythonから直接、テナントでスコープを絞ったセマンティッククエリを検証します:

# サンプルドキュメントポイントを挿入
client.upsert(
    collection_name="knowledge_base",
    points=[
        models.PointStruct(
            id="c56a4180-65aa-42ec-a945-5fd21dec0538",
            vector=[0.012] * 1536,
            payload={
                "tenant_id": "tenant_acme_corp",
                "doc_title": "Qdrantの本番アーキテクチャ",
                "created_at": 1717200000
            }
        )
    ]
)

# 事前フィルタリング付きでクエリを実行
search_hits = client.query_points(
    collection_name="knowledge_base",
    query=[0.012] * 1536,
    query_filter=models.Filter(
        must=[
            models.FieldCondition(
                key="tenant_id",
                match=models.MatchValue(value="tenant_acme_corp")
            )
        ]
    ),
    limit=5,
    with_payload=True
)

for hit in search_hits.points:
    print(f"スコア: {hit.score:.4f} | タイトル: {hit.payload['doc_title']}")

2. Prometheusメトリクスの統合

Qdrantは/metricsエンドポイント経由でHTTPベースのOpenTelemetryメトリクスを公開しています。中央のスクレイパーに直接接続します:

scrape_configs:
  - job_name: 'qdrant'
    metrics_path: '/metrics'
    bearer_token: 'YOUR_QDRANT_API_KEY'
    static_configs:
      - targets: ['qdrant_production:6333']

3. 注視すべき主要メトリクス

  • app_telemetry_rest_requests_duration_seconds_bucket: 検索レイテンシの分布を追跡します。5分間の移動窓でp99が35msを超えた場合にアラートを発報します。
  • collections_vector_count: コレクションごとの総ベクトルレコード数。データ増加の傾向を把握するために使用します。
  • process_resident_memory_bytes: プロセスによって直接割り当てられたメモリ量。LinuxによるOOM Killerの発動を防ぐため、コンテナのメモリ上限の80%で警告を設定します。
  • cluster_pending_operations: 未処理のオプティマイザキューの深さ。継続的な急増は、インデックスのコンパクション処理がバッチ書き込みの量に追いついていないことを示します。

6ヶ月間の稼働から得られた運用の教訓

インフラの安定稼働を支えた3つの具体的なプラクティスは以下の通りです:

  1. ペイロードのインデックス作成は必須: テナントIDやタイムスタンプなどのフィールドには、必ず専用のペイロードインデックスを作成してください。インデックスのないフィールドでフィルタリングを行うと、セグメント全体のフルスキャンが発生し、クエリの並行処理性能が著しく低下します。
  2. データ取り込みパイプラインをgRPCに切り替える: ポート6334経由のgRPCでベクトルを送信することで、取り込み時のCPU使用率が28%削減され、ワーカーあたりの書き込みスループットが毎秒1,800ポイントから4,200ポイントへと向上しました。
  3. スナップショットのローテーションを自動化する: /collections/{name}/snapshots APIを使用して毎晩ポイントインタイムスナップショットを取得し、圧縮してS3コールドストレージバケットに同期します。スナップショットの作成はロックフリーであり、読み取りトラフィックをブロックすることはありません。
Share: