Skip to content
Published on

LLM RAGパイプライン:チャンキング戦略とエンベディング最適化実践ガイド 2026

Authors
  • Name
    Twitter
LLM RAGパイプライン:チャンキング戦略とエンベディング最適化実践ガイド 2026

概要

RAG(Retrieval-Augmented Generation)パイプラインにおいて、LLMの応答品質を決定する最も重要な2つの軸はチャンキング戦略エンベディング最適化である。どんなに強力なLLMを使用しても、検索段階で関連文書を正確に取得できなければハルシネーションが発生し、逆に検索品質が高ければ小規模モデルでも十分な応答を生成できる。

2026年初頭時点で、RAGパイプライン構築時に実務で繰り返し直面する問題は以下の通りである。

  • チャンクサイズの設定ミスによる検索精度の急落
  • エンベディングモデルの選定基準なくコストだけが増大する問題
  • ベクトルDBインデキシング戦略のミスマッチによる検索遅延の発生
  • 検索品質を定量的に測定していないため改善方向が定まらない問題

本記事では、各問題に対する具体的な解決方法をコードとベンチマークデータとともに解説する。2026年2月時点の最新ベンチマーク結果を反映しており、本番環境で検証済みの設定値を中心に記述する。

チャンキング戦略の比較

チャンキング(Chunking)は、元の文書をベクトルエンベディングが可能なサイズの断片に分割するプロセスである。チャンキング戦略によって検索精度、エンベディングコスト、コンテキスト品質が大きく異なる。

固定サイズチャンキング(Fixed-Size Chunking)

最もシンプルな方式で、指定された文字数またはトークン数に従ってテキストを一定サイズに分割する。実装が簡単で予測可能だが、文や段落の境界を無視するため意味的な断絶が発生しうる。

from langchain.text_splitter import CharacterTextSplitter

# 固定サイズチャンキング - 最も基本的な方式
splitter = CharacterTextSplitter(
    separator="\n",
    chunk_size=512,
    chunk_overlap=50,       # 10%オーバーラップで文脈を維持
    length_function=len,
)

documents = splitter.split_text(raw_text)
print(f"総チャンク数: {len(documents)}")
print(f"平均チャンク長: {sum(len(d) for d in documents) / len(documents):.0f}文字")

メリット:実装コスト最小、処理速度最速、チャンク数が予測可能。 デメリット:文の途中での切断が発生、意味単位の保持が不可。

再帰的チャンキング(Recursive Character Splitting)

2026年2月のFloTorchベンチマークで512トークン再帰的分割が69%の精度で1位を記録した。再帰的チャンキングは段落(\n\n)-> 改行(\n)-> 空白( )-> 文字("")の順で分割を試み、指定されたサイズ内で可能な限り意味単位を維持する。

from langchain.text_splitter import RecursiveCharacterTextSplitter

# 2026年ベンチマーク基準の最適設定
splitter = RecursiveCharacterTextSplitter(
    chunk_size=512,
    chunk_overlap=64,          # 約12%オーバーラップ
    separators=["\n\n", "\n", ". ", " ", ""],
    length_function=len,
    is_separator_regex=False,
)

chunks = splitter.split_text(raw_text)

# チャンク品質の検証
for i, chunk in enumerate(chunks[:3]):
    print(f"[Chunk {i}] 長さ={len(chunk)} | 先頭: {chunk[:80]}...")

重要な設定値:2026年初頭時点で検証済みの推奨値はchunk_size 400-512、overlap 10-20%である。2,500トークンを超えると応答品質が急激に低下する「context cliff」現象が観察されている。

セマンティックチャンキング(Semantic Chunking)

エンベディングモデルを使用して文間の意味的類似度を計算し、意味が転換するポイントで分割する。理論的には最も精巧だが、2026年のベンチマークでは意外にも低い54%の精度を記録した。平均チャンクサイズが43トークンと過度に小さくなる問題が原因である。

