Skip to content
Published on

Qdrant ベクトルDB運用完全ガイド — コレクション設計からRAG連携まで

Authors
  • Name
    Twitter

はじめに

LLMベースのアプリケーションが急速に普及する中、ベクトルデータベースは選択肢ではなく必須インフラとなりました。その中でもQdrantはRustで記述された高性能エンジンを持ち、gRPCとREST APIの両方をサポートし、ペイロードフィルタリングやマルチテナンシーなどプロダクションに必要な機能を備えています。本記事では、Qdrantを本番環境に導入する方を対象に、コレクション設計、CRUD、フィルタリング、RAG連携、運用モニタリングまで実践中心でまとめます。


1. コア概念

ベクトルデータベースとは

ベクトルデータベースは高次元ベクトル(エンベディング)を格納し、**類似度検索(Similarity Search)**をミリ秒単位で実行する専用データベースです。テキスト、画像、音声などをエンベディングモデルでベクトルに変換して格納し、クエリベクトルに最も近いベクトルを検索して返します。

Qdrantの特徴

  • Rust製: メモリ安全性と高スループットを同時に実現
  • ペイロード(Payload): ベクトルにJSONメタデータを一緒に格納してフィルタリング可能
  • マルチテナンシー: コレクション内でテナント別のデータ分離をサポート
  • 量子化(Quantization): メモリ使用量を最大4倍削減
  • 分散モード: Raftコンセンサスプロトコルベースのクラスター構成

距離メトリクスの比較

メトリクス数式用途範囲
Cosine1 - cos(a, b)テキストエンベディング(OpenAI、Cohere)0 〜 2
Euclid||a - b||画像特徴ベクトル、座標ベース0 〜 +inf
Dot Product-a . b正規化されたエンベディング、推薦システム-inf 〜 +inf
Manhattansum(|a_i - b_i|)スパースベクトル、特殊ドメイン0 〜 +inf

ヒント: OpenAIのtext-embedding-3-small/largeは正規化されたベクトルを返すため、CosineとDot Productは同一の結果になります。この場合、Dot Productの方が計算が高速です。

2. アーキテクチャ

主要コンポーネント

  • セグメント: ベクトルとペイロードを物理的に分離格納する単位。並列検索の基本単位
  • WAL(Write-Ahead Log): 書き込み操作の永続性を保証。障害復旧時にリプレイ
  • HNSWインデックス: Hierarchical Navigable Small Worldグラフベースの近似最近傍インデックス
  • ペイロードインデックス: メタデータフィルタリング用の補助インデックス(keyword、integer、geoなど)

クラスターモード

クラスターモードでは、データはシャード単位で分散され、Raftコンセンサスプロトコルでメタデータの一貫性を維持します。各シャードは複数ノードにレプリカを配置して高可用性を確保できます。Docker Composeで構成する場合、環境変数QDRANT__CLUSTER__ENABLED=trueとP2Pポートを設定し、2台目以降は--bootstrapフラグで最初のノードを指定します。

3. コレクション設計とインデックス戦略

コレクションの作成

from qdrant_client import QdrantClient
from qdrant_client.models import Distance, VectorParams, HnswConfigDiff, OptimizersConfigDiff
from qdrant_client.models import ScalarQuantization, ScalarQuantizationConfig, ScalarType

client = QdrantClient(host="localhost", port=6333)

client.create_collection(
    collection_name="documents",
    vectors_config=VectorParams(
        size=1536,              # OpenAI text-embedding-3-small の次元数
        distance=Distance.COSINE,
    ),
    hnsw_config=HnswConfigDiff(
        m=16,                   # グラフ接続数
        ef_construct=128,       # インデックス構築時の探索幅
    ),
    optimizers_config=OptimizersConfigDiff(
        indexing_threshold=20000,
    ),
    quantization_config=ScalarQuantization(
        scalar=ScalarQuantizationConfig(type=ScalarType.INT8, quantile=0.99, always_ram=True),
    ),
)

HNSWパラメータガイド

