こっそりアップデートされたBedrock Knowledge BaseのAdvanced Parsing機能を解説

## この記事でなにを書くか
re:Invent 2024のタイミングで、Data Automationの影に隠れて静かにアップデートされたBedrock Knowledge Baseの(旧称)Advanced Parsingという機能について、その挙動を解説し、使い所の参考にしていただくことを目指しています。

:::note warn
実際に動かしてみて確認された挙動を解説しているものですので、仕組みは今後変わりうる可能性があります
:::

## そもそもAdvanced Parsingとは？
:::note info
現在(2024/12)、ドキュメント上ではAdvanced Parsingという機能名ではなくなっています。正式名ではありませんが、以降本記事では便宜上、Foundation Model Parsingという機能名で呼びます。
:::
RAGに使いたいソースドキュメントの中に表や画像のような単純なテキストとしては処理が難しい情報が含まれている場合があります。このような場合になにも考えずに、チャンキング→Embeddingという処理をすると、それらの情報を失ってしまう可能性が高いです。
そこで、ドキュメントのパースを工夫することで、表や画像といったマルチモーダルな情報を扱えるようにしたいというモチベーションがあります。そのための手法はいろいろありますが、BedrockのKnowledge Baseにおいての選択肢の１つがFoundation Model Parsing(旧称Advanced Parsing)という機能として提供されているものです。


## Bedrock Knowledge BaseでのマルチモーダルRAGの選択肢
Foundation Model Parsingの詳細に入る前に、マルチモーダルデータを扱ってRAGを行う他の選択肢を整理しておきます。Bedrock Knowledge BaseにおけるマルチモーダルRAGの選択肢はFoundation Model Parsingだけではありません。次の表のように3つの選択肢と特徴があります。

| 機能 | 特徴 | 参考リンク |
|:-:|:-:|:-:|
| Data Automation | マルチモーダルコンテンツのテキスト化を**LLMやそれ以外の様々な技術を総合的に使用してマネージドにできる**機能 | - 公式ブログ : https://aws.amazon.com/jp/blogs/aws/new-amazon-bedrock-capabilities-enhance-data-processing-and-retrieval/ <br> - [新機能]Amazon Bedrock Data Automation (プレビュー)が登場しました https://dev.classmethod.jp/articles/new-feature-amazon-bedrock-data-automation-preview-announced-at-aws-reinvent/ |
| Foundation Model Parsing  | マルチモーダルコンテンツのテキスト化を**LLMのみで実施している**  | 本記事で解説  |
| Custom Parsing  | **Lambdaで自由に**マルチモーダルコンテンツのテキスト化を実装することができます  |  Knowledge Bases for Amazon Bedrockに新しく追加されたカスタムチャンキングを試す #AWS - Qiita https://qiita.com/moritalous/items/ef2996a144ae1455ac2f |

:::note warn
Data Automationは2024/12時点ではプレビューの機能です。
**Data Automationは2024/12時点では日本語でうまく動かないことが確認されています。**
:::


## Foundation Model Parsingの詳細

まず、すでに旧Advanced Parsingを知っている人向けに変更点だけお伝えします。

:::note warn
「以前はこうだった」についてはアップデートで検証できなくなったため完全に裏取りできてはいません。あくまで筆者の記憶に基づいています。
:::

### アップデートによる変更点

- **以前は、ドキュメントファイルの中に含まれる画像はテキスト化されたあと元画像は消えてしまっていたが、アップデートで元画像ファイルが残り、検索後にLLMに渡されるようになりました**
- チャンキング戦略がデフォルトであっても、マルチモーダルコンテンツを含む部分に関しては階層チャンクのような戦略が取られるようになりました
- 以前は、Parseに使えるモデルがClaude 3 SonnetとClaude 3 Haikuだったのが、アップデートでClaude 3.5 SonnetとClaude3 Haikuになりました

個人的には、一番上のアップデートが最重要だと思っています。旧バージョンと新バージョンを簡易的に図示したものです。**検索するために画像を一度テキスト化するのですが、検索でヒットしたときに旧バージョンでは変換後のテキストを返していたのに対して、新バージョンでは元の画像を返すようになっています。** 

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322266/18e1656d-6934-ee0a-311a-d9e84874eb2e.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322266/face2425-767c-49df-8c58-3ad790c4c08d.png)