from langchain_experimental.text_splitter import SemanticChunker
from langchain_openai import OpenAIEmbeddings

# セマンティックチャンキング - 意味転換点に基づく分割
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
semantic_splitter = SemanticChunker(
    embeddings,
    breakpoint_threshold_type="percentile",  # percentile, standard_deviation, interquartile
    breakpoint_threshold_amount=75,          # 上位25%の類似度差で分割
)

semantic_chunks = semantic_splitter.split_text(raw_text)
print(f"セマンティックチャンク数: {len(semantic_chunks)}")
print(f"平均長さ: {sum(len(c) for c in semantic_chunks) / len(semantic_chunks):.0f}文字")

注意事項:セマンティックチャンキングは同一コーパスで再帰的分割と比較して3-5倍多くのベクトルを生成する。10,000件の文書の場合、再帰的分割は約50,000チャンクを作成するが、セマンティック分割は250,000まで増加しうる。

文書構造ベースチャンキング(Document Structure-Based)

Markdownヘッダー、HTMLタグ、PDFセクションなど文書自体の構造を活用して分割する。技術文書、APIリファレンス、法的文書のように明確な階層構造を持つ文書に効果的である。2025年11月のMDPI Bioengineering研究で、論理的トピック境界に合わせた適応型チャンキングが87%の精度を達成した。

from langchain.text_splitter import MarkdownHeaderTextSplitter

# Markdown構造ベースチャンキング
headers_to_split_on = [
    ("#", "Header 1"),
    ("##", "Header 2"),
    ("###", "Header 3"),
]

md_splitter = MarkdownHeaderTextSplitter(
    headers_to_split_on=headers_to_split_on,
    strip_headers=False,
)

md_chunks = md_splitter.split_text(markdown_text)

# 各チャンクにメタデータとしてヘッダー階層情報を含む
for chunk in md_chunks[:3]:
    print(f"メタデータ: {chunk.metadata}")
    print(f"内容: {chunk.page_content[:100]}...")
    print("---")

チャンキング戦略比較表

戦略精度(ベンチマーク)チャンクサイズ予測実装複雑度エンベディングコスト適した文書
固定サイズ60-65%高い低いベースライン非構造化テキスト
再帰的分割69%中程度低いベースライン汎用(推奨デフォルト)
セマンティック54%低い中程度3-5倍トピック転換が多い文書
文書構造ベース87%中程度中程度1-2倍構造化された技術文書
Propositionベース62%低い高い5倍以上研究論文

実務推奨:まずRecursiveCharacterTextSplitter(400-512トークン、10-20%オーバーラップ)で開始し、検索品質メトリクスを測定した後、構造ベースやセマンティック方式への切り替えの可否を判断する。

エンベディングモデルの選定

エンベディングモデルはRAGパイプラインの検索性能を直接的に左右する。2026年初頭時点のMTEB(Massive Text Embedding Benchmark)リーダーボードと実務適用結果を総合して整理する。

MTEBベンチマーク基準のモデル比較

モデルMTEBスコア次元最大トークン多言語ライセンスコスト(1Mトークン)
Cohere embed-v465.21024512OAPI$0.10
OpenAI text-embedding-3-large64.630728191OAPI$0.13
OpenAI text-embedding-3-small62.315368191OAPI$0.02
BGE-M363.010248192100+MITセルフホスティング
Qwen3-Embedding-8B70.5840968192多言語Apache 2.0セルフホスティング
E5-Mistral-7B63.5409632768OMITセルフホスティング

選定基準まとめ

  • APIベースの高速プロトタイピング:OpenAI text-embedding-3-small(コスト対性能最適)
  • 本番API:Cohere embed-v4またはOpenAI text-embedding-3-large
  • セルフホスティング多言語:BGE-M3(dense、sparse、multi-vectorを同時サポート)
  • 最高性能セルフホスティング:Qwen3-Embedding-8B(MTEB 70.58、GPUリソース必要)

