Milvusを6ヶ月本番運用して学んだこと
MySQL、PostgreSQL、MongoDBで本番システムを運用してきた。それぞれに存在意義がある。だが、数千万件のエンベディングを低レイテンシで処理しなければならない検索拡張生成(RAG)システムを構築しようとしたとき、どれも限界に達した。
一通りの候補を評価した。ChromaDBはプロトタイプ以上には物足りなかった。Pineconeは機能的には問題ないが、ベンダーロックインとデータ転送コストがネックだった。Weaviateは運用負荷が大きすぎた。Milvusは違った。十億スケールのベクトル検索のために一から設計されており、エンタープライズ環境での運用を後付けではなく最優先事項として扱っている。
このガイドは、本番RAGパイプラインでMilvusをDockerで6ヶ月運用した経験をもとにしている。デモではなく、約5000万件のベクトルを複数サービスが同時にクエリする実際の本番環境での話だ。
エンタープライズスケールAIにMilvusを選ぶ理由
答えはアーキテクチャにある。Milvusはストレージ、インデックス、クエリの各レイヤーを分離しており、コンポーネントごとに独立してスケールできる。パイプラインのクエリ負荷が2倍になったとき、ストレージに手を触れることなくクエリノードだけをスケールした。こうした柔軟性は、ベクトルワークロードにおいてPostgreSQLやMongoDBには存在しない——全体をまとめてスケールするしかないのだ。
Milvusが私のスタックに定着した主な理由:
- 複数のインデックスタイプ:HNSW、IVF_FLAT、IVF_SQ8、DISKANN — メモリと精度のトレードオフに基づいて選択
- ハイブリッド検索:密なベクトル検索とスカラーフィルタリングの組み合わせ(ANNサーチ実行中に
user_idやcreated_atでフィルタリング可能) - マルチテナンシー:パーティションレベルの分離によりSaaSアーキテクチャへの適用が現実的
- 活発なメンテナンス:Zillizが商用サポートを提供しており、本番環境の問題が迅速に解決される
数百万件以下のベクトルを扱う小規模プロジェクトなら、より軽量な選択肢で十分だ。そのしきい値を超えた場合、Milvusのセットアップの複雑さは投資に見合う価値をすぐに発揮する。
インストール:DockerでMilvusを構築する
Milvusには3つのデプロイモードがある:Lite(組み込み)、Standalone、Clusterだ。KubernetesなしでDockerを運用するチームには、Standaloneが最適な選択だ。etcdとMinIOを内部にバンドルし、管理しやすいコンテナセットとして動作する——オーケストレーションプラットフォームは不要だ。
前提条件
- Docker Engine 20.10+とDocker Compose v2
- 最低4コアCPUと8GB RAM(本番環境では16GB推奨)
- Linuxホスト推奨 — macOSは開発には使えるが本番には不向き
公式Docker Composeファイルのダウンロード
mkdir -p /opt/milvus && cd /opt/milvus
# 公式standaloneのcomposeファイルをダウンロード
wget https://github.com/milvus-io/milvus/releases/download/v2.4.9/milvus-standalone-docker-compose.yml \
-O docker-compose.yml
常に公式ファイルから始めよう。主要なセクションの概要を示す(見やすくするために整理済み):
version: '3.5'
services:
etcd:
image: quay.io/coreos/etcd:v3.5.5
environment:
- ETCD_AUTO_COMPACTION_MODE=revision
- ETCD_AUTO_COMPACTION_RETENTION=1000
- ETCD_QUOTA_BACKEND_BYTES=4294967296
volumes:
- ./volumes/etcd:/etcd
minio:
image: minio/minio:RELEASE.2023-03-13T19-46-17Z
environment:
MINIO_ACCESS_KEY: minioadmin
MINIO_SECRET_KEY: minioadmin
volumes:
- ./volumes/minio:/minio_data
command: minio server /minio_data
standalone:
image: milvusdb/milvus:v2.4.9
command: ["milvus", "run", "standalone"]
environment:
ETCD_ENDPOINTS: etcd:2379
MINIO_ADDRESS: minio:9000
ports:
- "19530:19530" # gRPC
- "9091:9091" # HTTP / メトリクス
volumes:
- ./volumes/milvus:/var/lib/milvus
depends_on:
- etcd
- minio
スタックを起動する
docker compose up -d
# すべてのコンテナが起動していることを確認
docker compose ps
etcd、minio、milvus-standaloneの3コンテナがすべて Up 状態になれば成功だ。Milvusがメタデータ構造を初期化するため、初回起動には30〜60秒かかる。
設定:本番向けチューニング
デフォルト設定はデモをこなすには十分だ。だが実際のトラフィックには耐えられない。実際の使用パターンが明らかになった1ヶ月後に行った変更を紹介する。
カスタムmilvus.yaml
イメージを再ビルドせずにデフォルト設定を上書きするために、カスタム設定ファイルをマウントする:
mkdir -p /opt/milvus/config
cat > /opt/milvus/config/milvus.yaml << 'EOF'
log:
level: warn # ログの冗長性を抑える;デバッグ時は "info" を使用
dataCoord:
segment:
maxSize: 512 # MB;セグメントが小さいほどインデックスが速い
sealProportion: 0.8
queryNode:
gracefulTime: 5000 # クエリ停止前の待機時間(ms)
common:
retentionDuration: 86400 # 秒;0 = データを永遠に保持
cache:
cacheSize: 4 # GB;利用可能なRAMの約30〜40%に設定
EOF
docker-compose.yml のstandaloneサービスの下に設定マウントを追加する:
volumes:
- ./volumes/milvus:/var/lib/milvus
- ./config/milvus.yaml:/milvus/configs/milvus.yaml
リソース制限
この設定を省略すると、Milvusは重いインデックス処理中に利用可能なメモリをすべて消費してしまう。standaloneサービスの定義にリソース制限を追加しよう:
deploy:
resources:
limits:
memory: 12G
reservations:
memory: 6G
適切なインデックスタイプの選択
インデックスの選択は、ほぼあらゆる設定の中で最も重要な決断だ。私のワークロード(768次元エンベディング、約5000万ベクトル)では、HNSWが最高の再現率を発揮した。設定方法を紹介する:
from pymilvus import Collection, FieldSchema, CollectionSchema, DataType, connections
connections.connect(host="localhost", port="19530")
# HNSW:最高の再現率、メモリ使用量は多め
index_params = {
"metric_type": "IP", # 正規化済みエンベディングには内積を使用
"index_type": "HNSW",
"params": {
"M": 16, # Mが大きいほど再現率が上がり、メモリも増える
"efConstruction": 200 # 大きいほどインデックス品質が上がるが、構築が遅くなる
}
}
collection = Collection("my_embeddings")
collection.create_index(field_name="embedding", index_params=index_params)
collection.load() # クエリ前に必ずメモリにロードする
メモリが逼迫している場合は IVF_SQ8 に切り替えよう。量子化によりメモリ使用量が約75%削減される一方、再現率の低下はわずか2〜3%にとどまる——ほとんどのユースケースで合理的なトレードオフだ。
動作確認と監視
ヘルスチェック
# HTTPヘルスエンドポイント
curl -f http://localhost:9091/healthz
# 期待される結果:{"status":"healthy"}
接続と動作確認テスト
pip install pymilvus
from pymilvus import connections, utility
connections.connect(host="localhost", port="19530")
print("接続済み:", utility.get_server_version())
print("コレクション:", utility.list_collections())
Attu:Milvus管理UI
AttuはMilvusの公式GUI——ベクトルデータベース向けのpgAdminだと思えばいい。composeファイルに追加しよう:
attu:
image: zilliz/attu:v2.4.9
environment:
MILVUS_URL: standalone:19530
ports:
- "3000:3000"
depends_on:
- standalone
再起動して http://localhost:3000 を開く。コレクションの閲覧、インデックス統計、インタラクティブな検索クエリ、セグメント状態——コードを一行も書かずにこれらすべてを確認できる。私は日々の運用可視性のためにAttuを毎日使っている。私のセットアップでは必須だ。
Prometheusメトリクス
Milvusは http://localhost:9091/metrics でPrometheus互換のメトリクスを公開している。本番環境で最も重視している3つを紹介する:
milvus_querynode_search_latency_bucket— p50/p99検索レイテンシmilvus_datanode_flush_segment_size_bytes— セグメントフラッシュの挙動milvus_rootcoord_collection_num— 時系列のコレクション数
# prometheus.yml スクレイプ設定
scrape_configs:
- job_name: 'milvus'
static_configs:
- targets: ['milvus-host:9091']
MilvusのGrafana公式ダッシュボード(ID 17777)と組み合わせれば、カスタム構築なしで十分な可視性が得られる。
ログ監視
# ライブログを監視してエラーのみフィルタリング
docker compose logs -f standalone | grep -E "ERROR|WARN"
# etcdのヘルスチェック — Milvusはetcdに強く依存している
docker compose exec etcd etcdctl endpoint health
最初からやり直すなら変えること
最初から知っておきたかった4つのこと:
- 内積メトリクスを使用する場合は、挿入前に必ずエンベディングを正規化する。これを一度忘れて、意味不明な検索結果に2時間悩んだことがある。
- サービス再起動後は必ず
collection.load()を明示的に呼び出す。Milvusはコレクションをメモリに自動ロードしない——これには誰もが一度はつまずく。 - Attuは後回しにせず、すぐに設定する。UIによる可視性なしにセグメントのコンパクション問題をデバッグするのは本当につらい。
- パーティション戦略を早めに計画する。数千万件の挿入後にパーティションを追加することは可能だが、クエリパターンに影響を与える。
6ヶ月が経った今、Milvusは私のスタック全体で最も手がかからないコンポーネントになっている。インフラに求めるのはまさにそれだ。最初の設定を正しく行えば、後は邪魔をしない。