これにより情報の損失が防がれるという非常に大きなメリットがあります。情報の損失とはどういうことか例を見てみます。例えば、Data Automationの機能についての[ブログ](https://aws.amazon.com/jp/blogs/aws/new-amazon-bedrock-capabilities-enhance-data-processing-and-retrieval/)内にあるこの画像をRAGで扱うことを考えてみましょう。これはData Automationを使ってみたときの画面のスクリーンショットです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322266/f82ae550-9959-eaa9-bdc9-a11ed542158b.png)

この画像を検索のためにテキスト化すると、例えば次のようになるでしょうか。

```
この画像はBedrockのData Automationのインターフェースを示しています。左側にサンフランシスコ市・郡の出生証明書のサンプルが表示されており、右側にはドキュメント解析の設定オプションと結果が表示されています。

主な機能として:
- ページレベルとエレメントレベルの粒度設定
- テキストフォーマット(Markdown形式など)の選択
- JSON形式での出力オプション
- バウンディングボックスの有効/無効切り替え

解析結果のセクションでは、出生証明書から抽出されたテキストが98%以上の信頼度で表示されています。抽出されたテキストには:
- "CERTIFICATION OF VITAL RECORD"
- "CITY AND COUNTY OF SAN FRANCISCO"
- "114521338745"
- "CERTIFICATE OF LIVE BIRTH STATE OF CALIFORNIA"

などが含まれています。

このツールは文書からテキストを自動的に抽出し、構造化されたデータとして利用可能にすることを目的としているようです。
```

検索のための文章としては十分なように見えますが、Data Automationのインターフェースに関心のあるユーザーがこのテキストを渡されても意味はないでしょう。ユーザーは元の画像を見せてほしいのです。

このように、検索のために情報を落として文章化するのはやむを得ませんが、ユーザーや後段のLLMに対しては元の画像を渡してあげるほうが良いケースが多いです。
(※RAGの後段で使うLLMが十分なマルチモーダルの性能を持っていることを前提としています)

### Foudation Model Parsingの仕組み概要

ここから仕組みの解説に移ります。Foundation Model Parsingは「**とにかく全部LLMでパースと変換をする**」ところが特徴に見えます。

手順を表すと次のようになります。
1. **PDFファイルを1ページごとに分割し、画像化する。** 画像ファイルの場合はそのまま渡す。
1. **画像をLLM(Claude)に渡して、Markdown構造のテキストに変換をかける**
 (以降は通常と同じ挙動)
1. 指定されたチャンキング戦略でチャンキング
1. 指定された埋め込みモデルで埋め込み
1. ベクトルDBにインデックス

### LLMによるMarkdown変換の仕組み

手順2では次のようなプロンプトを利用して、ページの画像をMarkdown化しています。元のプロンプトは英語ですが、翻訳ツールで直訳しました。このプロンプトはユーザーがカスタマイズすることができます。

特徴的な点は、テキストはMarkdownフォーマットで見出しだけ調整、画像は画像の説明を追加、表はMarkdownのフォーマット化、のようにタイプに応じて変換の使い分けをしている点です。

```
画像ページからコンテンツを抽出し、Markdown シンタックスで出力します。コンテンツを <markdown></markdown> タグで囲み、コードブロックは使用しないでください。画像が空の場合、何も入れない <markdown></markdown> を出力します。

以下の手順に従ってください。

1. 指定されたページを注意深く確認します。

2. ページに含まれるすべての要素を特定します。ヘッダー、本文、脚注、表、画像、キャプション、ページ番号などを含みます。

3. マークダウンの構文を使用して、出力をフォーマットします。
- 見出し：#はメイン、##はセクション、###はサブセクションなど
- リスト：*または-は箇条書き、1. 2. 3.は番号付き
- 繰り返し記述しない

4. 要素が画像（テーブルではない）の場合
- 画像内の情報がテーブルで表現できる場合は、画像の情報を含むテーブルを作成する
- そうでない場合は、画像内の情報について詳細な説明を記載する
- 要素を以下のいずれかに分類する：チャート、ダイアグラム、ロゴ、アイコン、自然画像、スクリーンショット、その他。クラスを <figure_type></figure_type> で囲む
- <figure></figure>タグで、<figure_type></figure_type>、表または説明、図のタイトルまたはキャプション（利用可能な場合）を囲む
- 表または説明を提供した後、画像内のテキストを転記しない

5. 要素が表の場合
- すべての行が同じ列数になるようにマークダウンの表を作成する
- セルの配置をできるだけ維持する
- 表を複数の表に分割しない
- 結合セルが複数の行または列にまたがる場合は、テキストを左上のセルに配置し、他のセルには「 」を出力する
- 列の区切りには「｜」を使用し、ヘッダー行の区切りには「｜-｜-｜」を使用する
- セルに複数の項目がある場合は、それらを別の行に列挙する
- 表にサブヘッダーが含まれる場合は、サブヘッダーをヘッダーとは別の行に分ける

6. 要素が段落の場合
- 各テキスト要素を正確に転記する

7. 要素がヘッダー、フッター、脚注、ページ番号の場合
- 各テキスト要素を正確に転記する

出力例：
<markdown>
<figure>
<figure_type>Chart</figure_type>
図3：このチャートは、年間売上高を百万単位で示しています。2020年は、新型コロナウイルス感染症のパンデミックにより大幅に減少しました。
棒グラフは、年間売上高の数値を示しており、Y軸は「売上高（百万ドル）」、X軸は「年」とラベル付けされています。このグラフには、2018年（1200万ドル）、2019年（1800万ドル）、2020年（800万ドル）、2021年（2200万ドル）の棒グラフがあります。
</figure>

<figure>
<figure_type>Chart</figure_type>
Figure 3: このグラフは、年間売上高を百万単位で示しています。2020年は、新型コロナウイルス感染症のパンデミックにより、大幅な減少となりました。
| 年 | 売上高（百万ドル）|
|-|-|
| 2018年 | 1200万ドル |
| 2019年 | 1800万ドル |
| 2020年 | 800万ドル |
| 2021年 | 2200万ドル |
</figure>

# 年次報告書

## 財務ハイライト

<figure>
<figure_type>ロゴ</figure_type>
Apple Inc.のロゴ
</figure>

* 収益：4000万ドル
* 利益：1200万ドル
* 1株当たり利益：1.25ドル

| | Year Ended December 31, | |
| | 2021 | 2022 |
|-|-|-|
| Cash provided by (used in): | | |
| Operating activities | $ 46,327 | $ 46,752 |
| Investing activities | (58,154) | (37,601) |
| Financing activities | 6,291 | 9,718 |

</markdown>
```

マルチモーダルなデータを扱う方法はいくつかありますが、この方法にはメリットとデメリットがそれぞれ考えられます。

メリット
- LLM単体でワンショットでできてしまうので楽
- うまくいかなかった場合にも、プロンプトエンジニアリングでなんとかなる(つまりAI/MLの専門家が不要)な可能性がある

デメリット
- LLMが各ページ分実行されるためコストとレイテンシーが追加される
- 画像情報からテキスト化しているため、テキストが書き換わってしまう可能性がある
    - 最近のマルチモーダルLLMの精度はすばらしいですが、可能性としてはあります。例えば、表の中の細かい数字の書き写しに失敗してしまうと困ってしまいます。(私の検証の中では見られませんでしたが)


<br>
実際に動かして変換の例を見てみましょう

こちらのブログを題材にしてみます
OpenSearch における 10 億規模のユースケースに適した k-NN アルゴリズムの選定 | Amazon Web Services ブログ https://aws.amazon.com/jp/blogs/news/choose-the-k-nn-algorithm-for-your-billion-scale-use-case-with-opensearch/

適度に画像も表も含まれています。このWebページをPDF化して、FM Parsingの機能に入力してみます。

例えば、このページは次のように変換されています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322266/969178b0-4e9a-6e03-5681-bc0098edc59f.png)

