6
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Amazon Bedrock Knowledge Bases × OpenSearch Serverless を使用して日本語RAGの精度を引き出す

6
Last updated at Posted at 2026-03-30

1. はじめに

RAGの導入において、「精度が期待より低い」という課題はよく起こります。
2025年以降、様々なAIサービスが発表され RAG は、手軽かつコストをかけずとも構築できるようになりました。
しかし 「動く RAG」「使えるRAG」 は別物です。
特に日本語RAGでは、英語環境で有効とされる設定をそのまま適用したとしても、期待した精度が得られないケースが少なくありません。

この記事では使えるRAGにするために重要となるパラメータチューニングについて実際に検証しながら、日本語RAG精度を引き出す設定を深堀してみたいと思います。

検証環境

本記事では、日本語RAGの実測評価が可能な環境を構築しています。

  • データセットには、日本語Wikipediaを元にした質問応答データセット MIRACL-ja を使用し、200件の評価クエリで精度を測定します
  • ベクトルDBには、マネージドサービスとして運用コストを抑えられる Amazon OpenSearch Serverless の Vector Search コレクションを採用しました
  • エンベディングモデルは、AWS Bedrock 経由でアクセス可能な Amazon Titan Embed Text V2Cohere Embed Multilingual V3 を比較対象とし、日本語対応度の違いが精度に与える影響を検証します
  • LLMについては、RAG全体の動作確認用に Amazon Nova Pro を使用していますが、本記事の評価対象は「検索精度」なのでLLMの回答品質は評価に含めません
項目 内容
データセット MIRACL-ja(日本語Wikipedia QAデータ、評価クエリ200件)
ベクトルDB Amazon OpenSearch Serverless(Vector Search コレクション)
エンベディングモデル Amazon Titan Embed Text V2 / Cohere Embed Multilingual V3
LLM Amazon Nova Pro(回答生成用)
リージョン ap-northeast-1
評価指標 Recall@5, Recall@10, MRR@10, Latency P50/P95

システムアーキテクチャ

本記事の検証で使用する RAG の全体構成です。

RAG.png

評価指標の定義

本記事では、検索精度を以下の3つの指標で評価します。いずれも情報検索の分野で標準的に使われる指標になります。

Recall@K(リコール)

「正解文書を取りこぼさずに検索できているか」を測る指標です。

  • 意味: クエリ(質問)に対して、正解となる文書が検索結果の上位K件に含まれている割合
  • : 「機械学習とは何か」という質問に対し、10件の検索結果のうち正解の文書が含まれていれば成功
  • Recall@10 = 0.956 の意味: 100件のクエリのうち、95.6件で正解文書が上位10件以内に入っている
  • RAGでの重要性: Recall が低いと、LLM に正しい情報が渡らず、回答の品質が落ちる

MRR@10(Mean Reciprocal Rank)

「正解文書がどれだけ上位に来ているか」を測る指標です。

  • 意味: 正解文書の順位の逆数を平均した値。1位なら 1/1=1.0、2位なら 1/2=0.5、10位なら 1/10=0.1
  • : あるクエリで正解が3位に出現 → スコアは 1/3 = 0.333
  • MRR@10 = 0.827 の意味: 平均すると、正解文書は1.2位程度(1÷0.827≒1.2)に出現している
  • RAGでの重要性: MRR が高いと、LLM がより確実に正解情報を参照して回答できる

Latency P50/P95(レイテンシのパーセンタイル)

「検索にかかる時間の分布」を測る指標です。

  • P50(中央値): 全クエリの50%がこの時間以内に完了する。「通常時の速度」を表す
  • P95(95パーセンタイル): 全クエリの95%がこの時間以内に完了する。「遅い場合でも」の上限を表す
  • : P50=100ms, P95=150msの場合
    • 半分のクエリは100ms以内に完了
    • 95%のクエリは150ms以内に完了
    • 残り5%は150msより遅い(バースト時・高負荷時)
  • RAGでの重要性: ユーザー体感速度やSLA(サービス品質保証)を設計する際の基準値になる

2. なぜ日本語 RAG は難しいのか

検証に入る前に、日本語における RAG のポイントについておさえておきたいと思います。

「英語の RAG はうまくいったが、日本語では精度が落ちる」という事象は珍しくありません。これは実装の問題ではなく、言語特性に起因する構造的な問題になります。
次の3つの観点から整理します。

① トークン境界問題

RAGでは、長い文書を小さな単位(チャンク)に分割してから検索します。この「分割」の処理が、英語と日本語で大きく異なります。

英語の場合:単語の切れ目がはっきりしている

英語では、スペースで単語が区切られています。そのため、「意味のある単位」と「機械が認識する単位」がほぼ一致します。

例:英語 "machine learning" → ["machine", "learning"]
    ↑ "機械"と"学習"という2つの意味単位として正しく認識される

日本語の場合:単語が細かく分解されて意味が壊れる

一方、日本語にはスペースがありません。そのため、多くのAIモデルが使う 「BPE」という文字列分割の仕組み では、単語が意味の途中で細切れにされてしまいます。

例:日本語 "機械学習" → ["機械", "##学", "##習"]
    ↑ "学習"が"##学"と"##習"に分割され、意味が壊れている

この 「##」は「前の文字の続き」を意味する記号 です。つまり、「学習」という一つの単語が「##学」「##習」とバラバラに扱われています。

なぜこれがRAGで問題になるのか

RAGでは、文書を 「512文字分」などの固定サイズで分割(Fixed-size チャンキング) します。

  • 英語の場合: 512文字分で切っても、文や単語の途中で切れる確率は低い
  • 日本語の場合: 単語どころか、意味の途中で強制的に切断されてしまう

具体例:

例えば、「機械学習アルゴリズムの性能評価」という文章を分割する場合:

  • 英語なら: "algorithm" という単語は1つの単位として扱われる
  • 日本語では: 「アルゴリ」と「ズム」が別のチャンクに分かれる可能性がある

こうなると、検索時に「アルゴリズム」を探しても、「アルゴリ」しか見つからず、正しく検索できません。

解決策は?

この問題は、第4章で説明する 「チャンキング戦略の工夫」 で部分的に緩和できます。しかし根本的には、日本語に強いエンベディングモデルを選ぶことが最も効果的 です。日本語データで学習されたモデルは、こうした分割の問題を内部的に吸収する能力が高くなっていることがポイントです。