パラメータデフォルト推奨範囲説明
m168 〜 64グラフ接続数。高いほどrecall向上、メモリ増加
ef_construct10064 〜 512インデックス構築品質。高いほど正確だが遅い
ef(検索時)12864 〜 512検索時の探索幅。recall/latencyのトレードオフ

量子化(Quantization)戦略

# Scalar Quantization: float32 → int8(メモリ4倍削減)
# 一般的なテキスト検索に適合、recall損失 < 1%

# Binary Quantization: float32 → 1-bit(メモリ32倍削減)
# 非常にアグレッシブ。高次元(1536+)でのみ使用。oversamplingが必要
from qdrant_client.models import BinaryQuantization, BinaryQuantizationConfig

client.update_collection(
    collection_name="documents",
    quantization_config=BinaryQuantization(
        binary=BinaryQuantizationConfig(
            always_ram=True,
        ),
    ),
)

4. CRUDと検索

ベクトルの挿入(Upsert)

from qdrant_client.models import PointStruct
import openai

def get_embedding(text: str) -> list[float]:
    resp = openai.embeddings.create(model="text-embedding-3-small", input=text)
    return resp.data[0].embedding

# 単件upsert
client.upsert(
    collection_name="documents",
    points=[
        PointStruct(
            id=1,
            vector=get_embedding("QdrantはRustで書かれたベクトルDBです。"),
            payload={"title": "Qdrant入門", "category": "database", "tenant_id": "team-alpha"},
        ),
    ],
)

# バッチupsert — 100件ずつ処理
BATCH_SIZE = 100
for i in range(0, len(points), BATCH_SIZE):
    client.upsert(collection_name="documents", points=points[i : i + BATCH_SIZE])

類似度検索(Search)

from qdrant_client.models import SearchParams

results = client.search(
    collection_name="documents",
    query_vector=get_embedding("ベクトルデータベースのパフォーマンス最適化"),
    limit=10,
    score_threshold=0.7,
    search_params=SearchParams(
        hnsw_ef=128,       # 検索時のef値(精度調整)
        exact=False,       # Trueにするとbrute-force(正確だが遅い)
    ),
    with_payload=True,
    with_vectors=False,    # ベクトル自体は返さない(レスポンスサイズ削減)
)

for result in results:
    print(f"ID: {result.id}, Score: {result.score:.4f}")
    print(f"  Title: {result.payload['title']}")

REST APIによる検索

curl -X POST "http://localhost:6333/collections/documents/points/search" \
  -H "Content-Type: application/json" \
  -d '{
    "vector": [0.1, 0.2, ...],
    "limit": 10,
    "with_payload": true,
    "params": {
      "hnsw_ef": 128
    }
  }'

更新と削除

from qdrant_client.models import PointIdsList, FilterSelector, Filter, FieldCondition, MatchValue

# ペイロードの更新
client.set_payload(collection_name="documents", payload={"category": "vector-database"}, points=[1, 2, 3])

# IDベースのポイント削除
client.delete(collection_name="documents", points_selector=PointIdsList(points=[10, 11, 12]))

# フィルタベースの削除
client.delete(
    collection_name="documents",
    points_selector=FilterSelector(
        filter=Filter(must=[FieldCondition(key="category", match=MatchValue(value="deprecated"))])
    ),
)

5. フィルタリングとペイロード

ペイロードインデックスの作成

フィルタリング性能を高めるには、必ずペイロードインデックスを作成する必要があります。

from qdrant_client.models import PayloadSchemaType, TextIndexParams, TokenizerType

# キーワードインデックス(完全一致)
client.create_payload_index(collection_name="documents", field_name="category", field_schema=PayloadSchemaType.KEYWORD)

# 整数インデックス(範囲検索)
client.create_payload_index(collection_name="documents", field_name="page_count", field_schema=PayloadSchemaType.INTEGER)

# テキストインデックス(全文検索 — 多言語トークナイザ)
client.create_payload_index(
    collection_name="documents",
    field_name="content",
    field_schema=TextIndexParams(type="text", tokenizer=TokenizerType.MULTILINGUAL, min_token_len=2, max_token_len=20),
)