エンベディング生成コード

from openai import OpenAI
import numpy as np

client = OpenAI()

def generate_embeddings(
    texts: list[str],
    model: str = "text-embedding-3-large",
    dimensions: int = 1024,    # 次元削減でコスト/速度を最適化
    batch_size: int = 100,
) -> np.ndarray:
    """バッチ単位のエンベディング生成(次元削減対応)"""
    all_embeddings = []

    for i in range(0, len(texts), batch_size):
        batch = texts[i:i + batch_size]
        response = client.embeddings.create(
            input=batch,
            model=model,
            dimensions=dimensions,  # text-embedding-3シリーズのみサポート
        )
        batch_embs = [item.embedding for item in response.data]
        all_embeddings.extend(batch_embs)

    return np.array(all_embeddings, dtype=np.float32)


# 使用例
chunks = ["RAGパイプラインの核心は検索品質である。", "チャンキング戦略によって結果が異なる。"]
embeddings = generate_embeddings(chunks, dimensions=1024)
print(f"エンベディングshape: {embeddings.shape}")  # (2, 1024)

次元削減のヒント:text-embedding-3-largeはデフォルトで3072次元だが、dimensionsパラメータで1024または256まで削減可能。3072から1024への削減時、MTEBスコアの低下は1-2%以内であり、ベクトルDB保存コストと検索速度で大きなメリットが得られる。

BGE-M3セルフホスティングエンベディング

from FlagEmbedding import BGEM3FlagModel

# BGE-M3: dense + sparse + colbertを同時サポート
model = BGEM3FlagModel("BAAI/bge-m3", use_fp16=True)

sentences = [
    "LLM RAGパイプラインにおけるチャンキング戦略は検索品質の核心である。",
    "ベクトルデータベースのインデキシングは検索レイテンシに直接影響する。",
]

# dense + sparse + colbertエンベディングを同時生成
output = model.encode(
    sentences,
    batch_size=12,
    max_length=512,
    return_dense=True,
    return_sparse=True,
    return_colbert_vecs=True,
)

dense_embeddings = output["dense_vecs"]       # shape: (2, 1024)
sparse_embeddings = output["lexical_weights"]  # スパースベクトル(BM25代替)
colbert_vecs = output["colbert_vecs"]          # マルチベクトル(精密マッチング)

print(f"Dense shape: {dense_embeddings.shape}")
print(f"Sparse keys例: {list(sparse_embeddings[0].keys())[:5]}")

BGE-M3の核心的な利点は、単一モデルでdense、sparse、multi-vector検索をすべてサポートする点である。これを活用すれば、別途のBM25インデックスなしでもHybrid Searchを実装できる。

ベクトルDBインデキシング戦略

エンベディングされたベクトルを保存・検索するベクトルデータベースの選択とインデキシング戦略は、検索レイテンシと精度に直接的な影響を与える。

ベクトルDB比較

特性ChromaPineconeWeaviateQdrantMilvus
ホスティングセルフ/クラウドマネージドセルフ/クラウドセルフ/クラウドセルフ/クラウド
p50レイテンシ(100K)~20ms~15ms~25ms~18ms~20ms
最大ベクトル数数百万数十億数億数十億数十億
メタデータフィルタ基本高度GraphQL高度高度
Hybrid SearchXOOOO
無料ティアローカル無制限制限付き14日間1GB無料オープンソース
プロトタイピング最適良好良好良好普通
エンタープライズ不適合最適良好良好良好

Chromaを活用したベクトル保存と検索

import chromadb
from chromadb.utils.embedding_functions import OpenAIEmbeddingFunction

# Chromaクライアント初期化(永続ストレージ)
client = chromadb.PersistentClient(path="./chroma_db")

embedding_fn = OpenAIEmbeddingFunction(
    api_key="sk-...",
    model_name="text-embedding-3-large",
)