② 形態論的多様性:BM25 側の精度低下

RAGの検索方式には、大きく2つのアプローチがあります。

  • ベクトル検索:文章の意味をベクトル(数値の羅列)で表現して類似度を計算する方法
  • キーワード検索(BM25):単語の出現頻度で関連性を計算する従来型の検索方法

ハイブリッド検索 は、この2つを組み合わせて精度を高める手法です。しかし日本語では、キーワード検索側(BM25)の精度が著しく低下する という問題があります。

その理由と解決策についても事前知識として説明します。

英語では「活用形の統一」がうまく機能する

英語には 「Stemming(語幹抽出)」 という仕組みがあります。これは「learning」「learned」「learns」のような変化形を、すべて「learn」という共通の語幹に統一する処理です。

英語の場合(Stemming が効く):
  "learning" → "learn"
  "learned"  → "learn"
  "learns"   → "learn"   ← すべて同一語幹にマッチする

このおかげで、ユーザーが「learning」で検索しても、「learned」を含む文書がヒットします。

日本語では「活用形の多様性」が問題になる

一方、日本語はさらに複雑です。

日本語の場合(活用形が多様):
  "学習する"
  "学習した"
  "学習"     ← 形態素解析なしでは、すべて別単語として扱われる
  "学習中"
  "学習できる"

英語のように単純な語幹抽出では対応できません。「学習する」と「学習した」が別の単語として扱われてしまう と、検索の取りこぼしが発生します。

解決策:日本語専用の「形態素解析器」が必要

この問題を解決するのが 「日本語アナライザ」 です。OpenSearchでは kuromoji(クロモジ) という日本語専用の解析エンジンが用意されています。

kuromojiが行うこと:

  1. 単語の切り出し:「機械学習を学習した」→ [「機械学習」「を」「学習」「し」「た」]
  2. 基本形への変換:「学習した」→「学習する」(動詞の基本形)
  3. 不要語の除去:助詞「を」「は」などを除去

この処理により、「学習する」で検索しても「学習した」「学習できる」を含む文書がヒットするようになります。

kuromojiを設定しないとどうなるか

OpenSearchのデフォルト設定では、kuromojiは 有効化されていません

その結果:

  • ハイブリッド検索を導入しても「ベクトル検索と大差なかった」
  • キーワード検索側が機能せず、むしろノイズ源になる

という事態が発生します。本記事ではアナライザ設定の詳細も第5章で扱います。

③ エンベディング空間での日本語表現精度

「エンベディング空間」とは何か

ベクトル検索では、文章を 「エンベディング(埋め込み)」 という処理で数値の列(ベクトル)に変換します。

例:
「機械学習とは何か」 → [0.23, -0.45, 0.78, ..., 0.12] (1024個の数値)
「AIの基礎知識」     → [0.19, -0.52, 0.81, ..., 0.09]

これらのベクトルが存在する仮想的な空間を 「エンベディング空間(ベクトル空間)」 と呼びます。意味が似ている文章は、この空間内で近い位置に配置されます。

英語モデルに日本語を入力すると何が起きるか

英語で事前学習されたモデル に日本語を入力すると、日本語特有の意味の違いを正しく捉えられない という問題が発生します。

つまり、本来は意味が違う文章が「似ている」と判定されたり、本来は似ている文章が「違う」と判定される ことになります。

実際に問題となるケース

日本語RAGで頻繁に遭遇する3つの典型的な失敗パターンです。

① 技術用語の英字混在

クエリ:「API の timeout エラーが発生した場合の対処法」

期待される検索結果:
  ✓ タイムアウトエラーの原因解説
  ✓ API設定のトラブルシューティング

実際の検索結果(英語モデルの場合):
  ✗ 「timeout」という英単語を含む無関係な文書が上位に来る
  ✗ 日本語部分の「対処法」の重みが相対的に低くなる

英語モデルは timeout という英単語を過度に重視してしまいます。

② 同音異義語の文脈依存

クエリ:「機関」

期待される検索結果(文脈:金融文書):
  ✓ 金融機関の役割
  ✓ 中央銀行という機関

実際の検索結果:
  ✗ 「機関車」に関する文書が混入
  ✗ 「政府機関」の文書が混入

日本語では、同じ「機関」でも文脈によって全く意味が異なります。日本語に最適化されていないモデルは、この区別が苦手です。

③ 敬語・文体の違い

文A:「ご確認していただく」
文B:「確認する」

期待:意味はほぼ同じなので、類似度が高いはず
実際:モデルによっては類似度が低く算出される

ビジネス文書では敬語と通常表現が混在しますが、英語モデルではこの違いを吸収できません。


①~③の要素が日本語RAGの精度において重要な観点になります。
これらの問題がどのモデルでどの程度発生するかを、実測データをもとに検証します。

日本語RAGにおいては、エンベディングモデルの選択がチューニング変数の中で最も精度に影響する というのが本記事の核心的な内容になります。

3. エンベディングモデル比較:日本語精度の実測

前置きが長くなりましたが、エンベディングモデル選択による日本語RAG精度への検証に入ります。

他のパラメータ(ef_search、チャンキングなど)を細かく調整するより、最初にモデル選択を正しく行う方が、遥かに大きな精度改善 につながります。

3-1. 比較対象モデルと特性整理

どのモデルを比較するか

AWS Bedrock経由で使える 日本語に対応したエンベディングモデル を比較します。

比較する2つのモデル:

  • Amazon Titan Embed Text V2: AWSが開発した第2世代エンベディングモデル。「V2」は単にバージョン番号(第2世代)を意味します。英語を中心に学習されていますが、日本語にも対応。
  • Cohere Embed Multilingual V3: Cohere社が開発した第3世代の多言語特化モデル。「V3」は第3世代を意味し、100以上の言語に対応するよう設計されています。

注意: V2とV3は異なる会社の異なる世代のモデルなので、「V3の方が新しい」という単純比較はできません。

モデル 次元数 最大入力トークン 多言語対応 コスト ($/1M tokens)
Amazon Titan Embed Text V2 256 / 512 / 1024 8,192 限定的(英語中心、日本語対応) $0.02
Cohere Embed Multilingual V3 1,024 512 広範囲(日本語含む100言語以上) $0.10

Titan V2の重要な2つの設定