複合フィルタ検索

from qdrant_client.models import Filter, FieldCondition, MatchValue, Range

# カテゴリが"database"でページ数が10以上のドキュメントの類似度検索
results = client.search(
    collection_name="documents",
    query_vector=get_embedding("ベクトルインデックスの最適化"),
    query_filter=Filter(
        must=[
            FieldCondition(
                key="category",
                match=MatchValue(value="database"),
            ),
            FieldCondition(
                key="page_count",
                range=Range(gte=10),
            ),
        ],
        must_not=[
            FieldCondition(
                key="status",
                match=MatchValue(value="archived"),
            ),
        ],
    ),
    limit=5,
)

ハイブリッド検索(ベクトル + テキスト)

Qdrantはスパースベクトルをサポートしており、BM25スタイルのキーワード検索とベクトル検索を組み合わせることができます。

from qdrant_client.models import SparseVector, SparseVectorParams, Prefetch, FusionQuery, Fusion

# dense + sparseベクトル設定でコレクション作成
client.create_collection(
    collection_name="hybrid_docs",
    vectors_config={"dense": VectorParams(size=1536, distance=Distance.COSINE)},
    sparse_vectors_config={"sparse": SparseVectorParams()},
)

# Reciprocal Rank Fusionベースのハイブリッド検索
results = client.query_points(
    collection_name="hybrid_docs",
    prefetch=[
        Prefetch(query=get_embedding("ベクトル検索のパフォーマンス"), using="dense", limit=20),
        Prefetch(query=SparseVector(indices=[1, 42, 1337], values=[0.9, 0.3, 0.7]), using="sparse", limit=20),
    ],
    query=FusionQuery(fusion=Fusion.RRF),
    limit=10,
)

6. RAGパイプライン連携

LangChain連携

from langchain_openai import OpenAIEmbeddings, ChatOpenAI
from langchain_qdrant import QdrantVectorStore
from langchain.chains import RetrievalQA
from langchain.text_splitter import RecursiveCharacterTextSplitter

# 1. ドキュメント分割
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=500,
    chunk_overlap=50,
    separators=["\n\n", "\n", ". ", " ", ""],
)
chunks = text_splitter.split_documents(documents)

# 2. ベクトルストア作成
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")

vectorstore = QdrantVectorStore.from_documents(
    documents=chunks,
    embedding=embeddings,
    url="http://localhost:6333",
    collection_name="rag_documents",
    force_recreate=True,
)

# 3. RAGチェーン構築
retriever = vectorstore.as_retriever(
    search_type="similarity_score_threshold",
    search_kwargs={
        "k": 5,
        "score_threshold": 0.7,
    },
)

llm = ChatOpenAI(model="gpt-4o", temperature=0)

qa_chain = RetrievalQA.from_chain_type(
    llm=llm,
    chain_type="stuff",
    retriever=retriever,
    return_source_documents=True,
)

# 4. 質問応答
response = qa_chain.invoke({"query": "QdrantのHNSWインデックスパラメータはどう調整しますか?"})
print(response["result"])

LlamaIndex連携

from llama_index.core import VectorStoreIndex, SimpleDirectoryReader, Settings
from llama_index.vector_stores.qdrant import QdrantVectorStore
from llama_index.embeddings.openai import OpenAIEmbedding
from llama_index.llms.openai import OpenAI
import qdrant_client

# 1. Qdrantクライアント & ベクトルストア
qclient = qdrant_client.QdrantClient(host="localhost", port=6333)

vector_store = QdrantVectorStore(
    client=qclient,
    collection_name="llamaindex_docs",
)

# 2. 設定
Settings.embed_model = OpenAIEmbedding(model="text-embedding-3-small")
Settings.llm = OpenAI(model="gpt-4o", temperature=0)
Settings.chunk_size = 512
Settings.chunk_overlap = 50

# 3. ドキュメント読み込みとインデキシング
documents = SimpleDirectoryReader("./data").load_data()

index = VectorStoreIndex.from_documents(
    documents,
    vector_store=vector_store,
)

