Azure AI Search / Document IntelligenceでExcel / フローチャートをRAGする方法
初めまして、Givery AI Lab所属の楊です。
今回はAzureでRAGシステムを構築する際に、Azure AI Searchを利用して外部情報(特にExcelファイルとフローチャート)を検索エンジンに取り込む方法について調査し、検証しました。その結果について、共有させていただきます。
RAGとは
RAGは、Retrieval-Augmented Generationの略称で、大規模言語モデル(LLM)によるテキスト生成に、外部の情報の検索を組み合わせることで、モデルの回答精度を向上させる技術です。「検索拡張生成」と訳されます。
このフローチャートでは一般的なRAGシステムを示しています。特に、「検索エンジン」は、事前に取り込まれた外部情報からユーザーのプロンプトに最も関連するコンテンツを検索し、「LLM/LLMに基づくアプリ」に提供するタスクを担当します。したがって、RAGシステムにおいて重要な役割を果たします。
Azure AI Search
Azure AI Searchとは
Azure AI Searchは、ユーザーが取り込んだ異種コンテンツに対応するための検索エンジンサービスを提供します。Azure上でRAGシステムをよりスムーズに構築するためにハイブリッド検索や応用AI(Applied AI)などさまざまな追加機能を備えています。
要するに、Azure AI SearchはRAGシステムにおいて、「検索エンジン」の部分を担当します。
調査の対象
今回は、主にAzure AI Searchを利用して「検索エンジン」を構築する際に、Excelファイルとフローチャートの外部情報をどのように検索エンジンのインデックスにインポートして活用するかという方法を調べました。
まず、対象の外部情報となるExcelファイルとフローチャートを簡単にご説明いたします。
Excelファイル
Excelファイルとは、一般的にXLSX
形式のファイルを指します。活用の可能性をさらに調べるために、今回の調査・検証では、XLSX
ファイルに加えてファイル内容のスクリーンショット(例えば.png
などの画像ファイル)も調査範囲に含めました。
フローチャート
フローチャートとは、一般的には画像ファイルの形式(例えば.png
など)で保存されるため、それを調査範囲に含めました。
Excelファイルとフローチャートは、どちらもビジネスでよく使用されるデータ形式で、検索エンジンのインデックスに直接取り込めばRAGシステムの構築に役立つので調査を行いました。特に両者はともに独特の構造を持っており、RAGに適用する際にはテキスト内容に加えてその構造にも注意を払う必要があるため、活用が難しくなります。
調査結果の共有
公式ドキュメントによれば、Azure AI Searchではインデックスに外部情報をインポートするために、2つの基本的な手法(プログラムでプッシュするワークフローと検査インデクサーを使用してデータをプルするワークフロー)が提供されています。
- プッシュするワークフローはプログラムでデータをインデックスにインポートする手法です。
データソースは任意ですが、インポート形式はJSONドキュメントであるため、ExcelファイルやフローチャートをJSON形式に転換する必要があります。
XLSX
ファイルの場合、一般的にはCSV
ファイルに変換してから、プログラムでそのテキストを分割してJSONドキュメントに転換しますが、実際の業務で検証したところ、このような転換が元のデータ構造(テーブルの結合セル)を損ない、RAGの精度に悪影響を与えることがわかりました。
なお、画像ファイルの場合、プログラムでJSONドキュメントに転換することが難しいです。
- プルするワークフローは"検索Indexer"を使用して、Azureにサポートされているデータソースに接続し、データをインデックスに自動的にインポートする手法です。
データソースはAzureにサポートされているデータソースのみですが、異なるデータ形式(通常はテーブル形式)を自動的にJSONドキュメントに転換する機能を備えているため、多くのファイル形式やデータ構造に直接対応しています。例えば、CSV
、PDF
、HTML
、table
、view
などが対応されています。完全なリストはAzure AI Searchの公式ドキュメントを参照してください。
XLSX
ファイルに対応していると公式ドキュメントに記述されていますが、試したところ、ここでの自動転換もプッシュと同様に結合セルなどの構造情報を保つのが難しいようです(詳細な検証は今後実施予定です)。
また、画像ファイルには対応していません。
これらの基本的な手法は生のテキストファイルを元に処理を行うため、対応できるファイル形式が限られており、構造を持つドキュメントにそのまま適用すると、構造情報が失われるという問題点があります。
そのため、Azureでは構造を持つ外部情報に対応するAIサービスを提供します。これらのサービスを利用することで、Azure AI Searchと連携し、より多くのファイル形式やデータ構造に対応する検索エンジンを構築することができます。特に、Azure AI Document Intelligenceサービスではドキュメント処理に特化したツールを提供しています。Excelファイルとフローチャートにも適用可能であるため、ブログ後半ではAzure AI Document Intelligenceの検証を行いました。
Azure AI Document Intelligence (AADI)
Azure AI Document Intelligenceとは
Azure AI Document Intelligence (AADI)は、Azure AI Serviceの一部として、自動的なドキュメント処理ソリューションを提供します。このサービスには、さまざまなドキュメント処理に特化した事前訓練済みのモデルが用意されており。これらのモデルを使用して、ドキュメントの構造を考慮した情報抽出が可能です。抽出された情報を基に、Azure AI Searchを利用して、より高精度な検索エンジンを構築することができます。
Layout model
今回の対象であるExcelファイルとフローチャートを考慮すると、最適なのはAADIのLayout modelです。Layout modelは異なる形式のドキュメントを受け取り、構造を持つデータ表現を返します。
このモデルでは、入力としてPDF
、JPEG/JPG
、PNG
、BMP
、TIFF
、HEIF
、DOCX
、XLSX
、PPTX
形式のドキュメントを受け入れ、出力としてテキスト、選択マーク、テーブル、ドキュメント構造などの情報を抽出します。
RAGの検索エンジンに使用すると考える上で、出力の中で最も注目すべきは、抽出されたテキスト(Excelファイルおよびフローチャート)とテーブル(Excelファイル)です。それらの有効性を確認するために、Excelファイルおよびフローチャートの各例において、Layout modelによる抽出を検証しました。
調査と検証結果の共有
XLSX
ファイル)
Excelファイル(公式ドキュメントによる説明
Layout modelの公式ドキュメントによれば、XLSX
ファイルがサポートされていることがわかりましたが、ただし、テーブルの抽出はサポートされていません。Excelファイルにおいて、テーブルが最も重要であるため、残念に思いますが、抽出されたテキストに注目することにしました。
検証結果
-
入力 (ここで表示しているのはスクリーンショットですが、実際に使用したのは
XLSX
ファイルです)
-
出力(抽出されたテキスト)
出力
# Sheet1
カテゴリ
製品名
販売数
単価(USD)
地域
販売員
販売日時
売上金額
エレクトロニクス
TV
50
300
東京
佐藤太郎
01/10/2024
スマートフォン
100
800
大阪
鈴木花子
01/15/2024
家電
冷蔵庫
30
1200
名古屋
田中一郎
02/01/2024
洗濯機
40
500
福岡
高橋次郎
02/05/2024
家具
ソファ
20
1000
京都
中村明子
03/01/2024
ベッド
15
1200
札幌
松本幸子
03/03/2024
合計
- 出力 (抽出されたテーブル)
試してみましたが、予想通り、抽出はできませんでした。 - 結論
XLSX
形式のファイルだと、Layout modelでは対応が難しいように思われます。テーブルの抽出は全くうまくいきませんでした。一方で、テキストの抽出においては、テーブルの主な内容がテキスト形式で抽出されましたが、計算式(e.g.=SUM()
など)が抽出されなかったし、テーブルの構造も若干破壊されました。
Excelファイル(画像ファイル)
公式ドキュメントによる説明
公式ドキュメントによると、入力として画像ファイルがサポートされていますので、実際のパフォーマンスを確認します。特にXLSX
形式では抽出できないテーブルの抽出に注目しています。
検証結果
- 入力 (.png
形式のスクリーンショット)
- 出力 (抽出されたテキスト)
出力
<table>
<tr>
<th>カテゴリ</th>
<th>製品名</th>
<th>販売数</th>
<th>単価(USD)</th>
<th>地域</th>
<th>販売員</th>
<th>販売日時</th>
<th>売上金額</th>
</tr>
<tr>
<td rowspan="2">エレクト ロニクス</td>
<td>TV</td>
<td>50</td>
<td>300</td>
<td>東京</td>
<td>佐藤太郎</td>
<td>2024/1/10</td>
<td>15,000</td>
</tr>
<tr>
<td>スマートフォン</td>
<td>100</td>
<td>800</td>
<td>大阪</td>
<td>鈴木花子</td>
<td>2024/1/15</td>
<td>80,000</td>
</tr>
<tr>
<td rowspan="4">家電 家具</td>
<td>冷蔵庫</td>
<td>30</td>
<td>1200</td>
<td>名古屋</td>
<td>田中一郎</td>
<td>2024/2/1</td>
<td>36,000</td>
</tr>
<tr>
<td>洗濯機</td>
<td>40</td>
<td>500</td>
<td>福岡</td>
<td>高橋次郎</td>
<td>2024/2/5</td>
<td>20,000</td>
</tr>
<tr>
<td>ソファ</td>
<td>20</td>
<td>1000</td>
<td>京都</td>
<td>中村明子</td>
<td>2024/3/1</td>
<td>20,000</td>
</tr>
<tr>
<td>ベッド</td>
<td>15</td>
<td>1200</td>
<td>札幌</td>
<td>松本幸子</td>
<td>2024/3/3</td>
<td>18,000</td>
</tr>
<tr>
<td>合計</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td>189,000</td>
</tr>
</table>
- 出力(抽出されたテーブル)
出力
Table # 0 has 8 rows and 8 columns
Table # 0 location on page: 1 is [7, 9, 1459, 8, 1460, 1012, 8, 1013]
...Cell[0][0] has text 'カテゴリ'
...content on page 1 is within bounding polygon '[4, 2, 132, 2, 132, 34, 4, 34]'
...Cell[0][1] has text '製品名'
...content on page 1 is within bounding polygon '[132, 2, 330, 2, 330, 36, 132, 34]'
...Cell[0][2] has text '販売数'
...content on page 1 is within bounding polygon '[330, 2, 463, 2, 463, 36, 330, 36]'
...Cell[0][3] has text '単価(USD)'
...content on page 1 is within bounding polygon '[463, 2, 680, 2, 680, 36, 463, 36]'
...Cell[0][4] has text '地域'
...content on page 1 is within bounding polygon '[680, 2, 811, 2, 811, 36, 680, 36]'
...Cell[0][5] has text '販売員'
...content on page 1 is within bounding polygon '[811, 2, 943, 2, 943, 36, 811, 36]'
...Cell[0][6] has text '販売日時'
...content on page 1 is within bounding polygon '[943, 2, 1225, 2, 1225, 36, 943, 36]'
...Cell[0][7] has text '売上金額'
...content on page 1 is within bounding polygon '[1225, 2, 1460, 3, 1460, 37, 1225, 36]'
...Cell[1][0] has text 'エレクト ロニクス'
...content on page 1 is within bounding polygon '[4, 34, 132, 34, 132, 270, 4, 270]'
...Cell[1][1] has text 'TV'
...content on page 1 is within bounding polygon '[132, 34, 330, 36, 330, 118, 132, 118]'
...Cell[1][2] has text '50'
...content on page 1 is within bounding polygon '[330, 36, 463, 36, 463, 118, 330, 118]'
...Cell[1][3] has text '300'
...content on page 1 is within bounding polygon '[463, 36, 680, 36, 680, 118, 463, 118]'
...Cell[1][4] has text '東京'
...content on page 1 is within bounding polygon '[680, 36, 811, 36, 811, 118, 680, 118]'
...Cell[1][5] has text '佐藤太郎'
...content on page 1 is within bounding polygon '[811, 36, 943, 36, 942, 118, 811, 118]'
...Cell[1][6] has text '2024/1/10'
...content on page 1 is within bounding polygon '[943, 36, 1225, 36, 1225, 118, 942, 118]'
...Cell[1][7] has text '15,000'
...content on page 1 is within bounding polygon '[1225, 36, 1460, 37, 1460, 119, 1225, 118]'
...Cell[2][1] has text 'スマートフォン'
...content on page 1 is within bounding polygon '[132, 118, 330, 118, 330, 270, 132, 270]'
...Cell[2][2] has text '100'
...content on page 1 is within bounding polygon '[330, 118, 463, 118, 463, 270, 330, 270]'
...Cell[2][3] has text '800'
...content on page 1 is within bounding polygon '[463, 118, 680, 118, 678, 270, 463, 270]'
...Cell[2][4] has text '大阪'
...content on page 1 is within bounding polygon '[680, 118, 811, 118, 811, 268, 678, 270]'
...Cell[2][5] has text '鈴木花子'
...content on page 1 is within bounding polygon '[811, 118, 942, 118, 942, 268, 811, 268]'
...Cell[2][6] has text '2024/1/15'
...content on page 1 is within bounding polygon '[942, 118, 1225, 118, 1225, 270, 942, 268]'
...Cell[2][7] has text '80,000'
...content on page 1 is within bounding polygon '[1225, 118, 1460, 119, 1460, 271, 1225, 270]'
...Cell[3][0] has text '家電 家具'
...content on page 1 is within bounding polygon '[4, 270, 132, 270, 132, 915, 4, 915]'
...Cell[3][1] has text '冷蔵庫'
...content on page 1 is within bounding polygon '[132, 270, 330, 270, 330, 337, 132, 337]'
...Cell[3][2] has text '30'
...content on page 1 is within bounding polygon '[330, 270, 463, 270, 463, 337, 330, 337]'
...Cell[3][3] has text '1200'
...content on page 1 is within bounding polygon '[463, 270, 678, 270, 678, 335, 463, 337]'
...Cell[3][4] has text '名古屋'
...content on page 1 is within bounding polygon '[678, 270, 811, 268, 811, 335, 678, 335]'
...Cell[3][5] has text '田中一郎'
...content on page 1 is within bounding polygon '[811, 268, 942, 268, 942, 335, 811, 335]'
...Cell[3][6] has text '2024/2/1'
...content on page 1 is within bounding polygon '[942, 268, 1225, 270, 1225, 337, 942, 335]'
...Cell[3][7] has text '36,000'
...content on page 1 is within bounding polygon '[1225, 270, 1460, 271, 1460, 337, 1225, 337]'
...Cell[4][1] has text '洗濯機'
...content on page 1 is within bounding polygon '[132, 337, 330, 337, 330, 406, 132, 406]'
...Cell[4][2] has text '40'
...content on page 1 is within bounding polygon '[330, 337, 463, 337, 463, 405, 330, 406]'
...Cell[4][3] has text '500'
...content on page 1 is within bounding polygon '[463, 337, 678, 335, 678, 405, 463, 405]'
...Cell[4][4] has text '福岡'
...content on page 1 is within bounding polygon '[678, 335, 811, 335, 811, 405, 678, 405]'
...Cell[4][5] has text '高橋次郎'
...content on page 1 is within bounding polygon '[811, 335, 942, 335, 942, 404, 811, 405]'
...Cell[4][6] has text '2024/2/5'
...content on page 1 is within bounding polygon '[942, 335, 1225, 337, 1225, 405, 942, 404]'
...Cell[4][7] has text '20,000'
...content on page 1 is within bounding polygon '[1225, 337, 1460, 337, 1460, 405, 1225, 405]'
...Cell[5][1] has text 'ソファ'
...content on page 1 is within bounding polygon '[132, 406, 330, 406, 332, 642, 132, 640]'
...Cell[5][2] has text '20'
...content on page 1 is within bounding polygon '[330, 406, 463, 405, 463, 643, 332, 642]'
...Cell[5][3] has text '1000'
...content on page 1 is within bounding polygon '[463, 405, 678, 405, 678, 645, 463, 643]'
...Cell[5][4] has text '京都'
...content on page 1 is within bounding polygon '[678, 405, 811, 405, 811, 643, 678, 645]'
...Cell[5][5] has text '中村明子'
...content on page 1 is within bounding polygon '[811, 405, 942, 404, 942, 643, 811, 643]'
...Cell[5][6] has text '2024/3/1'
...content on page 1 is within bounding polygon '[942, 404, 1225, 405, 1225, 645, 942, 643]'
...Cell[5][7] has text '20,000'
...content on page 1 is within bounding polygon '[1225, 405, 1460, 405, 1460, 645, 1225, 645]'
...Cell[6][1] has text 'ベッド'
...content on page 1 is within bounding polygon '[132, 640, 332, 642, 332, 914, 132, 915]'
...Cell[6][2] has text '15'
...content on page 1 is within bounding polygon '[332, 642, 463, 643, 463, 914, 332, 914]'
...Cell[6][3] has text '1200'
...content on page 1 is within bounding polygon '[463, 643, 678, 645, 678, 914, 463, 914]'
...Cell[6][4] has text '札幌'
...content on page 1 is within bounding polygon '[678, 645, 811, 643, 811, 915, 678, 914]'
...Cell[6][5] has text '松本幸子'
...content on page 1 is within bounding polygon '[811, 643, 942, 643, 942, 914, 811, 915]'
...Cell[6][6] has text '2024/3/3'
...content on page 1 is within bounding polygon '[942, 643, 1225, 645, 1226, 915, 942, 914]'
...Cell[6][7] has text '18,000'
...content on page 1 is within bounding polygon '[1225, 645, 1460, 645, 1460, 914, 1226, 915]'
...Cell[7][0] has text '合計'
...content on page 1 is within bounding polygon '[4, 915, 132, 915, 132, 1017, 4, 1015]'
...Cell[7][1] has text ''
...content on page 1 is within bounding polygon '[132, 915, 332, 914, 332, 1017, 132, 1017]'
...Cell[7][2] has text ''
...content on page 1 is within bounding polygon '[332, 914, 463, 914, 463, 1017, 332, 1017]'
...Cell[7][3] has text ''
...content on page 1 is within bounding polygon '[463, 914, 678, 914, 678, 1017, 463, 1017]'
...Cell[7][4] has text ''
...content on page 1 is within bounding polygon '[678, 914, 811, 915, 811, 1017, 678, 1017]'
...Cell[7][5] has text ''
...content on page 1 is within bounding polygon '[811, 915, 942, 914, 942, 1017, 811, 1017]'
...Cell[7][6] has text ''
...content on page 1 is within bounding polygon '[942, 914, 1226, 915, 1226, 1018, 942, 1017]'
...Cell[7][7] has text '189,000'
...content on page 1 is within bounding polygon '[1226, 915, 1460, 914, 1461, 1018, 1226, 1018]'
- 結論
テキストの抽出では、Markdown記法を用いて元のデータ構造を保ちながらテーブルの内容を抽出しました。テーブルの抽出では、セルごとにテーブルを抽出し、各セルの位置情報と内容を保存しました。以上の結果から、両方の抽出方法ともテーブルの構造を保持したまま内容を抽出できることがわかり、Layout modelが画像形式のExcelファイルに対応可能であると結論しました。もちろん、抽出されたコンテンツをRAGシステムに適用するためには、さらに加工が必要になるかもしれませんが、有望だと考えられます。
フローチャート(画像ファイル)
公式ドキュメントによる説明
Layout modelは画像ファイルが処理できるものの、フローチャートに対応することは公式ドキュメントに全く言及されていません。確認してみたいため、フローチャートから抽出されたテキストをチェックしました。最も注目したいのは、抽出されたテキストが元のフローチャートの構造をどれだけ保っているかという点です。
検証結果
- 入力
- 出力 (抽出された内容)
出力
<figure>
ユーザーが注文を入力
注文内容の確認
在庫確認
在庫あり
支払い処理
支払い方法
クレジットカード
PayPal
銀行振込
クレジットカード決済
PayPal決済
銀行振込手続き
支払い成功
在庫なし
支払い成功
配送手続き
配送準備と発送
支払い失敗
発送完了
注文完了通知
エラーメッセージ表示
在庫切れ通知
終了
</figure>
- 結論
RAGに適用するためには、抽出されたテキストが元のフローチャートが表すプロセス(フローチャートの構造)を保持する必要があります。しかし、抽出の結果から見ると、内容は全て抽出されましたが、構造が全く見受けられませんでした。したがって、Layout modelはフローチャートに対応できないと結論しました。
他の可能性
以上の検証から、AADIではフローチャートに対応できないことがわかりました。別の手法として、フローチャートを手作業でmermaid記法に転換し、インデックスにインポートする方法が考えられます。実際のRAGシステムでの検証により、LLMがmermaid記法で表現されたフローチャートを理解できることが確認され、この手法の有効性が証明されました。ただし、弱点は手作業で行う必要があり、労力がかかる点です。
一方で、AADI以外にも、LlamaIndexが提供するLlamaParseというドキュメント処理ツールがあります。まだ使用したことがなく、有効性については不明ですが、公式サイトにはフローチャートに対応できると記載されています。
The best genAI-native parsing platform built specifically to transform complex documents (with tables, charts, images, flow diagrams etc) into clean data for LLM applications
終わりに
今回の記事では、RAGシステムのためにAzure AI Searchを使って検索エンジンを構築する際に、Excelファイルとフローチャートの外部情報を活用する手法を調査・検証しました。
Azure AI Searchが提供する基本的な手法では難しいという背景のもと、Azure AI Document IntelligenceのLayout modelが利用可能かどうかを検証しました。Excelに関して、XLSX
ファイルの場合、Layout modelによる抽出では元のテーブル構造が破壊されたため対応できない一方、画像ファイルの場合は対応可能でした。フローチャートに関して、画像ファイルのみを検証しましたが、構造が全く保たれず、対応できませんでした。
注意すべき点は、ここでの調査と検証はRAGシステムの検索エンジンの構築にとどまったため、さらにRAGシステムでどのように利用するのか、またはRAGの精度に具体的にどんな影響を与えるのかはさらに深掘りする必要があります。筆者も今後、この方向に引き続き取り組んでいきます。今回の記事は皆さんのお役に立てれば幸いです。
生成AIを活用したPoCや支援にご興味があれば、以下リンクよりお問い合わせください。
Discussion