① 次元数の選択(256 / 512 / 1024)

次元数が大きい:
  → 精度は高くなる(はず)
  → ストレージコストが増える
  → 検索時間が長くなる

次元数が小さい:
  → 精度が下がる(はず)
  → ストレージコストが減る
  → 検索時間が短くなる

normalize オプション(true / false)

normalize = true  : ベクトルを正規化する(コサイン類似度で必須)
normalize = false : 正規化しない(デフォルト、しかし精度が大幅低下)

3-2. 実測結果(MIRACL-ja)

では検証を行います。
以下の、条件で2つのモデルに対して (Titan V2 は normalize 設定の有無、各次元数ごと) 計測を行いました。

実験条件

  • データセット: MIRACL-ja(11,665文書、200クエリ)
  • 検索設定: 上位10件取得、コサイン類似度
  • 評価指標: Recall@10(正解が10件以内に含まれるか)

実測データ

モデル 次元 Recall@5 Recall@10 MRR@10 P50 (ms) P95 (ms) $/1M tokens
Titan V2 (normalize=false) 1024 0.730 0.816 0.736 114.4 132.6 $0.02
Titan V2 (normalize=true) 1024 0.812 0.925 0.807 117.4 134.7 $0.02
Titan V2 (normalize=true) 512 0.839 0.951 0.818 109.2 126.6 $0.02
Titan V2 (normalize=true) 256 0.848 0.956 0.827 126.4 156.7 $0.02
Cohere Multilingual V3 1024 0.655 0.700 0.641 93.5 135.7 $0.10

実測結果からわかった4つのこと

normalize=true の効果が決定的

normalize = false → Recall@10 = 81.6%
normalize = true  → Recall@10 = 92.5%

差分:+10.9ポイント!

たった1行の設定で、精度が10%向上しました。
コサイン類似度インデックスでは normalize=true の設定が結果に大きく影響していることがわかります。

② 次元数を下げると精度が上がる(日本語特有の現象?)

1024次元 → Recall@10 = 92.5%
512次元  → Recall@10 = 95.1%  (+2.6pt)
256次元  → Recall@10 = 95.6%  (最高)

通常、次元数が高いほど精度が上がるはずですが、日本語では逆の結果になりました。

可能性のある理由としては、以下が考えられると思います。
やはり、実測からチューニングを行うことは大事であると再認識できます。

  • 1024次元は英語データでの学習に最適化されている
  • 日本語では過剰な次元数が「ノイズ」として働く
  • 256次元の方がコンパクトで本質的な意味を捉えやすい

③ Cohereは意外に低精度

Cohere Multilingual V3  → Recall@10 = 70.0%
Titan V2 (256次元)      → Recall@10 = 95.6%

差分:25.6ポイント!

多言語モデルなのに、なぜTitanより低いのか?

こちらも予想とは少し違った結果になりました。
考えられる原因:

  1. 入力長制限: Cohereは512トークンが上限。長い文書が切り詰められて情報が損失された
  2. モデルの特性: 100言語対応のため、日本語専用の最適化が不十分

④ コスト対効果でTitanが優秀という結果に

Titan V2 (256次元):
  精度: 95.6%
  コスト: $0.02/1M tokens  (最安)

 Cohere:
  精度: 70.0%
  コスト: $0.10/1M tokens  (5倍高い)

3-3. エンベディングモデル比較の結論

今回の検証だけでは一概には言い切れませんが日本語RAGでは、ほぼ全てのケースで Titan V2 が優秀という結果になりました。

■ 推奨構成 (迷ったらこれ):
  モデル: Amazon Titan Embed Text V2
  設定:   normalize=true, dimension=256
  理由:   最高精度(95.6%), 最低コスト, 十分な速度

■ 代替構成 (最速を優先):
  モデル: Amazon Titan Embed Text V2
  設定:   normalize=true, dimension=512
  理由:   精度も高い(95.1%), P50=109msで最速

Cohereを選ぶべきケース:

  • 今回の実測では見つからなかった
  • 他のデータセットでは異なる結果になる可能性あり
  • 必ずA/Bテストで検証 してから本番投入を推奨

3-4. 補足:normalize オプションの設定について

今回の検証で結果に大きく影響があった normalize オプションですが、Bedrock Knowledge Bases の管理画面から Knowledge Base を作成する場合、normalize オプションは設定画面に表示されません

そのため、今回の検証では IaC による設定を行っています。

カスタムインデックスをIaCで作成

IaC(Terraform、CloudFormation、CDK等)でOpenSearchインデックスを事前作成し、Bedrock KBに紐付けます。

ステップ1: IaCでOpenSearchインデックス作成

Terraformの例:

# OpenSearch Serverless コレクション作成
resource "aws_opensearchserverless_collection" "rag_collection" {
  name = "rag-vector-collection"
  type = "VECTORSEARCH"
  # ... その他の設定
}

# Python等でインデックス作成(Terraform local-execやLambdaで実行)
resource "null_resource" "create_index" {
  provisioner "local-exec" {
    command = "python create_index.py"
  }
  depends_on = [aws_opensearchserverless_collection.rag_collection]
}

create_index.py の内容:

from opensearchpy import OpenSearch, RequestsHttpConnection
from requests_aws4auth import AWS4Auth
import boto3

# OpenSearchクライアント作成
host = "your-collection-endpoint.region.aoss.amazonaws.com"
service = 'aoss'
credentials = boto3.Session().get_credentials()
awsauth = AWS4Auth(credentials.access_key,
                   credentials.secret_key,
                   'ap-northeast-1', service,
                   session_token=credentials.token)

client = OpenSearch(
    hosts=[{'host': host, 'port': 443}],
    http_auth=awsauth,
    use_ssl=True,
    verify_certs=True,
    connection_class=RequestsHttpConnection
)

# インデックス作成
index_body = {
    "settings": {
        "index.knn": True
    },
    "mappings": {
        "properties": {
            "embedding": {
                "type": "knn_vector",
                "dimension": 256,  # Titan V2の次元数
                "method": {
                    "name": "hnsw",
                    "space_type": "cosinesimil",  # ← ここがポイント
                    "engine": "faiss"
                }
            },
            "text": {"type": "text"},
            "metadata": {"type": "text"}
        }
    }
}

client.indices.create(index="bedrock-kb-index", body=index_body)