# コレクション作成(HNSWインデックス自動適用)
collection = client.get_or_create_collection(
    name="rag_knowledge_base",
    embedding_function=embedding_fn,
    metadata={
        "hnsw:space": "cosine",       # 類似度メトリック
        "hnsw:M": 32,                 # HNSW接続数(高いほど正確、メモリ増加)
        "hnsw:ef_construction": 200,  # インデックス構築時の探索幅
    },
)

# ドキュメント追加(バッチ)
collection.add(
    documents=["RAGにおけるチャンキングは検索品質の80%を決定する。", "エンベディングモデルの選択が残りの20%を左右する。"],
    metadatas=[
        {"source": "rag_guide", "section": "chunking", "date": "2026-03"},
        {"source": "rag_guide", "section": "embedding", "date": "2026-03"},
    ],
    ids=["doc_001", "doc_002"],
)

# 検索(メタデータフィルタ + 類似度)
results = collection.query(
    query_texts=["RAGパイプラインで最も重要な要素は?"],
    n_results=5,
    where={"source": "rag_guide"},
    include=["documents", "distances", "metadatas"],
)

for doc, dist, meta in zip(
    results["documents"][0], results["distances"][0], results["metadatas"][0]
):
    print(f"[距離: {dist:.4f}] {meta['section']} | {doc[:80]}")

HNSWインデックスパラメータチューニング

ほとんどのベクトルDBが使用するHNSW(Hierarchical Navigable Small World)インデックスの重要なパラメータは3つである。

パラメータ説明デフォルト本番推奨影響
M各ノードの接続数1632-48高いほど精度向上、メモリ使用量増加
ef_constructionインデックス構築探索幅100200-400高いほどインデックス品質向上、構築時間増加
ef_search検索時の探索幅50100-200高いほどrecall向上、検索レイテンシ増加

実務のヒント:100万ベクトル基準で、M=32からM=48に上げるとRecall@10が約2-3%向上するが、メモリ使用量は40%増加する。メモリ制約がある場合、ef_searchを上げる方がコスト対効果が高い。

検索品質メトリクス:MRR、NDCG、Recall@K

RAGパイプラインの検索品質を定量的に測定しなければ改善方向を定められない。核心メトリクス3つをコードとともに整理する。

メトリクスの定義

  • MRR(Mean Reciprocal Rank):最初の関連文書のランク逆数の平均。「正解がどれだけ早く出るか」を測定する。
  • NDCG@K(Normalized Discounted Cumulative Gain):上位K件の結果の関連性をランクに応じて加重評価する。ランクが高いほど高い重みを付与する。
  • Recall@K:全関連文書のうち上位K件の結果に含まれる割合。「関連文書をどれだけ多く見つけたか」を測定する。

評価コードの実装

import numpy as np
from typing import List, Set


def mean_reciprocal_rank(
    retrieved_ids: List[List[str]],
    relevant_ids: List[Set[str]],
) -> float:
    """MRR計算:各クエリにおける最初の関連文書のランク逆数平均"""
    mrr_scores = []
    for retrieved, relevant in zip(retrieved_ids, relevant_ids):
        for rank, doc_id in enumerate(retrieved, 1):
            if doc_id in relevant:
                mrr_scores.append(1.0 / rank)
                break
        else:
            mrr_scores.append(0.0)
    return np.mean(mrr_scores)


def recall_at_k(
    retrieved_ids: List[List[str]],
    relevant_ids: List[Set[str]],
    k: int = 10,
) -> float:
    """Recall@K:上位K件の結果における関連文書の割合"""
    recalls = []
    for retrieved, relevant in zip(retrieved_ids, relevant_ids):
        top_k = set(retrieved[:k])
        if len(relevant) == 0:
            continue
        recalls.append(len(top_k & relevant) / len(relevant))
    return np.mean(recalls)