↓↓

```
# Hierarchical Navigable Small Worlds (HNSW) アルゴリズム

Hierarchical Navigable Small Worlds (HNSW) アルゴリズムは、ANN 検索で最も良く使われるアルゴリズムの1 つです。k-NN プラグインがサポートした最初のアルゴリズムであり、近傍検索ライブラリ [nmslib](https://github.com/nmslib/nmslib) の非常に効率的な実装を使用しています。これはクエリのレイテンシーと Recall (再現率) のトレードオフが最も優れており、また学習が不要です。このアルゴリズムの核となる考え方は、互いに近いインデックスベクトルを結ぶエッジを持つグラフを構築することです。そして、検索時にはこのグラフを部分的に走査してクエリベクトルの近似最近傍を見つけます。クエリの最近傍の方向に向けて走査するために、このアルゴリズムは常にクエリに最も近い候補を次に訪れます。

しかし、どのベクトルから走査を始めるべきでしょうか？ランダムなベクトルを選ぶこともできますが、インデックスが大きい場合にはクエリの実際の最近傍から大きく離れてしまう可能性があり、悪い結果に繋がってしまいます。クエリベクトルから大まかに近いベクトルから探索を始めるために、このアルゴリズムでは 1 つのグラフだけでなく、階層構造のグラフを構築します。全てのベクトルは最下層に追加され、ランダムなサブセットがその上の層に追加され、それらのランダムなサブセットがその上の層に追加され、といったような具合で構築されます。

検索時には、最上層のランダムなベクトルから初め、部分的に走査してその層におけるクエリベクトルの最近傍点を (近似的に) 見つけ、そしてこのベクトルを出発点として下の層を走査していきます。これを最下層に到達するまで繰り返します。最下層では、走査を行いますが、今回は最近傍をただ探索するのではなく、途中で訪れた k 最近傍点を記録します。

下図はその過程を表しています。 (この図は原著論文 [Efficient and robust approximate nearest neighbor search using Hierarchical Navigable Small World graphs](https://www.amazon.com/jp/blogs/news/choose-the-k-nn-algorithm-for-your-billion-scale-use-case-with-opensearch/) 内の画像から着想を得ています)

<figure>
<figure_type>Diagram</figure_type>
The image shows a hierarchical structure of layers in the Hierarchical Navigable Small Worlds (HNSW) algorithm. The top layer (Layer 2) contains a sparse set of randomly selected nodes, the middle layer (Layer 1) contains more nodes, and the bottom layer (Layer 0) contains all the nodes. The arrows indicate the connections between the layers.
</figure>
```
画像が、画像に対する説明文章に変換されているのが確認できます



