0Pricing
AI Engineering Academy · レッスン

RAGアーキテクチャ:インデックス作成と検索

RAGの2つのフェーズを整理します。ドキュメントをチャンク化し、embeddingして保存するオフラインのインデックス作成フェーズと、各クエリに関連するコンテキストを見つけるオンラインの検索フェーズです。

「RAGアーキテクチャ:インデックス作成と検索」はCoddyKit上の無料AI Engineering Academyレッスンです。 これはレッスン2/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはAI Engineering Academy学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 AI Engineering Academyコースには全4レッスンが含まれています。

RAGには2つの異なるフェーズがある

RAGシステムは、実行されるタイミングが異なる、根本的に異なる2つのフェーズで動作します。オフラインのインデックス作成フェーズでは、ドキュメントを一度(または変更時に)処理して、検索可能なインデックスを準備します。オンラインの検索フェーズは、ユーザーからのクエリごとにリアルタイムで実行されます。この分割を理解することは、クエリ時の高速性と長期的な保守性を両立するシステムを設計するうえで不可欠です。

インデックス作成フェーズ:ステップ1 — Load

インデックス作成は、ドキュメントの読み込みから始まります。これは、ソースシステムから生のファイルを読み取る処理です。ドキュメントには、PDF、Wordファイル、HTMLページ、Markdownファイル、データベースの行、その他あらゆるテキストソースを使用できます。各ドキュメントは、可能な限り構造を保持したプレーンテキストとしてメモリに読み込まれます。pypdf、python-docx、unstructuredなどのライブラリが、形式ごとの解析処理を担います。

from pypdf import PdfReader

def load_pdf(path):
    reader = PdfReader(path)
    pages = []
    for i, page in enumerate(reader.pages):
        text = page.extract_text()
        pages.append({'text': text, 'page': i + 1, 'source': path})
    return pages

docs = load_pdf('company_policy.pdf')
print(f'Loaded {len(docs)} pages')

インデックス作成フェーズ:ステップ2 — Chunk

LLMのコンテキストウィンドウには上限があり、ドキュメント全体を取得するのは非効率です。読み込んだテキストは、1つあたりおよそ200~1000トークンの小さなチャンクに分割します。適切なチャンク分割では、意味のまとまりが保たれます。つまり、1つのチャンクで完結した内容を表せるようにします。一般的な方法として重複するウィンドウを使い、チャンクの境界付近にある文を2つのチャンクに含めます。これにより、分割箇所での情報の欠落を防ぎます。

from langchain_text_splitters import RecursiveCharacterTextSplitter

splitter = RecursiveCharacterTextSplitter(
    chunk_size=500,      # characters per chunk
    chunk_overlap=50,    # overlap between chunks
    separators=['\n\n', '\n', '. ', ' ']
)

for page in docs:
    chunks = splitter.split_text(page['text'])
    for chunk in chunks:
        # Each chunk carries metadata from its source page
        print(f'Chunk ({len(chunk)} chars): {chunk[:80]}...')

インデックス作成フェーズ:ステップ3 — Embed

各テキストチャンクを、意味を数値として捉えた高密度ベクトルembeddingに変換します。チャンクごとにOpenAIのtext-embedding-3-smallなどのembeddingモデルを呼び出すと、高次元のfloat配列が返されます。意味が近いチャンクは、この高次元空間で互いに近いベクトルになります。これによって、意味的類似性検索が可能になります。

from openai import OpenAI

client = OpenAI()

def embed_chunks(chunks):
    texts = [c['text'] for c in chunks]
    response = client.embeddings.create(
        model='text-embedding-3-small',
        input=texts
    )
    for i, chunk in enumerate(chunks):
        chunk['embedding'] = response.data[i].embedding
    return chunks

# Batch up to 2048 texts per API call
embedded = embed_chunks(all_chunks)

インデックス作成フェーズ:ステップ4 — Store