def ndcg_at_k(
    retrieved_ids: List[List[str]],
    relevant_ids: List[Set[str]],
    k: int = 10,
) -> float:
    """NDCG@K:ランク加重関連性評価"""
    ndcg_scores = []
    for retrieved, relevant in zip(retrieved_ids, relevant_ids):
        # DCG計算
        dcg = 0.0
        for rank, doc_id in enumerate(retrieved[:k], 1):
            if doc_id in relevant:
                dcg += 1.0 / np.log2(rank + 1)

        # Ideal DCG計算
        ideal_hits = min(len(relevant), k)
        idcg = sum(1.0 / np.log2(r + 1) for r in range(1, ideal_hits + 1))

        ndcg_scores.append(dcg / idcg if idcg > 0 else 0.0)
    return np.mean(ndcg_scores)


# 使用例
retrieved = [["d1", "d3", "d5", "d2", "d4"]]
relevant = [{"d1", "d2", "d7"}]

print(f"MRR:       {mean_reciprocal_rank(retrieved, relevant):.4f}")
print(f"Recall@3:  {recall_at_k(retrieved, relevant, k=3):.4f}")
print(f"Recall@5:  {recall_at_k(retrieved, relevant, k=5):.4f}")
print(f"NDCG@5:    {ndcg_at_k(retrieved, relevant, k=5):.4f}")

メトリクスの解釈基準

メトリクス悪い普通良い目標
MRR0.3未満0.3-0.50.5-0.80.7超
NDCG@100.4未満0.4-0.60.6-0.80.7超
Recall@100.5未満0.5-0.70.7-0.90.8超

MRRが低くRecall@Kが高い場合、関連文書は見つけているがランクが低いことを意味する。この場合、リランキング(Reranking)を導入すると効果が大きい。

Hybrid Searchの実装

純粋なベクトル検索(Dense Retrieval)だけではキーワード完全一致が必要な場合(固有名詞、コード名、製品番号など)に限界がある。Hybrid Searchはベクトル検索とキーワード検索(BM25/Sparse)を組み合わせて両方の利点を活用する。

from qdrant_client import QdrantClient, models
from qdrant_client.models import Distance, VectorParams, SparseVectorParams

client = QdrantClient(host="localhost", port=6333)

# Dense + Sparseベクトルを同時に保存するコレクション作成
client.create_collection(
    collection_name="hybrid_rag",
    vectors_config={
        "dense": VectorParams(size=1024, distance=Distance.COSINE),
    },
    sparse_vectors_config={
        "sparse": SparseVectorParams(),
    },
)

# ドキュメントのインデキシング(dense + sparseベクトルを同時保存)
client.upsert(
    collection_name="hybrid_rag",
    points=[
        models.PointStruct(
            id=1,
            vector={
                "dense": dense_embedding.tolist(),
                "sparse": models.SparseVector(
                    indices=list(sparse_weights.keys()),
                    values=list(sparse_weights.values()),
                ),
            },
            payload={"text": "RAGパイプラインチャンキングガイド", "source": "blog"},
        ),
    ],
)

# Hybrid Search実行(RRFベースのスコア統合)
results = client.query_points(
    collection_name="hybrid_rag",
    prefetch=[
        models.Prefetch(
            query=dense_query_vector.tolist(),
            using="dense",
            limit=20,
        ),
        models.Prefetch(
            query=models.SparseVector(
                indices=list(sparse_query.keys()),
                values=list(sparse_query.values()),
            ),
            using="sparse",
            limit=20,
        ),
    ],
    query=models.FusionQuery(fusion=models.Fusion.RRF),  # Reciprocal Rank Fusion
    limit=10,
)

for point in results.points:
    print(f"[Score: {point.score:.4f}] {point.payload['text']}")

Dense vs. Sparse vs. Hybrid 性能比較

