所要時間: 約45分 | 難易度: ★★☆☆☆
この記事で作るもの
自分の手元にあるテキストデータをベクトル化してPineconeに保存し、自然言語で高速に検索できるPythonスクリプトを作ります。 LangChainなどの便利なライブラリをあえて使わず、PineconeのネイティブSDKとOpenAIのAPIのみを利用することで、ベクトルデータベースの内部構造を理解し、実務でカスタマイズ可能な土台を完成させます。
前提知識:Pythonの基本的な文法(変数、関数、リスト操作)がわかること 必要なもの:PineconeのAPIキー、OpenAIのAPIキー、Python 3.9以上
先に確認するスペック・料金
ベクトルデータベースの選定で最も重要なのは「自前運用(セルフホスト)」か「マネージドサービス(SaaS)」かという点です。 私は実務でMilvusの自前構築も経験しましたが、小〜中規模のプロジェクトならPineconeのServerlessプラン一択だと考えています。
Pinecone Serverlessの料金は、ストレージが1GBあたり月額$0.25、書き込み操作が100万回あたり$1.00程度と非常に安価です。 特に無料枠(Free Tier)が優秀で、1つ目のインデックスは特定のリージョン(AWS us-east-1など)を選べば、実質無料で100万ベクトル程度まで検証できます。
一方、埋め込みモデル(Embedding)にはOpenAIの text-embedding-3-small を使います。
これは1,000,000トークンあたりわずか$0.02という驚異的な安さで、文庫本100冊分をベクトル化しても数十円で収まります。
ローカルでRTX 4090を回して bge-m3 などのモデルを動かすのも手ですが、APIの方が精度と速度のバランスが良く、最初はここから始めるのが正解です。
なぜこの方法を選ぶのか
ベクトルデータベースにはChromaやWeaviate、Qdrantなど多くの選択肢があります。 しかし、Pineconeを選ぶ理由は「運用負荷の低さ」と「スケーラビリティ」にあります。 Chromaはローカルで手軽に動きますが、本番環境への移行時にサーバー構築の手間が発生します。
Pinecone Serverlessなら、データ量が増えてもインフラの再設計が必要ありません。 また、他のDBは「メタデータフィルタリング(特定のユーザーのデータだけを検索するなど)」の挙動が不安定なことがありますが、Pineconeはこの分野で最も枯れています。 「最初はローカル、後でクラウド」と考えるより、最初からクラウドネイティブな構成で組む方が、結果的に開発効率が3倍は変わります。
Step 1: 環境を整える
まずは必要なライブラリをインストールします。 PineconeのSDKは頻繁にアップデートされるため、現時点で安定しているバージョン3以上を明示的に指定します。
# pinecone-client: Pinecone操作用SDK
# openai: テキストをベクトルに変換するためのSDK
# python-dotenv: APIキーを環境変数から読み込むため
pip install pinecone-client>=3.0.0 openai python-dotenv
pinecone-client はバージョン2系から3系でAPIが大きく変わりました。
古い記事のコードをコピペしても動かないことが多いので、必ず最新版を使うようにしてください。
また、APIキーをコード内に直接書くのは、GitHubに誤ってプッシュした際の漏洩リスクが高すぎるため、必ず .env ファイルを使います。
⚠️ 落とし穴:
PineconeのServerlessプランで無料枠を使いたい場合、インデックス作成時にリージョンを us-east-1 (AWS) に指定する必要があります。
これを間違えて他のリージョン(gcp-starterなど)を選ぶと、無料期間が終わった後に課金されるか、あるいはプロジェクトが作成できないことがあります。
Step 2: 基本の設定
プロジェクトのルートディレクトリに .env ファイルを作成し、キーを記述します。
PINECONE_API_KEY="あなたのPinecone APIキー"
OPENAI_API_KEY="あなたのOpenAI APIキー"
次に、PineconeとOpenAIのクライアントを初期化するスクリプトを書きます。
import os
from dotenv import load_dotenv
from pinecone import Pinecone, ServerlessSpec
from openai import OpenAI
# .envから環境変数をロード
load_dotenv()
# クライアントの初期化
pc = Pinecone(api_key=os.environ["PINECONE_API_KEY"])
client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])
# インデックス名の設定
index_name = "my-first-vector-db"
# なぜ1536次元にするのか:
# OpenAIの text-embedding-3-small モデルの出力次元数が 1536 だからです。
# ここが一致していないと、データの保存時にエラーになります。
dimension = 1536
# インデックスが存在しない場合のみ作成
if index_name not in pc.list_indexes().names():
pc.create_index(
name=index_name,
dimension=dimension,
metric="cosine", # 自然言語にはコサイン類似度が最も適している
spec=ServerlessSpec(
cloud="aws",
region="us-east-1"
)
)
# インデックスに接続
index = pc.Index(index_name)
「メトリクス」に cosine を選ぶ理由は、文章の長さに関わらず「意味の方向性」が近いものを探すのに適しているからです。
euclidean(ユークリッド距離)は、画像のピクセル値比較など、絶対的な数値の差が重要な場合に適しています。
実務のRAG(検索拡張生成)では、9割以上のケースで cosine を選べば間違いありません。
Step 3: 動かしてみる
テキストをベクトル化して、Pineconeに放り込んでみましょう。 単に保存するだけでなく、後で検索しやすいように「ID」と「メタデータ」を付与するのがポイントです。
def get_embedding(text):
"""テキストをベクトルに変換する関数"""
# model="text-embedding-3-small" は現時点でコスパ最強のモデル
response = client.embeddings.create(
input=[text],
model="text-embedding-3-small"
)
return response.data[0].embedding
# 1. データの準備
sample_data = [
{"id": "v1", "text": "美味しいラーメン屋は新宿にある。"},
{"id": "v2", "text": "最新のAIニュースはTwitterで追うのが速い。"},
{"id": "v3", "text": "Pythonのライブラリ管理はPoetryが便利。"}
]
# 2. ベクトル化して保存
vectors_to_upsert = []
for item in sample_data:
v = get_embedding(item["text"])
# metadataに元の文章を入れておくのが実務の鉄則。
# 検索結果として「ベクトル」だけ返ってきても、人間には読めないからです。
vectors_to_upsert.append({
"id": item["id"],
"values": v,
"metadata": {"text": item["text"]}
})
index.upsert(vectors=vectors_to_upsert)
# 3. 検索の実行
query_text = "プログラミングの効率を上げたい"
query_vector = get_embedding(query_text)
results = index.query(
vector=query_vector,
top_k=1, # 上位1件を取得
include_metadata=True # メタデータ(元の文章)を結果に含める
)
print(f"検索クエリ: {query_text}")
print(f"最も近い回答: {results['matches'][0]['metadata']['text']}")
print(f"類似度スコア: {results['matches'][0]['score']:.4f}")
期待される出力
検索クエリ: プログラミングの効率を上げたい
最も近い回答: Pythonのライブラリ管理はPoetryが便利。
類似度スコア: 0.4215(数値はモデルにより変動)
結果の読み方で重要なのは score です。
コサイン類似度の場合、1に近いほど似ていることを示します。
実務では、このスコアが一定値(0.3など)以下の場合は「該当なし」と判定する処理を入れることで、AIが無理やり関係ない答えを出すのを防ぎます。
Step 4: 実用レベルにする
実務では、1つの長いPDFや大量のドキュメントを扱います。 これらをそのまま1つのベクトルにすると、意味がぼやけて検索精度が劇的に落ちます。 解決策として、テキストを「チャンク(塊)」に分割し、それぞれをベクトル化します。
また、Pineconeの「名前空間(Namespace)」を活用することで、ユーザーAのデータとユーザーBのデータが混ざらないように設計します。
def chunk_text(text, chunk_size=200):
"""テキストを指定した文字数で分割する(簡易版)"""
return [text[i:i+chunk_size] for i in range(0, len(text), chunk_size)]
def upsert_document(doc_id, full_text, user_id):
"""ドキュメントを分割して特定のユーザー用スペースに保存する"""
chunks = chunk_text(full_text)
vectors = []
for i, chunk in enumerate(chunks):
v = get_embedding(chunk)
vectors.append({
"id": f"{doc_id}#{i}",
"values": v,
"metadata": {
"text": chunk,
"doc_id": doc_id,
"user_id": user_id
}
})
# namespaceを分けることで、物理的に検索範囲を制限できる。
# これはマルチテナント(複数顧客)向けアプリを作る際の必須テクニック。
index.upsert(vectors=vectors, namespace=f"user_{user_id}")
# 実行例
long_text = "(ここに数千文字のドキュメントが入ると想定)" * 50
upsert_document("doc_001", long_text, "user_123")
# 検索時もnamespaceを指定する
query_res = index.query(
vector=get_embedding("ドキュメントの内容について質問"),
top_k=3,
namespace="user_123",
include_metadata=True
)
この「チャンク分割」と「名前空間」の組み合わせが、RAG構築の8割を占める重要なポイントです。
特に名前空間(namespace)を使わずに metadata のフィルタリングだけでユーザーを分けようとすると、データ量が増えたときに検索速度が低下します。
最初から物理的に分けておくのが、賢いアーキテクチャの選択です。
よくあるトラブルと解決法
| エラー内容 | 原因 | 解決策 |
|---|---|---|
Dimension mismatch | インデックスの次元数とモデルの次元数が違う | index作成時の dimension を1536(OpenAI)等に合わせる |
Namespace not found | データが入っていないnamespaceを指定している | 初回の upsert 時にnamespaceが自動作成されるまで待つ |
Rate limit reached | OpenAI APIの無料枠制限または支払い未設定 | OpenAIのダッシュボードでクレジットをチャージ(最低$5〜)する |
Index is initializing | インデックス作成直後にアクセスしている | 数分待つ。Serverlessでも初期化には数十秒かかる |
次のステップ
この記事で、ベクトルデータベースの「基本」と「実務での定石」はマスターしました。 次にやるべきことは、この検索機能をChatGPTなどのLLMと組み合わせることです。 具体的には、以下の流れを実装してみてください。
- ユーザーの質問をベクトル化する
- Pineconeから関連するチャンク(メタデータのtext)を上位3件取ってくる
- 取ってきたテキストを「参考資料」として、ChatGPTに「以下の資料に基づいて回答してください」とプロンプトを投げる
これが、今世の中で最も求められている「RAG(Retrieval-Augmented Generation)」の正体です。
また、もしデータが10万件を超えるようなら、インデックス作成時のメトリクスを dotproduct に変え、埋め込みモデルを text-embedding-3-large にアップグレードして精度を比較するのも面白いでしょう。
私がRTX 4090で検証した限りでは、モデルの性能よりも「チャンクの切り方」の方が回答精度に寄与することが多かったです。
よくある質問
Q1: ベクトルDBを使わずに普通のDB(PostgreSQLなど)で検索してはダメですか?
PostgreSQLには pgvector という拡張があり、最近は非常に優秀です。
ただ、インフラのセットアップがPineconeより数倍面倒です。
まずはPineconeでプロトタイプを作り、コストが問題になる規模になったら pgvector への移行を検討するのが、スピード感のある開発スタイルです。
Q2: 検索結果が全然関係ないものばかり返ってきます。
原因の多くは「チャンクサイズが不適切」か「クエリ(質問)が短すぎる」かのどちらかです。 チャンクが短すぎると文脈が消え、長すぎると意味が混ざります。 まずは200〜500文字程度で調整し、精度が上がらなければ、質問文をLLMに「検索に適した詳細な文章」へ書き直させてからベクトル化する手法(Query Expansion)を試してください。
Q3: データの更新はどうすればいいですか?
Pineconeには update や delete のAPIも用意されています。
ドキュメントを更新する場合は、古いIDを指定して再度 upsert すれば上書きされます。
ただし、チャンク分割している場合は、古いチャンクを一度全て delete してから新しいチャンクを upsert しないと、古いカスデータが残って検索精度を下げる原因になります。
メタデータ
1. X投稿用ツイート本文 (TWEET_TEXT) 2. アフィリエイト商品情報 (AFFILIATE_CONTEXT)
3. SNS拡散用ハッシュタグ (HASHTAGS) 4. SEOタグ (SEO_TAGS) 5. URLスラッグ (SLUG)
📦 この記事に関連する商品(楽天メインで価格確認)
Dell U2723QEコードと Pinecone コンソールを並べてデバッグするのに最適な4K解像度
※アフィリエイトリンクを含みます





