AI Engineering Academy · レッスン

親子チャンクと小から大への検索

正確な検索のために小さな子チャンクを保存し、より豊かな文脈をLLMに渡すために大きな親チャンクを返すことで、検索精度と生成品質のバランスを取ります。

レッスン 3/413 ステップ

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

精度とコンテキストのジレンマ

RAGシステムにはトレードオフがあります。小さなチャンクは、それぞれが1つの考えに焦点を絞っているため高い精度で検索できますが、LLMが完全な回答を生成するために必要な周辺コンテキストが不足します。大きなチャンクは豊富なコンテキストを提供する一方、1つのクエリに強く一致するのではなく、多くのクエリに弱く一致するため、検索精度が低下します。親子チャンク分割はこのジレンマを解決します。

親子アーキテクチャ

親子チャンク分割では、同じ文書から2層のチャンクを作成します。子チャンクは小さく(例:1~3文)、検索用に埋め込んでインデックス化します。親チャンクはより大きなセクション(例:段落全体やページ全体)で、別途保存します。子チャンクが検索されたときは、代わりにその親をLLMへ返します。

チャンク階層の構築

最初のステップでは、文書を大きな親チャンクに分割し、次に各親チャンクを小さな子チャンクへ分割します。各子チャンクには通常、parent_idメタデータフィールドなどの参照情報を保持させ、親を指し示します。この対応付けにより、検索された子チャンクから完全な親パッセージを検索できます。

from langchain.text_splitter import RecursiveCharacterTextSplitter

parent_splitter = RecursiveCharacterTextSplitter(chunk_size=1500, chunk_overlap=0)
child_splitter = RecursiveCharacterTextSplitter(chunk_size=300, chunk_overlap=30)

parent_chunks = parent_splitter.split_documents(docs)
child_chunks = []
for i, parent in enumerate(parent_chunks):
    children = child_splitter.split_documents([parent])
    for child in children:
        child.metadata['parent_id'] = i
    child_chunks.extend(children)

子チャンクだけをインデックス化する

子チャンクだけを埋め込み、ベクトルデータベースに保存します。親チャンクは、別のキーバリューストア(インメモリの辞書、Redis、ドキュメントデータベースなど)に保存します。これにより、ベクトルインデックスを密で正確に保ちながら、豊富なコンテキストをその外部に置けます。

# Store parents in a docstore
parent_store = {i: chunk.page_content for i, chunk in enumerate(parent_chunks)}

# Embed and index only children
vectorstore = Chroma.from_documents(
    child_chunks,
    embedding=OpenAIEmbeddings()
)

検索:子を入力し、親を出力する

検索時には、ユーザーのクエリを埋め込み、子チャンクと照合します。上位k件の子チャンクを取得し、それらのparent_id参照を使って親ストアから親を検索します。その後、LLMのプロンプトに注入するのは子ではなく親パッセージです。LLMは広いコンテキストを受け取り、検索は正確に行われます。

def retrieve_with_parents(query, vectorstore, parent_store, k=5):
    child_results = vectorstore.similarity_search(query, k=k)
    seen_parent_ids = set()
    parent_contexts = []
    for child in child_results:
        pid = child.metadata['parent_id']
        if pid not in seen_parent_ids:
            parent_contexts.append(parent_store[pid])
            seen_parent_ids.add(pid)
    return parent_contexts

LangChain ParentDocumentRetriever

LangChain は、このパターンをすぐに利用できる形で実装した ParentDocumentRetriever クラスを提供しています。親チャンク分割器、子チャンク分割器、子の埋め込み用のベクトルストア、親ドキュメント用のドキュメントストアを指定します。これにより階層構造が構成され、取得処理も透過的に行われます。

from langchain.retrievers import ParentDocumentRetriever
from langchain.storage import InMemoryStore

store = InMemoryStore()
retriever = ParentDocumentRetriever(
    vectorstore=vectorstore,
    docstore=store,
    child_splitter=child_splitter,
    parent_splitter=parent_splitter
)
retriever.add_documents(docs)
results = retriever.invoke('What is the refund policy?')

