🌊
[LLM&RAG]Qdrantとロックンロールには感謝している
全然新しい話ではないが、Qdrantはずっと自分のお道具箱にあってもいい、というオススメをしたい話。
使いどころはクイックにRAGをやりたいとき
LLMを使って何かをしたいってなったときに、ベクトルDBとRAGがないとやりたいことはできないというケースは結構出てくるはずで、個人的にはQdrantに1年以上前に出会ってから、他のベクトルDBを検討しないといけなくなることがあまりなくて、これだけで全然いいなと思って使い続けているので、さらっと簡単なRAGを作りたい人向けに紹介する。
Qdrantって?
- ベクトルDBでかつオープンソースでセルフホストできる製品の一つ。クラウドもある。
- Apache License 2.0なので、商用利用もできる。改変も再配布も問題ない。
- Pythonで操作するライブラリがあって手軽に使える
ハンズオン
前提
- Windows 11でWSL2のUbuntu 24.04を使っている
- Dockerがインストール済み
- AzureOpenAIのAPIキーを持っている(他のLLMでもEmbeddingがあればOK)
いったん適当のフォルダにDocker Composeファイルを作る
services:
qdrant:
image: qdrant/qdrant:latest
container_name: qdrant
ports:
- "6333:6333"
- "6334:6334"
volumes:
- qdrant_data:/qdrant/storage
environment:
- QDRANT__STORAGE__BACKEND=local
- QDRANT__STORAGE__LOCAL__PATH=/qdrant/storage
- QDRANT__SERVICE__API_KEY=WatashiWaHimitsuNoKey
volumes:
qdrant_data:
Docker Composeで立ち上げる
docker compose up -d
ダッシュボードを見てみる
自分のWSL2マシンのURLホストのIPアドレスを確認して、以下のURLにアクセスしてみる。
http://<WSLホストのIPアドレス>:6333/dashboard/
PythonでQdrantを操作する(テストデータを登録)
ChatGPTと一緒に、最低限のテストデータ登録コードを作成した。適当に、架空のアニメのキャラクターとそのプロフィールを30人分作らせてみた。
import os
import json
from qdrant_client import QdrantClient
from qdrant_client.http import models as rest
import openai
# 埋め込みクライアント設定
embclient = openai.AzureOpenAI(
azure_endpoint=os.environ.get('AZURE_OPENAI_ENDPOINT', "https://xxxxxxx.openai.azure.com/"),
api_key=os.environ.get('AZURE_OPENAI_API_KEY', ""),
api_version=os.environ.get('AZURE_OPENAI_API_VERSION', "")
)
VECTOR_DIMENSION = 1536 # 使用する埋め込み次元数
# Qdrantクライアント設定
qdrant = QdrantClient(
url=os.environ.get('QDRANT_URL', 'http://xxxxxxxxxxxxxxx:6333'),
api_key=os.environ.get('QDRANT_API_KEY', "WatashiWaHimitsuNoKey")
)
# コレクション名
COLLECTION_NAME = "anime_characters"
# コレクションを再作成(存在しない場合)
qdrant.recreate_collection(
collection_name=COLLECTION_NAME,
vectors_config=rest.VectorParams(
size=VECTOR_DIMENSION,
distance=rest.Distance.COSINE
)
)
# キャラプロフィール辞書を定義
profiles = [
{"id": 1, "name": "烈火・カグヤ", "text": "振るうたびに炎をまとった剣撃を繰り出す女性剣士"},
{"id": 2, "name": "月影・ユエ", "text": "闇夜の影を糸のように操り、標的を縫い取るスナイパー"},
{"id": 3, "name": "大河・サン", "text": "地面を叩くと雷鳴が轟き、範囲攻撃を発生させるタンク型戦士"},
{"id": 4, "name": "碧海・ナギサ", "text": "揺らめく水流で攻撃・防御を自在に操る若き術士"},
{"id": 5, "name": "風神・フウタ", "text": "空中から雷を纏った槍を飛ばす、天空の導師"},
{"id": 6, "name": "鉄壁・ガイオ", "text": "全身を鋼鉄の鎧に変え、無敵の防御を発揮する重防御タイプ"},
{"id": 7, "name": "影狼・ルクス", "text": "高速移動と鋭い牙で一気に標的を仕留める接近戦アサシン"},
{"id": 8, "name": "星見・セイラ", "text": "星の力を集めて隕石弾を落とす遠距離魔法使い"},
{"id": 9, "name": "漆黒・カロン", "text": "黒い結界を張り、味方を守りつつ敵を封じ込める防御型"},
{"id": 10, "name": "白銀・ヒエン", "text": "幻惑の狐火で敵を混乱させ、隙を作るサポートタイプ"},
{"id": 11, "name": "音速・ソル", "text": "音速を超える刃で敵を切り裂く高速戦士"},
{"id": 12, "name": "黒曜・オブシ", "text": "一瞬で敵の背後に回り、一撃で仕留める剛刃アサシン"},
{"id": 13, "name": "海王・ポセード", "text": "巨大な水柱を吹き上げ、敵を海中に引き込む海賊船長"},
{"id": 14, "name": "深淵・リヴァイ", "text": "強固な鱗で受け流しつつ鋭い切りつけを行う"},
{"id": 15, "name": "風雅・カザネ", "text": "音楽の旋律で嵐を巻き起こし、敵を吹き飛ばす"},
{"id": 16, "name": "金剛・ドリアン", "text": "巨大な盾を展開し、集団戦での要塞となる"},
{"id": 17, "name": "苍炎・ライ", "text": "魔力を込めた剣で斬撃と炎術を同時に繰り出す"},
{"id": 18, "name": "月牙・ルナ", "text": "月の光を集中して強力なビームを放つ"},
{"id": 19, "name": "琥珀・アンバー", "text": "優れた嗅覚で敵を追跡し、一撃必中の矢を放つ"},
{"id": 20, "name": "疾風・カゼ", "text": "風を纏った狼影で高速かつ隠密に動く"},
{"id": 21, "name": "鋼牙・アイアン", "text": "鋼の顎で何でも噛み砕くパワー型戦士"},
{"id": 22, "name": "翠玉・エメラルド","text": "錬金した結晶を武器や防具に変化させる"},
{"id": 23, "name": "隼眼・ホーク", "text": "空高く飛んで情報を集め、偵察を隠蔽する"},
{"id": 24, "name": "深雪・ミユキ", "text": "戦場に氷壁を出現させ、敵を凍結させる"},
{"id": 25, "name": "炎舞・カエデ", "text": "炎の舞で敵を切り裂き、範囲焼却する"},
{"id": 26, "name": "錬鉄・スチール", "text": "巨大ハンマーで地面を叩き、震動波を発生させる"},
{"id": 27, "name": "獄火・イグニス", "text": "地面から灼熱の火柱を噴き出す強力な魔導師"},
{"id": 28, "name": "狂翠・マラカイト","text": "毒結晶を飛ばし、中毒効果でじわじわと蝕む"},
{"id": 29, "name": "黄金・オーロ", "text": "光の矢を高速で連射し、暗黒を切り裂く"},
{"id": 30, "name": "朱雀・スザク", "text": "生命力を宿した炎で味方を蘇生しつつ敵を焼き尽くす"}
]
# 埋め込みと登録処理
points = []
for p in profiles:
response = embclient.embeddings.create(
model="text-embedding-3-large",
dimensions=VECTOR_DIMENSION,
input=p["text"]
)
emb = response.data[0].embedding
point = rest.PointStruct(
id=p["id"],
vector=emb,
payload={"name": p["name"], "description": p["text"]}
)
points.append(point)
# バッチ登録
qdrant.upsert(
collection_name=COLLECTION_NAME,
points=points
)
print(f"{len(points)} characters embedded and uploaded to Qdrant collection '{COLLECTION_NAME}'.")
これをPythonのコンテナをちょこっと借りてきて流す。
docker run -it --rm \
--network host \
-v $(pwd):/app \
-w /app \
python:3.11-slim \
bash -c "pip install qdrant-client openai && python3 qdrant_test.py"
Visual機能でポイントを見てみる
これが好きなんですよね・・・にゅにゅにゅっと最初動いたりして。
エンベディングされたベクトル情報を持ったポイント(テキスト情報)が大規模言語モデル上の位置情報なので、その分布のような形でベクトルDB内の座標にプロットされたものを、本当は4092次元あるのを無理やり2次元に落とし込んで見せてくれているだけなので、実際にはだいぶ人間に寄せて本当の姿ではないといえるはずだけれど、すごくイメージを沸かせてくれる。
このあと(未執筆)
次に「ヒエンとエメラルドが協力して、イグニスと戦うシーンを書いて」みたいなリクエストをしたら、ベクトル検索して必要な情報を持ってきたうえでそれを参考にプロフィールを考慮した物語を出力するようなプログラムとプロンプトのサンプルを書こうかと思いますがいったんここまで。
Discussion