ステップ2: エンベディング生成時にnormalize=true指定

import boto3
import json

bedrock_runtime = boto3.client('bedrock-runtime')

def embed_text(text):
    response = bedrock_runtime.invoke_model(
        modelId='amazon.titan-embed-text-v2:0',
        body=json.dumps({
            "inputText": text,
            "dimensions": 256,
            "normalize": True  # ← ここが重要!
        })
    )

    return json.loads(response['body'].read())['embedding']

ステップ3: Bedrock Knowledge Basesでカスタムインデックスを指定

Bedrock console → Create Knowledge Base
  → Vector store: Existing OpenSearch Serverless collection
  → Collection ARN: [作成したコレクション]
  → Index name: bedrock-kb-index
  → Field mapping: embedding, text, metadata

4. チャンキング戦略の比較と選択基準

第4章では、チャンキング戦略の検証比較になります。
チャンキングは「どの単位でドキュメントを分割してベクトル化するか」の設計です。
チャンクが大きすぎると検索精度が落ち、小さすぎると文脈が失われるというトレードオフがあります。

4-1. Bedrock Knowledge Bases が提供するチャンキング戦略

戦略 概要 主なパラメータ
Fixed-size 固定トークン数で分割。シンプルで高速 チャンクサイズ, オーバーラップ率
Semantic 意味的なまとまりを検出して分割。文脈保持性が高い バッファサイズ, ブレークポイント閾値
Hierarchical Parent チャンク(大)と Child チャンク(小)の2層構造 Parentサイズ, Childサイズ, オーバーラップ
Custom Lambda Lambda 関数で完全カスタム 任意

4-2. 各戦略の動作と日本語への影響

Fixed-size チャンキング

入力: "機械学習は統計的手法を用いてコンピュータがデータから学習する技術である。..."
設定: chunkSize=256, overlapPercentage=10

→ 256トークンごとに切断し、前後 25トークン分を重複させて次チャンクに含める

日本語の場合、文境界を無視して切断されるリスクがあります。ただし、オーバーラップを 10〜20% 設定することで文脈の断絶を緩和できます。

Semantic チャンキング

文章の意味的なまとまり(トピックの転換点)を検出して分割します。技術文書、議事録、FAQ のような構造化されたドキュメントで特に効果を発揮します。

# Bedrock Knowledge Bases での設定例
chunking_configuration = {
    "chunkingStrategy": "SEMANTIC",
    "semanticChunkingConfiguration": {
        "maxTokens": 300,          # 1チャンクの最大トークン数
        "bufferSize": 1,           # 前後に含める文の数(文脈ウィンドウ)
        "breakpointPercentileThreshold": 95  # 意味的分割を判定する閾値
    }
}

breakpointPercentileThreshold は「どこで分割するか」の感度を決めます。高い値(95)にすると分割が少なくなり(大きめのチャンク)、低い値(80)にすると細かく分割されます。

Hierarchical チャンキング(最も複雑・最も強力)

Parent チャンク(大): 1500トークン ─ LLM に渡す文脈として使用
  └─ Child チャンク(小): 300トークン ─ 検索用ベクトルとして使用

検索フロー:
  1. クエリベクトル ↔ Child チャンクベクトルで類似検索
  2. ヒットした Child の Parent チャンクを取得
  3. Parent チャンク(文脈豊富)を LLM に渡す

これにより「細粒度で検索、粗粒度で回答」という精度と文脈品質の両立が可能になります。

chunking_configuration = {
    "chunkingStrategy": "HIERARCHICAL",
    "hierarchicalChunkingConfiguration": {
        "levelConfigurations": [
            {"maxTokens": 1500},  # Parent
            {"maxTokens": 300}    # Child
        ],
        "overlapTokens": 60
    }
}

4-3. チャンキング戦略の比較実測

では検証を行います。
なお、今回の実測では記事の文量を考慮してチャンキング戦略の深掘りは優先度を下げ、Fixed-size チャンキングのみ を対象としています。

条件: Titan V2 (normalize=true, 1024dim), ef_search=32, MIRACL-ja データセット

戦略 チャンクサイズ オーバーラップ Recall@10 MRR@10 Latency P50
Fixed-size 256 tokens 0% 0.953 0.837 116.9ms
Fixed-size 512 tokens 0% 0.884 0.784 115.7ms
Fixed-size 512 tokens 10% 0.931 0.813 114.3ms
Fixed-size 512 tokens 20% 0.910 0.823 114.1ms

補足でオーバーラップに絞った検証も実施しています

オーバーラップの効果(実測・512トークン):

オーバーラップ率 Recall@10 インデックスサイズ コスト増加
0% 0.884 ベースライン 0%
10% 0.931 ×1.1 +10%
20% 0.910 ×1.2 +20%

また、上記2つの検証を踏まえて 256 tokens & 10% overlap も追加検証してみました。
しかし、結果は 256 tokens + 0% overlap より 2.8 ポイント下げる結果となりました。

戦略 チャンクサイズ オーバーラップ Recall@10 MRR@10 Latency P50
Fixed-size 256 tokens 10% 0.925 0.812 116.4ms

実測結果の考察:

  1. 256トークン + overlap 0% が最高精度: Recall@10 = 95.3%、MRR@10 = 0.837で最優秀。小さいチャンクの方が検索精度は高い。
  2. 256トークンでは overlap は逆効果: overlap 10%を追加すると92.5%に低下(-2.8ポイント)。
  3. 512トークンは精度低下: overlap なしでは 88.4% に低下(-6.9ポイント)。チャンクが大きすぎると意味的な焦点がぼやける。
  4. 512トークンでは overlap 10%で改善: 93.1% まで回復(+4.7ポイント)。大きめのチャンクでは文脈の断絶を緩和する効果あり。
  5. overlap 20%は過剰: 10%から精度が下がる(91.0%)。重複が多すぎると同じ文書が複数回ヒットしてノイズになる。
  6. 推奨値: 256トークン, overlap 0% が精度・レイテンシのバランスで最良。チャンクサイズが小さければ overlap は不要。

4-4. チャンキング戦略の選択フロー

チャンキング戦略の選択フローをまとめました。

一般的な長文ドキュメント(技術文書、マニュアル等)では Semantic/Hierarchical が有効なケースもあるため、参考として見ていただければと思います。