embedding化したチャンクは、メタデータ(ソースファイル、ページ番号、セクションタイトル)とともにベクトルデータベースに保存します。ベクトルストアは、近似最近傍検索を高速に実行できるインデックス構造(通常はHNSW)を構築します。このインデックスはディスクに永続化されるため、再起動後も保持されます。インデックス作成は通常、セットアップ時に一度行い、新しいドキュメントが追加されたときに段階的に実行します。

import pinecone

pc = pinecone.Pinecone(api_key='YOUR_KEY')
index = pc.Index('rag-documents')

# Upsert vectors with metadata
vectors_to_upsert = [
    (
        chunk['id'],
        chunk['embedding'],
        {'text': chunk['text'], 'source': chunk['source'], 'page': chunk['page']}
    )
    for chunk in embedded_chunks
]

# Upsert in batches of 100
for i in range(0, len(vectors_to_upsert), 100):
    index.upsert(vectors=vectors_to_upsert[i:i+100])
print('Indexing complete')

検索フェーズ:クエリのembedding化

ユーザーがクエリを送信すると、オンラインの検索フェーズが始まります。最初のステップは、インデックス作成時と同じembeddingモデルを使ってユーザーの質問をembedding化することです。これは重要です。text-embedding-3-smallでインデックスを作成した場合は、クエリにも必ずtext-embedding-3-smallを使用してください。クエリembeddingは、ユーザーが尋ねている内容の意味を符号化したベクトルです。

def embed_query(question):
    response = client.embeddings.create(
        model='text-embedding-3-small',  # MUST match indexing model
        input=[question]
    )
    return response.data[0].embedding

user_question = 'What is our parental leave policy?'
query_vector = embed_query(user_question)
print(f'Query embedded: {len(query_vector)}-dim vector')

検索フェーズ:ANN検索

クエリembeddingはベクトルストアに送られ、クエリベクトルと最も類似するembeddingを持つK個のチャンクを見つけるために、近似最近傍(ANN)検索が実行されます。HNSWインデックスは、総当たり検索と比べて再現率をわずかに犠牲にする代わりに、大幅な速度向上を実現するため、この検索は非常に高速です(通常は10ミリ秒未満)。上位K個のチャンクを取得します。K=5~K=20が一般的です。

results = index.query(
    vector=query_vector,
    top_k=5,
    include_metadata=True
)

print(f'Retrieved {len(results.matches)} chunks:')
for match in results.matches:
    print(f'  Score: {match.score:.3f} | Source: {match.metadata["source"]}')
    print(f'  Text: {match.metadata["text"][:100]}...')
    print()

2つのフェーズをつなぐ

重要なポイントは、インデックス作成と検索が連携するように設計されていることです。ベクトルが存在する数学的な空間はモデルごとに異なるため、両方のフェーズで同一のembeddingモデルを使用する必要があります。embeddingモデルを変更する場合は、すべてのドキュメントを再インデックスする必要があります。ベクトルストアはその橋渡しをします。インデックス作成時にはベクトルを受け取り、検索時にはベクトルを返すことで、時間上は2つのフェーズを分離しながら、ベクトル空間上では整合性を保ちます。

検索時のメタデータフィルタリング

ベクトル検索では意味的に類似したチャンクを見つけますが、場合によってはメタデータでフィルタリングする必要もあります。たとえば、2025年にアップロードされたドキュメントのチャンクだけを取得したり、HR部門のフォルダーにあるチャンクだけを取得したりします。ベクトルストアは、メタデータフィールドに対する事前フィルタリングまたは事後フィルタリングに対応しています。事前フィルタリング(PineconeとQdrantが対応)はANN検索の前にフィルターを適用するため、上位K件の結果に対して行う事後フィルタリングより高速で精度も高くなります。

# Retrieve only from HR department documents
results = index.query(
    vector=query_vector,
    top_k=5,
    filter={'department': {'$eq': 'HR'}},
    include_metadata=True
)

