所要時間: 約45分 | 難易度: ★★★☆☆
この記事で作るもの
PythonとオープンソースのChroma、そしてマネージドサービスのPineconeを使い分け、テキストデータをベクトル化して高速に検索するスクリプトを構築します。 単純なキーワード検索では不可能な「意味の近さ」を基準にした情報抽出を、ローカル環境とクラウド環境の両方で実装します。
- Pythonの基本的な文法(リスト、辞書、関数)が理解できていること
- OpenAI APIまたはHuggingFaceのモデルに触れたことがあるとスムーズ
- 必要なもの:OpenAI APIキー(埋め込み用)、Pinecone APIキー(無料枠でOK)、Python 3.9以上
先に確認するスペック・料金
ベクトルデータベース(Vector DB)を運用する際、最もコストに跳ね返るのは「インデックスの保存量」と「計算リソース」です。 ローカルで動かすChromaの場合、メモリ16GB以上のPCであれば、数万件程度のドキュメントならレスポンス0.1秒以下で処理できます。 ただし、ベクトル化(Embedding)にGPUを使いたい場合は、VRAM 8GB以上のNVIDIA製GPU(RTX 3060以上)があると劇的に速くなります。
Pineconeを使用する場合、2024年現在の「Serverless」プランが圧倒的に安いです。 以前のPodベースのプランは月額$70ほどかかりましたが、Serverlessなら1GBのストレージが月額数ドル、検索1000回あたり$0.0000825程度で済みます。 個人開発やプロトタイプなら、無料枠の範囲内で十分に商用レベルの検証が可能です。
もし自社サーバーで完結させたいなら、最低でもRAM 32GB、できれば64GBは積んでおきましょう。 私は自宅のサーバーにRTX 4090を2枚挿していますが、数百万件規模のベクトル検索をローカルで行うには、これくらいのパワーがないとレイテンシ(遅延)が実用レベルに達しません。
なぜこの方法を選ぶのか
現在、ベクトルDBはWeaviate、Milvus、Qdrantなど20種類以上乱立していますが、実務で選ぶならChromaとPineconeの2択から始めるのが合理的です。 Chromaはセットアップが不要な「sqlite」のような手軽さがあり、開発初期のプロトタイピングに最適だからです。 一方で、本番環境でサーバーの運用保守に時間を割きたくないなら、フルマネージドのPinecone一択になります。
他のDBも高機能ですが、ドキュメントの充実度とPythonライブラリの安定性において、この2つが頭一つ抜けています。 「とりあえず動くものを作る」段階ではChromaを使い、データ量が増えてきたらPineconeへ移行するのが、開発コストを最小限に抑える定石です。
Step 1: 環境を整える
まずは必要なライブラリを一括でインストールします。 今回はベクトル化(Embedding)にOpenAIのモデルを使い、DBとしてChromaとPineconeの両方を操作できるようにします。
pip install chromadb pinecone-client openai python-dotenv sentence-transformers
chromadbはローカルDB用、pinecone-clientはクラウドDB用です。
sentence-transformersは、APIを使わずにローカルのCPU/GPUでベクトル化を試すために使います。
API代を節約したい初期段階では、ローカルモデルの併用が非常に有効です。
⚠️ 落とし穴:
Pythonのバージョンが3.12以上だと、一部の依存ライブラリ(特にHnswlib周辺)のビルドに失敗することがあります。
その場合は pip install --only-binary :all: chromadb を試すか、安定しているPython 3.10または3.11を使用してください。
Step 2: 基本の設定
まずはローカルDBであるChromaの設定から入ります。 APIキーをコードに直書きするのは、GitHubに誤ってプッシュした際に即座に悪用されるため、絶対に避けてください。
import os
from dotenv import load_dotenv
import chromadb
from chromadb.utils import embedding_functions
# .envファイルから環境変数を読み込む
load_dotenv()
# OpenAIの埋め込み関数を定義
# デフォルトの'text-embedding-3-small'は、1000トークンあたり$0.00002と非常に安価です
openai_ef = embedding_functions.OpenAIEmbeddingFunction(
api_key=os.environ.get("OPENAI_API_KEY"),
model_name="text-embedding-3-small"
)
# データベースの保存先を指定(カレントディレクトリの'db'フォルダ)
# PersistentClientを使うことで、プログラムを終了してもデータが消えません
client = chromadb.PersistentClient(path="./my_vector_db")
# コレクション(テーブルのようなもの)を作成
# 距離計算は'cosine'を指定します。日本語の検索ではこれが最も安定します
collection = client.get_or_create_collection(
name="my_knowledge_base",
embedding_function=openai_ef,
metadata={"hnsw:space": "cosine"}
)
ここでhnsw:spaceにcosineを指定した理由は、ベクトルの「向き」で類似度を測るためです。
ユークリッド距離(l2)でも動きますが、文章の長さがバラバラなドキュメントを扱う場合、コサイン類似度の方が精度が安定します。
Step 3: 動かしてみる
実際にデータを投入し、検索を実行してみます。 ここでは「仕事で使えるか」を意識し、単純なテキストだけでなくメタデータ(属性情報)も一緒に保存します。
# データの登録
# idsは一意である必要があります。実務ではUUIDやURLのハッシュ値を使います
collection.add(
documents=[
"明日の天気は晴れです。",
"Pythonのベクトルデータベース選定ガイド。",
"RTX 4090はAI開発において最強のGPUです。"
],
metadatas=[
{"category": "weather", "priority": "low"},
{"category": "tech", "priority": "high"},
{"category": "hardware", "priority": "high"}
],
ids=["id1", "id2", "id3"]
)
# 検索の実行
# query_textsに質問を投げると、内部で自動的にベクトル化して検索してくれます
results = collection.query(
query_texts=["AI向けのビデオカードについて教えて"],
n_results=1
)
print(results["documents"])
期待される出力
[['RTX 4090はAI開発において最強のGPUです。']]
「ビデオカード」という単語はドキュメントに含まれていませんが、ベクトル化によって「GPU」と意味が近いと判断され、正しく抽出されています。 これが従来のキーワード検索(Elasticsearchなど)との決定的な違いです。
Step 4: 実用レベルにする(Pineconeへの移行とフィルタリング)
データが10万件を超えてくると、ローカルDBの管理やバックアップが手間でしかなくなります。 そこでPineconeに切り替える際の実装パターンを紹介します。 実務では「特定のカテゴリ内だけで検索したい」という要望が必ず出るため、メタデータフィルタリングを実装するのが鉄則です。
from pinecone import Pinecone, ServerlessSpec
pc = Pinecone(api_key=os.environ.get("PINECONE_API_KEY"))
# インデックスの作成(既存でない場合のみ)
index_name = "ai-blog-index"
if index_name not in pc.list_indexes().names():
pc.create_index(
name=index_name,
dimension=1536, # text-embedding-3-smallの次元数
metric="cosine",
spec=ServerlessSpec(cloud="aws", region="us-east-1")
)
index = pc.Index(index_name)
# データのベクトル化(OpenAI APIを直接叩く)
from openai import OpenAI
oa_client = OpenAI()
def get_embedding(text):
return oa_client.embeddings.create(input=[text], model="text-embedding-3-small").data[0].embedding
# データのアップサート(登録・更新)
text_content = "Claude 3.5 Sonnetはコーディング能力が極めて高いです。"
vector = get_embedding(text_content)
index.upsert(
vectors=[
{
"id": "cl-001",
"values": vector,
"metadata": {"text": text_content, "category": "LLM", "version": "3.5"}
}
]
)
# メタデータフィルタリングを使った検索
# categoryが'LLM'のものだけを対象に検索します
query_vector = get_embedding("最新のAIモデルについて")
search_results = index.query(
vector=query_vector,
top_k=2,
filter={"category": {"$eq": "LLM"}},
include_metadata=True
)
for match in search_results["matches"]:
print(f"Score: {match['score']}, Text: {match['metadata']['text']}")
ここで重要なのは dimension=1536 の設定です。
使用するEmbeddingモデルによって次元数は決まっており、これが1つでもズレるとエラーになります。
OpenAIの新しい text-embedding-3-small は1536次元ですが、ローカルモデル(例えば all-MiniLM-L6-v2)を使う場合は384次元になります。
「なぜか動かない」という原因の8割は、この次元数の不一致です。
よくあるトラブルと解決法
| エラー内容 | 原因 | 解決策 |
|---|---|---|
Dimension mismatch | モデルの出力次元とDBの設定が異なる | model_nameを確認し、DB作成時の次元数を修正する |
APIKeyNotFound | 環境変数が正しく読み込まれていない | load_dotenv()を呼んでいるか、.envのファイル名が正しいか確認 |
Out of Memory | Chromaで大量のデータをメモリに読み込みすぎ | 永続化(PersistentClient)を使い、小分けにデータを投入する |
次のステップ
ベクトルDBが動くようになったら、次は「RAG(検索拡張生成)」の構築に挑戦してください。 検索した結果のテキストをGPT-4やClaude 3.5に渡し、「以下の情報を参考にして、質問に答えてください」というプロンプトを組み立てる作業です。
実務で精度を上げるには、単にベクトル検索するだけでなく、検索結果を再度並び替える「Rerank(リランク)」という工程が不可欠です。 Cohere RerankなどのAPIを組み合わせることで、検索精度はさらに20〜30%向上します。 また、ローカルLLM(Llama 3など)と組み合わせれば、機密情報を一切外に出さないプライベートAI環境を構築することも可能です。 次はLangChainやLlamaIndexといったフレームワークを使って、これらの一連の流れを自動化することを目指してみてください。
よくある質問
Q1: 無料で使い続けることはできますか?
Chromaであれば完全に無料です。PineconeもServerlessプランの無料枠($100分のクレジットが最初にもらえる等)がありますが、保存量が増えれば課金されます。趣味の範囲ならChromaをローカルPCのDockerで動かすのが最も安上がりです。
Q2: 日本語の検索精度が低い気がします。
Embeddingモデルを日本語に特化したもの(multilingual-e5-largeなど)に変更してください。OpenAIのモデルも優秀ですが、特定のドメイン(医療、法律など)では日本語特化モデルの方が「単語のニュアンス」を正確に捉えられます。
Q3: データの更新はどうすればいいですか?
ベクトルDBには「Update」という概念がありますが、一般的には同じ id で再度 add または upsert を行うことで上書きされます。メタデータだけを更新したい場合も、基本的にはベクトルごと再登録するのが確実です。
📦 この記事に関連する商品(楽天メインで価格確認)
RTX 4060 Ti 16GBVRAM 16GBでローカルの埋め込みモデルやLLMを動かすのに最も現実的な選択肢
※アフィリエイトリンクを含みます