ドキュメントの構造は?
 │
 ├─ 構造が明確(見出し・章立てあり)
 │    → Hierarchical を検討(精度最高、コスト中)
 │
 ├─ 意味的なまとまりがある(会話ログ・議事録・レポート)
 │    → Semantic を推奨(コスト・精度バランスが良い)
 │
 └─ 構造がない or 短文の羅列(FAQ・仕様書の箇条書き)
      → Fixed-size (256〜512 トークン, overlap 10〜20%) で十分

コストの許容度は?
 │
 ├─ 厳しい → Fixed-size (256トークン, overlap 0%)
 └─ 余裕あり → Hierarchical(精度向上に対してコスト増加が合理的)

5. ハイブリッド検索の実装と日本語での効果

3章・4章では Bedrock Knowledge Bases が提供する機能(エンベディングモデル、チャンキング戦略)を扱ってきました。5章では、検索実行時の OpenSearch 側の機能であるハイブリッド検索に焦点を移します。

ハイブリッド検索は、意味的類似性を捉えるベクトル検索と、キーワードの完全一致に強いBM25を組み合わせる手法です。

5-1. ベクトル検索とBM25キーワード検索の違い

2つの検索方法の得意・不得意

RAGには主に2つの検索アプローチがあります。ハイブリッド検索 は、この2つを組み合わせて精度を高める手法です。

特性 ベクトル検索 BM25 キーワード検索
意味の類似性 ◎ 得意 ✗ 苦手
完全一致(製品コード等) △ 不安定 ◎ 得意
日本語の活用形 △ モデル次第 ✗ アナライザ次第
未知語・固有名詞 ✗ 苦手 ◎ 得意
計算コスト

具体例で理解する:

クエリ: 「製品ABC-123の不具合対応」

ベクトル検索:
  → 「不具合」「対応」の意味から、関連する修理手順書を発見◎
  → でも「ABC-123」の完全一致は不安定△

BM25キーワード検索:
  → 「ABC-123」という製品コードを正確に発見◎
  → でも「不具合対応」と「トラブル解決」が同じ意味だと気づかない✗

ハイブリッド検索の狙いは、両方の強みを組み合わせ「意味も捉えつつ、固有名詞も正確に拾う」検索が可能となります。

5-2. RRF(Reciprocal Rank Fusion):ランキングの統合方法

RRFとは「順位」で統合するアルゴリズム

OpenSearchのハイブリッド検索では、ベクトル検索とBM25の結果を 「スコアではなく順位」 で統合します。これが RRF(Reciprocal Rank Fusion) です。

RRFの計算式:

RRF スコア = Σ( 1 / (k + rank_i) )

k      : ランク補正定数(通常60)
rank_i : 各検索手法での順位

具体例でRRFを理解する

ドキュメントXの場合:

ベクトル検索での順位: 3位
  → RRFスコア = 1 / (60 + 3) = 0.0159

BM25検索での順位: 1位
  → RRFスコア = 1 / (60 + 1) = 0.0164

合計RRFスコア = 0.0159 + 0.0164 = 0.0323

ドキュメントYの場合:

ベクトル検索での順位: 1位
  → RRFスコア = 1 / (60 + 1) = 0.0164

BM25検索での順位: 20位
  → RRFスコア = 1 / (60 + 20) = 0.0125

合計RRFスコア = 0.0164 + 0.0125 = 0.0289

結果: X(0.0323)> Y(0.0289)なので、Xが上位に来ます。

なぜ「k=60」なのか

この値はFacebook Researchが大規模実験で見つけた経験値です。

k が小さい(例:k=10)
  → 上位の順位の影響が強すぎる
  → 片方だけ強い検索手法に引っ張られる

k が大きい(例:k=100)
  → すべての順位の影響が均等化されすぎる
  → 上位と下位の区別がつきにくくなる

k=60
  → バランスが良い(実測で検証済み)

5-3. 日本語対応に必須:Kuromoji アナライザの設定

OpenSearch で日本語の BM25 精度を出すには、Kuromoji アナライザを明示的に設定する必要があります。Bedrock Knowledge Bases のマネージドインデックスでは自動設定されますが、カスタムインデックスでは以下の設定が必要です。

