- Authors
- Name
- はじめに
- 1. コア概念
- 2. アーキテクチャ
- 3. コレクション設計とインデックス戦略
- 4. CRUDと検索
- 5. フィルタリングとペイロード
- 6. RAGパイプライン連携
- 7. 運用チェックリスト
- 8. よくある間違い
- 9. まとめ
はじめに
LLMベースのアプリケーションが急速に普及する中、ベクトルデータベースは選択肢ではなく必須インフラとなりました。その中でもQdrantはRustで記述された高性能エンジンを持ち、gRPCとREST APIの両方をサポートし、ペイロードフィルタリングやマルチテナンシーなどプロダクションに必要な機能を備えています。本記事では、Qdrantを本番環境に導入する方を対象に、コレクション設計、CRUD、フィルタリング、RAG連携、運用モニタリングまで実践中心でまとめます。
1. コア概念
ベクトルデータベースとは
ベクトルデータベースは高次元ベクトル(エンベディング)を格納し、**類似度検索(Similarity Search)**をミリ秒単位で実行する専用データベースです。テキスト、画像、音声などをエンベディングモデルでベクトルに変換して格納し、クエリベクトルに最も近いベクトルを検索して返します。
Qdrantの特徴
- Rust製: メモリ安全性と高スループットを同時に実現
- ペイロード(Payload): ベクトルにJSONメタデータを一緒に格納してフィルタリング可能
- マルチテナンシー: コレクション内でテナント別のデータ分離をサポート
- 量子化(Quantization): メモリ使用量を最大4倍削減
- 分散モード: Raftコンセンサスプロトコルベースのクラスター構成
距離メトリクスの比較
| メトリクス | 数式 | 用途 | 範囲 |
|---|---|---|---|
| Cosine | 1 - cos(a, b) | テキストエンベディング(OpenAI、Cohere) | 0 〜 2 |
| Euclid | ||a - b|| | 画像特徴ベクトル、座標ベース | 0 〜 +inf |
| Dot Product | -a . b | 正規化されたエンベディング、推薦システム | -inf 〜 +inf |
| Manhattan | sum(|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パラメータガイド
| パラメータ | デフォルト | 推奨範囲 | 説明 |
|---|---|---|---|
m | 16 | 8 〜 64 | グラフ接続数。高いほどrecall向上、メモリ増加 |
ef_construct | 100 | 64 〜 512 | インデックス構築品質。高いほど正確だが遅い |
ef(検索時) | 128 | 64 〜 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. よくある間違い
ペイロードインデックスなしでフィルタ検索を使用: ペイロードインデックスがないと、すべてのポイントを順次スキャンします。フィルタで使用するフィールドには必ずインデックスを作成してください。
HNSWの
ef値を低く設定しすぎる: デフォルト値(128)より低くするとrecallが急激に低下します。パフォーマンスが十分であればデフォルト値を維持してください。ベクトル次元とモデルの不一致: コレクション作成時の
sizeとエンベディングモデルの次元数が異なると、upsert時にエラーが発生します。text-embedding-3-smallは1536次元、text-embedding-3-largeは3072次元です。バッチサイズが大きすぎる: 一度に10,000件以上をupsertするとメモリスパイクが発生する可能性があります。100〜500件単位でバッチ処理してください。
量子化適用後のテスト省略: Scalar Quantizationはrecall損失が少ないですが、Binary Quantizationは必ずrecallベンチマークを実施する必要があります。
単一コレクションに全データを格納: 異なるエンベディングモデルや次元数のデータを1つのコレクションに入れると、検索品質が低下します。用途別にコレクションを分離してください。
スナップショットバックアップ未設定: WALだけではディスク障害時の復旧ができません。定期的なスナップショットを外部ストレージ(S3など)に保管してください。
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メトリクスのモニタリングと定期スナップショットを必ず設定し、トラフィック増加に備えてクラスターモードとシャーディング戦略を事前に計画しておくことが重要です。