もうひとつ、こちらの表が含まれたページは次のように変換されます

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322266/27f5d2bb-6604-dd7b-6957-32a7cecbd5e8.png)


```

その中間を取る

それぞれの最適化戦略では、2 つの構成を選択しました。

HNSW では、⽬的のトレードオフを実現するために、 m 、 ef_construction 、 ef_search  のパラメータを調整することができます。

m – グラフ内のノードが持つことができるエッジの数の最⼤数を制御します。各ノードがその全てのエッジを保存する必要があるため、 この値を⼤きくするとメモリフットプリントが増加しますが、グラフの接続性が向上して recall が改善されます。

ef_construction – グラフにノードを追加する時のエッジの候補キューのサイズを制御します。この値を⼤きくすると検討する候補の数が増え、インデックスのレイテンシーが増加します。しかし、より多くの候補が考慮されるため、グラフの質が向上し、検索時の recall の向上に繋がります。

ef_search – ef_construction  と似ており、検索時にグラフを⾛査するための候補キューのサイズを制御します。この値を⼤きくすると、検索レイテンシーは増加しますが、recall も向上します。

⼀般に、次の表に詳述するように、パラメータを徐々に増加させるような構成としました。

| Config ID | 最適化戦略 | m | ef_construction | ef_search |
| --- | --- | --- | --- | --- |
| hnsw1 | Optimize for memory and search latency | 8 | 32 | 32 |
| hnsw2 | Optimize for memory and search latency | 16 | 32 | 32 |
| hnsw3 | Balance between latency, memory, and recall | 16 | 128 | 128 |
| hnsw4 | Balance between latency, memory, and recall | 32 | 256 | 256 |
| hnsw5 | Optimize for recall | 32 | 512 | 512 |
| hnsw6 | Optimize for recall | 64 | 512 | 512 |

IVFの場合、2つのパラメータを調整することができます。

* nlist – パーティションの粒度を制御します。このパラメータの推奨値は、インデックスに含まれるベクトル数に基づいた関数から得られます。注意すべき点は、Lucene セグメントに対応する Faiss インデックスが存在するということです。シャードごとに複数の Lucene セグメントがあり、OpenSearch のインデックスごとに複数のシャードが存在します。今回の試算では、シャードあたり 100 個のセグメント、シャードが 24 個と仮定したので、Faiss インデックスあたり約 42 万個のベクトル存在することになります。この値をもとに、適切な値を 4096 と推定し、実験ではこれを⼀定としました。
* nprobes – 検索するときの nlist バケットの数を制御します。値を⼤きくすると、検索レイテンシーが増加する代わりに、recall が改善します。

PQの場合、2つのパラメータを調整することができます。
```
表がMarkdown形式になっているのが確認できます


### 変換されたMarkdownと画像のインデックス

次に、このように変換されたMarkdownテキストと画像をどのように紐づけてベクトルDBに保存しているのかを見ていきます。



<details open><summary>OpenSearchを検索すると次のように保存されています</summary>