# Or filter by date range
results = index.query(
    vector=query_vector,
    top_k=5,
    filter={
        'upload_year': {'$gte': 2024},
        'doc_type': {'$eq': 'policy'}
    },
    include_metadata=True
)

更新に対応する増分インデックス作成

本番環境では、ドキュメントのコーパスは時間とともに変化します。効果的なインデックス作成アーキテクチャは、増分更新に対応します。ドキュメントが編集されたら、既存のベクトルをIDで削除し、新しいベクトルをupsertします。ドキュメントが削除されたら、そのベクトルを削除します。各チャンクには、ソースドキュメントのパスとチャンクの位置をもとに決定的なIDを割り当てます。これにより、すべてを再インデックスしなくても、常に正しいベクトルを見つけて更新できます。

import hashlib

def make_chunk_id(source_path, chunk_index):
    # Deterministic, stable ID for each chunk
    key = f'{source_path}::chunk_{chunk_index}'
    return hashlib.md5(key.encode()).hexdigest()

def update_document(source_path, index):
    # Delete old vectors for this document
    index.delete(filter={'source': source_path})
    # Re-index the updated document
    new_chunks = load_and_chunk(source_path)
    new_embedded = embed_chunks(new_chunks)
    index.upsert(vectors=new_embedded)
    print(f'Updated {source_path}: {len(new_chunks)} chunks')

RAGパイプライン全体の概要

RAGアーキテクチャ全体は次のようになります。オフライン:Documents → Loader → Chunker → Embedding Model → Vector Store。オンライン:User Query → Embedding Model → Vector Store (ANN search) → Top-K Chunks → Prompt Assembly → LLM → Answer。オフラインパイプラインは、ドキュメントが更新されるたびに一度実行します。オンラインパイプラインはユーザーからのクエリごとに数ミリ秒で実行され、LLMが目にするのはドキュメントコーパス全体ではなく、関連するコンテキストだけです。

理解度チェック

このレッスンで学んだAIエンジニアリングの概念を確認しましょう。

レッスンのまとめ

このレッスンでは、ドキュメントごとに一度実行する、load、chunk、embed、storeの各ステップからなるオフラインのインデックス作成フェーズ、クエリをembedding化してANN検索を実行し、数ミリ秒で上位K個のチャンクを返すオンラインの検索フェーズ、そしてドキュメントの変更に応じてベクトルストアを最新状態に保つ増分インデックス作成の方法を学びました。次は、取得したコンテキストを効果的に使う拡張プロンプトの作成方法について学びます。

よくある質問

「RAGアーキテクチャ:インデックス作成と検索」レッスンは無料ですか?

はい。「RAGアーキテクチャ:インデックス作成と検索」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、AI Engineering Academyコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 AI Engineering Academyコースには全4レッスンが含まれています。

「RAGアーキテクチャ:インデックス作成と検索」で何を学びますか?

RAGの2つのフェーズを整理します。ドキュメントをチャンク化し、embeddingして保存するオフラインのインデックス作成フェーズと、各クエリに関連するコンテキストを見つけるオンラインの検索フェーズです。 ブラウザで直接実行するハンズオンコードでAI Engineering Academyを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。

AI Engineering Academyを始めるのに経験は必要ですか?

事前経験は必要ありません。CoddyKitのAI Engineering Academyは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン2/4です。

「RAGアーキテクチャ:インデックス作成と検索」レッスンにはどのくらい時間がかかりますか?

ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。

このAI Engineering Academyレッスンでコードを書いて実行できますか?

はい。すべてのAI Engineering Academyレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。

このコースのすべてのレッスン

  1. RAGが解決する問題
  2. RAGアーキテクチャ:インデックス作成と検索
  3. Augmented Promptの作成
  4. RAGとFine-Tuning:使い分け
← AI Engineering Academyに戻る