# 4. クエリエンジン
query_engine = index.as_query_engine(
    similarity_top_k=5,
)

response = query_engine.query("Qdrantでフィルタリングとベクトル検索を同時に行う方法は?")
print(response)

7. 運用チェックリスト

モニタリング

QdrantはPrometheus形式のメトリクスを/metricsエンドポイントで提供しています。

# メトリクスの確認
curl http://localhost:6333/metrics

# 主要メトリクス
# qdrant_grpc_responses_total        — gRPCリクエスト数
# qdrant_rest_responses_total        — RESTリクエスト数
# qdrant_collection_points_total     — コレクション別ポイント数
# qdrant_collection_search_latency   — 検索レイテンシー

運用チェックリスト表

項目確認事項頻度
メモリRAM使用率80%未満を維持毎日
ディスクストレージ空き容量20%以上毎日
検索レイテンシーp99 latency < 100msリアルタイム
インデックス状態optimizer_status = "ok"毎時
スナップショット自動スナップショットの正常作成確認毎日
レプリカ全シャードのreplica状態がactive毎時
WALサイズWALディレクトリの異常増加を監視毎日
APIエラー率5xxレスポンス比率 < 0.1%リアルタイム

スナップショットとバックアップ

# スナップショットの作成と一覧取得
client.create_snapshot(collection_name="documents")
snapshots = client.list_snapshots(collection_name="documents")
for snap in snapshots:
    print(f"Name: {snap.name}, Size: {snap.size}")
# REST APIでスナップショットをダウンロード
curl -o snapshot.tar "http://localhost:6333/collections/documents/snapshots/snapshot-2026-03-09.snapshot"

# 全ストレージスナップショット(全コレクション含む)
curl -X POST "http://localhost:6333/snapshots"

8. よくある間違い

  1. ペイロードインデックスなしでフィルタ検索を使用: ペイロードインデックスがないと、すべてのポイントを順次スキャンします。フィルタで使用するフィールドには必ずインデックスを作成してください。

  2. HNSWのef値を低く設定しすぎる: デフォルト値(128)より低くするとrecallが急激に低下します。パフォーマンスが十分であればデフォルト値を維持してください。

  3. ベクトル次元とモデルの不一致: コレクション作成時のsizeとエンベディングモデルの次元数が異なると、upsert時にエラーが発生します。text-embedding-3-smallは1536次元、text-embedding-3-largeは3072次元です。

  4. バッチサイズが大きすぎる: 一度に10,000件以上をupsertするとメモリスパイクが発生する可能性があります。100〜500件単位でバッチ処理してください。

  5. 量子化適用後のテスト省略: Scalar Quantizationはrecall損失が少ないですが、Binary Quantizationは必ずrecallベンチマークを実施する必要があります。

  6. 単一コレクションに全データを格納: 異なるエンベディングモデルや次元数のデータを1つのコレクションに入れると、検索品質が低下します。用途別にコレクションを分離してください。

  7. スナップショットバックアップ未設定: WALだけではディスク障害時の復旧ができません。定期的なスナップショットを外部ストレージ(S3など)に保管してください。

  8. RESTのみ使用してgRPCを使わない: 大量データ処理時、gRPCはRESTと比較して2〜3倍高速です。Pythonクライアントではprefer_grpc=Trueに設定すると自動的にgRPCを使用します。

# gRPC使用を推奨
client = QdrantClient(
    host="localhost",
    port=6333,
    grpc_port=6334,
    prefer_grpc=True,
)

9. まとめ

Qdrantは、Rustのパフォーマンスと豊富な機能を兼ね備えたベクトルデータベースであり、特にペイロードフィルタリング量子化ハイブリッド検索機能がプロダクションRAGパイプラインに適しています。コレクション設計時にはHNSWパラメータと量子化戦略を慎重に選択し、ペイロードインデックスを積極的に活用してください。運用段階では、Prometheusメトリクスのモニタリングと定期スナップショットを必ず設定し、トラフィック増加に備えてクラスターモードとシャーディング戦略を事前に計画しておくことが重要です。