RAGにおけるExcelの「美味いデータ調理法」を探してみた
はじめに
こんにちは!satto workspaceでRAGやAgent周りの料理人をしている 藤田(@kazumasa_fujita)です。
RAGで多くのファイル形式を扱えるようになったけど、業務で様々な使われ方をするExcel、どのように調理してLLMに与えていますか?
Excelを単純なテキストとして扱うと、次のような問題が発生します:
- 行と列のコンテキストが失われる
- セル結合や独自の記載方法などの複雑な構造を表現できない
- グラフや画像が多用されて本来の構造を維持できない
そこで本記事では、LLM(Gemini 2.5 pro)がExcelの構造を正しく理解できるよう、いくつかの「データ調理法(前処理)」を試し、どの手法が最も「美味い」のかを検証します。
今回の検証はベクトル検索や回答精度を上げる試みは行わず、純粋な前処理の効果に焦点を当てます。
今回の実験セット
🔧 調理器具(使用技術)
- 言語: TypeScript
- LLM: Google Gemini 2.5 pro
- ライブラリ: XLSX, ExcelJS, JSZip, LibreOffice
🍽️ お品書き(検証する調理法)
- 生食(CSV変換):そのままテキストに変換
- 焼き(JSON変換):構造化データに変換
- 煮込み(HTML変換):テーブル形式を保持
- 揚げ物(PDF画像変換):ビジュアルをそのまま保持
- コース料理(ハイブリッド):テキスト+PDF画像の組み合わせ
📊 食材(検証用Excelデータ)と評価方法
実務でよく使われる4つのパターンのExcelを用意し、各調理法で処理したデータをGeminiに渡して質問を投げかけます。
1. 出勤表 - 一般的な月次出勤データ
出勤表:日付ごとの出勤・休暇状況を管理
検証用の質問:
-
Q1: 9/22が休みの人って誰?
- 正解: 鈴木さん、高橋さん、山本さん、小林さん、加藤さん、吉田さん、山口さん、松本さん、林さん、清水さん(10名)
-
Q2: 佐々木さんの休みっていつ?
- 正解: 9/6(土)、9/7(日)、9/9(火)、9/13(土)、9/14(日)、9/15(月)、9/20(土)、9/21(日)、9/23(火)(9日間)
2. ガントチャート - 色分けを多用したプロジェクト管理
プロジェクト管理:タスクごとの期間と担当者を視覚的に表現
検証用の質問:
-
Q1: 要件定義の担当って誰で、いつからいつまでだっけ?
- 正解: 鈴木さんで9/19(金)から9/25(木)まで
-
Q2: リリース準備は全部でいつからいつまでだっけ?
- 正解: 9/29(月)から10/18(土)まで
3. グラフ付き売上レポート - データとビジュアルが混在
月次売上レポート:数値データとグラフが共存
検証用の質問:
-
Q1: B店舗が一番良い月はいつ?
- 正解: 2024/12
-
Q2: グラフから赤と黄色の差が一番大きい月っていつ?
- 正解: 2024/8と2024/12(赤=B店、黄色=C店)
4. 画像入り手順書 - 画像等を含むドキュメント
操作手順書:スクリーンショットとテキストで手順を説明
検証用の質問:
-
Q1: ステップ1のボタンって画面のどこにある?
- 正解: 画面真ん中上部
-
Q2: ステップ2のボタンって画面のどこにある?
- 正解: 画面右上と左下の2箇所
調理開始!5つの前処理手法を試す
1. 調理法:生食(CSV変換)
考え方
最もシンプル。Excelシートを単純なカンマ区切りのテキストに変換します。
実装コード
import XLSX from "xlsx";
function convertExcelToCSV(filePath: string): string {
const workbook = XLSX.readFile(filePath);
const worksheet = workbook.Sheets[workbook.SheetNames[0]];
return XLSX.utils.sheet_to_csv(worksheet);
}
検証結果
質問 | 結果 | 備考 |
---|---|---|
9/22が休みの人 | ❌ 0/3 | 余計な人も含まれていた |
佐々木さんの休み | ⚠️ 2/3 | 1回は日付が一部ずれていた |
要件定義の担当・期間 | ❌ 0/3 | 担当者は当てられたが、色部分が空白なので記載なしと回答 |
リリース準備の期間 | ❌ 0/3 | タスクは判別できたが、開始日と終了日が取れない |
B店舗が一番良い月 | ✅ 3/3 | 表データから回答が取得できる |
グラフから赤と黄色の差 | ❌ 0/3 | グラフ情報が含まれておらず、色の判別ができない |
ステップ1のボタン位置 | ❌ 0/3 | 画像情報が含まれていないため、適当な回答 |
ステップ2のボタン位置 | ❌ 0/3 | 画像情報が含まれていないため、適当な回答 |
考察
CSV変換は最もシンプルで実装が容易だが、LLMが行と列の関係を正確に理解するのが難しい。特に「9/22が休みの人」のような列方向の検索では余計な人も含まれてしまい、精度が低い。一方で「佐々木さんの休み」のような行方向の検索は比較的うまくいく傾向がある。
最大の弱点は色情報や画像が一切含まれないこと。ガントチャートのような色で期間を表現するデータや、手順書のような画像を含むデータには全く対応できない。シンプルな表データの抽出には使えるが、実務の複雑なExcelには不向き。
2. 調理法:焼き(JSON変換)
考え方
データを構造化されたJSONオブジェクトの配列に変換。プログラムでの扱いが容易で、キーと値の関係が明確。
実装コード
import XLSX from "xlsx";
function convertExcelToJSON(filePath: string): string {
const workbook = XLSX.readFile(filePath);
const worksheet = workbook.Sheets[workbook.SheetNames[0]];
return XLSX.utils.sheet_to_json(worksheet);
}
検証結果
質問 | 結果 | 備考 |
---|---|---|
9/22が休みの人 | ✅ 3/3 | JSON形式ではキーと値の関係が明確 |
佐々木さんの休み | ✅ 3/3 | JSON形式ではキーと値の関係が明確 |
要件定義の担当・期間 | ❌ 0/3 | 担当者は当てられたが、色部分が空白なので記載なしと回答 |
リリース準備の期間 | ❌ 0/3 | タスクは判別できたが、開始日と終了日が取れない |
B店舗が一番良い月 | ✅ 3/3 | 表データから正解情報が取得できる |
グラフから赤と黄色の差 | ❌ 0/3 | グラフ情報が含まれておらず、色の判別ができない |
ステップ1のボタン位置 | ❌ 0/3 | 画像情報が含まれていないため、回答不可 |
ステップ2のボタン位置 | ❌ 0/3 | 画像情報が含まれていないため、回答不可 |
考察
JSON変換はCSVと比べて構造化されており、キーと値の関係が明確なため、出勤表のような単純な表データでは高い精度を示した。「9/22が休みの人」「佐々木さんの休み」といった質問に3回とも正解できたのは、JSON形式のおかげでLLMが行と列の関係を正確に理解できたため。
しかし、色情報や画像は含まれないため、ガントチャートや手順書には対応できない。また、CSVと同様にグラフや視覚的な要素も失われる。構造化データとしては優秀だが、実務の複雑なExcelを扱うには限界がある。
3. 調理法:煮込み(HTML変換)
考え方
HTMLテーブルとして出力。ExcelJSを使って背景色、フォント色、太字、イタリック、下線、フォントサイズ、テキスト配置などのスタイル情報を詳細に抽出し、HTMLのstyle属性として反映。ブラウザで表示されるイメージに近い形でLLMに伝えられる。
実装コード
import ExcelJS from 'exceljs';
async function convertExcelToHTMLWithStyles(filePath: string): Promise<string> {
const workbook = new ExcelJS.Workbook();
await workbook.xlsx.readFile(filePath);
// 最初のワークシートを取得
const worksheet = workbook.worksheets[0];
if (!worksheet) {
// シートが存在しない場合は空の文字列を返す
return "";
}
const htmlParts: string[] = [];
htmlParts.push('<table border="1" cellspacing="0" cellpadding="5">');
worksheet.eachRow((row) => {
htmlParts.push("<tr>");
row.eachCell({ includeEmpty: true }, (cell) => {
const value = cell.value?.toString() || "";
const styles: string[] = [];
// 背景色
if (cell.fill && "fgColor" in cell.fill && cell.fill.fgColor) {
const bgColor =
typeof cell.fill.fgColor === "object" && "argb" in cell.fill.fgColor
? cell.fill.fgColor.argb
: undefined;
if (bgColor) {
// ARGBからRGBに変換 (最初の2桁のアルファ値を除く)
const rgb = bgColor.length === 8 ? bgColor.substring(2) : bgColor;
styles.push(`background-color: #${rgb}`);
}
}
// フォント色
if (cell.font?.color && "argb" in cell.font.color) {
const fontColor = cell.font.color.argb;
if (fontColor) {
const rgb = fontColor.length === 8 ? fontColor.substring(2) : fontColor;
styles.push(`color: #${rgb}`);
}
}
// 太字
if (cell.font?.bold) {
styles.push("font-weight: bold");
}
// イタリック
if (cell.font?.italic) {
styles.push("font-style: italic");
}
// 下線
if (cell.font?.underline) {
styles.push("text-decoration: underline");
}
// フォントサイズ
if (cell.font?.size) {
styles.push(`font-size: ${cell.font.size}pt`);
}
// テキスト配置
if (cell.alignment?.horizontal) {
styles.push(`text-align: ${cell.alignment.horizontal}`);
}
const styleAttr = styles.length > 0 ? ` style="${styles.join("; ")}"` : "";
htmlParts.push(`<td${styleAttr}>${value}</td>`);
});
htmlParts.push("</tr>");
});
htmlParts.push("</table>");
return htmlParts.join("\n");
}
検証結果
質問 | 結果 | 備考 |
---|---|---|
9/22が休みの人 | ✅ 3/3 | HTML形式でテーブル構造が保たれる |
佐々木さんの休み | ✅ 3/3 | HTML形式でテーブル構造が保たれる |
要件定義の担当・期間 | ⚠️ 1/3 | HTMLでセルの色情報が取得できた、失敗の2回は終了日が1日前にずれた |
リリース準備の期間 | ⚠️ 1/3 | セルの色情報が取得できたが、失敗の2回は終了日が1日前で回答 |
B店舗が一番良い月 | ❌ 0/3 | 変換方法が悪く、太文字の店舗名が「Object」として変換され、店舗名の判別ができない |
グラフから赤と黄色の差 | ❌ 0/3 | グラフ情報が反映されていない |
ステップ1のボタン位置 | ❌ 0/3 | 画像情報が含まれていないため、回答不可 |
ステップ2のボタン位置 | ❌ 0/3 | 画像情報が含まれていないため、回答不可 |
考察
ExcelJSを使った詳細なHTML変換により、背景色、フォントスタイル、テキスト配置などの情報を網羅的に取得できた。これにより、ガントチャートの「要件定義」や「リリース準備」の期間を色から判別でき、出勤表のような表データでもJSON同様に高い精度を示した。
ただし、精度は不安定で「終了日の1日ズレ」が多く、実装コードも複雑になる。日付等のフォーマットの反映やセル結合の処理など、さらに調整を加えれば精度向上が見込めそう。
グラフや画像は依然として含まれないため、視覚的な情報を必要とする質問には答えられない。スタイル情報の取得には成功したものの、実装コストと精度のバランスを考慮する必要がある。
4. 調理法:揚げ物(PDF画像変換)
考え方
ExcelをPDFに変換し、Base64エンコードしてFileとしてGeminiに渡す。視覚的な情報(色、レイアウト、グラフ)を完全に保持。
シンプルなLibreOfficeのコマンド実行だけではPDF変換時にコンテンツがちぎれてしまう問題があるため、テンプレートファイルのページ設定を適用する方式を採用。Excel → ODS → ページ設定適用 → PDF という段階的な変換プロセスにより、今回のデータにおいては綺麗にPDF変換できた。
実装コード
import fs from "fs";
import path from "path";
import childProcess from "child_process";
import JSZip from "jszip";
async function convertExcelToPDF(filePath: string): Promise<{ buffer: Buffer; savedPath: string }> {
// パスの設定
const absolutePath = path.resolve(filePath);
const outputDir = path.dirname(absolutePath);
const baseName = path.basename(absolutePath, path.extname(absolutePath));
const savedPath = path.join(outputDir, `${baseName}.pdf`);
const templatePath = path.join(__dirname, "test-data", "excel-pdf-template.ods");
// 1. ExcelをODSに変換
const sourceOdsPath = path.join(outputDir, `${baseName}_source.ods`);
const generatedOdsPath = path.join(outputDir, `${baseName}.ods`);
const convertToOdsCmd = `soffice --headless --convert-to ods --outdir "${outputDir}" "${absolutePath}"`;
childProcess.execSync(convertToOdsCmd);
fs.renameSync(generatedOdsPath, sourceOdsPath);
// 2. ODSファイルを読み込み
const templateBuffer = fs.readFileSync(templatePath);
const sourceBuffer = fs.readFileSync(sourceOdsPath);
const templateZip = await JSZip.loadAsync(templateBuffer);
const sourceZip = await JSZip.loadAsync(sourceBuffer);
// 3. テンプレートのページ設定(styles.xml)を適用
const templateStylesXml = await templateZip.file("styles.xml")?.async("text");
if (templateStylesXml) {
sourceZip.file("styles.xml", templateStylesXml);
}
// 4. 修正したODSファイルを再圧縮して保存
const mergedOdsPath = path.join(outputDir, `${baseName}_merged.ods`);
const mergedBuffer = await sourceZip.generateAsync({ type: "nodebuffer" });
fs.writeFileSync(mergedOdsPath, mergedBuffer);
// 5. 修正済みODSをPDFに変換
const generatedPdfPath = path.join(outputDir, `${baseName}_merged.pdf`);
const convertToPdfCmd = `soffice --headless --convert-to pdf --outdir "${outputDir}" "${mergedOdsPath}"`;
childProcess.execSync(convertToPdfCmd);
fs.renameSync(generatedPdfPath, savedPath);
// 一時ファイルを削除
fs.unlinkSync(sourceOdsPath);
fs.unlinkSync(mergedOdsPath);
// 変換されたPDFを読み込んで返す
const pdfBuffer = fs.readFileSync(savedPath);
return { buffer: pdfBuffer, savedPath };
}
検証結果
質問 | 結果 | 備考 |
---|---|---|
9/22が休みの人 | ❌ 0/3 | PDF画像では細かい関係性が読み取れなかった |
佐々木さんの休み | ❌ 0/3 | PDF画像では行と列の認識が難しい |
要件定義の担当・期間 | ✅ 3/3 | PDF画像情報から担当と期間を判別できた |
リリース準備の期間 | ⚠️ 1/3 | タスクが2つあることは洗い出せたが、期間が1日ずれていた |
B店舗が一番良い月 | ✅ 3/3 | 表とグラフから読み取れた |
グラフから赤と黄色の差 | ✅ 3/3 | グラフから赤色と黄色を判別し、表データから正確な差を判断 |
ステップ1のボタン位置 | ✅ 3/3 | Excelの見た目のまま画像やオブジェクトが反映されている |
ステップ2のボタン位置 | ✅ 3/3 | Excelの見た目のまま画像やオブジェクトが反映されている |
考察
PDF画像変換の最大の強みは、Excelの見た目をそのまま保持できること。色、グラフ、画像、フォントスタイルなど、視覚的な情報が反映される。そのため、ガントチャートの色から期間を判別したり、手順書の画像からボタン位置を特定したり、グラフの色から店舗を識別するといったタスクで高い精度を示した。
実装面では、LibreOfficeの単純なコマンド実行ではコンテンツがちぎれるため、テンプレートファイルを使ったページ設定の適用が必要となった。実装は複雑になるものの、綺麗なPDFを生成でき高い精度が出た。
しかし、出勤表のような細かい表データでは、行と列の関係性を正確に読み取れず、「9/22が休みの人」「佐々木さんの休み」といった質問に全く答えられなかった。PDF画像では、表の構造をテキストとして解析するのが難しいためと考えられる。
視覚的な情報が重要な場合には最適だが、細かいデータ抽出には不向き。単独での使用よりも、他の手法と組み合わせることで真価を発揮しそう。
5. 調理法:コース料理(ハイブリッド)
考え方
HTML変換とPDF画像変換の両方の結果をGeminiに同時に渡す手法。
新しい実装は不要で、すでにある2つの手法の結果を組み合わせるだけ。HTMLからテーブル構造と色情報を、PDF画像からビジュアル情報(グラフ、画像、レイアウト)を取得することで、それぞれの強みを活かし、弱点を補完し合う。
検証結果
質問 | 結果 | 備考 |
---|---|---|
9/22が休みの人 | ✅ 3/3 | HTMLのテーブル構造で行列関係を正確に把握 |
佐々木さんの休み | ✅ 3/3 | HTMLのテーブル構造で行列関係を正確に把握 |
要件定義の担当・期間 | ✅ 3/3 | HTMLの色情報とPDF画像の両方から期間を判別 |
リリース準備の期間 | ✅ 3/3 | HTMLの色情報とPDF画像の両方から期間を判別 |
B店舗が一番良い月 | ✅ 3/3 | HTMLの表データとPDF画像のグラフから取得 |
グラフから赤と黄色の差 | ✅ 3/3 | PDF画像のグラフから色を識別し、HTMLの表データから数値を取得 |
ステップ1のボタン位置 | ✅ 3/3 | PDF画像から画像情報を取得 |
ステップ2のボタン位置 | ✅ 3/3 | PDF画像から画像情報を取得 |
考察
ハイブリッド(HTML + PDF画像)は、全ての質問で3回とも正解という結果(怪しいけど今回の検証では全問正解してしまった...)。
HTMLからテーブル構造と色情報を、PDF画像から視覚的な情報(グラフ、画像、レイアウト)を取得することで、それぞれの弱点を補完し合えた。
HTMLの変換処理をさらに工夫(フォントスタイルや日付フォーマットの反映など)すれば、より高精度かつ汎用的な前処理が可能になる。ただし、実装コストとのトレードオフを考慮する必要がある。今回のシンプルな実装でも十分に実用的な結果が得られた。
実食!どの調理法が一番美味かったか?
結果まとめ
各手法の正解率を集計した結果がこちらです。
質問 | CSV | JSON | HTML | ハイブリッド | |
---|---|---|---|---|---|
出勤表:9/22が休みの人 | ✕ | ◎ | ◎ | ✕ | ◎ |
出勤表:佐々木さんの休み | △ | ◎ | ◎ | ✕ | ◎ |
ガントチャート:要件定義の担当・期間 | ✕ | ✕ | △ | ◎ | ◎ |
ガントチャート:リリース準備の期間 | ✕ | ✕ | △ | △ | ◎ |
売上表:B 店舗が一番良い月 | ◎ | ◎ | ✕ | ◎ | ◎ |
売上表:グラフから赤と黄色の差 | ✕ | ✕ | ✕ | ◎ | ◎ |
手順書:ステップ1のボタン位置 | ✕ | ✕ | ✕ | ◎ | ◎ |
手順書:ステップ2のボタン位置 | ✕ | ✕ | ✕ | ◎ | ◎ |
*: ◎ 3回とも正解 / △ 1-2回正解 / ✕ 全て不正解。
総合考察
CSV・JSONの限界
実装は簡単に済むものの、色情報・画像・グラフが一切扱えないため、ガントチャートや手順書では全滅。ただしJSONは構造化データとして優秀で、出勤表のような単純な表では精度を出せた。
HTML(ExcelJS使用)は一長一短
背景色やフォントスタイルを詳細に抽出できるため、ガントチャートの期間判別では正解もあったが精度は不安定。実装コードも複雑になる。日付フォーマットやセル結合の処理などを追加すれば精度向上が見込めそうだが、実装コストとのトレードオフが課題。
PDF画像は結構いい
視覚情報(色、グラフ、画像)を保持できるため、手順書やグラフ付き売上表で高精度。しかし出勤表のような細かい表データでは壊滅的。PDF画像では表の行列関係をテキストとして解析するのが困難なため、今後LLMの精度が上がれば解決しそう。
ハイブリッド(HTML + PDF)の勝利
HTMLが苦手な視覚情報はPDF画像が、PDF画像が苦手な表構造はHTMLがカバー。実装コストは高いですが、複雑なExcelを確実に処理したいなら、この組み合わせが今はベスト。
おわりに:美味いRAGのための次のステップ
今回は前処理に絞った検証だったが、今後は以下の観点を考えた方が良さそう!
- 調理済みデータをどうやって検索フェーズに乗せるか?
- チャンキング(分割)するならどの単位(テーブル単位、行単位)が良いか?
- プロンプトの改善や、多段処理等の回答時の工夫
- 新たなハイブリッドな手法
皆さんの現場ではどんなExcelレシピを使っていますか?ぜひコメントで教えてください!
Discussion