"AMAZON_BEDROCK_TEXT": "# Hierarchical Navigable Small Worlds (HNSW) アルゴリズム Hierarchical Navigable Small Worlds (HNSW) アルゴリズムは、ANN 検索で最も良く使われるアルゴリズムの1 つです。k-NN プラグインがサポートした最初のアルゴリズムであり、近傍検索ライブラリ [nmslib](https://github.com/nmslib/nmslib) の非常に効率的な実装を使用しています。これはクエリのレイテンシーと Recall (再現率) のトレードオフが最も優れており、また学習が不要です。このアルゴリズムの核となる考え方は、互いに近いインデックスベクトルを結ぶエッジを持つグラフを構築することです。そして、検索時にはこのグラフを部分的に走査してクエリベクトルの近似最近傍を見つけます。", 
"AMAZON_BEDROCK_METADATA": """{"source":"s3://hogehogehoge/opensearch_blog.pdf","parentText":"# Hierarchical Navigable Small Worlds (HNSW) アルゴリズム\nHierarchical Navigable Small Worlds (HNSW) アルゴリズムは、ANN 検索で最も良く使われるアルゴリズムの1 つです。k-NN プラグインがサポートした最初のアルゴリズムであり、近傍検索ライブラリ [nmslib](https://github.com/nmslib/nmslib) の非常に効率的な実装を使用しています。これはクエリのレイテンシーと Recall (再現率) のトレードオフが最も優れており、また学習が不要です。このアルゴリズムの核となる考え方は、互いに近いインデックスベクトルを結ぶエッジを持つグラフを構築することです。そして、検索時にはこのグラフを部分的に走査してクエリベクトルの近似最近傍を見つけます。クエリの最近傍の方向に向けて走査するために、このアルゴリズムは常にクエリに最も近い候補を次に訪れます。\nしかし、どのベクトルから走査を始めるべきでしょうか？ランダムなベクトルを選ぶこともできますが、インデックスが大きい場合にはクエリの実際の最近傍から大きく離れてしまう可能性があり、悪い結果に繋がってしまいます。クエリベクトルから大まかに近いベクトルから探索を始めるために、このアルゴリズムでは 1 つのグラフだけでなく、階層構造のグラフを構築します。全てのベクトルは最下層に追加され、ランダムなサブセットがその上の層に追加され、それらのランダムなサブセットがその上の層に追加され、といったような具合で構築されます。\n検索時には、最上層のランダムなベクトルから初め、部分的に走査してその層におけるクエリベクトルの最近傍点を (近似的に) 見つけ、そしてこのベクトルを出発点として下の層を走査していきます。これを最下層に到達するまで繰り返します。最下層では、走査を行いますが、今回は最近傍をただ探索するのではなく、途中で訪れた k 最近傍点を記録します。\n下図はその過程を表しています。 (この図は原著論文 [Efficient and robust approximate nearest neighbor search using Hierarchical Navigable Small World graphs](https://www.amazon.com/jp/blogs/news/choose-the-k-nn-algorithm-for-your-billion-scale-use-case-with-opensearch/) 内の画像から着想を得ています)\n<figure>\nThe image shows a hierarchical structure of layers in the Hierarchical Navigable Small Worlds (HNSW) algorithm. The top layer (Layer 2) contains a sparse set of randomly selected nodes, the middle layer (Layer 1) contains more nodes, and the bottom layer (Layer 0) contains all the nodes. The arrows indicate the connections between the layers.\n</figure>\n","relatedContent":[{"locationType":"S3","s3Location":{"uri":"s3://hogehoge/aws/bedrock/knowledge_bases/fugagufa/QYDJYUKR2S/9ddb8b4d-d020-4dd3-83da-d997496bfa0a.png"}}]}"""
</details>

<br>

わかりやすく抜粋して特徴を説明します

[特徴1. 階層チャンクのようになっている]
チャンクテキストはAMAZON_BEDROCK_TEXT_CHUNKというフィールドにあり、そのテキストが含まれる1ページ分のすべてのテキストがAMAZON_BEDROCK_METADATAのparentTextの中に入っています。

[特徴2. 元画像が紐づけられている]
PDFのページを画像化したものが、AMAZON_BEDROCK_METADATAのrelatedContentの中に、S3のURIとして保存されています。このS3バケットはKnowledge Baseを作成する際に、"Multimodal storage destination"という項目で指定します。

※Knowledge Baseを作成するときにこのバケットを作成しておかないと画像が紐づいて保存されませんので注意してください

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322266/4cabc192-cef4-c08d-c247-1addb0d97e12.png)

### 検索時の挙動

検索を行うと、ヒットしたテキストだけでなく、画像が返ってきているのがわかります。この画像がLLMに渡されて、回答が生成されるという流れになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322266/8da9c220-a197-1f59-36c8-929822b76c60.png)


## まとめ

BedrockのKnowledge BaseのAdvanced Parsing機能で、元の画像が使えるようになったというアップデートとその詳しい挙動を紹介しました。Data Automationが派手なアップデートとして注目され、日本語が使えないと落胆されている人も多いかと思いますが、こちらのひっそりアップデートにも注目してあげてください。