{
  "settings": {
    "analysis": {
      "analyzer": {
        "japanese_analyzer": {
          "type": "custom",
          "tokenizer": "kuromoji_tokenizer",
          "filter": [
            "kuromoji_baseform", // 基本形への変換(学習した  学習する)
            "kuromoji_part_of_speech", // 品詞フィルタ
            "kuromoji_stemmer", // 長音の正規化(コンピューター  コンピュータ)
            "lowercase"
          ]
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "text_field": {
        "type": "text",
        "analyzer": "japanese_analyzer"
      }
    }
  }
}

kuromoji_baseform フィルタが活用形の統一を行います。これにより「学習した」「学習する」「学習」が同一の基本形にマッピングされ、BM25 の再現率が向上します。

5-4. 実測結果

検証の背景

日本語RAGにおけるハイブリッド検索の効果を検証するため、段階的に各検索方式の精度を測定しました。

検証条件:

  • データセット: MIRACL-ja(11,665文書、200クエリ)
  • モデル: Titan V2 (normalize=true, 256dim)
  • 設定: ef_search=32、Kuromojiアナライザ有効

ステップ1: 初期検証(単体性能の測定)

まず、各検索方式を個別に評価し、ベースラインを確立しました。

ベクトル検索のみ

ベクトル検索単体では、95.6%の正解文書を上位10件に含めることができました
これは日本語RAGとして十分に実用的な精度だと思います。

検索方式 Recall@10 MRR@10 Latency P50
ベクトル検索のみ 95.6% 82.7% 107.0ms

BM25単体(Kuromoji有効)

続いてBM25単体は、Kuromojiアナライザを設定することで一定の精度を出しましたがベクトル検索には及びませんでした。

検索方式 Recall@10 MRR@10 Latency P50
BM25のみ(Kuromoji有効) 75.5% 53.4% 46.7ms

サンプルクエリの結果:

  • 「機械学習」→「筑北村立聖南中学校」が1位(❌ 不適切)
  • 「人工知能」→「ジョシュア・レーダーバーグ」が1位(✅ 適切)
  • 「深層学習」→「筑北村立聖南中学校」が1位(❌ 不適切)

BM25単体では一部で無関係な文書が上位に来るケースがありました。

ステップ2: ハイブリッド検索(RRF統合)

次に、ベクトル検索とBM25をRRF(Reciprocal Rank Fusion)で統合する hybrid クエリを検証しました。
結果は、ベクトル検索のみと比べ -54.4 ポイントの低下となりました。

検索方式 Recall@10 MRR@10 Latency P50
ハイブリッド検索(RRF統合) 41.2% 35.4% 139.9ms

異常動作の検出:

サンプルクエリ「機械学習」でのハイブリッド検索結果を詳しく調べたところ、以下の異常な数値がありました

1位: docid=1075973#2, score=-9,549,512,000  ← 異常な負の値(約-95億)
2位: docid=1075973#2, score=-4,422,440,400  ← 同じIDが重複
3位: docid=1075973#2, score=8.818038        ← 同じIDが3回目
4位: docid=1077693#24, score=8.212091
5位: docid=1075014#10, score=7.500025

問題点:

  • ⚠️ RRFスコアが極端な負の値(-95億)になる
  • ⚠️ 同一docidが複数回出現(重複)
  • ⚠️ 検索精度が壊滅的に低下(41.2%)

これは OpenSearch Serverlessのhybridクエリ実装に問題がある 可能性があります。

ステップ3: 代替実装(bool should)

hybrid クエリの問題を回避するため、bool should による代替実装を検証しました。

# bool should(代替実装)
"query": {
    "bool": {
        "should": [
            {"match": {"text": query_text}},
            {"knn": {"embedding": {"vector": vector, "k": 10}}}
        ]
    }
}

ベクトル検索のみには及ばないですが、BM25のみと比べるとわずかに精度が上がりました。
また、異常なスコアやdocid重複は発生せず、正常に動作することも確認できました。

検索方式 Recall@10 MRR@10 Latency P50
ハイブリッド検索(bool should 統合) 79.4% 54.9% 115.4ms
  • BM25単体(75.5%)より+3.9ポイント改善
  • ベクトル単体(95.6%)より-16.2ポイント低下
  • RRF(Reciprocal Rank Fusion)ではなく単純なスコア加算

検証結果まとめ

全検索方式を比較した結果:

検索方式 Recall@10 MRR@10 Latency P50 スコア異常 docid重複 判定
ベクトル検索のみ 95.6% 82.7% 107.0ms なし なし ✅ 最優秀
bool should 79.4% 54.9% 115.4ms なし なし ✅ 使用可能
BM25単体 75.5% 53.4% 46.7ms なし なし △ 改善も不十分
hybrid (RRF) 41.2% 35.4% 139.9ms 検出 検出 ❌ 実装問題

今回の検証により、2026/3/30 時点では hybrid クエリにおける RRF 統合には実装上の問題がある可能性がわかりました。
そのため、ハイブリッド検索においては以下の考え方が良いかもしれません。

推奨事項

✅ 推奨構成:

  1. ベクトル検索のみで十分

    • Recall@10 = 95.6%の高精度を達成
    • シンプルで安定
    • ハイブリッド化の必要性は低い
  2. どうしてもBM25を併用したい場合

    • bool shouldを使用(RRFではないが動作は正常)
    • Kuromojiアナライザの設定は必須
    • Recall@10 = 79.4%程度を期待
  3. 避けるべき実装

    • OpenSearch Serverlessのhybridクエリ(実装問題あり)
    • Kuromoji未設定でのBM25(精度が壊滅的)

⚠️ 注意事項:

  • 英語環境で有効な手法が日本語で逆効果になることがある
  • 導入前に必ずA/Bテストで検証
  • セルフマネージドOpenSearchなら問題回避できる可能性あり

5-5. 追記:異常動作の原因と正しい実装方法(2026年3月31日)

5-4 で記載した hybrid クエリの異常動作について追加の調査をしたところ、主な原因として normalization-processor を持つ search pipeline の欠如があげられそうでした。

これは、OpenSearch公式フォーラムでも報告されており、本記事で観測した数値(-9,549,512,000)も一致してました(参考スレッド)。

どうやら、OpenSearch の hybrid 検索は、設計上 search pipeline(正規化処理)とセットで使用することが必須であり、パイプラインなしで実行すると、faiss エンジンの生スコア(負値を含む)がそのまま返され、本記事で観測したような異常が発生するとのことです。

  • RRFスコアが極端な負の値(-95億)になる
  • 同一docidが複数回出現する
  • 検索精度が壊滅的に低下する

ただ以下の理由から、この記事でのベクトル検索のみを推奨するという意見は変わりません

  1. 実測で最高精度: ベクトル検索のみで Recall@10=95.6% を達成しており、これは本記事で検証したすべての手法の中で最高値
  2. シンプルさ: search pipeline の設定が不要で、実装・運用が容易
  3. 安定性: 新機能(2025年8月リリース)であるハイブリッド検索より、成熟したベクトル検索の方が安定

6. OpenSearch Serverless の HNSW インデックス Deep Dive

Amazon OpenSearch Serverless の Vector Search コレクションは、内部的に HNSW(Hierarchical Navigable Small World) アルゴリズムでベクトルインデックスを構築しています。

6章では、この HNSW のパラメータチューニングに対して検証を行います。

6-1. HNSW とは何か:グラフ構造の直感的理解

検証の前にまずは、この HNSW アルゴリズムの動作について説明します。

HNSWの役割:大量のベクトルから「似ているもの」を高速に見つける

ベクトル検索では、クエリを受け取ったとき、データベース内の数万〜数百万のベクトルの中から 「最も似ているベクトル」 を見つける必要があります。

素朴な方法(全件スキャン):

  • すべてのベクトルと1つずつ比較する
  • 100万件のベクトルがあれば、100万回の計算が必要
  • 遅い → 実用的でない

HNSWの方法(階層的な高速検索):

  • 「階層構造のショートカット」を使って、効率的に絞り込む
  • 100万件でも数百回程度の計算で済む
  • 速い → RAGに実用的

HNSW は 「Hierarchical Navigable Small World」 の略で、階層的な地図を使って効率よく目的地にたどり着く仕組み と考えてください。

階層構造のイメージ:「広域地図」から「詳細地図」へ

HNSWは、「粗い地図」から「詳細な地図」へと段階的に絞り込んでいく仕組みです。

【階層構造のイメージ】

Layer 2(広域地図):  ●─────────────●
                      ↓ 大まかに「この辺」と絞り込む

Layer 1(中域地図):  ●──●──────●──●
                      ↓ さらに「この近く」に絞り込む

Layer 0(詳細地図):  ●─●─●──●─●─●─●─●
                      ↑ 最終的に「最も近いもの」を発見

検索の流れ:

  1. Layer 2(広域): まず大ざっぱに「東京エリア」のように範囲を絞る
  2. Layer 1(中域): 次に「渋谷区」のように範囲を絞る
  3. Layer 0(詳細): 最後に「この建物」のように正確に特定する

この階層的アプローチにより、全件を調べる必要がなく、10万件のデータでも数十〜数百回の計算で済みます

「近似」である点が重要

HNSWは 「近似アルゴリズム」 です。つまり、100%正確な最近傍を保証するわけではなく、「ほぼ正確」な結果を高速に返します

具体例:

100個のベクトルから「最も近い10個」を探す場合:

完全一致アルゴリズム
  → 本当に最も近い10個を返す(遅い・確実)

HNSW(近似)
  → 95%の確率で正しい10個を返す(速い・ほぼ確実)
  → 5%の確率で「11位〜15位くらいのもの」が混入する

RAGの実用では、この5%の誤差は許容できる範囲 です。検索速度が10倍以上速くなるメリットの方が大きいからです。

パラメータで「精度」と「速度」を調整できる

HNSWでは、設定によって「より正確に探す(遅い)」か「より速く探す(やや不正確)」かを選べます。これが後で説明する ef_search パラメータの役割です。

6-2. インデックス構築パラメータ

HNSWのインデックスを作成する際、2つの重要な設定値 があります。これらは 「一度作ったら変更できない」 ため、初期設計で慎重に決める必要があります。

ただし、Amazon OpenSearch Serverless では、これらの値はAWSが自動で管理しているため、ユーザーが変更できません。そのため、この説明は「内部で何が起きているか」の理解として読んでください。

パラメータ① m(各ポイントの接続数)

m は「各ベクトルが何個の他のベクトルと繋がるか」を決める値 です。

日常的な例え:道路網の密度

m = 8  : 各交差点から8本の道が出ている(道が少ない)
           → 目的地まで遠回りになりやすい(精度△)
           → インフラ整備が安い(メモリ消費少)

m = 16 : 各交差点から16本の道が出ている(標準)
           → そこそこ効率的にたどり着ける(精度〇)
           → 標準的なコスト(デフォルト)

m = 32 : 各交差点から32本の道が出ている(道が多い)
           → 目的地まで最短ルートが見つかりやすい(精度◎)
           → インフラ整備が高い(メモリ消費が約2倍)

OpenSearch での設定例:

{
  "type": "knn_vector",
  "dimension": 1024,
  "method": {
    "name": "hnsw",
    "space_type": "cosinesimil",
    "parameters": {
      "m": 16, //  この値を調整
      "ef_construction": 512
    }
  }
}

m値の影響:

m値 検索精度 メモリ消費 インデックス構築時間
8 速い
16 中(デフォルト)
32 約2倍 遅い
64 最高 約4倍 非常に遅い

実用的な考え方:

  • m値を大きくすると、検索精度は上がるが、メモリ消費がほぼ比例して増える
  • 100万件のドキュメント(1024次元)で m=16 と m=32 を比較すると、メモリ差は数十GB
  • コスト制約がある場合は m=16、精度最優先なら m=32 が目安

パラメータ② ef_construction(構築時の探索範囲)

ef_construction は「インデックスを作るときに、どれだけ丁寧に最適なつながりを探すか」を決める値 です。

日常的な例え:道路建設の品質基準

ef_construction = 128
  → 「とりあえず近くの交差点8箇所だけ調べて道を作る」
  → 建設は速いが、最適なルートではない可能性がある
  → 後で検索するときに精度が下がる

ef_construction = 512(デフォルト)
  → 「周辺の交差点50箇所を比較して最適な道を作る」
  → 建設に時間はかかるが、バランスが良い

ef_construction = 1024
  → 「周辺の交差点100箇所以上を徹底的に比較して道を作る」
  → 建設時間は2倍かかるが、最高品質のネットワークができる

設定の目安:

ef_construction インデックス品質 構築時間 用途
128 やや低 速い 開発・テスト環境
512 標準 一般的な本番環境
1024 高品質 約2倍 高精度が求められる用途

6-3. 検索時パラメータ:ef_search(最重要チューニング変数)

ef_search は、検索時に「どれだけ丁寧に探すか」を決める設定になります。
ef_search は、インデックス作成後でも自由に変更できる唯一のチューニングパラメータ です。
また、OpenSearch Serverless でもユーザー側での変更が可能です

これが最も重要な理由は:

  • mef_construction一度作ったら変更不可(再構築が必要)
  • ef_search検索のたびに指定できる(いつでも調整可能)
  • 精度とレイテンシのバランスを リアルタイムで調整 できる

日常的な例え:地図検索の精度設定

ef_search = 16  : 「ざっくり近くを探す」
                   → 速いが、たまに最適解を見逃す

ef_search = 32  : 「しっかり周辺を探す」
                   → 速度と精度のバランスが良い(推奨)

ef_search = 128 : 「徹底的に探す」
                   → 確実だが時間がかかる
# Bedrock Knowledge Bases 経由の検索では直接指定できないが、
# OpenSearch API を直接叩く場合は以下のように指定
body = {
    "query": {
        "knn": {
            "embedding_field": {
                "vector": query_vector,
                "k": 10,
                "method_parameters": {
                    "ef_search": 64  # ここがチューニングポイント
                }
            }
        }
    }
}

実測結果(MIRACL-ja, 文書数:11,665件, Titan V2 256dim)

ef_search Recall@10 MRR@10 Latency P50 Latency P95
16 0.947 0.819 132.6ms 165.1ms
32 0.956 0.827 101.4ms 142.5ms
64 0.956 0.827 98.8ms 143.5ms
128 0.956 0.827 98.8ms 123.4ms
256 0.956 0.827 98.4ms 154.4ms

実測結果からわかること

① ef_search=32で精度が頭打ちになる

ef_search=16  → Recall@10 = 94.7%  (やや低い)
ef_search=32  → Recall@10 = 95.6%  (十分高い)
ef_search=64  → Recall@10 = 95.6%  (変わらない)
ef_search=256 → Recall@10 = 95.6%  (変わらない)

32以降はいくら上げても精度が変わらない。これはデータセットの規模(11,665件)では、32で十分な探索範囲をカバーできているため。

② ef_search=32が最もコスパが良い

  • 最高精度(95.6%)に到達
  • レイテンシも速い(P50=101.4ms)
  • これ以上上げても精度向上なし

③ 過剰な ef_search は逆効果

  • 64以上に上げても精度は変わらない
  • レイテンシのバラツキ(P95)が大きくなる可能性
  • 計算コストだけが増える

7. 精度・コスト・レイテンシのトレードオフ総合設計

ここまで各コンポーネントを個別に見てきました。このセクションでは全要素を統合し、ユースケース別の推奨構成と、その根拠となる設計判断フレームワークを示します。

7-1. チューニング変数の精度寄与度(重要度順)

実測結果を踏まえた精度寄与度の整理:

チューニング変数 精度への寄与 実測での効果 理由
① エンベディングモデル選択 最大 Cohere 70% vs Titan 96% 日本語対応度で26ポイント差
② normalize オプション false 82% vs true 96% コサイン類似度で+14ポイント
③ チャンキング戦略 256tok 95% vs 512tok 88% 小さいチャンクで+7ポイント
④ 次元数 1024次元 93% vs 256次元 96% 低次元で過学習回避
⑤ ef_search 16: 95% vs 32+: 96% 32以降は変化なし
⑥ ハイブリッド検索 負の効果 Vector 96% vs Hybrid 44% 日本語では逆効果
⑤ チャンクオーバーラップ 小〜中 10〜20% で限界収益が逓減
m パラメータ(Serverless では変更不可) セルフマネージドの場合のみ調整可能

7-2. ユースケース別推奨構成(実測データに基づく)

構成A:最高精度(品質最優先)

エンベディング: Amazon Titan Embed V2 (normalize=true, 256次元)
チャンキング: Fixed-size (256トークン, overlap 0%)
検索方式: ベクトルのみ
ef_search: 32
実測Recall@10: 95.6%
実測MRR@10: 0.839
レイテンシP50: 126.4ms(256次元ベース)

選択理由: 実測で最も高い精度を達成した構成。256トークンの小さいチャンクは検索の焦点を絞り、overlap なしでも十分な精度を実現。実測で overlap 10% を追加すると逆に精度が下がる(92.5%)ことが確認されており、小さいチャンクでは重複がノイズになる。シンプルで最高精度を実現できるため、高精度が求められる社内ナレッジベース、FAQ、技術サポートに最適。

構成B:速度重視(低レイテンシ要求)

エンベディング: Amazon Titan Embed V2 (normalize=true, 512次元)
チャンキング: Fixed-size (256トークン, overlap 0%)
検索方式: ベクトルのみ
ef_search: 32
実測Recall@10: 95.1%
実測MRR@10: 0.818
レイテンシP50: 109.2ms

選択理由: 精度を維持しながら最速のレイテンシを実現(P50=109ms)。512次元は256次元より若干速く、精度もわずか0.5ポイント差。256トークンの小さいチャンクでは overlap 0% が最適(実測で overlap 10% は逆効果を確認)。レスポンス時間がSLAで重要なチャットボット、リアルタイム検索に最適。

構成C:コストバランス型(中規模・汎用)

エンベディング: Amazon Titan Embed V2 (normalize=true, 1024次元)
チャンキング: Fixed-size (512トークン, overlap 10%)
検索方式: ベクトルのみ
ef_search: 32
実測Recall@10: 93.1%
実測MRR@10: 0.813
レイテンシP50: 114.3ms

選択理由: やや大きめのチャンク(512トークン)で文脈を保持しつつ、10%オーバーラップで文境界の断絶を緩和。93.1%の精度は多くのユースケースで十分。長めの文書(レポート、論文、マニュアル)で文脈が重要な場合に適する。

7-3. チューニングロードマップ:どの順番で改善するか

新たに RAG を構築する場合、または精度改善が必要な場合の優先順位です。

Step 1: エンベディングモデルの選定(最大インパクト)
   → 日本語なら Titan V2 (normalize=true, 256次元) から始める
   → これだけで Recall@10 = 95.6% を達成可能

Step 2: normalize オプションの確認(必須・最重要)
   → Titan V2 使用時、normalize=true が設定されているか確認
   → この1点で精度が10ポイント以上変わる

Step 3: チャンキング戦略の選定
   → まず Fixed-size (256トークン, overlap 0%) で baseline を取る
   → 大きめのチャンク(512トークン)なら overlap 10% で改善の可能性あり
   → **256トークン + overlap 0% が実測最高精度構成(95.3%)**
   → 256トークンでは overlap は逆効果(92.5%に低下)

Step 4: ef_search のチューニング
   → まず ef_search=32 でテスト(多くの場合これで十分)
   → レイテンシに余裕があれば 64 まで上げて効果を確認
   → 実測では 32 以降で精度が頭打ちになる傾向

Step 5: ハイブリッド検索の慎重な評価
   ⚠️ 日本語では逆効果の可能性あり(実測で -51ポイント低下)
   → A/Bテストで必ず検証してから導入
   → Kuromoji 設定なしでは絶対に有効化しない

Step 6: チャンクオーバーラップの最終調整
   → overlap 0% で baseline を取った後、10% を試す(推奨)
   → 512トークンで +4.7pt の改善実績あり
   → 20%以上は過剰(精度が逆に低下)

8. さいごに

本記事では、Amazon Bedrock Knowledge Bases × OpenSearch Serverless を使用して日本語RAGの精度を引き出すためのパラメータチューニングについて記載しました。

今回の検証では MIRACL-ja という学術的なデータセットを使用しましたが、実際のビジネス文書、技術文書、カスタマーサポート文書など、ドメインが変われば最適なパラメータも変わるかと思います。本記事の実測結果は一つの参考値として、何かのお役に立てれば幸いです。

RAGの精度改善は地道な作業の繰り返しだと思いますが、パラメータが持つ意味を理解し、実測で効果を確認しながら進めることで、着実に「使えるRAG」に近づいていけるのかなと思いました。

参考リンク

Amazon Bedrock

Amazon OpenSearch Serverless

その他

6
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?