所要時間: 約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 MemoryChromaで大量のデータをメモリに読み込みすぎ永続化(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 16GB

VRAM 16GBでローカルの埋め込みモデルやLLMを動かすのに最も現実的な選択肢

楽天で価格を見る Amazonでも確認

※アフィリエイトリンクを含みます


あわせて読みたい