検索方式キーワードマッチ意味的類似度固有名詞/コード一般的な質問推奨用途
Dense Only弱い強い弱い強い自然言語Q&A主体
Sparse Only (BM25)強い弱い強い弱いキーワード検索主体
Hybrid (RRF)強い強い強い強い本番RAG推奨

Hybrid SearchにおけるDenseとSparseの重み比率はドメインに応じた調整が必要である。技術文書ではSparse比率を高め(0.6)、一般的な対話型Q&AではDense比率を高める(0.7)のが経験的に効果的である。

リランキング(Reranking)

リランキングは初期検索結果をCross-Encoderモデルで再評価してランクを再調整するプロセスである。Databricksの研究によると、リランキング適用時に検索品質が最大48%向上し、一般的にNDCG@10で20-35%の改善効果がある。

リランキングアーキテクチャ

  1. 第1段階 - 候補検索:ベクトル検索(またはHybrid Search)で上位50-100件の文書を高速に抽出する。
  2. 第2段階 - リランキング:Cross-Encoderがクエリ-文書ペアを直接比較して精密な関連性スコアを算出する。
  3. 第3段階 - 最終選択:リランキングスコア基準で上位5-10件の文書をLLMコンテキストに渡す。

リランキングモデル比較

モデルNDCG@10改善レイテンシ(50文書)コスト推奨
Cohere Rerank v3+30-35%~300msAPI課金本番
cross-encoder/ms-marco-MiniLM-L-6-v2+20-25%~150ms無料コスト重視
BGE-Reranker-v2-m3+25-30%~200ms無料多言語
Jina Reranker v2+28-32%~250msAPI/セルフバランス

重要なトレードオフ:Cross-Encoderリランキングは精度を20-35%向上させるが、クエリあたり200-500msのレイテンシが追加される。リアルタイムチャットアプリケーションではリランキング候補を20-30件に制限してレイテンシを150ms以内に管理する。

トラブルシューティング

本番RAGパイプラインで頻繁に発生する問題とその解決方法を整理する。

問題1:検索結果がクエリと無関係な文書を返す

原因分析:チャンクサイズが大きすぎる(2,500トークン超)か、オーバーラップが不足して意味単位が壊れている場合がほとんどである。

解決方法

  • チャンクサイズを400-512に縮小し、オーバーラップを10-20%に設定する。
  • エンベディング前にチャンクの先頭に元文書のタイトルやセクションヘッダーをprepend する。
  • メタデータフィルタリングを追加して検索範囲を絞り込む。

問題2:関連文書は見つかるがランクが低い(低MRR、高Recall)

原因分析:Dense検索のみ使用時、意味的に関連はあるが直接的な回答ではない文書が上位に来る場合。

解決方法

  • Cross-Encoderリランキングを導入する。ほとんどの場合MRRが0.2-0.3上昇する。
  • クエリにドメインプレフィックスを追加する。例:"質問: {query}"形式でエンベディングする。
  • Hybrid Searchを適用してキーワードマッチングシグナルを補強する。

問題3:エンベディングコストが予算を超過

原因分析:セマンティックチャンキングで不必要に多くのベクトルが生成された、または高次元エンベディングを使用している場合。

解決方法

  • text-embedding-3-largeのdimensionsパラメータで3072から1024次元に削減する。MTEBスコアの低下は1-2%以内。
  • セマンティックチャンキングから再帰的分割に切り替えるとベクトル数が3-5倍減少する。
  • アクセス頻度の低い古い文書は別のコールドストレージに分離する。

問題4:ベクトル検索レイテンシがSLAを超過

原因分析:HNSWインデックスパラメータの未チューニング、ベクトル数増加に伴うメモリ不足、ディスクベース検索の発生。

解決方法

  • ef_search値を段階的に調整する(50 -> 100 -> 200)。RecallとLatencyのトレードオフを測定する。
  • ベクトルを量子化(Scalar/Product Quantization)してメモリ使用量を50-75%削減する。
  • コレクションを日付ベースでシャーディングして検索対象ベクトル数を減らす。