小さいチャンクから大きいコンテキストを取得する方法

小さいチャンクから大きいコンテキストを取得する方法は、同じ概念を指す別の呼び方です。小さく精度の高いチャンクを取得し、LLM に送る前に周囲のコンテキストまで広げます。実装によっては、固定された親チャンクまで広げるのではなく、隣接する文のウィンドウまで広げます。つまり、一致したチャンクの前後にある文もモデルに渡し、コンテキストの連続性を保ちます。

def retrieve_with_window(query, vectorstore, sentences, window=2, k=5):
    results = vectorstore.similarity_search(query, k=k)
    expanded = []
    for r in results:
        idx = r.metadata['sentence_index']
        start = max(0, idx - window)
        end = min(len(sentences), idx + window + 1)
        expanded.append(' '.join(sentences[start:end]))
    return expanded

親チャンクの重複除去

1つのクエリに対して、同じ親チャンクに属する複数の子チャンクが取得されることがあります。重複除去を行わないと、同じ親の文章がプロンプト内に何度も現れ、トークンを無駄に消費します。コンテキストを組み立てる前に、必ず親 ID で重複除去してください。上の retrieve 関数のコード例では、seen_parent_ids セットを使ってこの処理を行っています。

親子チャンク分割を使う場面

親子検索は、章と節、記事と段落、またはサブセクションを持つ wiki など、ドキュメントが明確な階層構造を持つ場合に最も効果を発揮します。特に、正確な質問には局所的な回答が必要でありながら、その回答がより広いセクションのコンテキストの中でなければ意味をなさない、長い技術ドキュメントで有効です。

子チャンクと親チャンクのサイズを選ぶ

一般的な構成は、子チャンクを 200~400 トークン(1つの考えに集中)にし、親チャンクを 1000~2000 トークン(セクション全体)にする方法です。子チャンクが小さすぎると、単独では意味を持たない個々の文になってしまいます。親チャンクが大きすぎると、避けようとしていたコンテキストの希薄化問題が再び生じます。

アプローチの比較:まとめ

ここまでのチャンク分割戦略をまとめると、固定サイズ分割は高速ですがコンテキストを分断し、セマンティックチャンク分割はトピックの一貫性を保ち、親子分割は検索の精度と LLM に渡すコンテキストの豊富さを両立します。長いドキュメントを扱うほとんどの本番 RAG システムでは、親子チャンク分割が、複雑さを抑えながら最も高い検索品質を実現します。

理解度チェック

このレッスンで学んだ親子チャンク分割について、理解度を確認しましょう。

レッスンのまとめ

このレッスンでは、子チャンクが精度の高い検索を提供し、親チャンクが豊富なコンテキストを提供すること、LangChain の ParentDocumentRetriever がこれを自動的に実装すること、そして親 ID による重複除去でコンテキストの重複を防げることを学びました。次は、コードファイルや HTML ドキュメントに適した、ドキュメント固有のチャンク分割戦略について学びます。

無料で開始

AI チューターと学ぶ Python — 無料

ブラウザでリアルコードを書いて実行し、24/7 の AI チューターから瞬時にサポートを受け、ウェブまたはアプリで続きから学習できます。

コース
30
レッスン
120

よくある質問

「親子チャンクと小から大への検索」レッスンは無料ですか?

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

「親子チャンクと小から大への検索」で何を学びますか?

正確な検索のために小さな子チャンクを保存し、より豊かな文脈をLLMに渡すために大きな親チャンクを返すことで、検索精度と生成品質のバランスを取ります。 ブラウザで直接実行するハンズオンコードでAI Engineering Academyを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。

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

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

「親子チャンクと小から大への検索」レッスンにはどのくらい時間がかかりますか?

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

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

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

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

  1. 素朴なチャンク分割が検索に悪影響を与える理由
  2. 埋め込み類似度による意味的チャンク分割
  3. 親子チャンクと小から大への検索
  4. コードとHTMLに特化したドキュメント戦略
← AI Engineering Academyに戻る