インデックス作成:チャンクのEmbeddingと保存
OpenAI embeddings APIを使って各チャンクをembeddingし、得られたベクトルをメタデータ付きでvector storeにupsertして、ドキュメントの検索可能なインデックスを構築します。
「インデックス作成:チャンクのEmbeddingと保存」はCoddyKit上の無料AI Engineering Academyレッスンです。 これはレッスン3/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはAI Engineering Academy学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 AI Engineering Academyコースには全4レッスンが含まれています。
インデックス化の段階:概要
ドキュメントの読み込みとチャンク分割が終わると、インデックス化の段階に進みます。ここでは、テキストチャンクをベクトル埋め込みに変換し、検索可能なベクトルデータベースに保存します。これは、クエリに回答できるようになる前の最後のオフライン処理です。埋め込みの品質と、保存およびインデックス化戦略の効率が、クエリ実行時のRAGシステムの速度と精度を直接左右します。
OpenAI APIによる埋め込みの生成
最も一般的な方法は、チャンクのテキストをOpenAIの埋め込みAPIに渡すことです。text-embedding-3-smallモデルは1,536次元のベクトルを生成し、料金は100万トークンあたり0.02ドルです。ほとんどのワークロードにとって非常に安価です。スループットを最大化するため、1回のAPI呼び出しで複数のテキストを送信してください(最大2,048入力)。レスポンスには、入力テキストと同じ順序で、入力ごとに1つの埋め込みベクトルが含まれます。
from openai import OpenAI
client = OpenAI()
def embed_batch(texts, model='text-embedding-3-small'):
response = client.embeddings.create(
model=model,
input=texts # up to 2048 texts per call
)
return [item.embedding for item in response.data]
# Embed one batch of 100 chunk texts
texts = [chunk['text'] for chunk in chunks[:100]]
vectors = embed_batch(texts)
print(f'Embedding dimension: {len(vectors[0])}')
print(f'Embedded {len(vectors)} chunks')効率化のためのバッチ処理
何千ものチャンクをインデックス化する場合、効率が重要です。スループットとメモリ使用量のバランスを取るため、チャンクを100~500個のバッチで処理してください。位置を記録しておけば、失敗後に処理済みのチャンクを再度埋め込まずに再開できます。進捗も定期的にログに記録します。100,000チャンクを1バッチ500個で処理する場合、API呼び出しは200回になり、通常は数分で完了します。
def embed_all_chunks(chunks, batch_size=200):
embedded = []
total = len(chunks)
for i in range(0, total, batch_size):
batch = chunks[i:i+batch_size]
texts = [c['text'] for c in batch]
vectors = embed_batch(texts)
for chunk, vector in zip(batch, vectors):
embedded.append({
**chunk,
'embedding': vector
})
if (i // batch_size) % 10 == 0:
print(f'Progress: {min(i+batch_size, total)}/{total}')
return embeddedインデックス化中のレート制限への対応
OpenAIの埋め込みAPIには、1分あたりのトークン数(TPM)で測定されるレート制限があります。大規模なインデックス化処理ではこの制限に達し、RateLimitErrorが発生します。ジッターを伴う指数バックオフを実装してください。レート制限エラーが発生したら、再試行する前に短いランダムな時間だけ待機し、その後も失敗するたびに待機時間を2倍にします。これにより再試行が分散され、すべての並列ワーカーが同時にAPIへ大量のリクエストを送るのを防げます。
import time
import random
from openai import RateLimitError
def embed_batch_with_retry(texts, max_retries=5):
for attempt in range(max_retries):
try:
return embed_batch(texts)
except RateLimitError:
if attempt == max_retries - 1:
raise
wait = (2 ** attempt) + random.uniform(0, 1)
print(f'Rate limited. Waiting {wait:.1f}s...')
time.sleep(wait)
return []Pineconeへのベクトルのアップサート
埋め込みを生成したら、ベクトルストアにアップサートします。アップサートとは、新しいベクトルを挿入するか、同じIDがすでに存在する場合は既存のベクトルを更新することです。設計上、べき等な操作になっています。Pineconeでは、アップサートする各レコードに、ベクトルID、埋め込み値、後でフィルタリングや表示に使用するフィールドのメタデータ辞書が含まれます。スループットを最適化するため、1回の呼び出しにつき最大100レコードのバッチでアップサートしてください。
import pinecone
pc = pinecone.Pinecone(api_key='YOUR_KEY')
index = pc.Index('rag-index')
def upsert_to_pinecone(embedded_chunks, batch_size=100):
for i in range(0, len(embedded_chunks), batch_size):
batch = embedded_chunks[i:i+batch_size]
vectors = [
(
chunk['id'],
chunk['embedding'],
{
'text': chunk['text'],
'source': chunk['metadata']['source'],
'page': chunk['metadata'].get('page', 0)
}
)
for chunk in batch
]
index.upsert(vectors=vectors)
print(f'Upserted {min(i+batch_size, len(embedded_chunks))}/{len(embedded_chunks)}')pgvectorへの保存
pgvectorでは、標準SQLを使用して埋め込みをPostgreSQLのテーブルに直接挿入します。vectorデータ型には、文字列としてシリアライズしたPythonの浮動小数点数リストを渡せます。すべての行を挿入したら、高速な近似最近傍クエリ用にHNSWインデックスを作成します。数百万行が存在するテーブルにインデックスを作成すると数分かかることがあるため、インデックスは先に作成せず、一括挿入の後に構築してください。
import psycopg2
from psycopg2.extras import execute_values
def upsert_to_pgvector(conn, embedded_chunks):
with conn.cursor() as cur:
records = [
(
chunk['id'],
chunk['text'],
chunk['metadata']['source'],
chunk['metadata'].get('page', 0),
chunk['embedding'] # list of floats
)
for chunk in embedded_chunks
]
execute_values(cur, '''
INSERT INTO document_chunks (id, text, source, page, embedding)
VALUES %s
ON CONFLICT (id) DO UPDATE
SET text = EXCLUDED.text, embedding = EXCLUDED.embedding
''', records)
conn.commit()HNSWインデックスの構築
HNSW(Hierarchical Navigable Small World)は、高速な近似最近傍検索を可能にするインデックス型です。クエリベクトルを保存されているすべてのベクトルと比較する総当たり検索とは異なり、HNSWは多層グラフ構造を構築して検索空間を絞り込みます。mパラメーターは各ノードが持つ接続数を制御します(値を大きくすると再現率は向上しますが、メモリ使用量も増えます)。ef_constructionは構築時のインデックス品質を制御します。
-- Build HNSW index after bulk insertion
CREATE INDEX CONCURRENTLY ON document_chunks
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
-- Set ef_search at query time to trade recall vs speed
SET hnsw.ef_search = 100;
-- Verify index was created
SELECT indexname, indexdef
FROM pg_indexes
WHERE tablename = 'document_chunks';メタデータスキーマの設計
各ベクトルとともに保存したメタデータによって、強力なフィルタリング付き検索が可能になります。インデックスを作成する前にメタデータスキーマを設計してください。後から新しいフィールドを追加するには、再インデックスが必要です。フィルタリングに使用するフィールド(department、doc_type、日付範囲)、引用に表示するフィールド(title、page、author)、デバッグに役立つフィールド(chunk_index、total_chunks、indexed_at)を含めます。メタデータの値は単純に保ってください。文字列、数値、ブール値は効率よくインデックス化およびフィルタリングできますが、ネストされたオブジェクトはできません。
# Well-designed metadata schema
METADATA_SCHEMA = {
# For filtering at retrieval time
'department': 'HR', # string
'doc_type': 'policy', # string
'year': 2025, # integer
'is_active': True, # boolean
# For display in citations
'title': 'Employee Handbook 2025',
'author': 'HR Team',
'page': 12,
'source': 's3://docs/handbook_2025.pdf',
# For debugging and updates
'chunk_index': 3,
'total_chunks': 24,
'indexed_at': '2025-09-01T10:00:00Z'
}長時間のインデックス作成ジョブでのチェックポイント
大規模なコーパスのインデックス作成には数時間かかることがあります。途中でクラッシュすると、それまでの進捗がすべて失われます。正常にインデックス化されたチャンクを記録するチェックポイントファイルを実装してください。再起動時には、すでにインデックス化されたチャンクをスキップし、中断した場所から続行します。これにより、インデックス作成ジョブがべき等になり、安全に再開できるようになります。チェックポイントは、処理済みチャンクIDの集合として、JSONファイルまたはデータベーステーブルに保存してください。
import json
from pathlib import Path
CHECKPOINT_FILE = '/tmp/index_checkpoint.json'
def load_checkpoint():
if Path(CHECKPOINT_FILE).exists():
return set(json.loads(Path(CHECKPOINT_FILE).read_text()))
return set()
def save_checkpoint(indexed_ids):
Path(CHECKPOINT_FILE).write_text(json.dumps(list(indexed_ids)))
def index_with_checkpoint(chunks, index):
done = load_checkpoint()
remaining = [c for c in chunks if c['id'] not in done]
print(f'Resuming: {len(done)} done, {len(remaining)} remaining')
for chunk in remaining:
upsert_to_pinecone([chunk], index)
done.add(chunk['id'])
save_checkpoint(done)インデックスの完全性の検証
インデックス作成後、すべてのチャンクがベクトルストアに登録されたことを確認します。分割処理によって生成されたチャンク数と、インデックスが報告するベクトル数を比較してください。既知のドキュメントのテキストでインデックスをクエリし、期待する結果が上位5件に現れることを確認します。ゴールデンテストセットから既知のクエリをいくつか実行し、適合率が期待どおりの水準にあるか確認してください。検証せずに、インデックスが完全だと決めつけてはいけません。
def verify_index(index, chunks, sample_size=10):
index_stats = index.describe_index_stats()
total_vectors = index_stats.total_vector_count
expected = len(chunks)
print(f'Index vectors: {total_vectors}, Expected: {expected}')
if total_vectors != expected:
print('WARNING: mismatch — some chunks may not have been indexed')
# Spot-check retrieval
import random
sample = random.sample(chunks, sample_size)
for chunk in sample:
vec = embed_batch([chunk['text']])[0]
results = index.query(vector=vec, top_k=1, include_metadata=True)
top_id = results.matches[0].id if results.matches else None
if top_id != chunk['id']:
print(f'WARNING: expected {chunk["id"]}, got {top_id}')ローカル埋め込みの代替手段
インフラの外部に持ち出せないプライバシーに敏感なデータには、ローカルホスト型の埋め込みモデルを使用します。sentence-transformersライブラリには、all-MiniLM-L6-v2(384次元、22MB、非常に高速)やbge-large-en-v1.5(1024次元、より高品質)などの高品質なモデルが用意されています。中規模の処理にはCPU、大規模なインデックス作成ジョブにはGPUで実行します。ローカルモデルを使うとAPIコストとデータの外部送信をなくせますが、モデルファイルと計算リソースの管理が必要です。
from sentence_transformers import SentenceTransformer
# Load once at startup
model = SentenceTransformer('sentence-transformers/all-MiniLM-L6-v2')
def embed_locally(texts, batch_size=64):
# encode() handles batching internally
embeddings = model.encode(
texts,
batch_size=batch_size,
show_progress_bar=True,
convert_to_numpy=True
)
return embeddings.tolist() # convert numpy array to Python list
vectors = embed_locally([c['text'] for c in chunks])クイックチェック
このレッスンで学んだAIエンジニアリングの概念について理解度を確認しましょう。
レッスンのまとめ
このレッスンでは、OpenAI APIを使用して埋め込みをバッチで生成し、指数バックオフでレート制限に対処する方法、メタデータ付きでPineconeとpgvectorにベクトルをアップサートする方法、高速な近似最近傍検索のためにHNSWインデックスを構築する方法、そしてチェックポイントからの再開、メタデータスキーマの設計、インデックスの完全性の検証などの本番環境向けのベストプラクティスを学びました。次は、チャンクを検索して根拠のある回答を生成するクエリパイプラインを構築します。
よくある質問
「インデックス作成:チャンクのEmbeddingと保存」レッスンは無料ですか?
はい。「インデックス作成:チャンクのEmbeddingと保存」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、AI Engineering Academyコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 AI Engineering Academyコースには全4レッスンが含まれています。
「インデックス作成:チャンクのEmbeddingと保存」で何を学びますか?
OpenAI embeddings APIを使って各チャンクをembeddingし、得られたベクトルをメタデータ付きでvector storeにupsertして、ドキュメントの検索可能なインデックスを構築します。 ブラウザで直接実行するハンズオンコードでAI Engineering Academyを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。
AI Engineering Academyを始めるのに経験は必要ですか?
事前経験は必要ありません。CoddyKitのAI Engineering Academyは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン3/4です。
「インデックス作成:チャンクのEmbeddingと保存」レッスンにはどのくらい時間がかかりますか?
ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。
このAI Engineering Academyレッスンでコードを書いて実行できますか?
はい。すべてのAI Engineering Academyレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。
このコースのすべてのレッスン
- ドキュメントの読み込みとテキスト抽出
- チャンク化戦略:固定長・文単位・再帰的分割
- インデックス作成:チャンクのEmbeddingと保存
- クエリ・検索・生成