問題5:多言語文書での言語横断検索の失敗

原因分析:英語中心のエンベディングモデル使用時、韓国語・日本語など非英語クエリのエンベディング品質が低下する。

解決方法

  • BGE-M3(100以上の言語サポート)またはCohere embed-v4(多言語最適化)に切り替える。
  • クエリ言語と文書言語が異なる場合、クエリを文書言語に翻訳してから検索するパイプラインを追加する。

運用チェックリスト

本番RAGパイプラインのデプロイ前に必ず確認すべき項目を整理する。

チャンキング設定

  • チャンクサイズを400-512トークンに設定したか
  • オーバーラップを10-20%に設定したか
  • 2,500トークン超のチャンクがないことを確認したか
  • 文書タイプ別にチャンキング戦略を分けたか(PDF、Markdown、コードなど)
  • 空チャンク・重複チャンクのフィルタリングロジックがあるか

エンベディング

  • エンベディングモデルのMTEBスコアとコストを比較検討したか
  • 次元削減の適用可否をテストしたか(3072 -> 1024)
  • バッチエンベディング処理時のrate limitハンドリングが実装されているか
  • エンベディングモデルバージョン変更時の全再インデキシング手順が文書化されているか

ベクトルDB

  • HNSWインデックスパラメータ(M、ef_construction、ef_search)をチューニングしたか
  • ベクトル数増加に伴うメモリスケーリング計画があるか
  • バックアップ/リカバリ手順をテストしたか
  • メタデータフィルタリングインデックスを適切に設定したか

検索品質

  • 評価データセット(クエリ-正解ペア)を50件以上構築したか
  • MRR、NDCG@10、Recall@10基準の目標値を設定したか
  • A/Bテストパイプラインが構築されているか
  • 検索失敗ログを収集・分析する体制があるか

モニタリング

  • クエリあたりの検索レイテンシをp50/p95/p99でモニタリングしているか
  • エンベディングAPI呼び出し失敗率を追跡しているか
  • ベクトルDBのディスク/メモリ使用量アラートが設定されているか
  • 検索品質メトリクスを定期的に自動評価するバッチがあるか

失敗事例

事例1:セマンティックチャンキングの罠

ある企業が「精巧なチャンキングの方が当然良いはず」という想定のもと、全文書をセマンティックチャンキングで処理した。結果は以下の通りだった。

  • ベクトル数が既存の再帰的分割と比較して4.2倍に増加
  • Pineconeの月額コストが800から800から3,400に上昇
  • 平均チャンクサイズが38トークンに縮小してコンテキストが不足し、検索精度がむしろ12%低下

教訓:チャンキング戦略は必ずベンチマークに基づいて選択すべきである。「より精巧な方法 = より良い結果」という仮定は2026年のベンチマークで繰り返し反証されている。

事例2:エンベディングモデル交換時の再インデキシング漏れ

エンベディングモデルをtext-embedding-ada-002からtext-embedding-3-largeにアップグレードする際、既存ベクトルを再インデキシングしなかった事例。異なるエンベディング空間のベクトルが混在し、検索結果がほぼランダムに近くなった。

教訓:エンベディングモデル変更時は必ず全ベクトルを再生成する必要がある。ゼロダウンタイム移行のために、新しいコレクションに再インデキシングし、検証後にトラフィックを切り替えるBlue-Greenデプロイ戦略を使用する。

事例3:HNSW ef_search未設定による障害

ベクトルが100万を超えて検索レイテンシが500msを超過したが、ef_searchのデフォルト値(10)をそのまま使用していた。ef_searchを100に上げたところ、Recall@10が72%から91%に上昇しつつレイテンシは80ms程度を維持した。

教訓:HNSWパラメータチューニングはデータ規模に応じて必ず再調整する必要がある。ベクトル数が10倍増加するたびにef_searchとef_constructionを再評価する。

参考資料