レシートからセルフメディケーションの抽出

セルフメディケーションの抽出

レシートの識別で

ピアゴのレシートはできた
なるべく拡大撮影した方がうまく識別できる

Coopも問題ない

ただし向きが縦向きになっていないと正しく認識しない

杏林堂は横向きでもできた

他のCreate やココカラファインでも
画像を拡大すればできる

つまり解像度も重要だが文字の大きさが重要
画像のズームなどが現時点では自動でできないし
向きの修正もできないため
まずは面倒だがレシートを人力で行い
その後に作成されたCSVファイルをDBに取り込むようにする

次に問題となるのがCSVファイルをどんどん追加でsqliteDBに後で追加できるかということ

GPTでのチャットが乱立しているので
SQLに関してのものを探す

レシート分析システム
レシート情報最適化提案
Graph RAG活用法
日用品食料品管理法

でプロンプトも重要

レシート画像から 購入した店舗名 購入日時 商品名 単価 を取り出しDBへ保存。
このデータを毎日蓄積していき 購入した店舗、商品価格から最適な店舗での購入を知らせるようにしたい。
レシートのOCRには gemini を使う また商品購入の間隔からどれくらいの期間で消費するか予測し、購入のタイミングを知らせるようにしたい。
理由としては、特売のときに購入しておけばコストを削減できるため

だけだと足りない

次にセルフメディケーションの抽出を試す

杏林堂なら商品名に♦️マークがつく
ピアゴならセ[ と商品名につく

ココカラファインはまだ試していない

Gemini で
DBで購入管理したい
そのために
購入店舗名
購入年月日
商品名
価格
個数
をCSVファイルにしたい

また食品なども同時に購入することがあるため
セルフメディケーション対象商品を識別できるようにしたい

この場合、◆をCSVにいれる以外の方法で何かあるか?

に対して

承知いたしました。セルフメディケーション税制対象商品を明確に識別しつつ、購入データをCSVで管理したいというご要望ですね。
◆マークをCSVに直接入れる以外の方法で、よりデータとして扱いやすく、かつ後から分析しやすい方法をいくつか提案します。
1. 専用の「セルフメディケーション対象フラグ」列を追加する
これが最も一般的で、データ管理のベストプラクティスです。

購入店舗名	購入年月日	商品名	価格	個数	セルフメディケーション対象
杏林堂袋井旭町店	2025/06/18	サンテビオ 15mL	871	1	TRUE (または 1, Y, ○など)
杏林堂袋井旭町店	2025/06/18	(食品A)	200	1	FALSE (または 0, N, ×など)
...	...	...	...	...	...

利点:
* 明確な識別: セルフメディケーション対象か否かが一目で分かります。
* フィルタリング・ソートが容易: CSVをDBにインポートしたり、スプレッドシートで開いたりした際に、「セルフメディケーション対象」列でフィルタリングやソートを行うことで、対象商品のみを簡単に抽出できます。
* 集計が容易: 対象商品の合計金額などを計算する際に、このフラグ列を条件に設定することで簡単に集計できます。
* データの一貫性: 「◆」のような記号を直接入れるよりも、TRUE/FALSEや1/0といった標準的なデータ形式の方が、後のデータ処理(プログラミングによる集計など)がしやすくなります。
* OCRとの連携: OCRで「◆」マークや「セルフメディケーション対象」という文字列を認識した場合、その情報に基づいてこのフラグ列に自動的に「TRUE」を設定するようなシステムを構築することも可能です。
2. 「セルフメディケーション対象金額」列を追加する(より詳細な管理)
セルフメディケーション対象の商品であればその金額を、そうでなければ0円を記入する列を設ける方法です。これにより、対象金額の合計がより簡単に算出できます。

購入店舗名	購入年月日	商品名	価格	個数	セルフメディケーション対象金額
杏林堂袋井旭町店	2025/06/18	サンテビオ 15mL	871	1	871
杏林堂袋井旭町店	2025/06/18	(食品A)	200	1	0
...	...	...	...	...	...

利点:
* セルフメディケーション対象金額の合計が、この列を単純に合計するだけで算出できます。
* 「セルフメディケーション対象」かどうかのフラグも兼ねることができます(金額が0でなければ対象)。
OCRで実現する場合の考慮事項:
* OCRでレシートを読み取った際に、商品名に「◆」があることを認識し、その商品に対応する価格を「セルフメディケーション対象金額」の列に転記するロジックが必要になります。
* 食品などの非対象商品は、この列には0を自動で入力するように設定します。
どちらを選ぶべきか?
* シンプルに「対象か否か」だけを識別したいなら、「セルフメディケーション対象フラグ」列(1番目の案)がおすすめです。 後から合計金額を出すのは簡単です。
* 合計金額の算出をより直接的に行いたい場合や、金額をベースに集計を行いたい場合は、「セルフメディケーション対象金額」列(2番目の案)も有効です。
どちらの方法でも、CSVファイルとしては非常に扱いやすくなります。DBに取り込んだ後も、この列を使って柔軟な集計や分析が可能になります。
CSVファイルの例(1番目の案を採用した場合):
コード スニペット

購入店舗名,購入年月日,商品名,価格,個数,セルフメディケーション対象
杏林堂袋井旭町店,2025/06/18,サンテビオ 15mL,871,1,TRUE
杏林堂袋井旭町店,2025/06/18,牛乳,250,1,FALSE
杏林堂袋井旭町店,2025/06/18,パン,150,2,FALSE
杏林堂袋井旭町店,2025/06/18,〇〇鎮痛剤,1280,1,TRUE

とりあえずCSVファイルを作成できるので

mkdir purcharge_history

で作業ディレクトリの作成

購入店舗名,購入年月日,商品名,価格,個数,セルフメディケーション対象金額 杏林堂 袋井旭町店,2025/06/18,サンテビオ 15mL,871,1,871 ピアゴ 袋井店,2025/04/18,タイレノールA 30錠,1772,1,1772 

このcsvファイルを 日付_購入店舗名.csv で作成
とchatfgpt で行ったが
ファイルは作成されない
面倒なので gemini で作成する

vim 20250618_杏林堂袋井旭町店.csv

中身を

購入店舗名,購入年月日,商品名,価格,個数,セルフメディケーション対象金額
杏林堂 袋井旭町店,2025/06/18,サンテビオ 15mL,871,1,871
vim 20250418_ピアゴ袋井店.csv
中身を
購入店舗名,購入年月日,商品名,価格,個数,セルフメディケーション対象金額
ピアゴ 袋井店,2025/04/18,タイレノールA 30錠,1772,1,1772

コープとクリエイトで試したら単品価格がないので
単品価格のカラムの追加

一度

rm *.csv

で削除して作り直す

 vim 20240618-19.csv

内容を

購入店舗名,購入年月日,商品名,単品税抜価格,価格,個数,セルフメディケーション対象金額
杏林堂 袋井旭町店,2025/06/18,サンテビオ 15mL,968,871,1,871
ピアゴ 袋井店,2025/04/18,タイレノールA 30錠,1790,1772,1,1772
COOP ユーコープ 袋井町店,2025/06/19,COゼリーエネルギー,118,354,3,0
COOP ユーコープ 袋井町店,2025/06/19,鮭・くず桜,238,190,1,0
COOP ユーコープ 袋井町店,2025/06/19,天ぷら用えび,398,238,1,0
COOP ユーコープ 袋井町店,2025/06/19,たい,398,398,1,0
COOP ユーコープ 袋井町店,2025/06/19,塩銀鮭切身,398,398,1,0
COOP ユーコープ 袋井町店,2025/06/19,真あじ開き,398,796,2,0
ドラッグストア クリエイト 袋井山名店,2025/06/18,日興フーズ 王様のチーズ,298,298,1,0
ドラッグストア クリエイト 袋井山名店,2025/06/18,ロイヤルブレッド山型(5),139,139,1,0
ドラッグストア クリエイト 袋井山名店,2025/06/18,家族の潤い白桃ミックス,108,216,2,0
ドラッグストア クリエイト 袋井山名店,2025/06/18,鶏卵卵パック MS 10個,228,228,1,0

で保存

Geminiでは回数制限があるようで時間をおけばできるが10回ぐらいで
無料枠が終わる
Pro にすると3回が限界

残りは chatGPT 4.5 で作成した

vim 20250614-19.csv

内容を

購入店舗名,購入年月日,商品名,単品税抜価格,価格,個数,セルフメディケーション対象金額
杏林堂 袋井旭町店,2025/06/18,サンテビオ 15mL,968,871,1,871
ピアゴ 袋井店,2025/04/18,タイレノールA 30錠,1790,1772,1,1772
COOP ユーコープ 袋井町店,2025/06/19,COゼリーエネルギー,118,354,3,0
COOP ユーコープ 袋井町店,2025/06/19,鮭・くず桜,238,190,1,0
COOP ユーコープ 袋井町店,2025/06/19,天ぷら用えび,398,238,1,0
COOP ユーコープ 袋井町店,2025/06/19,たい,398,398,1,0
COOP ユーコープ 袋井町店,2025/06/19,塩銀鮭切身,398,398,1,0
COOP ユーコープ 袋井町店,2025/06/19,真あじ開き,398,796,2,0
ドラッグストア クリエイト 袋井山名店,2025/06/18,日興フーズ 王様のチーズ,298,298,1,0
ドラッグストア クリエイト 袋井山名店,2025/06/18,ロイヤルブレッド山型(5),139,139,1,0
ドラッグストア クリエイト 袋井山名店,2025/06/18,家族の潤い白桃ミックス,108,216,2,0
ドラッグストア クリエイト 袋井山名店,2025/06/18,鶏卵卵パック MS 10個,228,228,1,0
ドラッグストア クリエイト 袋井山名店,2025/06/17,ぶなしめじ,98,98,1,0
ドラッグストア クリエイト 袋井山名店,2025/06/17,森永バター 200g,458,412,1,0
ドラッグストア クリエイト 袋井山名店,2025/06/17,鶏卵パック MS 10個,228,228,1,0
COOP ユーコープ 袋井町店,2025/06/17,バジルMIXピッツァ,428,342,1,0
COOP ユーコープ 袋井町店,2025/06/17,若鶏ムネ肉,594,594,1,0
COOP ユーコープ 袋井町店,2025/06/17,ボスコEVオリーブオイル,898,898,1,0
COOP ユーコープ 袋井町店,2025/06/17,豚小間切れ,190,190,1,0
COOP ユーコープ 袋井町店,2025/06/17,赤魚粕漬け,398,398,1,0
COOP ユーコープ 袋井町店,2025/06/17,ししゃも,298,298,1,0
COOP ユーコープ 袋井町店,2025/06/17,塩銀鮭切身,597,597,1,0
COOP ユーコープ 袋井町店,2025/06/17,真あじ開き,398,796,2,0
COOP ユーコープ 袋井町店,2025/06/17,なす/袋,138,138,1,0
COOP ユーコープ 袋井町店,2025/06/17,まいたけ,88,88,1,0
COOP ユーコープ 袋井町店,2025/06/17,COゼリーエネルギー,118,354,3,0
COOP ユーコープ 袋井町店,2025/06/17,フィリピンバナナ,138,276,2,0
COOP ユーコープ 袋井町店,2025/06/14,シュガーコーン,278,278,1,0
COOP ユーコープ 袋井町店,2025/06/14,CO冷凍茶豆,158,158,1,0
COOP ユーコープ 袋井町店,2025/06/14,豚小間,535,535,1,0
COOP ユーコープ 袋井町店,2025/06/14,生豚挽肉,522,522,1,0
COOP ユーコープ 袋井町店,2025/06/14,豚ヒレかたまりカナダ,630,567,1,0
COOP ユーコープ 袋井町店,2025/06/14,かつおたたき,398,398,1,0
COOP ユーコープ 袋井町店,2025/06/14,塩銀鮭切身,498,996,2,0
COOP ユーコープ 袋井町店,2025/06/14,真あじ開き,398,1194,3,0
COOP ユーコープ 袋井町店,2025/06/14,COゼリーエネルギー60F,178,178,1,0
COOP ユーコープ 袋井町店,2025/06/14,COジョウエラミックスナッツ,298,298,1,0
COOP ユーコープ 袋井町店,2025/06/14,黄金しょうが,178,178,1,0
とれたて食楽部,2025/06/14,イチゴ/鈴木康友,230,230,1,0
とれたて食楽部,2025/06/14,雑貨/小路仁徳,390,390,1,0
とれたて食楽部,2025/06/14,コーヒー/濱小路徳,1090,1090,1,0
とれたて食楽部,2025/06/14,アーリーレッド,230,230,1,0
とれたて食楽部,2025/06/14,きゅうり/丸数農園,150,150,1,0
とれたて食楽部,2025/06/14,白ねぎ/鈴木正孝,170,170,1,0
とれたて食楽部,2025/06/14,バラ/大場安輝子,300,150,1,0
とれたて食楽部,2025/06/14,なす/大石真智子,260,130,1,0
とれたて食楽部,2025/06/14,レタス/山田さち子,140,140,1,0
とれたて食楽部,2025/06/14,ピーマン/小林宗作,140,140,1,0
とれたて食楽部,2025/06/14,じゃが芋/澤口正則,250,250,1,0
とれたて食楽部,2025/06/14,大根/石原義浩,150,150,1,0
とれたて食楽部,2025/06/14,菌床椎茸/永井晃太,260,260,1,0

として保存

次に店舗ごとのcsvの作成

 vim 20250613.csv

購入店舗名,購入年月日,商品名,単品税抜価格,価格,個数,セルフメディケーション対象金額
遠鉄ストア 浅羽店,2025/06/13,イチジク習慣,398,1592,4,0
遠鉄ストア 浅羽店,2025/06/13,はごろも シーチキンLフ,338,338,1,0
vim 20250612.csv

購入店舗名,購入年月日,商品名,単品税抜価格,価格,個数,セルフメディケーション対象金額
ピアゴ 袋井店,2025/06/12,明治おいしい牛乳900ml,247,247,1,0
ピアゴ 袋井店,2025/06/12,明和水産 切身・丸魚,478,478,1,0
ピアゴ 袋井店,2025/06/12,だしのきいたなめらか玉子,128,128,1,0
ピアゴ 袋井店,2025/06/12,おかめ納豆 まろやか旨味ミ,97,97,1,0
vim 20250605.csv

購入店舗名,購入年月日,商品名,単品税抜価格,価格,個数,セルフメディケーション対象金額
とれたて食楽部,2025/06/05,その他切り花/竹原秀,210,210,1,0
とれたて食楽部,2025/06/05,コーヒー/満り路仁徳,1090,1090,1,0
とれたて食楽部,2025/06/05,ベビーリーフ/南グロー,120,120,1,0
とれたて食楽部,2025/06/05,きゅうり/河井 恵美,100,100,1,0
とれたて食楽部,2025/06/05,レタス/山田さち子,150,150,1,0
とれたて食楽部,2025/06/05,豚小間切,336,336,1,0
とれたて食楽部,2025/06/05,静岡そだち牛肉入りコロ,390,390,1,0
とれたて食楽部,2025/06/05,ルッコラ/Gファーム,180,180,1,0
とれたて食楽部,2025/06/05,ごぼう/とれたて食楽部,280,280,1,0
vim 20250611.csv

購入店舗名,購入年月日,商品名,単品税抜価格,価格,個数,セルフメディケーション対象金額
ドラッグストア クリエイト 袋井山名店,2025/06/11,鶏卵パック MS 10個,228,228,1,0
ドラッグストア クリエイト 袋井山名店,2025/06/11,ロイヤルブレッド(6),139,139,1,0
ドラッグストア クリエイト 袋井山名店,2025/06/11,フルーツフェスタアップル,198,198,1,0
ドラッグストア クリエイト 袋井山名店,2025/06/11,フルーツフェスタフルーツ,198,198,1,0
vim 20250621.csv

購入店舗名,購入年月日,商品名,単品税抜価格,価格,個数,セルフメディケーション対象金額
ドラッグストア クリエイト 袋井山名店,2025/06/21,ロイヤルブレッド(6),139,139,1,0
ドラッグストア クリエイト 袋井山名店,2025/06/21,日興フーズ 王様のチーズ,298,298,1,0
ドラッグストア クリエイト 袋井山名店,2025/06/21,家族の潤いマスカット,108,108,1,0
ドラッグストア クリエイト 袋井山名店,2025/06/21,家族の潤い白桃ミックス,108,216,2,0
ドラッグストア クリエイト 袋井山名店,2025/06/21,ビオヨーグルト プレーン加糖,178,356,2,0
ドラッグストア クリエイト 袋井山名店,2025/06/21,雪印メグミルク 牧場の朝ヨー,99,198,2,0
ドラッグストア クリエイト 袋井山名店,2025/06/21,ブルボン デザートマルシェ白,93,278,3,0
ドラッグストア クリエイト 袋井山名店,2025/06/21,フルーツフェスタフルーツミックス,198,396,2,0
ドラッグストア クリエイト 袋井山名店,2025/06/21,鶏卵パック MS 10個,228,228,1,0

これだと商品名が違っていたので修正する

購入店舗名,購入年月日,商品名,単品税抜価格,価格,個数,セルフメディケーション対象金額
ドラッグストア クリエイト 袋井山名店,2025/06/21,ロイヤルブレッド(6),139,139,1,0
ドラッグストア クリエイト 袋井山名店,2025/06/21,日興フーズ 王様のデーツ,298,298,1,0
ドラッグストア クリエイト 袋井山名店,2025/06/21,家族の潤いマスカット,108,108,1,0
ドラッグストア クリエイト 袋井山名店,2025/06/21,家族の潤い白桃ミックス,108,216,2,0
ドラッグストア クリエイト 袋井山名店,2025/06/21,ビオヨーグルト プレーン加糖,178,356,2,0
ドラッグストア クリエイト 袋井山名店,2025/06/21,雪印メグミルク 牧場の朝ヨー,99,198,2,0
ドラッグストア クリエイト 袋井山名店,2025/06/21,ブルボン デザートマルシェ白,93,278,3,0
ドラッグストア クリエイト 袋井山名店,2025/06/21,フルーツフェスタフルーツミックス,198,396,2,0
ドラッグストア クリエイト 袋井山名店,2025/06/21,鶏卵パック MS 10個,228,228,1,0

が正解

これを使ってsqliteにインポートする

yomitokuでレシートOCR

yomitokuでレシートOCR

pip install yomitoku

でインストール

https://qiita.com/kanzoo/items/9d382fe4ec991a7eacd2
を参考に

VRAMは8GB以内で動作

# HTMLファイルを出力
yomitoku ./images -f html
# JSONファイルを出力
yomitoku ./images -f json
# Markdownファイルを出力
yomitoku ./images -f md
# CSVファイルを出力
yomitoku ./images -f csv

で実行

実行データは
https://template.the-board.jp/invoice_templates/article/invoice_template_001/
を使っている

コマンドを実行すると、最初にモデルをダウンロードします。これは自動的に行われ、初回のみで2回目以降はない

解析を実行し、 results ディレクトリに結果が出力

実行時間はGPUによって異なりますが、 Google ColabのA100で実行した場合は2.5〜3.5秒程度でした。T4 GPUなどにしても高速になる訳ではない

HTMLでは、表部分が table タグで出力

JSONファイルの場合
表になっている部分は table キー以下にあるので、
システム連携する際にはこの部分を利用します。 box は認識された座標

Markdownの場合、表の部分はMarkdownのテーブル形式で出力

CSVの場合、表の部分はCSV形式で出力されます。Excelなどで開いた際にも、レイアウトが維持されているので確認しやすい

コマンドオプション -v を付けると、解析結果を可視化した画像を出力

yomitoku ./images -f html -v

レシートの実例を探した

https://qiita.com/ayoyo/items/033b5d46acc641b91208
にサンクスのレシートで試しているのがあるので
これを使う

! yomitoku 0003.jpg -f json -o results -v --figure

が実行コマンド

とりあえず実践

 mkdir receipt
cd receipt 
mv ../PXL_20250531_012457081* .

で実験フォルダに移動

yomitoku PXL_20250531_002845534.jpg -f json -o results -v --figure

で実行

2025-06-10 06:08:32,215 - yomitoku.base - INFO - Initialize TextDetector
2025-06-10 06:08:34,123 - yomitoku.base - WARNING - CUDA is not available. Use CPU instead.
2025-06-10 06:08:34,125 - yomitoku.base - INFO - Initialize TextRecognizer
2025-06-10 06:08:35,802 - yomitoku.base - WARNING - CUDA is not available. Use CPU instead.
2025-06-10 06:08:35,803 - yomitoku.base - INFO - Initialize LayoutParser
2025-06-10 06:08:37,339 - yomitoku.base - WARNING - CUDA is not available. Use CPU instead.
2025-06-10 06:08:37,342 - yomitoku.base - INFO - Initialize TableStructureRecognizer
2025-06-10 06:08:38,195 - yomitoku.base - WARNING - CUDA is not available. Use CPU instead.
2025-06-10 06:08:38,200 - yomitoku.cli.main - INFO - Output directory: results
2025-06-10 06:08:38,200 - yomitoku.cli.main - INFO - Processing file: PXL_20250531_002845534.jpg
2025-06-10 06:08:39,332 - yomitoku.base - INFO - LayoutParser __call__ elapsed_time: 1.0590169429779053
2025-06-10 06:08:39,381 - yomitoku.base - INFO - TableStructureRecognizer __call__ elapsed_time: 0.04888606071472168
2025-06-10 06:08:41,263 - yomitoku.base - INFO - TextDetector __call__ elapsed_time: 2.9893980026245117
2025-06-10 06:08:48,173 - yomitoku.base - INFO - TextRecognizer __call__ elapsed_time: 6.865458965301514
2025-06-10 06:08:48,226 - yomitoku.cli.main - INFO - Output file: results/_PXL_20250531_002845534_p1_ocr.jpg
2025-06-10 06:08:48,256 - yomitoku.cli.main - INFO - Output file: results/_PXL_20250531_002845534_p1_layout.jpg
2025-06-10 06:08:48,259 - yomitoku.cli.main - INFO - Total Processing time: 10.06 sec

結果は resultsフォルダに入っているので
JSONファイルをみると結果が見れる

また、どのように認識しているかを画像で見ることができる

実際に行った結果はgeminiで抽出した結果と変わらないため
レシートによってできないものもある

レシートのOCR

レシートのOCR

yomitokuもしくは Google Cloud Vision API または
Gemini を使ってレシートをOCRして解析結果から
店舗名、商品名、商品の価格、購入年月日、購入した商品の数量を抽出する

なお
Google Cloud Vision API でもある程度はできるが
購入数量が複数の場合はまとめた金額が表示されてしまう
また、直売所のようなところで購入した場合
生産者の名前が商品名に含まれるのでこれを削除する必要がある

コードである程度の添削は可能だが
店舗ごとに処理を分ける必要があったため
以前、Google Cloud Vision API でOCR結果をLLMでCSVにしたが
うまくいかなかったため
今回はgemini で行ってみた

このレシートから
店舗名、商品名、商品の価格、購入年月日、購入した商品の数量を抽出して

結果は
承知いたしました。提供されたレシート画像から、店舗名、商品名、商品の価格、購入年月日、購入した商品の数量を抽出します。

抽出結果:
* 店舗名: とれたて食菜館
* 購入年月日: 2025年5月31日
* 商品と価格、数量:
    1. 商品名: イチゴ / 鈴木康友
        * 価格: ¥350
        * 数量: 1 (画像からは数量を特定できませんが、一般的にこの形式では1点と解釈されます)
    2. 商品名: ニラ
        * 価格: ¥110
        * 数量: 1
    3. 商品名: 加子 / 山本 貢
        * 価格: ¥160
        * 数量: 1
    4. 商品名: 新玉ねぎ&さや / 守屋 昇
        * 価格: ¥180
        * 数量: 1
    5. 商品名: コーヒー / 浅路小徳
        * 価格: ¥1,090
        * 数量: 1
    6. 商品名: ミニトマト
        * 価格: ¥230
        * 数量: 1
    7. 商品名: きゅうり / 稲田政正
        * 価格: ¥150
        * 数量: 1
    8. 商品名: 白ねぎ / 鈴木正孝
        * 価格: ¥180
        * 数量: 1
    9. 商品名: キャベツ / 鈴木正孝
        * 価格: ¥100
        * 数量: 1
    10. 商品名: バラ / 山崎きぬ子
        * 価格: ¥300
        * 数量: 1
    11. 商品名: レタス / 山田さち子
        * 価格: ¥140
        * 数量: 1
    12. 商品名: 乾しいたけ / 大石礼店
        * 価格: ¥620
        * 数量: 1
    13. 商品名: 園味椎茸 / 永井克太
        * 価格: ¥260
        * 数量: 1

補足:
* レシートの各項目には購入数量が明示されていませんが、一般的に商品の価格がそのまま記載されている場合は「1点」の購入と解釈されます。
* 商品名の後ろに記載されている「鈴木康友」などの名前は、生産者名であると推測されます。

この結果をCSVファイルにして

店舗名,購入年月日,商品名,価格,数量
とれたて食菜館,2025年5月31日,イチゴ / 鈴木康友,350,1
とれたて食菜館,2025年5月31日,ニラ,110,1
とれたて食菜館,2025年5月31日,加子 / 山本 貢,160,1
とれたて食菜館,2025年5月31日,新玉ねぎ&さや / 守屋 昇,180,1
とれたて食菜館,2025年5月31日,コーヒー / 浅路小徳,1090,1
とれたて食菜館,2025年5月31日,ミニトマト,230,1
とれたて食菜館,2025年5月31日,きゅうり / 稲田政正,150,1
とれたて食菜館,2025年5月31日,白ねぎ / 鈴木正孝,180,1
とれたて食菜館,2025年5月31日,キャベツ / 鈴木正孝,100,1
とれたて食菜館,2025年5月31日,バラ / 山崎きぬ子,300,1
とれたて食菜館,2025年5月31日,レタス / 山田さち子,140,1
とれたて食菜館,2025年5月31日,乾しいたけ / 大石礼店,620,1
とれたて食菜館,2025年5月31日,園味椎茸 / 永井克太,260,1

CSVファイルの内容について:
* 1行目はヘッダー行で、各列が何のデータであるかを示しています。
* 各行が1つの商品の情報に対応しており、店舗名と購入年月日も各行に繰り返して記載しています。
* 価格は数字のみを記載し、通貨記号「¥」は除外しています。
* 数量は、レシートに明記されていないため、一般的な解釈として「1」としています。

ここまでは cloud Vision API と同じ

CSVファイルにするときに、商品名から人物名を削除して

店舗名,購入年月日,商品名,価格,数量
とれたて食菜館,2025年5月31日,イチゴ,350,1
とれたて食菜館,2025年5月31日,ニラ,110,1
とれたて食菜館,2025年5月31日,加子,160,1
とれたて食菜館,2025年5月31日,新玉ねぎ&さや,180,1
とれたて食菜館,2025年5月31日,コーヒー,1090,1
とれたて食菜館,2025年5月31日,ミニトマト,230,1
とれたて食菜館,2025年5月31日,きゅうり,150,1
とれたて食菜館,2025年5月31日,白ねぎ,180,1
とれたて食菜館,2025年5月31日,キャベツ,100,1
とれたて食菜館,2025年5月31日,バラ,300,1
とれたて食菜館,2025年5月31日,レタス,140,1
とれたて食菜館,2025年5月31日,乾しいたけ,620,1
とれたて食菜館,2025年5月31日,園味椎茸,260,1

変更点:
* 「イチゴ / 鈴木康友」 → 「イチゴ」
* 「加子 / 山本 貢」 → 「加子」
* 「新玉ねぎ&さや / 守屋 昇」 → 「新玉ねぎ&さや」
* 「コーヒー / 浅路小徳」 → 「コーヒー」
* 「きゅうり / 稲田政正」 → 「きゅうり」
* 「白ねぎ / 鈴木正孝」 → 「白ねぎ」
* 「キャベツ / 鈴木正孝」 → 「キャベツ」
* 「バラ / 山崎きぬ子」 → 「バラ」
* 「レタス / 山田さち子」 → 「レタス」
* 「乾しいたけ / 大石礼店」 → 「乾しいたけ」
* 「園味椎茸 / 永井克太」 → 「園味椎茸」

段階的に処理したけど目的は達成できる

次に個数の問題のテスト

今回は別のレシートを使う
しかし
クリエイト、ピアゴのレシートではうまくいかない
横向きなのを縦にして実験してみる

判定を自動化できるかgeminiでためした
このレシートは横向き、縦向きどっち?

このレシート画像は、横向き です。
文字が右から左に流れるように見え、日付や金額の表記も横方向になっているため、画像を時計回りに90度回転させると、文字が正しく上から下へ読める縦向きになると思われます。 提供されたレシート画像は、縦向きです。
画像の左側に「ピアゴ」のロゴがあり、その下に文字が縦書きで並び、右側には商品名や価格が横書きで並んでいますが、全体としてはレシートの長い辺が縦になるように撮影されています。
この向きで情報を抽出することができますので、ご安心ください。

となるので gemini で回転しようとしたけど
Geminiでは画像の回転ができない

pip install Pillow

で必要なライブラリインストール

vim rotate_image.py

で中身を

from PIL import Image
import os

def rotate_image_90_degrees(image_path, output_path=None, clockwise=True):
    """
    指定された画像を90度回転させる関数。

    Args:
        image_path (str): 回転させたい画像のパス。
        output_path (str, optional): 回転後の画像を保存するパス。
                                     指定しない場合、元のファイル名に '_rotated' を追加して保存。
        clockwise (bool): Trueの場合時計回りに90度、Falseの場合反時計回りに90度回転。
    """
    try:
        # 画像を開く
        img = Image.open(image_path)
        print(f"画像をロードしました: {image_path}")

        # 回転方向を決定
        if clockwise:
            rotated_img = img.rotate(-90, expand=True) # 時計回りに90度回転 (反時計回りの-90度と同じ)
            rotation_direction = "時計回り"
        else:
            rotated_img = img.rotate(90, expand=True)  # 反時計回りに90度回転
            rotation_direction = "反時計回り"

        # 出力パスが指定されていない場合、自動生成
        if output_path is None:
            base, ext = os.path.splitext(image_path)
            output_path = f"{base}_rotated{ext}"

        # 回転した画像を保存
        rotated_img.save(output_path)
        print(f"{rotation_direction}に90度回転した画像を保存しました: {output_path}")

    except FileNotFoundError:
        print(f"エラー: 指定されたファイルが見つかりません - {image_path}")
    except Exception as e:
        print(f"画像の回転中にエラーが発生しました: {e}")

if __name__ == "__main__":
    # --- 使用例 ---
    # 回転したい画像のファイル名
    # スクリプトと同じディレクトリにある場合、ファイル名のみでOK
    # 別の場所にある場合、'path/to/your/image.jpg' のようにフルパスを指定
    input_image_name = "PXL_20250531_012457081.jpg" # ← ここを回転させたい画像ファイル名に置き換えてください

    # 回転後の画像の保存パス (Noneにすると自動で '_rotated' が追加される)
    output_image_name = None # 例: "rotated_image.jpg"

    # 時計回りに回転するかどうか (True: 時計回り, False: 反時計回り)
    rotate_clockwise = True

    rotate_image_90_degrees(input_image_name, output_image_name, rotate_clockwise)

    print("\n-----------------------------------------------------")
    print("スクリプトの実行が完了しました。")
    print(f"回転後の画像は '{input_image_name}' (または指定したパス) の場所に保存されています。")
    print("-----------------------------------------------------\n")

これを

cp ~/Downloads/PXL_20250531_012457081.jpg . 
python rotate_image.py PXL_20250531_012457081.jpg        
画像をロードしました: PXL_20250531_012457081.jpg
時計回りに90度回転した画像を保存しました: PXL_20250531_012457081_rotated.jpg

-----------------------------------------------------
スクリプトの実行が完了しました。

として
再度 gemini で実行したけど結果は変わらない

レシートの向きは関係ないらしい

* クエリが正常に完了しました


承知いたしました。90度回転された「ピアゴ」のレシート画像から、店舗名、商品名、商品の価格、購入年月日、購入した商品の数量を抽出します。

抽出結果:
* 店舗名: ピアゴ
* 購入年月日: 2025年5月31日
* 商品と価格、数量:
    1. 商品名: コミックス
        * 価格: ¥436
        * 数量: 1
    2. 商品名: 大豆もやし 1.900M
        * 価格: ¥288
        * 数量: 1
    3. 商品名: 明治おやつヨーグルト
        * 価格: ¥130
        * 数量: 1
    4. 商品名: Bio 旬摘みフルーツ
        * 価格: ¥178
        * 数量: 1
    5. 商品名: Bio プチダノン
        * 価格: ¥90
        * 数量: 1
    6. 商品名: 減塩カツオ,2個
        * 価格: ¥198
        * 数量: 1
    7. 商品名: 青森県産にんにく
        * 価格: ¥165
        * 数量: 1
    8. 商品名: 鶏肉もも肉
        * 価格: ¥645
        * 数量: 1
    9. 商品名: 大根
        * 価格: ¥323
        * 数量: 1
    10. 商品名: 信州田舎造りみそ
        * 価格: ¥136
        * 数量: 1
    11. 商品名: 田中商店もめんとうふ
        * 価格: ¥116
        * 数量: 1
    12. 商品名: (品名不明、単価 ¥271)
        * 価格: ¥813 (271 x 3個と推測)
        * 数量: 3
    13. 商品名: まとめ値引き
        * 価格: -¥8
        * 数量: 1

補足:
* 回転後の画像でも、前回の画像と同様に情報を読み取ることができました。
* レシートの各項目には購入数量が明示されていませんが、一般的にこの形式では1点と解釈されます。ただし、12番目の商品については「3個」の記載があるため、数量を3としました。価格も271円が3個で813円と計算されているようです。
* 商品名の一部には、ブランド名や種類が含まれています。
* 最後の「まとめ値引き」は商品というよりは値引き項目ですが、形式に合わせて抽出しています。
* 商品名の後ろの「JAN」や数字は、商品のJANコードや識別番号と思われます。

プロンプトでは限界っぽいので yomitoku を使うことにする

RAGデータの作成

RAGデータの作成

GoogleDrive に
学校関連はSchool
特に使うのがshizensaigai.pdfに書いてある
休校や自然災害の時の対処方法

保険フォルダにある
自動車保険、県民共済の約款

取扱説明書フォルダにある
各種家電の取説

これらをダウンロードしてRAGにする

まず以前wikipediaから一部記事をダウンロードしRAGにした

/aw10s/ollama/save_index_test.py

のコード

import ollama
from llama_index.core import Settings
from llama_index.llms.ollama import Ollama
from llama_index.embeddings.huggingface import HuggingFaceEmbedding
from llama_index.readers.web import SimpleWebPageReader
from llama_index.core.node_parser import TokenTextSplitter
from llama_index.core import VectorStoreIndex

# --- 1. LLMと埋め込みモデルの設定 ---
# OllamaのLLMを設定
# 'elyza8b'モデルが既にOllamaサーバーに存在していることを前提とします。
Settings.llm = Ollama(model="elyza8b", request_timeout=360.0)

# 日本語性能の高い埋め込みモデルを設定
Settings.embed_model = HuggingFaceEmbedding(model_name="intfloat/multilingual-e5-large")
print("LLMと埋め込みモデルの設定が完了しました。")

# --- 2. データのロード ---
# Wikipediaの指定されたページからデータをロード
print("Wikipediaページからデータをロード中...")
urls = [
    "https://ja.wikipedia.org/wiki/%E7%81%AB%E8%BB%8A_(%E5%B0%8F%E8%AA%AC)",
    "https://ja.wikipedia.org/wiki/%E7%90%86%E7%94%B1_(%E5%B0%8F%E8%AA%AC)",
    "https://ja.wikipedia.org/wiki/%E5%90%8D%E3%82%82%E3%81%AA%E3%81%8D%E6%AF%92",
    "https://ja.wikipedia.org/wiki/%E3%82%BD%E3%83%AD%E3%83%A2%E3%83%B3%E3%81%AE%E5%81%BD%E8%A8%BC"
]
documents = SimpleWebPageReader(html_to_text=True).load_data(urls)
print(f"合計 {len(documents)} ページのドキュメントをロードしました。")

# --- 3. ドキュメントのチャンク化とノード化 ---
# 読み込んだドキュメントをチャンクに分割し、ノードとして処理
print("ドキュメントをノードに分割中...")
splitter = TokenTextSplitter(
    chunk_size=1000,
    chunk_overlap=100,
    separator=" ",
)
nodes = splitter.get_nodes_from_documents(documents)
print(f"合計 {len(nodes)} 個のノードが生成されました。")
# 確認のため、一部のノードを表示(オプション)
# print("\n一部のノードの例:")
# for i, node in enumerate(nodes[10:13]):
#     print(f"Node {i+10}: {node.text[:100]}...") # 最初の100文字を表示
# print("---")

# --- 4. VectorStoreIndexの作成 ---
# ノードからVectorStoreIndexを作成
print("VectorStoreIndexを作成中...")
index = VectorStoreIndex(nodes, show_progress=True)
print("VectorStoreIndexの作成が完了しました。")

# --- 5. インデックスの保存 ---
# インデックスにIDを設定し、指定したディレクトリに保存
# 保存先ディレクトリが存在しない場合は自動で作成されます。
index_id = "miyabe_miyuki"
persist_dir = "./storage/simple_vs"

print(f"インデックスを '{persist_dir}' に ID '{index_id}' で保存中...")
index.set_index_id(index_id)
index.storage_context.persist(persist_dir)
print("インデックスの保存が完了しました。")
print("これで、次回からはこのインデックスを再利用できます。")

これを改造して
wikipedia のページからではなく

data/
  school/
    shizensaigai.pdf
    他の学校関連PDF
  insurance/
    自動車保険.pdf
    県民共済.pdf
    他の保険PDF
  manuals/
    洗濯機.pdf
    冷蔵庫.pdf
    他の家電PDF

のRAGインデックスを作成するようにする

なお
llama-index-ocr
も考えたけど
https://x.com/alexweberk/status/1858316517668249755
によれば
2024年9月時点だと日本語はダメっぽい

とりあえず実験する

GPTによれば

pip install llama-index llama-index-pdf

となっているけど
そんなパッケージは存在しない…

 pip install llama-index llama-index-pdf

Collecting llama-index
  Downloading llama_index-0.12.38-py3-none-any.whl.metadata (12 kB)
ERROR: Could not find a version that satisfies the requirement llama-index-pdf (from versions: none)

[notice] A new release of pip is available: 24.3.1 -> 25.1.1
[notice] To update, run: pip install --upgrade pip
ERROR: No matching distribution found for llama-index-pdf

となる

pip install llama-index-readers-file

が正解
これは geminiの回答

次にいくつか疑問点を解決していく

    │ ├── shizensaigai.pdf
│ └── other_school_document.pdf
├── insurance/
│ ├── 自動車保険.pdf
│ └── 県民共済.pdf
└── manuals/
├── 洗濯機.pdf
└── 冷蔵庫.pdf

のPDFファイル名はインデックスデータの精度に関係するか?
例:家電のマニュアルが製品の型番、例えば001.pdf だった場合など

これに関しては
ファイル名自体がインデックスデータの精度に直接的な影響を与えることは、ほとんどありません。
LlamaIndexのようなRAGシステムでは、インデックスを作成する際にPDFファイルの中身(テキストコンテンツ)を読み込み、それをチャンクに分割し、埋め込みモデルがそのチャンクのテキストからベクトル表現を生成します。このベクトル表現が、検索時の関連度計算の基礎となります。
ファイル名が精度に間接的に影響する可能性
しかし、ファイル名が間接的に、そして限定的ながら影響を与える可能性はあります。
1. メタデータとしての利用: もし、あなたがRAGシステムを高度にカスタマイズし、PDFファイル名からメタデータを抽出してノードに追加するような処理を実装した場合、そのメタデータは検索の精度に影響を与える可能性があります。 例えば、「洗濯機.pdf」というファイル名から「製品カテゴリ:家電、製品名:洗濯機」といったメタデータを自動生成し、ノードに付与するとします。ユーザーが「家電の修理方法」と質問した際に、このメタデータを使って関連ドキュメントを絞り込むことができます。 ただし、これはLlamaIndexのデフォルトの動作ではなく、カスタムで実装が必要です。
2. 可読性と管理: これは直接的な精度とは異なりますが、ファイル名が「001.pdf」のような意味のないものだと、システムを管理する側がどのファイルが何を指しているのか把握しにくくなります。デバッグやドキュメントの追加・削除の際に、非効率的になる可能性があります。

結論
基本的には、shizensaigai.pdf や 001.pdf のようなファイル名の違いは、インデックス作成後のRAGの検索精度や回答精度に直接的な影響を与えません。 RAGの精度は、主に以下の要素によって決まります。
* PDFファイルの内容: 情報の質、網羅性、テキストのクリーンさ。
* 埋め込みモデルの性能: テキストをどれだけ適切にベクトル化できるか。
* チャンク化の戦略: チャンクサイズ、オーバーラップ、セパレーターの選び方。
* LLMの性能: 検索されたコンテキストを理解し、クエリに適切に回答する能力。
ですので、製品の型番や具体的な内容がわかるようなファイル名(例: 洗濯機_ABC123.pdf)にしておくことは、人間がファイルを管理する上での利便性には繋がりますが、RAGシステムの内部的な精度向上に劇的に貢献するわけではありません。

とのこと

後々、参照元のPDFを回答の根拠として表示する場合には影響するか?
については

その場合はファイル名が非常に重要になります。
LlamaIndexのようなRAGシステムでは、最終的な回答を生成する際に、どのドキュメント(チャンク)から情報が取得されたかを示す「参照元(ソース)」情報を提供できます。この参照元情報には、通常、以下のものが含まれます。
* ファイル名: どのPDFファイルから情報が得られたか。
* ページ番号: PDFの何ページ目から情報が得られたか(PDFReaderが対応している場合)。
* チャンクのテキスト: 実際に参照されたテキストスニペット。
ファイル名が「参照元のPDFを回答の根拠として表示する」際に影響する理由
1. ユーザーへの視認性・理解度:
* 分かりやすい例: ユーザーが「洗濯機の操作方法について」質問し、回答とともに参照元として「洗濯機_ABC123.pdf」と表示されれば、ユーザーはすぐに「これは私が持っている洗濯機のマニュアルだ」と理解し、その根拠を信頼できます。
* 分かりにくい例: もし参照元が「001.pdf」と表示された場合、ユーザーはそれがどのドキュメントなのか全く分かりません。自分でファイルを探し出して内容を確認することが困難になります。
2. 根拠の信頼性:
* ユーザーが回答の正確性を検証したい場合、ファイル名が具体的であれば、そのファイルを直接開いて確認できます。抽象的なファイル名では、確認作業が非常に煩雑になります。
3. デバッグとシステム管理:
* RAGシステムを開発・運用する側にとっても、どの回答がどのソースドキュメントに由来しているのか、ファイル名を見るだけで一目瞭然であることは非常に重要です。問題が発生した場合の原因特定や、特定のドキュメントの更新・削除などの管理が容易になります。
対策と推奨されるファイル名
参照元の表示を考慮すると、PDFファイル名は以下の点を満たすようにすることをお勧めします。
* 内容を簡潔に表す: そのファイルに何が書かれているのかが、ファイル名からある程度推測できること。
* 具体的な情報を含める: 製品名、型番、バージョン、日付など、ドキュメントを一意に特定できる情報を含めること。
* 日本語名でもOK: LlamaIndexは日本語のファイル名を問題なく扱えます。
良い例:
* shizensaigai_防災ガイド_2023年版.pdf
* 自動車保険_ABC社_約款_2024.pdf
* Panasonic_洗濯機_NA-VX9000_取扱説明書.pdf
* 冷蔵庫_三菱_MR-B46F_マニュアル.pdf
悪い例:
* doc1.pdf
* file.pdf
* image.pdf
* 123.pdf
コード上の考慮点
LlamaIndexのSimpleDirectoryReaderやPDFReaderは、読み込んだドキュメントにファイルパス(したがってファイル名)をメタデータとして自動的に付与します。このメタデータは、後でクエリエンジンからソースノードを取得する際に利用可能です。
例えば、クエリ結果からソース情報を取得する際には、以下のようにアクセスできます。

# Assuming 'response' is the object returned by query_engine.query(query_text)
for source_node in response.source_nodes:
    file_name = source_node.metadata.get('file_name')
    page_label = source_node.metadata.get('page_label') # ページ番号
    text = source_node.text # チャンクのテキスト

    print(f"参照元ファイル: {file_name} (ページ: {page_label or 'N/A'})")
    print(f"関連テキスト: {text[:200]}...") # 関連するテキストの一部

したがって、参照元の表示を考えている場合は、PDFファイル名に意味を持たせるように命名規則を設けることが非常に重要

つまりファイル名を家電や約款の場合ダウンロードしてからわかりやすい名前に変更する必要がある

 mkdir rag_data

ここにファイルをまとめる

cd rag_data 
mkdir school insurance manuals

Mkdirコマンドでスペースで区切れば複数フォルダを一度に作成可能

cp ~/Downloads/取扱説明書/* manuals 
cp ~/Downloads/保険/* insurance    
cp ~/Downloads/School/shizensaigai.pdf school
mv shizensaigai.pdf 自然災害発生時、警報発表・避難情報発表時等に伴う学校の対処.pdf

でファイル名を変更しておく

また manualsの中の家電ごとのファイル名も変更しておく

cd ../manuals 
mv axha20_mn.pdf ウォーターオーブンAX-HA20.pdf
mv np_tr8.pdf 食洗機NP-TR8.pdf
mv MCK70ZKS-T_取扱説明書_3P698812-3E.pdf 空気清浄機MCK70ZKS-W.pdf

とりあえずこれくらいにして実験を開始

なお
elyza8bで作成したRAGデータを他のモデルで使用するとどうなるか?
に対しては
elyza8b (LLM) で作成したRAGデータを他のLLMで使用することは、一般的に問題なく可能です。RAGのデータ保存とLLMの利用は、それぞれ異なる役割を持つ
とのこと

RAGシステムにおけるLLMの役割
RAG (Retrieval Augmented Generation) システムは、大きく以下の2つの主要なコンポーネントに分けられます。
1. Retrieval (検索) フェーズ:
* 埋め込みモデル (Embedding Model): ドキュメントのテキストやクエリのテキストを数値ベクトル(埋め込みベクトル)に変換します。このベクトルが類似度計算に使用され、関連性の高いドキュメントを検索します。
* ベクトルデータベース/インデックス: 生成された埋め込みベクトルを保存し、クエリベクトルとの類似度に基づいて関連ドキュメントを効率的に検索します。
2. Generation (生成) フェーズ:
* LLM (Large Language Model): 検索フェーズで取得された関連ドキュメントのコンテキスト情報とユーザーのクエリを受け取り、それに基づいて最終的な回答を生成します。
elyza8bで作成したRAGデータとは?
ご質問の「elyza8bで作成したRAGデータ」という文脈では、おそらく以下の部分を指していると考えられます。
* VectorStoreIndex (ベクトルストアインデックス): これは、ドキュメントのテキストを埋め込みモデル(この場合は intfloat/multilingual-e5-large)がベクトル化したものを保存したものです。elyza8b (LLM) は、このインデックスの作成には直接関与していません。LLMは、RAGの「生成」フェーズで使われるものです。
つまり、elyza8bはRAGの「生成」フェーズで使われるLLMであり、RAGデータの「作成」には関与していません。RAGデータ(VectorStoreIndex)は埋め込みモデルによって作成されます。
埋め込みモデルとLLMの分離
* 埋め込みモデル: intfloat/multilingual-e5-large このモデルがドキュメントのテキストをベクトルに変換し、そのベクトルがVectorStoreIndexに保存されます。この埋め込みモデルを変更しない限り、RAGデータ(VectorStoreIndex)の構造や内容は変わりません。
* LLM: elyza8b このモデルは、ユーザーのクエリと検索されたコンテキストを受け取って最終的な回答を生成します。
elyza8bで作成したRAGデータを他のLLMで使用するとどうなるか?
「elyza8bで作成したRAGデータ」が、実際にはintfloat/multilingual-e5-largeという埋め込みモデルによって生成されたVectorStoreIndexを指していると仮定します。
このVectorStoreIndexは、特定のLLM(elyza8b)とは独立したデータです。したがって、このRAGデータ(VectorStoreIndex)を、異なるLLM(例: gemma:2b、gpt-3.5-turbo、mixtralなど)と組み合わせて使用することは全く問題ありません。

LLMを変更することで、RAGシステムの回答生成の質が変わります。
1. 回答のスタイルと質:
* 自然さ、流暢さ: 新しいLLMの言語生成能力に依存します。例えば、gemma:2bは高速ですが、elyza8bと比較して日本語の自然さや文脈理解に差が出る可能性があります(特に日本語に特化していない場合)。
* 正確性: LLMが提供されたコンテキスト情報をどれだけ正確に理解し、それに基づいて回答を生成できるかに影響します。推論能力の高いLLMであれば、より適切な回答が期待できます。
* 情報統合能力: 検索された複数の関連チャンクをどれだけうまく統合し、矛盾のない回答を生成できるか。
* 冗長性: 回答が長すぎたり短すぎたりするか。
* トーン: 回答のトーン(フォーマル、カジュアルなど)が変わる可能性があります。
2. 推論速度:
* LLMのモデルサイズやアーキテクチャによって、回答生成にかかる時間が大きく変わります。gemma:2bのような小型モデルに変更すれば、推論速度が向上する可能性が高いです。
3. リソース消費:
* LLMを変更すると、VRAM使用量やCPU使用率が変わります。小型のLLMに変更すれば、必要なリソースが減り、より効率的に動作する可能性があります。
4. ハルシネーション(幻覚)の傾向:
* LLMによって、提供されたコンテキストから逸脱した情報を生成する「ハルシネーション」の傾向が変わります。一般的に、より高性能なLLMほどハルシネーションが少ないとされますが、これはモデルの特性やチューニングにも依存します。
まとめ
* elyza8bで「作成」されたRAGデータ(VectorStoreIndex)は、実際には埋め込みモデルによって作成されており、LLMとは独立しています。
* このRAGデータは、他の任意のLLMと組み合わせて使用できます。
* LLMを変更すると、RAGシステムの回答生成の質、スタイル、推論速度、リソース消費が変わります。
* 異なるLLMを試すことで、ご自身の用途や環境に最適なRAGシステムを構築できる可能性があります。

あと
ファイル名自体がインデックスデータの精度に直接的な影響を与えることは、ほとんどありません

しかし、ファイル名が間接的に、そして限定的ながら影響を与える可能性はあります。
1. メタデータとしての利用: もし、あなたがRAGシステムを高度にカスタマイズし、PDFファイル名からメタデータを抽出してノードに追加するような処理を実装した場合、そのメタデータは検索の精度に影響を与える可能性があります。 例えば、「洗濯機.pdf」というファイル名から「製品カテゴリ:家電、製品名:洗濯機」といったメタデータを自動生成し、ノードに付与するとします。ユーザーが「家電の修理方法」と質問した際に、このメタデータを使って関連ドキュメントを絞り込むことができます。 ただし、これはLlamaIndexのデフォルトの動作ではなく、カスタムで実装が必要です。
2. 可読性と管理: これは直接的な精度とは異なりますが、ファイル名が「001.pdf」のような意味のないものだと、システムを管理する側がどのファイルが何を指しているのか把握しにくくなります。デバッグやドキュメントの追加・削除の際に、非効率的になる可能性があります。

とりあえず問題はなさそうなので
実際に実験を開始する

 touch create_rag_data.py

でファイルを作成

import ollama
from llama_index.core import Settings
from llama_index.llms.ollama import Ollama
from llama_index.embeddings.huggingface import HuggingFaceEmbedding
from llama_index.core import SimpleDirectoryReader
from llama_index.core.node_parser import TokenTextSplitter
from llama_index.core import VectorStoreIndex
import os

# --- 1. LLMと埋め込みモデルの設定 ---
# OllamaのLLMを設定
# 'elyza8b'モデルが既にOllamaサーバーに存在していることを前提とします。
Settings.llm = Ollama(model="elyza8b", request_timeout=360.0)

# 日本語性能の高い埋め込みモデルを設定
Settings.embed_model = HuggingFaceEmbedding(model_name="intfloat/multilingual-e5-large")
print("LLMと埋め込みモデルの設定が完了しました。")

# --- 2. データのロード ---
# 指定されたディレクトリからPDFファイルをロード
print("指定されたディレクトリからPDFファイルをロード中...")
base_data_dir = "./data"
sub_dirs = ["school", "insurance", "manuals"]
all_documents = []

for sub_dir in sub_dirs:
    dir_path = os.path.join(base_data_dir, sub_dir)
    if os.path.exists(dir_path) and os.path.isdir(dir_path):
        print(f"ディレクトリ '{dir_path}' からPDFを読み込み中...")
        # PDFReaderはllama-index-readers-fileに含まれています
        reader = SimpleDirectoryReader(input_dir=dir_path, required_exts=[".pdf"])
        documents = reader.load_data()
        all_documents.extend(documents)
        print(f"'{dir_path}' から {len(documents)} ページのドキュメントをロードしました。")
    else:
        print(f"警告: ディレクトリ '{dir_path}' が見つからないか、ディレクトリではありません。スキップします。")

print(f"合計 {len(all_documents)} ページのドキュメントをロードしました。")

# ロードされたドキュメントがない場合は処理を終了
if not all_documents:
    print("ロードされたドキュメントがありません。インデックスの作成をスキップします。")
else:
    # --- 3. ドキュメントのチャンク化とノード化 ---
    # 読み込んだドキュメントをチャンクに分割し、ノードとして処理
    print("ドキュメントをノードに分割中...")
    splitter = TokenTextSplitter(
        chunk_size=1000,
        chunk_overlap=100,
        separator=" ",
    )
    nodes = splitter.get_nodes_from_documents(all_documents)
    print(f"合計 {len(nodes)} 個のノードが生成されました。")

    # --- 4. VectorStoreIndexの作成 ---
    # ノードからVectorStoreIndexを作成
    print("VectorStoreIndexを作成中...")
    index = VectorStoreIndex(nodes, show_progress=True)
    print("VectorStoreIndexの作成が完了しました。")

    # --- 5. インデックスの保存 ---
    # インデックスにIDを設定し、指定したディレクトリに保存
    # 保存先ディレクトリが存在しない場合は自動で作成されます。
    index_id = "my_local_pdfs" # インデックスIDを分かりやすい名前に変更
    persist_dir = "./storage/my_local_pdfs" # 保存先ディレクトリを分かりやすい名前に変更

    print(f"インデックスを '{persist_dir}' に ID '{index_id}' で保存中...")
    index.set_index_id(index_id)
    index.storage_context.persist(persist_dir)
    print("インデックスの保存が完了しました。")
    print("これで、次回からはこのインデックスを再利用できます。")

として保存
しかし実行時にエラー
フォルダ名が
Dataではなく rag_dataだったので修正

import ollama
from llama_index.core import Settings
from llama_index.llms.ollama import Ollama
from llama_index.embeddings.huggingface import HuggingFaceEmbedding
from llama_index.core import SimpleDirectoryReader
from llama_index.core.node_parser import TokenTextSplitter
from llama_index.core import VectorStoreIndex
import os

# --- 1. LLMと埋め込みモデルの設定 ---
# OllamaのLLMを設定
# 'elyza8b'モデルが既にOllamaサーバーに存在していることを前提とします。
Settings.llm = Ollama(model="elyza8b", request_timeout=360.0)

# 日本語性能の高い埋め込みモデルを設定
Settings.embed_model = HuggingFaceEmbedding(model_name="intfloat/multilingual-e5-large")
print("LLMと埋め込みモデルの設定が完了しました。")

# --- 2. データのロード ---
# 指定されたディレクトリからPDFファイルをロード
print("指定されたディレクトリからPDFファイルをロード中...")
base_data_dir = "./rag_data"
sub_dirs = ["school", "insurance", "manuals"]
all_documents = []

for sub_dir in sub_dirs:
    dir_path = os.path.join(base_data_dir, sub_dir)
    if os.path.exists(dir_path) and os.path.isdir(dir_path):
        print(f"ディレクトリ '{dir_path}' からPDFを読み込み中...")
        # PDFReaderはllama-index-readers-fileに含まれています
        reader = SimpleDirectoryReader(input_dir=dir_path, required_exts=[".pdf"])
        documents = reader.load_data()
        all_documents.extend(documents)
        print(f"'{dir_path}' から {len(documents)} ページのドキュメントをロードしました。")
    else:
        print(f"警告: ディレクトリ '{dir_path}' が見つからないか、ディレクトリではありません。スキップします。")

print(f"合計 {len(all_documents)} ページのドキュメントをロードしました。")

# ロードされたドキュメントがない場合は処理を終了
if not all_documents:
    print("ロードされたドキュメントがありません。インデックスの作成をスキップします。")
else:
    # --- 3. ドキュメントのチャンク化とノード化 ---
    # 読み込んだドキュメントをチャンクに分割し、ノードとして処理
    print("ドキュメントをノードに分割中...")
    splitter = TokenTextSplitter(
        chunk_size=1000,
        chunk_overlap=100,
        separator=" ",
    )
    nodes = splitter.get_nodes_from_documents(all_documents)
    print(f"合計 {len(nodes)} 個のノードが生成されました。")

    # --- 4. VectorStoreIndexの作成 ---
    # ノードからVectorStoreIndexを作成
    print("VectorStoreIndexを作成中...")
    index = VectorStoreIndex(nodes, show_progress=True)
    print("VectorStoreIndexの作成が完了しました。")

    # --- 5. インデックスの保存 ---
    # インデックスにIDを設定し、指定したディレクトリに保存
    # 保存先ディレクトリが存在しない場合は自動で作成されます。
    index_id = "my_local_pdfs" # インデックスIDを分かりやすい名前に変更
    persist_dir = "./storage/my_local_pdfs" # 保存先ディレクトリを分かりやすい名前に変更

    print(f"インデックスを '{persist_dir}' に ID '{index_id}' で保存中...")
    index.set_index_id(index_id)
    index.storage_context.persist(persist_dir)
    print("インデックスの保存が完了しました。")
    print("これで、次回からはこのインデックスを再利用できます。")

これで再度実行
5:12 からスタート

python create_rag_data.py
LLMと埋め込みモデルの設定が完了しました。
指定されたディレクトリからPDFファイルをロード中...
ディレクトリ './rag_data/school' からPDFを読み込み中...
'./rag_data/school' から 4 ページのドキュメントをロードしました。
ディレクトリ './rag_data/insurance' からPDFを読み込み中...
Impossible to decode XFormObject /Fm0: Invalid Elementary Object starting with b'\x80' @0: b'\x80'
Impossible to decode XFormObject /Fm1: Invalid Elementary Object starting with b'\x81' @0: b'\x81'
Impossible to decode XFormObject /Fm0: Invalid Elementary Object starting with b'\x9a' @0: b'\x9a'
Impossible to decode XFormObject /Fm1: Invalid Elementary Object starting with b'\x97' @0: b'\x97'
Impossible to decode XFormObject /Fm0: Invalid Elementary Object starting with b'\x80' @0: b'\x80'
Impossible to decode XFormObject /Fm1: Invalid Elementary Object starting with b'\xc4' @0: b'\xc4'
Impossible to decode XFormObject /Fm0: Invalid Elementary Object starting with b'\xc7' @0: b'\xc7'
Impossible to decode XFormObject /Fm1: Invalid Elementary Object starting with b'\xb1' @0: b'\xb1'
Impossible to decode XFormObject /Fm0: Invalid Elementary Object starting with b'\xd7' @0: b'\xd7'
Impossible to decode XFormObject /Fm1: Invalid Elementary Object starting with b'\x8e' @0: b'\x8e'
Impossible to decode XFormObject /Fm0: Invalid Elementary Object starting with b'\xc8' @0: b'\xc8'
Impossible to decode XFormObject /Fm1: Invalid Elementary Object starting with b'\x99' @0: b'\x99'
'./rag_data/insurance' から 322 ページのドキュメントをロードしました。
ディレクトリ './rag_data/manuals' からPDFを読み込み中...
Failed to load file /Users/snowpool/aw10s/ollama/rag_data/manuals/ウォーターオーブンAX-HA20.pdf with error: RetryError[<Future at 0x367318710 state=finished raised DependencyError>]. Skipping...
Failed to load file /Users/snowpool/aw10s/ollama/rag_data/manuals/食洗機NP-TR8.pdf with error: RetryError[<Future at 0x368388250 state=finished raised DependencyError>]. Skipping...
'./rag_data/manuals' から 48 ページのドキュメントをロードしました。
合計 374 ページのドキュメントをロードしました。
ドキュメントをノードに分割中...
合計 1180 個のノードが生成されました。
VectorStoreIndexを作成中...
Generating embeddings: 100%|████████████████| 1180/1180 [05:33<00:00,  3.54it/s]
VectorStoreIndexの作成が完了しました。
インデックスを './storage/my_local_pdfs' に ID 'my_local_pdfs' で保存中...
インデックスの保存が完了しました。
これで、次回からはこのインデックスを再利用できます。

実行終了まで約7分

次に回答が出るかテストする

touch chat_with_rag.py

中身を

import sys
from llama_index.core import StorageContext, load_index_from_storage
from llama_index.core import Settings
from llama_index.llms.ollama import Ollama
from llama_index.embeddings.huggingface import HuggingFaceEmbedding

# --- 1. LLMと埋め込みモデルの設定 ---
# インデックス作成時と同じモデルを設定する必要があります。
# これにより、クエリのベクトル化と回答生成が正しく行われます。
print("LLMと埋め込みモデルの設定を開始します...")
try:
    Settings.llm = Ollama(model="elyza:jp8b", request_timeout=360.0)
    # 日本語性能の高い埋め込みモデルを設定
    Settings.embed_model = HuggingFaceEmbedding(model_name="intfloat/multilingual-e5-large")
    print("LLMと埋め込みモデルの設定が完了しました。")
except Exception as e:
    print(f"LLMまたは埋め込みモデルの設定中にエラーが発生しました: {e}")
    print("Ollamaサーバーが起動しており、'elyza:jp8b'モデルがダウンロードされているか確認してください。")
    sys.exit(1)

# --- 2. インデックスのロード ---
print("インデックスをロード中...")
try:
    # 保存先のディレクトリとインデックスIDを指定
    # 以前のインデックス保存時のパスとIDに合わせる必要があります。
    persist_dir = "./storage/my_local_pdfs" # あなたのインデックス保存パスに修正
    index_id = "my_local_pdfs"             # あなたのインデックスIDに修正

    # StorageContext を設定し、保存先のディレクトリを指定
    storage_context = StorageContext.from_defaults(persist_dir=persist_dir)

    # 指定されたストレージコンテキストとインデックスIDでインデックスをロード
    loaded_index = load_index_from_storage(storage_context, index_id=index_id)
    print("インデックスのロードが完了しました。")

    # ロードしたインデックスからクエリエンジンを作成
    # similarity_top_k は、回答生成のために検索するノードの数を指定します。
    # この値を調整することで、回答の精度や網羅性が変わる可能性があります。
    query_engine = loaded_index.as_query_engine(similarity_top_k=3)
    print("クエリエンジンの準備ができました。これで質問を開始できます。")

    # --- 3. ユーザーからの質問を受け付けるループ ---
    print("\n--------------------------------------------------")
    print("質問を入力してください(終了するには 'exit' と入力)。")
    print("--------------------------------------------------")
    while True:
        query = input("あなたの質問: ")

        if query.lower() == 'exit':
            print("プログラムを終了します。ご利用ありがとうございました!")
            break

        if not query.strip(): # 空の入力の場合
            print("質問を入力してください。")
            continue

        print("回答を生成中...")
        try:
            # クエリエンジンで質問を処理し、回答を取得
            response = query_engine.query(query)
            print("\n--- 回答 ---")
            print(response.response) # .response をつけて回答テキストのみを出力
            print("------------\n")
        except Exception as e:
            print(f"質問処理中にエラーが発生しました: {e}")
            print("もう一度お試しください。")

except FileNotFoundError:
    print(f"\nエラー: インデックスデータが見つかりません。")
    print(f"'{persist_dir}' ディレクトリが存在し、以前にインデックスがID '{index_id}' で保存されているか確認してください。")
    print("インデックスを保存するには、インデックス作成コードを先に実行する必要があります。")
    sys.exit(1) # プログラムを終了
except Exception as e:
    print(f"\n予期せぬエラーが発生しました: {e}")
    sys.exit(1) # プログラムを終了

これを実行すると

LLMと埋め込みモデルの設定を開始します...
LLMと埋め込みモデルの設定が完了しました。
インデックスをロード中...
インデックスのロードが完了しました。
クエリエンジンの準備ができました。これで質問を開始できます。

--------------------------------------------------
質問を入力してください(終了するには 'exit' と入力)。
--------------------------------------------------
あなたの質問: 台風の時の対処
回答を生成中...

--- 回答 ---
暴風警報や特別警報が発表された場合、下校後は放課後児童クラブに登所し、状況に応じて速やかな迎えを依頼し、安全を確認した後保護者への引き渡しが行われます。
------------

あなたの質問: 大雨警報の時は
回答を生成中...

--- 回答 ---
原則として登校します。
------------

あなたの質問: 保険金の支払いはいつ?
回答を生成中...

--- 回答 ---
請求完了日からその日を含めて30日以内に、自転車賠償保険金が支払われます。
------------

あなたの質問: エアコンから異音がする
回答を生成中...

--- 回答 ---
運転中、「シュー」や「ヒュー」という音がする場合、ストリーマの運転中はストリーマ放電の音であり、異常ではありません。また、ご使用環境により、音が小さくなったり、音質が変わることがありますが、異常ではありません。
------------

あなたの質問: 食洗機の乾燥時間は?
回答を生成中...

--- 回答 ---
There is no mention of食洗機 or乾燥時間 in the provided context information.
------------

あなたの質問: 対物の金額
回答を生成中...

--- 回答 ---
対物保険金額とは、同一事故につき既に当社が支払った対物賠償保険金または第8条(損害賠償請求権者の直接請求権)の損害賠償額がある場合、その全額を差し引いた額をいいます。
------------

あなたの質問: 対物の補償金額は?
回答を生成中...

--- 回答 ---
対物保険金額とは、同一事故につき既に当社が支払った対物賠償保険金または第8条(損害賠償請求権者の直接請求権)の損害賠償額がある場合、その全額を差し引いた額をいいます。
------------

あなたの質問: 生命 次回の保険の更新日は?
回答を生成中...

--- 回答 ---
この保険の更新日を特定する情報が提供されていないため、次回の保険の更新日はわかりません。
------------

あなたの質問: 保険期間は?
回答を生成中...

--- 回答 ---
保険期間が1年未満の場合、保険期間の日数を365で除した割合を乗じて保険料を算出したものと記載されています。
------------

あなたの質問: 自動車保険の保険期間は?
回答を生成中...

--- 回答 ---
「条」の中に保険期間に関する規定があるため、自動車保険の保険期間は始期と終期があります。始期は記名被保険者の管理下に入った時もしくは保険契約の始期、終期は保険契約の終期です。
------------

あなたの質問: 登録番号
回答を生成中...

--- 回答 ---
登録番号とは、自動車や交通乗用具が持つ一意に識別される番号を指します。
------------

あなたの質問: 自動車保険の内容で仕様について
回答を生成中...

--- 回答 ---
自家用8車種は、以下のいずれかに該当する自動車をいう。
① 自家用普通乗用車
② 自家用小型乗用車
③ 自家用軽四輪乗用車
④ 自家用普通貨物車 (最大積載量0.5 トン超2 トン以下)
⑤ 自家用普通貨物車 (最大積載量0.5 トン以下)
⑥ 自家用小型貨物車
⑦ 自家用軽四輪貨物車
⑧ 特種用途自動車
------------

あなたの質問: 自動車保険証の中で仕様を知りたい
回答を生成中...

--- 回答 ---
自動車の用途や種類は、当社が定める区分表によるものとし、例えば登録番号標や車両番号標の分類番号や塗色に基づいて区分しています。
------------

実験結果からPDFの単純なものはすぐに出る
例:学校のPDF
しかし保険証券のように少し複雑になると途端に精度が下がる

Gemma3 4b + faster-whisperのコード公開

Gemma3 4b + faster-whisperのコード公開

音声でやり取りできるようにしたのでコードを公開する

まずリポジトリ公開のため作成する
とりあえず voice_llm としておく

いくつか忘れているのでメモ
ライセンスはMITにして
Readme も最初に作成しておく

mkdir github_open
cd github_open 


これから公開ずみコードはここへおく

git clone git@github.com:Snowpooll/voice_llm.git

でリポジトリコピー

cd voice_llm

あとはファイルをコピーしていく

cp -rp ../../gemma/configs .

でfaster-whisper の設定ファイル

cp -rp ../../gemma/module .

でモジュール関連
なおウェイクワード関連は未完成なので削除する

rm module/module_wakeword*
rm -r module/__pycache__

メイン処理部分のコピーとリネーム

cp -rp ../../gemma/main5.py .
 mv main5.py main.py         

必要なものはコピーできたので
次に requestment.txt の作成

from module.module_audio_to_text import AudioToTextCorrector
from module.module_speaker import Speaker
from ollama import chat, ChatResponse
import time
from module.module_whisper import FasterWhisperModel
from module.module_recorder import Recorder
from ollama_text_correction import OllamaTextCorrector
import numpy as np
import sounddevice as sd

import os
import configparser
# ファイルの存在チェック用モジュール
import errno
import pyttsx3
import emoji
import re
from faster_whisper import WhisperModel
import numpy as np
import torch

import os
import configparser
# ファイルの存在チェック用モジュール
import errno

これらを使えるように
requirements.txt
を作成する

touch requirements.txt

内容は

ollama
numpy
sounddevice
faster-whisper
pyttsx3
emoji
torch
configparser

| ライブラリ名           | 説明                               |
| ---------------- | -------------------------------- |
| `ollama`         | ローカルで実行可能なLLMを利用するためのPythonパッケージ |
| `numpy`          | 数値演算を効率よく行うためのライブラリ              |
| `sounddevice`    | オーディオ入力・録音機能のためのライブラリ            |
| `faster-whisper` | Whisperモデルの高速音声認識ライブラリ           |
| `pyttsx3`        | テキスト読み上げ(TTS)ライブラリ(オフライン動作可)     |
| `emoji`          | 絵文字を扱うためのライブラリ                   |
| `torch`          | PyTorch機械学習フレームワーク(Whisperで必要)   |
| `configparser`   | 設定ファイル(.ini形式)を扱うためのライブラリ        |

これで

pip install -r requirements.txt

でモジュールの一括インストールが可能

次に

vim .gitignore

で除外するファイルを設定

なお
README.md や LICENSE はプロジェクトの利用方法やライセンス情報を示すために GitHubなどに必ず公開するべきファイル です。
これらを .gitignore に書いてしまうと、Gitリポジトリに追加されず、外部に公開できなくなります。
とのこと

# OS固有の隠しファイル
.DS_Store

# Pythonキャッシュ関連
__pycache__/
*.py[cod]
*$py.class

を追記して保存

git add .
git commit -m "add source code"

あとは

git push origin main

でsshキーのパスワードを入れればOK

これで
https://github.com/Snowpooll/voice_llm#

コードが公開された

あとは readme の作成

LlamaIndexのVectorStore(ChromaDB、Faiss、Qdrant)

LlamaIndexのVectorStore(ChromaDB、Faiss、Qdrant)

https://highreso.jp/edgehub/machinelearning/llamaindexvectorstore.html
の記事を読んでみる

スペックは
* GPU:NVIDIA A100 80GB
* GPUメモリ(VRAM):80GB
* OS :Ubuntu 22.04

Colabでなら再現できるかも

Docker部分はいらないので飛ばす

Llama-3-ELYZA-JP-8B-q4_k_m.gguf
を使用してる

LLMの実行にはOllamaを使用

日本語性能が高く、無料で使える埋め込みモデル「intfloat/multilingual-e5-large」を指定

from llama_index.embeddings.huggingface import HuggingFaceEmbedding

Settings.embed_model = HuggingFaceEmbedding(model_name="intfloat/multilingual-e5-large")

Wikipediaの『宮部みゆきの小説』に関するページを指定

from llama_index.readers.web import SimpleWebPageReader
documents = SimpleWebPageReader(html_to_text=True).load_data(
    ["https://ja.wikipedia.org/wiki/%E7%81%AB%E8%BB%8A_(%E5%B0%8F%E8%AA%AC)",
    "https://ja.wikipedia.org/wiki/%E7%90%86%E7%94%B1_(%E5%B0%8F%E8%AA%AC)",
    "https://ja.wikipedia.org/wiki/%E5%90%8D%E3%82%82%E3%81%AA%E3%81%8D%E6%AF%92",
    "https://ja.wikipedia.org/wiki/%E3%82%BD%E3%83%AD%E3%83%A2%E3%83%B3%E3%81%AE%E5%81%BD%E8%A8%BC"]
)

SimpleWebPageReader(html_to_text=True).load_data()で
指定したWebページから取得したHTMLデータをテキスト形式で読み込み

テキストをチャンクに分割
読み込んだデータをチャンクに分割し、各チャンクをノードとして処理します。
ノードは効率的な検索やクエリ処理をサポートするため、メタデータや関係情報を含む単位

from llama_index.core.node_parser import TokenTextSplitter

splitter = TokenTextSplitter(
    chunk_size=1000,
    chunk_overlap=100,
    separator=" ",
)
nodes = splitter.get_nodes_from_documents(documents)
nodes[10:13]

コード解説

splitter = TokenTextSplitter(…

* chunk_size=1000: 1つのチャンクに含まれるトークンの最大数を1000トークンに設定しています。これにより、テキストが1000トークンずつ分割されます。
* chunk_overlap=100: 隣接するチャンク間で100トークンの重複が設定されています。これにより、文脈の連続性を保つことができ、モデルがより意味を理解しやすくなります。
* separator=” “: トークンを分割する際の区切り文字として、スペース(” “)を使用しています。これは、一般的に単語の区切りを意味します。

nodes = splitter.get_nodes_from_documents(documents)

指定したdocumentsをトークン単位で分割し、それぞれをノードに変換します。ノードはテキストの一部分を保持し、後でモデルに入力するためのチャンクとなります

LlamaIndexで用意されているシンプルなVectorStoreの構築について

pip install llama-index llama-index-core

でインストール

VectorStoreIndexクラスを使って、ベクトルストアのインデックスを作成

from llama_index.core import VectorStoreIndex

index = VectorStoreIndex(nodes, show_progress=True)

なお

show_progress=True

だと
インデックスの構築プロセス中に進捗バー(プログレスバー)が表示される
特に大量のノードを処理する場合、インデックスの構築には時間がかかることがあります。このオプションを True にすることで、現在の処理状況や残り時間を視覚的に確認できるようになり、ユーザーエクスペリエンスが向上
なおデフォルトはfalse

次に
インデックスをもとにクエリエンジンを作成

query_engine = index.as_query_engine(similarity_top_k=3)
response = query_engine.query("火車の概要を教えて下さい")
print(response)


index.as_query_engine():

* これは、VectorStoreIndex オブジェクトが持つメソッドで、このインデックスを基にしたクエリエンジンを生成

オプションの解説

similarity_top_k=3:

* これは、クエリエンジンに渡すオプションの引数です。
* similarity_top_k は、与えられたクエリに対して、ベクトルストア内で最も類似度が高い上位 k 個のノード(チャンク)を取得するように指定します。
* この場合、similarity_top_k=3 なので、クエリのベクトルと最も近い3つのノードがインデックスから検索され、それらのノードの内容が回答生成の文脈として使用されます。
* この値を調整することで、取得する情報の量と関連性のバランスを取ることができます。値を大きくするとより多くの情報が取得されますが、必ずしも関連性の高い情報ばかりとは限りません。

インデックスのIDを設定し、指定したディレクトリに保存

index.set_index_id("miyabe_miyuki")
index.storage_context.persist("./storage/simple_vs")

解説

index.set_index_id(“miyabe_miyuki”)

* indexに対して、IDとして “miyabe_miyuki” を設定しています。インデックスIDは、インデックスを一意に識別するためのもので、後でインデックスをロードしたり、クエリを実行したりする際に役立ちます。

index.storage_context.persist(“./storage/simple_vs”)

データの保存や管理を行う部分(storage_context)を指定されたディレクトリ(”./storage/simple_vs”)に保存しています。このディレクトリにインデックスのデータが保存され、次回以降の使用時に再インデックスすることなく、同じデータをすぐに利用できるようになります。

なお

./storage/simple_vs

がないと自動で作成される
また、
index.storage_context.persist(“./my_custom_index_data”)
というように
カレントディレクトリ直下の別の名前のフォルダに保存するのも可能

次に
保存されたインデックスをストレージからロードして再利用

from llama_index.core import StorageContext, load_index_from_storage

storage_context = StorageContext.from_defaults(persist_dir="./storage/simple_vs")

simple_vc_index = load_index_from_storage(storage_context, index_id="miyabe_miyuki")

解説

storage_context = StorageContext.from_defaults(persist_dir=”./storage/simple_vs”)

* ここでは、”./storage/simple_vs” というディレクトリにある保存済みデータを扱うための StorageContext を作成しています。

simple_vc_index = load_index_from_storage(storage_context, index_id=”miyabe_miyuki”)

* storage_context と index_id=”miyabe_miyuki” を使って、ストレージからインデックスをロードしています。
• index_id=”miyabe_miyuki” は、保存したインデックスに設定された一意のIDです。このIDを指定することで、その特定のインデックスをロードしています。これにより、以前に保存したインデックスを再構築することなく、再度使用できるようになります。

つまり
先の部分で

index.set_index_id("miyabe_miyuki")
index.storage_context.persist("./storage/simple_vs")

でIDとデータ保存先を指定しているので
これを読み込んでいるということ
データ先を指定してIDを指定して読み込んでいる

ここまでがllamaindexのデータ保存

ここからがChromaDBを使用してベクトルストアを保存および再利用する方法

pip install chromadb llama-index-vector-stores-chroma

でインストール

ChromaDBを使用してベクトルストアのインデックスを作成

import chromadb
from llama_index.core import VectorStoreIndex
from llama_index.vector_stores.chroma import ChromaVectorStore
from llama_index.core import StorageContext

chroma_client = chromadb.EphemeralClient()
chroma_collection = chroma_client.create_collection("miyabe_miyuki")

vector_store = ChromaVectorStore(chroma_collection=chroma_collection)
storage_context = StorageContext.from_defaults(vector_store=vector_store)

chroma_index = VectorStoreIndex(nodes, storage_context=storage_context)

解説

chroma_client = chromadb.EphemeralClient()

ChromaDBのエフェメラル(短期間の)クライアントを作成します。これは、データを一時的に保存し、サーバーが終了すると削除されるメモリ内のデータベースを意味します。

chroma_collection = chroma_client.create_collection(“miyabe_miyuki”)

chroma_clientを使用して、新しいコレクション(データのコンテナ)を作成しています。ここではコレクション名を”miyabe_miyuki”として指定しています。

vector_store = ChromaVectorStore(chroma_collection=chroma_collection)

ChromaVectorStoreを使って、ChromaDB内のコレクション(chroma_collection)をベクトルストアとして扱うためのオブジェクトを作成しています。これにより、コレクション内のデータに対してベクトル検索を行えるようになります。

storage_context = StorageContext.from_defaults(vector_store=vector_store)

StorageContextは、データの保存や管理を行うためのオブジェクトです。ここでは、先に作成したvector_store(ChromaDBベースのベクトルストア)をStorageContextにセットして、データ管理を行えるようにしています。

そして
インデックスをもとにクエリエンジンを作成

query_engine = chroma_index.as_query_engine()
response = query_engine.query("『理由』で事件のあったマンション名を教えて下さい。")
print(response)

解説

query_engine = chroma_index.as_query_engine()

• as_query_engine:インデックスもとにユーザーの質問に対して回答を生成するクエリエンジンを構築します。

つまり

llamaindexの時にはquery_engine.query

だったけど
chromaなら

chroma_index.as_query_engine()

を使う

インデックスのIDを設定し、指定したディレクトリに保存

db = chromadb.PersistentClient(path="./storage/chroma")
chroma_collection = db.get_or_create_collection("miyabe_miyuki")
vector_store = ChromaVectorStore(chroma_collection=chroma_collection)
storage_context = StorageContext.from_defaults(vector_store=vector_store)
chroma_index = VectorStoreIndex(nodes, storage_context=storage_context, show_progress=True)

解説

db = chromadb.PersistentClient(path=”./storage/chroma”)

* PersistentClientを作成し、path=”./storage/chroma”という指定されたディレクトリにデータを永続化します。

chroma_collection = db.get_or_create_collection(“miyabe_miyuki”)

* get_or_create_collectionメソッドは、”miyabe_miyuki”という名前のコレクションを取得するか、存在しない場合は新しく作成します。

vector_store = ChromaVectorStore(chroma_collection=chroma_collection)

* ChromaVectorStoreを作成し、先ほど作成したchroma_collectionをベクトルストアとして使用します。

storage_context = StorageContext.from_defaults(vector_store=vector_store)

* StorageContextを作成し、vector_storeをその一部として設定します。

chroma_index = VectorStoreIndex(nodes, storage_context=storage_context, show_progress=True)

* VectorStoreIndexを作成し、nodesとstorage_contextをもとにインデックスを構築します。
* VectorStoreIndexは、ベクトル検索用のインデックスを作成し、指定されたnodesをインデックス化します。
• show_progress=Trueは、インデックス作成中に進行状況を表示するオプションで、インデックスの構築が進んでいるかを確認できます。

以前のllamaindex単体の時には

index.set_index_id("miyabe_miyuki")
index.storage_context.persist("./storage/simple_vs")

chromaの時には

db = chromadb.PersistentClient(path="./storage/chroma")
chroma_collection = db.get_or_create_collection("miyabe_miyuki")
vector_store = ChromaVectorStore(chroma_collection=chroma_collection)
storage_context = StorageContext.from_defaults(vector_store=vector_store)
chroma_index = VectorStoreIndex(nodes, storage_context=storage_context, show_progress=True)

というように最初に
DB保存先を指定
get_or_create_collectionでDB情報の取得、ないなら新規作成する
ChromaVectorStoreを作成してベクトルスコアを指定

from_defaults() によって設定されるデフォルトのインメモリ vector_store を、
ユーザーが事前に定義した vector_store オブジェクトで上書きしている

デフォルトでは、すべてのストア(docstore, vector_store, index_store)は、シンプルなインメモリ(RAM上)のストアとして初期化される

つまり

storage_context = StorageContext.from_defaults(vector_store=vector_store)

によって
このコード行の目的は、VectorStoreIndex を構築する際に、ベクトル埋め込みの保存先として、デフォルトのインメモリストアではなく、指定された外部のベクトルデータベース(vector_store 変数で指定されたもの)を使用するように設定

次に
保存されたインデックスをストレージからロードして再利用

db = chromadb.PersistentClient(path="./storage/chroma")
chroma_collection = db.get_or_create_collection("miyabe_miyuki")
chroma_vector_store = ChromaVectorStore(chroma_collection=chroma_collection)
chroma_index = VectorStoreIndex.from_vector_store(vector_store=chroma_vector_store)

解説

chroma_collection = db.get_or_create_collection(“miyabe_miyuki”)

PersistentClientを使って、以前保存したChromaDBのデータベースを再利用するための接続を行います。
pathで指定したディレクトリ (./storage/chroma) に保存されたデータベースを読み込み、再利用します。

chroma_vector_store = ChromaVectorStore(chroma_collection=chroma_collection)

すでに保存されている “miyabe_miyuki” という名前のコレクションを取得します。g
et_or_create_collectionは、もしコレクションが存在しない場合に新たに作成しますが、
ここでは既存のコレクションを再利用しています。

chroma_index = VectorStoreIndex.from_vector_store(vector_store=chroma_vector_store)

取得したコレクションを元にChromaVectorStoreを読み込み、保存されたベクトルストアを再利用します。
これにより、新規にベクトルストアを構築することなく、保存済みのベクトルデータをそのまま利用することができます。

chroma_index = VectorStoreIndex.from_vector_store(vector_store=chroma_vector_store)

from_vector_storeメソッドを使って、保存されたベクトルストア (chroma_vector_store) を元にVectorStoreIndexを再利用します。

とりあえず解説はこれがわかりやすい

他の事例も調べる
ChromaDBのコレクションを作成またはアクセスする際にwatsonx.aiのEmbeddingモデルを使用する方法(watsonx.ai + LlamaIndex + Chroma DB)

なお

import ollama
from llama_index.core import Settings
from llama_index.llms.ollama import Ollama

modelfile='''
FROM ./Llama-3-ELYZA-JP-8B-q4_k_m.gguf
TEMPLATE """{{ if .System }}<|start_header_id|>system<|end_header_id|>

{{ .System }}<|eot_id|>{{ end }}{{ if .Prompt }}<|start_header_id|>user<|end_header_id|>

{{ .Prompt }}<|eot_id|>{{ end }}<|start_header_id|>assistant<|end_header_id|>

{{ .Response }}<|eot_id|>"""
PARAMETER stop "<|start_header_id|>"
PARAMETER stop "<|end_header_id|>"
PARAMETER stop "<|eot_id|>"
PARAMETER stop "<|reserved_special_token"
'''

ollama.create(model='elyza8b', modelfile=modelfile)
Settings.llm = Ollama(model="elyza8b", request_timeout=360.0)

でモデルを作成しているが既にあるなら

modelfile='''
FROM ./Llama-3-ELYZA-JP-8B-q4_k_m.gguf
TEMPLATE """{{ if .System }}<|start_header_id|>system<|end_header_id|>

{{ .System }}<|eot_id|>{{ end }}{{ if .Prompt }}<|start_header_id|>user<|end_header_id|>

{{ .Prompt }}<|eot_id|>{{ end }}<|start_header_id|>assistant<|end_header_id|>

{{ .Response }}<|eot_id|>"""
PARAMETER stop "<|start_header_id|>"
PARAMETER stop "<|end_header_id|>"
PARAMETER stop "<|eot_id|>"
PARAMETER stop "<|reserved_special_token"
'''

ollama.create(model='elyza8b', modelfile=modelfile)

は不要

また

embed_model = HuggingFaceEmbedding(model_name="all-MiniLM-L6-v2")

よりも

Settings.embed_model = HuggingFaceEmbedding(model_name="intfloat/multilingual-e5-large")

の方が日本語取扱には適するらしい

 touch save_index_test.py

でテストする

import ollama
from llama_index.core import Settings
from llama_index.llms.ollama import Ollama

Settings.llm = Ollama(model="elyza8b", request_timeout=360.0)

#日本語性能が高く、無料で使える埋め込みモデル「intfloat/multilingual-e5-large」を指定
from llama_index.embeddings.huggingface import HuggingFaceEmbedding
Settings.embed_model = HuggingFaceEmbedding(model_name="intfloat/multilingual-e5-large")

#Wikipediaの『宮部みゆきの小説』に関するページを指定
from llama_index.readers.web import SimpleWebPageReader
documents = SimpleWebPageReader(html_to_text=True).load_data(
    ["https://ja.wikipedia.org/wiki/%E7%81%AB%E8%BB%8A_(%E5%B0%8F%E8%AA%AC)",
    "https://ja.wikipedia.org/wiki/%E7%90%86%E7%94%B1_(%E5%B0%8F%E8%AA%AC)",
    "https://ja.wikipedia.org/wiki/%E5%90%8D%E3%82%82%E3%81%AA%E3%81%8D%E6%AF%92",
    "https://ja.wikipedia.org/wiki/%E3%82%BD%E3%83%AD%E3%83%A2%E3%83%B3%E3%81%AE%E5%81%BD%E8%A8%BC"]
)

#読み込んだデータをチャンクに分割し、各チャンクをノードとして処理します
from llama_index.core.node_parser import TokenTextSplitter

splitter = TokenTextSplitter(
    chunk_size=1000,
    chunk_overlap=100,
    separator=" ",
)
nodes = splitter.get_nodes_from_documents(documents)
nodes[10:13]

#VectorStoreIndexクラスを使って、ベクトルストアのインデックスを作成
from llama_index.core import VectorStoreIndex

index = VectorStoreIndex(nodes, show_progress=True)

#インデックスをもとにクエリエンジンを作成
query_engine = index.as_query_engine(similarity_top_k=3)
response = query_engine.query("火車の概要を教えて下さい")
print(response)

#インデックスのIDを設定し、指定したディレクトリに保存
index.set_index_id("miyabe_miyuki")
index.storage_context.persist("./storage/simple_vs")

#保存されたインデックスをストレージからロードして再利用
from llama_index.core import StorageContext, load_index_from_storage

storage_context = StorageContext.from_defaults(persist_dir="./storage/simple_vs")

simple_vc_index = load_index_from_storage(storage_context, index_id="miyabe_miyuki")

というようにまとめたものを
インデックス作成のみにコード変更
作成は gemini を使用

import ollama
from llama_index.core import Settings
from llama_index.llms.ollama import Ollama
from llama_index.embeddings.huggingface import HuggingFaceEmbedding
from llama_index.readers.web import SimpleWebPageReader
from llama_index.core.node_parser import TokenTextSplitter
from llama_index.core import VectorStoreIndex

# --- 1. LLMと埋め込みモデルの設定 ---
# OllamaのLLMを設定
# 'elyza8b'モデルが既にOllamaサーバーに存在していることを前提とします。
Settings.llm = Ollama(model="elyza8b", request_timeout=360.0)

# 日本語性能の高い埋め込みモデルを設定
Settings.embed_model = HuggingFaceEmbedding(model_name="intfloat/multilingual-e5-large")
print("LLMと埋め込みモデルの設定が完了しました。")

# --- 2. データのロード ---
# Wikipediaの指定されたページからデータをロード
print("Wikipediaページからデータをロード中...")
urls = [
    "https://ja.wikipedia.org/wiki/%E7%81%AB%E8%BB%8A_(%E5%B0%8F%E8%AA%AC)",
    "https://ja.wikipedia.org/wiki/%E7%90%86%E7%94%B1_(%E5%B0%8F%E8%AA%AC)",
    "https://ja.wikipedia.org/wiki/%E5%90%8D%E3%82%82%E3%81%AA%E3%81%8D%E6%AF%92",
    "https://ja.wikipedia.org/wiki/%E3%82%BD%E3%83%AD%E3%83%A2%E3%83%B3%E3%81%AE%E5%81%BD%E8%A8%BC"
]
documents = SimpleWebPageReader(html_to_text=True).load_data(urls)
print(f"合計 {len(documents)} ページのドキュメントをロードしました。")

# --- 3. ドキュメントのチャンク化とノード化 ---
# 読み込んだドキュメントをチャンクに分割し、ノードとして処理
print("ドキュメントをノードに分割中...")
splitter = TokenTextSplitter(
    chunk_size=1000,
    chunk_overlap=100,
    separator=" ",
)
nodes = splitter.get_nodes_from_documents(documents)
print(f"合計 {len(nodes)} 個のノードが生成されました。")
# 確認のため、一部のノードを表示(オプション)
# print("\n一部のノードの例:")
# for i, node in enumerate(nodes[10:13]):
#     print(f"Node {i+10}: {node.text[:100]}...") # 最初の100文字を表示
# print("---")

# --- 4. VectorStoreIndexの作成 ---
# ノードからVectorStoreIndexを作成
print("VectorStoreIndexを作成中...")
index = VectorStoreIndex(nodes, show_progress=True)
print("VectorStoreIndexの作成が完了しました。")

# --- 5. インデックスの保存 ---
# インデックスにIDを設定し、指定したディレクトリに保存
# 保存先ディレクトリが存在しない場合は自動で作成されます。
index_id = "miyabe_miyuki"
persist_dir = "./storage/simple_vs"

print(f"インデックスを '{persist_dir}' に ID '{index_id}' で保存中...")
index.set_index_id(index_id)
index.storage_context.persist(persist_dir)
print("インデックスの保存が完了しました。")
print("これで、次回からはこのインデックスを再利用できます。")

実行したら

Traceback (most recent call last):
  File "/Users/snowpool/aw10s/ollama/save_index_test.py", line 5, in <module>
    from llama_index.readers.web import SimpleWebPageReader
ModuleNotFoundError: No module named 'llama_index.readers.web'

となるので

pip install llama-index-readers-web

これを実行すると

ERROR: pip's dependency resolver does not currently take into account all the packages that are installed. This behaviour is the source of the following dependency conflicts.
ollama 0.4.6 requires httpx<0.28.0,>=0.27.0, but you have httpx 0.28.1 which is incompatible.
Successfully installed chromedriver-autoinstaller-0.6.4 cssselect-1.3.0 feedfinder2-0.0.4 feedparser-6.0.11 httpx-0.28.1 jieba3k-0.35.1 llama-index-readers-web-0.4.1 lxml-5.4.0 lxml-html-clean-0.4.2 newspaper3k-0.2.8 oxylabs-2.0.0 requests-file-2.1.0 sgmllib3k-1.0.0 spider-client-0.0.27 tinysegmenter-0.3 tldextract-5.3.0

となる

なので

pip install httpx==0.28.0

これで再度

 python save_index_test.py     

を実行

modules.json: 100%|█████████████████████████████| 387/387 [00:00<00:00, 631kB/s]
README.md: 100%|██████████████████████████████| 160k/160k [00:00<00:00, 931kB/s]
sentence_bert_config.json: 100%|██████████████| 57.0/57.0 [00:00<00:00, 250kB/s]
config.json: 100%|█████████████████████████████| 690/690 [00:00<00:00, 2.76MB/s]
model.safetensors: 100%|███████████████████| 2.24G/2.24G [03:19<00:00, 11.2MB/s]
tokenizer_config.json: 100%|███████████████████| 418/418 [00:00<00:00, 1.71MB/s]
sentencepiece.bpe.model: 100%|█████████████| 5.07M/5.07M [00:00<00:00, 11.6MB/s]
tokenizer.json: 100%|██████████████████████| 17.1M/17.1M [00:01<00:00, 12.2MB/s]
special_tokens_map.json: 100%|█████████████████| 280/280 [00:00<00:00, 1.32MB/s]
config.json: 100%|██████████████████████████████| 201/201 [00:00<00:00, 994kB/s]
LLMと埋め込みモデルの設定が完了しました。
Wikipediaページからデータをロード中...
合計 4 ページのドキュメントをロードしました。
ドキュメントをノードに分割中...
合計 211 個のノードが生成されました。
VectorStoreIndexを作成中...
Generating embeddings: 100%|██████████████████| 211/211 [01:05<00:00,  3.20it/s]
VectorStoreIndexの作成が完了しました。
インデックスを './storage/simple_vs' に ID 'miyabe_miyuki' で保存中...
インデックスの保存が完了しました。
これで、次回からはこのインデックスを再利用できます。

次に
保存されたインデックスをロードし、キーボードからの質問に回答する

 ls storage/simple_vs/
default__vector_store.json	image__vector_store.json
docstore.json			index_store.json
graph_store.json


インデックスデータが保存されていることを確認

touch ask_index.py

import sys
from llama_index.core import StorageContext, load_index_from_storage
from llama_index.core import Settings
from llama_index.llms.ollama import Ollama
from llama_index.embeddings.huggingface import HuggingFaceEmbedding

# --- 1. LLMと埋め込みモデルの設定 ---
# インデックス作成時と同じモデルを設定する必要があります。
# これにより、クエリのベクトル化と回答生成が正しく行われます。
Settings.llm = Ollama(model="elyza8b", request_timeout=360.0)
Settings.embed_model = HuggingFaceEmbedding(model_name="intfloat/multilingual-e5-large")
print("LLMと埋め込みモデルの設定が完了しました。")

# --- 2. インデックスのロード ---
print("インデックスをロード中...")
try:
    # 保存先のディレクトリとインデックスIDを指定
    persist_dir = "./storage/simple_vs"
    index_id = "miyabe_miyuki"

    # StorageContext を設定し、保存先のディレクトリを指定
    storage_context = StorageContext.from_defaults(persist_dir=persist_dir)

    # 指定されたストレージコンテキストとインデックスIDでインデックスをロード
    # これにより、ディスクからデータがメモリに読み込まれ、再利用可能になります。
    loaded_index = load_index_from_storage(storage_context, index_id=index_id)
    print("インデックスのロードが完了しました。")

    # ロードしたインデックスからクエリエンジンを作成
    # similarity_top_k は、回答生成のために検索するノードの数を指定します。
    query_engine = loaded_index.as_query_engine(similarity_top_k=3)
    print("クエリエンジンの準備ができました。")

    # --- 3. ユーザーからの質問を受け付けるループ ---
    print("\n質問を入力してください(終了するには 'exit' と入力)。")
    while True:
        query = input("あなたの質問: ")

        if query.lower() == 'exit':
            print("プログラムを終了します。")
            break

        if not query.strip(): # 空の入力の場合
            print("質問を入力してください。")
            continue

        print("回答を生成中...")
        try:
            # クエリエンジンで質問を処理し、回答を取得
            response = query_engine.query(query)
            print("\n--- 回答 ---")
            print(response)
            print("------------\n")
        except Exception as e:
            print(f"質問処理中にエラーが発生しました: {e}")
            print("もう一度お試しください。")

except FileNotFoundError:
    print(f"エラー: インデックスデータが見つかりません。'{persist_dir}' ディレクトリが存在し、以前にインデックスがID '{index_id}' で保存されているか確認してください。")
    print("インデックスを保存するには、提供された「インデックス作成と保存コード」を実行する必要があります。")
    sys.exit(1) # プログラムを終了
except Exception as e:
    print(f"予期せぬエラーが発生しました: {e}")
    sys.exit(1) # プログラムを終了

で保存し

python ask_index.py 
LLMと埋め込みモデルの設定が完了しました。
インデックスをロード中...
インデックスのロードが完了しました。
クエリエンジンの準備ができました。

質問を入力してください(終了するには 'exit' と入力)。
あなたの質問: 火車の概要を教えて下さい 
回答を生成中...
質問処理中にエラーが発生しました: model "elyza8b" not found, try pulling it first
もう一度お試しください。
あなたの質問: exit
プログラムを終了します。

となるので、モデル名を修正
正しくは

Settings.llm = Ollama(model="elyza:jp8b", request_timeout=360.0)

これで実行すると

 python ask_index.py
LLMと埋め込みモデルの設定が完了しました。
インデックスをロード中...
インデックスのロードが完了しました。
クエリエンジンの準備ができました。

質問を入力してください(終了するには 'exit' と入力)。
あなたの質問: 火車の概要を教えて下さい 
回答を生成中...

--- 回答 ---
『火車』は、宮部みゆきの推理小説である。社会問題としての消費者金融のありかたをテーマとしており、サラリーマン金融やカード破産などの借財と多重債務を題材にした作品となっている。
------------

というように問題なく動作することができる
ただし挙動が少し遅い

他の質問をすると

あなたの質問: 日本の総理大臣は?
回答を生成中...

--- 回答 ---
この情報には日本の総理大臣に関する記述がありません。
------------

あなたの質問: exit
プログラムを終了します。
となってしまう

この結果を Gemini で調べてみると

うまく動作していますね! LlamaIndex を使った RAG (Retrieval-Augmented Generation) システムが意図通りに機能していることを示しています。
動作の分析
あなたの実行結果は、以下のように解釈できます。
1. 火車の概要を教えて下さい:
* 期待通りの動作: 『火車』に関する情報は、インデックスの作成時に指定したWikipediaのURL(https://ja.wikipedia.org/wiki/%E7%81%AB%E8%BB%8A_(%E5%B0%8F%E8%AA%AC))に含まれています。
* RAGの成功: LlamaIndex が質問をベクトル化し、関連するノードをインデックスから検索し、その情報に基づいて Ollama の elyza8b モデルが正確な概要を生成しました。

2. 陰陽五行について教えて:
* 「記述がありません。」: これは、インデックス作成時に与えたWikipediaのURL(宮部みゆきさんの小説に関するページ)に陰陽五行に関する情報が含まれていないため、LlamaIndex が関連するノードを見つけられなかったことを意味します。
* LLMの正直な回答: 大規模言語モデル(LLM)は、検索された情報に基づいて回答を生成します。関連情報が見つからない場合、このように「記述がありません」といった回答を返すのは、ハルシネーション(でっち上げ)を防ぐための正しい挙動です。

3. 日本の総理大臣は?:
* 「この情報には日本の総理大臣に関する記述がありません。」: 陰陽五行 の場合と同様に、インデックスのソースデータには日本の総理大臣に関する情報が含まれていません。
* LLMの正しい挙動: LLMは与えられた知識ベース(今回RAGで検索されたノード)の範囲内で回答を生成しようとします。その範囲外の知識については、このように情報がないことを伝えます。

まとめ
あなたの LlamaIndex 環境は、指定した知識ベース(今回の場合、宮部みゆきさんのWikipediaページ)に基づいて質問に回答できる状態になっています。これは、RAG システムが意図通りに機能している証拠です。
* インデックスに含まれる情報: 質問に正確に回答。
* インデックスに含まれない情報: 情報がないことを正直に回答。
この挙動は、限定された専門知識を持つAIシステムを構築する上で非常に重要です。もし汎用的な知識も必要とする場合は、インデックスに追加するドキュメントの範囲を広げたり、RAGと通常のLLMの推論を組み合わせたりするなどの工夫が必要になります。

つまり動作は正常だが
RAGと通常のLLMの推論を組み合わせが必要になる

import sys
from llama_index.core import StorageContext, load_index_from_storage, Settings
from llama_index.llms.ollama import Ollama
from llama_index.embeddings.huggingface import HuggingFaceEmbedding

# --- 1. LLMと埋め込みモデルの設定 ---
Settings.llm = Ollama(model="elyza:jp8b", request_timeout=360.0)
Settings.embed_model = HuggingFaceEmbedding(model_name="intfloat/multilingual-e5-large")
print("LLMと埋め込みモデルの設定が完了しました。")

# --- 2. インデックスのロード ---
print("インデックスをロード中...")
try:
    persist_dir = "./storage/simple_vs"
    index_id = "miyabe_miyuki"

    storage_context = StorageContext.from_defaults(persist_dir=persist_dir)
    loaded_index = load_index_from_storage(storage_context, index_id=index_id)
    print("インデックスのロードが完了しました。")

    query_engine = loaded_index.as_query_engine(similarity_top_k=3)
    print("クエリエンジンの準備ができました。")

    direct_llm = Settings.llm  # 直接LLM回答用のLLM

    print("\n質問を入力してください(終了するには 'exit' と入力)。")
    while True:
        query = input("あなたの質問: ")

        if query.lower() == 'exit':
            print("プログラムを終了します。"); break

        if not query.strip():
            print("質問を入力してください。"); continue

        print("回答を生成中...")
        try:
            response = query_engine.query(query)
            
            # RAGの回答が空または不十分な場合
            if not response or "情報がありません" in response.response or len(response.response.strip()) == 0:
                print("RAGに該当情報がないため、直接LLMで回答します。");
                direct_response = direct_llm.complete(query)
                final_response = direct_response.text
            else:
                final_response = response.response

            print("\n--- 回答 ---")
            print(final_response)
            print("------------\n")

        except Exception as e:
            print(f"質問処理中にエラーが発生しました: {e}")

except FileNotFoundError:
    print(f"エラー: インデックスデータが見つかりません。'{persist_dir}' を確認してください。");
    sys.exit(1)
except Exception as e:
    print(f"予期せぬエラーが発生しました: {e}");
    sys.exit(1)

としたが

実行すると

LLMと埋め込みモデルの設定が完了しました。 インデックスをロード中... インデックスのロードが完了しました。 クエリエンジンの準備ができました。 質問を入力してください(終了するには 'exit' と入力)。 あなたの質問: 日本の総理大臣は 回答を生成中... --- 回答 --- 提供された文書には、日本の総理大臣に関する情報は含まれていません。 ------------ 

となる

elyza:jp8b単体では答えが出る

これは、現在のコードでRAGの回答が「情報がない」と認識できるように明示的な処理ができていないために起きています。
具体的には、LlamaIndexのquery_engine.query()メソッドは、情報がないときでも通常「情報がありません」というような明確なテキストを返さず、「提供された文書には〜情報は含まれていません」という形式のメッセージを返します。このため、現在の条件式では不十分

解決方法:
RAGの回答に「含まれていません」「情報は含まれていません」などの特定のフレーズが含まれているかで判定し、直接LLMを使って回答を得るよう調整

response = query_engine.query(query)

# RAGの回答が不十分な場合の判断を改善
insufficient_phrases = ["含まれていません", "情報は含まれていません", "情報がありません", "該当情報がありません"]

if any(phrase in response.response for phrase in insufficient_phrases) or not response.response.strip():
    print("RAGに該当情報がないため、直接LLMで回答します。")
    direct_response = direct_llm.complete(query)
    final_response = direct_response.text
else:
    final_response = response.response

print("\n--- 回答 ---")
print(final_response)
print("------------\n")

しかし

質問を入力してください(終了するには 'exit' と入力)。
あなたの質問: 日本の総理大臣
回答を生成中...

--- 回答 ---
提供された文書には、特定のページに関する情報やリンクが含まれていますが、日本の総理大臣に関する記述はありません。
------------

となる

RAGの回答判定がうまく働いていないようです。この問題の原因は、返ってくる回答オブジェクトの型や内容が期待と異なるためです。
以下のように、明確に response.response の内容をチェックし、不十分な回答だと判断したらLLMへフォールバックするようコードを厳密に書き直す

import sys
from llama_index.core import StorageContext, load_index_from_storage, Settings
from llama_index.llms.ollama import Ollama
from llama_index.embeddings.huggingface import HuggingFaceEmbedding

# --- 1. LLMと埋め込みモデルの設定 ---
Settings.llm = Ollama(model="elyza:jp8b", request_timeout=360.0)
Settings.embed_model = HuggingFaceEmbedding(model_name="intfloat/multilingual-e5-large")
print("LLMと埋め込みモデルの設定が完了しました。")

# --- 2. インデックスのロード ---
print("インデックスをロード中...")
try:
    persist_dir = "./storage/simple_vs"
    index_id = "miyabe_miyuki"

    storage_context = StorageContext.from_defaults(persist_dir=persist_dir)
    loaded_index = load_index_from_storage(storage_context, index_id=index_id)
    print("インデックスのロードが完了しました。")

    query_engine = loaded_index.as_query_engine(similarity_top_k=3)
    print("クエリエンジンの準備ができました。")

    direct_llm = Settings.llm  # 直接LLM回答用のLLM

    print("\n質問を入力してください(終了するには 'exit' と入力)。")
    while True:
        query = input("あなたの質問: ")

        if query.lower() == 'exit':
            print("プログラムを終了します。"); break

        if not query.strip():
            print("質問を入力してください。"); continue

        print("回答を生成中...")
        try:
            response = query_engine.query(query)
            
            # RAGの回答が不十分な場合の判断を改善
            insufficient_phrases = [
                "含まれていません",
                "情報は含まれていません",
                "情報がありません",
                "該当情報がありません",
                "記述はありません",
                "見つかりません"
            ]


            # 判定をresponse.responseのテキストで明示的に行う
            if (not response.response.strip()) or any(phrase in response.response for phrase in insufficient_phrases):
                print("RAGに該当情報がないため、直接LLMで回答します。")
                direct_response = direct_llm.complete(query)
                final_response = direct_response.text
            else:
                final_response = response.response

            print("\n--- 回答 ---")
            print(final_response)
            print("------------\n")

        except Exception as e:
            print(f"質問処理中にエラーが発生しました: {e}")

except FileNotFoundError:
    print(f"エラー: インデックスデータが見つかりません。'{persist_dir}' を確認してください。");
    sys.exit(1)
except Exception as e:
    print(f"予期せぬエラーが発生しました: {e}");
    sys.exit(1)

これで
* insufficient_phrases に『記述はありません』や『見つかりません』といった、今回表示された具体的なフレーズを追加しました。
* これにより、今回のような回答が出た場合でも、確実にLLM(elyza:jp8b)にフォールバックします。
この修正版のコードを適用すると、質問『日本の総理大臣』のようにRAGに情報がない場合でも、elyza:jp8bを使用して正しく回答を生成できるようになります

実行すると


LLMと埋め込みモデルの設定が完了しました。
インデックスをロード中...
インデックスのロードが完了しました。
クエリエンジンの準備ができました。

質問を入力してください(終了するには 'exit' と入力)。
あなたの質問: 日本の総理
回答を生成中...
RAGに該当情報がないため、直接LLMで回答します。

--- 回答 ---
日本の歴代総理大臣の一覧です。

1. 伊藤博文 (1878年12月22日 - 1885年12月24日)
2. 黒田清隆 (1885年12月24日 - 1889年4月10日)
3. 山縣有朋 (1889年4月10日 - 1891年11月30日)
4. 松方正義 (1891年11月30日 - 1896年9月18日)
5. 伊藤博文 (1896年9月18日 - 1898年11月30日)
6. 小松原英太郎 (1898年11月30日 - 1900年10月31日)
7. 加藤高明 (1900年10月31日 - 1901年6月13日)
8. 松方正義 (1901年6月13日 - 1902年9月15日)
9. 尾崎行雄 (1902年9月15日 - 1903年7月30日)
10. 上原勇作 (1903年7月30日 - 1904年11月29日)
11. 福島県知事・大隈重信 (1905年8月31日 - 1906年12月25日)
12. 桂太郎 (1907年1月18日 - 1911年10月28日)
13. 小村寿太郎 (1912年4月30日 - 1913年12月21日)
14. 内閣総理大臣・清浦奎吾 (1914年5月15日 - 1916年8月10日)
15. 加藤友三郎 (1916年8...
------------

あなたの質問: 今の日本の総理大臣
回答を生成中...

--- 回答 ---
この情報には、特定の人物や役職の名前が多数記載されています。ただし、現在の日本の総理大臣は明記されていません。
------------

あなたの質問: 今年の日本の総理大臣
回答を生成中...
RAGに該当情報がないため、直接LLMで回答します。

--- 回答 ---
2023年現在の日本の内閣総理大臣は、岸田文雄です。
------------

あなたの質問: 日本の首都は?
回答を生成中...

--- 回答 ---
東京
------------

あなたの質問: 現在のアメリカの大統領
回答を生成中...
RAGに該当情報がないため、直接LLMで回答します。

--- 回答 ---
2021年現在、アメリカ合衆国の大統領はジョー・バイデンです。
------------

となる

この状況が起きている原因は、LLMの回答においてモデルが「現在の年(2023年や2021年)」を正しく認識しておらず、古い情報や曖昧な回答を出してしまっているためです。
これはモデルが持つ知識が訓練データ(2023年10月まで)に依存しているため、現在(2025年)の最新情報とずれが生じていることが原因

これは
推論時に「現在の日付」をプロンプトに含めることで、回答を現在の日付に基づいた正確なものに誘導することができます

from datetime import datetime

response = query_engine.query(query)

insufficient_phrases = [
    "含まれていません",
    "情報は含まれていません",
    "情報がありません",
    "該当情報がありません",
    "記述はありません",
    "見つかりません"
]

current_date = datetime.now().strftime("%Y年%m月%d日")

if (not response.response.strip()) or any(phrase in response.response for phrase in insufficient_phrases):
    print("RAGに該当情報がないため、直接LLMで回答します。")
    
    prompt_with_date = f"今日は{current_date}です。この日付を基準として次の質問に答えてください。\n質問:{query}"
    direct_response = direct_llm.complete(prompt_with_date)
    final_response = direct_response.text
else:
    final_response = response.response

print("\n--- 回答 ---")
print(final_response)
print("------------\n")

というようにコード変更する

これで実験する

LLMと埋め込みモデルの設定が完了しました。
インデックスをロード中...
インデックスのロードが完了しました。
クエリエンジンの準備ができました。

質問を入力してください(終了するには 'exit' と入力)。
あなたの質問: 現在のアメリカの大統領
回答を生成中...

--- 回答 ---
この質問は、ウィキペディアやウィキデータに該当する情報が含まれています。
------------

あなたの質問: 今の日本の総理大臣
回答を生成中...

--- 回答 ---
この情報は、Wikipediaのページ編集用のメニューであり、政治や人物に関する情報が含まれています。したがって、今の日本の総理大臣を特定することができません。
------------

という結果になる

このため
RAGの応答が新たなパターンで返ってきているため、現在の条件文では対応できていないからです。
今回返ってきた文言:
* 「ウィキペディアやウィキデータに該当する情報が含まれています。」
* 「Wikipediaのページ編集用のメニューであり〜特定することができません。」
を追加の判定条件として組み込み対処

            insufficient_phrases = [
                "含まれていません",
                "情報は含まれていません",
                "情報がありません",
                "該当情報がありません",
                "記述はありません",
                "見つかりません",
                "特定することができません",
                "Wikipediaのページ編集用",
                "ウィキペディアやウィキデータ"
            ]

というように対応ワードを追加

実行結果で気になるのが

import sys
from llama_index.core import StorageContext, load_index_from_storage, Settings
from llama_index.llms.ollama import Ollama
from llama_index.embeddings.huggingface import HuggingFaceEmbedding
from datetime import datetime

# --- 1. LLMと埋め込みモデルの設定 ---
Settings.llm = Ollama(model="elyza:jp8b", request_timeout=360.0)
Settings.embed_model = HuggingFaceEmbedding(model_name="intfloat/multilingual-e5-large")
print("LLMと埋め込みモデルの設定が完了しました。")

# --- 2. インデックスのロード ---
print("インデックスをロード中...")
try:
    persist_dir = "./storage/simple_vs"
    index_id = "miyabe_miyuki"

    storage_context = StorageContext.from_defaults(persist_dir=persist_dir)
    loaded_index = load_index_from_storage(storage_context, index_id=index_id)
    print("インデックスのロードが完了しました。")

    query_engine = loaded_index.as_query_engine(similarity_top_k=3)
    print("クエリエンジンの準備ができました。")

    direct_llm = Settings.llm  # 直接LLM回答用のLLM

    print("\n質問を入力してください(終了するには 'exit' と入力)。")
    while True:
        query = input("あなたの質問: ")

        if query.lower() == 'exit':
            print("プログラムを終了します。"); break

        if not query.strip():
            print("質問を入力してください。"); continue

        print("回答を生成中...")
        try:
            response = query_engine.query(query)

            insufficient_phrases = [
                "含まれていません",
                "情報は含まれていません",
                "情報がありません",
                "該当情報がありません",
                "記述はありません",
                "見つかりません",
                "特定することができません",
                "Wikipediaのページ編集用",
                "ウィキペディアやウィキデータ",
                "記載がありません",
                "MediaWiki",
                "閲覧したとしても"
            ]

            current_date = datetime.now().strftime("%Y年%m月%d日")

            if (not response.response.strip()) or any(phrase in response.response for phrase in insufficient_phrases):
                print("RAGに該当情報がないため、直接LLMで回答します。")
                
                prompt_with_date = f"今日は{current_date}です。この日付を基準として次の質問に答えてください。\n質問:{query}"
                direct_response = direct_llm.complete(prompt_with_date)
                final_response = direct_response.text
            else:
                final_response = response.response

            print("\n--- 回答 ---")
            print(final_response)
            print("------------\n")


        except Exception as e:
            print(f"質問処理中にエラーが発生しました: {e}")

except FileNotFoundError:
    print(f"エラー: インデックスデータが見つかりません。'{persist_dir}' を確認してください。");
    sys.exit(1)
except Exception as e:
    print(f"予期せぬエラーが発生しました: {e}");
    sys.exit(1)

を実行すると

あなたの質問: 現在時刻を教えて
回答を生成中...

--- 回答 ---
2025年5月11日(日)10:52
------------

となる
現在の日付とあっていない
しかし

あなたの質問: 今日は何曜日?
回答を生成中...
RAGに該当情報がないため、直接LLMで回答します。

--- 回答 ---
2025年05月29日は、木曜日にあたります。
------------

では正確な答えになる

これは
質問に日付を含めてモデルに渡してはいるものの、「現在時刻を教えて」の質問に対して、モデルが自分の持つ内部情報(訓練時の情報)を参照して回答を返してしまっているから
具体的に言うと、質問に『今日は2025年05月29日です』と日付は含めていますが、『時刻』までは渡していません。そのため、LLMが「時刻」に関しては独自の推測(誤った日時)を返しています

日付だけでなく、現在時刻も明示的にプロンプトに含めることで正確な回答を得られるようになります
ということなので

            current_date = datetime.now().strftime("%Y年%m月%d日")

            current_datetime = datetime.now().strftime("%Y年%m月%d日 %H:%M")

            prompt_with_datetime = f"現在の日時は {current_datetime} です。この日時を基準として次の質問に答えてください。\n質問:{query}"

と変更

全体コードとしては

import sys
from llama_index.core import StorageContext, load_index_from_storage, Settings
from llama_index.llms.ollama import Ollama
from llama_index.embeddings.huggingface import HuggingFaceEmbedding
from datetime import datetime

# --- 1. LLMと埋め込みモデルの設定 ---
Settings.llm = Ollama(model="elyza:jp8b", request_timeout=360.0)
Settings.embed_model = HuggingFaceEmbedding(model_name="intfloat/multilingual-e5-large")
print("LLMと埋め込みモデルの設定が完了しました。")

# --- 2. インデックスのロード ---
print("インデックスをロード中...")
try:
    persist_dir = "./storage/simple_vs"
    index_id = "miyabe_miyuki"

    storage_context = StorageContext.from_defaults(persist_dir=persist_dir)
    loaded_index = load_index_from_storage(storage_context, index_id=index_id)
    print("インデックスのロードが完了しました。")

    query_engine = loaded_index.as_query_engine(similarity_top_k=3)
    print("クエリエンジンの準備ができました。")

    direct_llm = Settings.llm  # 直接LLM回答用のLLM

    print("\n質問を入力してください(終了するには 'exit' と入力)。")
    while True:
        query = input("あなたの質問: ")

        if query.lower() == 'exit':
            print("プログラムを終了します。"); break

        if not query.strip():
            print("質問を入力してください。"); continue

        print("回答を生成中...")
        try:
            response = query_engine.query(query)

            insufficient_phrases = [
                "含まれていません",
                "情報は含まれていません",
                "情報がありません",
                "該当情報がありません",
                "記述はありません",
                "見つかりません",
                "特定することができません",
                "Wikipediaのページ編集用",
                "ウィキペディアやウィキデータ",
                "記載がありません",
                "MediaWiki",
                "閲覧したとしても"
            ]
            current_datetime = datetime.now().strftime("%Y年%m月%d日 %H:%M")
            prompt_with_datetime = f"現在の日時は {current_datetime} です。この日時を基準として次の質問に答えてください。\n質問:{query}"

            if (not response.response.strip()) or any(phrase in response.response for phrase in insufficient_phrases):
                print("RAGに該当情報がないため、直接LLMで回答します。")
                direct_response = direct_llm.complete(prompt_with_datetime)
                final_response = direct_response.text
            else:
                final_response = response.response

            print("\n--- 回答 ---")
            print(final_response)
            print("------------\n")

        except Exception as e:
            print(f"質問処理中にエラーが発生しました: {e}")

except FileNotFoundError:
    print(f"エラー: インデックスデータが見つかりません。'{persist_dir}' を確認してください。");
    sys.exit(1)
except Exception as e:
    print(f"予期せぬエラーが発生しました: {e}");
    sys.exit(1)

しかし結果は

LLMと埋め込みモデルの設定が完了しました。
インデックスをロード中...
インデックスのロードが完了しました。
クエリエンジンの準備ができました。

質問を入力してください(終了するには 'exit' と入力)。
あなたの質問: 現在の時刻は?
回答を生成中...
RAGに該当情報がないため、直接LLMで回答します。

--- 回答 ---
現在の時刻は、00:46です。
------------

あなたの質問: 現在時刻を教えて
回答を生成中...

--- 回答 ---
2025年5月11日 (日) 10:52
------------

あなたの質問: 今の時間は?
回答を生成中...
RAGに該当情報がないため、直接LLMで回答します。

--- 回答 ---
現在の時間は00:47です。
------------

あなたの質問: 今日は何日?
回答を生成中...

--- 回答 ---
この情報には、日付に関する記述は見当たりません。
------------

あなたの質問: 

というようにプロンプトにより結果が変わってしまう

前提条件を変更するため
Gemini で原因を聞いてみる

ご提示の実行結果を見ると、同じ「現在の時刻」に関する質問でも、RAG(Retrieval-Augmented Generation)の有無やLLMの回答内容に違いが生じていますね。この現象の主な原因は、プロンプトがLLMの回答に与える影響、特にRAGが機能しなかった場合のフォールバックメカニズムにあります。

プロンプトによる回答の正確さの変動要因
1. RAGの回答の有無と品質
RAGは、事前に用意された知識ベース(この場合はmiyabe_miyukiインデックス)から関連情報を検索し、その情報を基にLLMが回答を生成する仕組みです。
* RAGが機能した場合:
* あなたの質問「現在時刻を教えて」に対しては、RAGが何らかの情報を取得し、その情報をLLMが処理して「2025年5月11日 (日) 10:52」という回答を生成しています。これは、RAGが参照した情報源に、たまたま古い日付と時刻が含まれていた可能性を示唆しています。RAGは、その情報源に「現在時刻」に関する情報が含まれていない限り、リアルタイムの時刻を知ることはできません。 今回のインデックスが「宮部みゆき」氏に関するものであれば、時刻情報が含まれている可能性は極めて低いでしょう。
* RAGが機能しなかった場合(フォールバック):
* 「現在の時刻は?」や「今の時間は?」のように、RAGの検索結果に「不十分なフレーズ」が含まれていると判断された場合、スクリプトは直接LLMに質問を投げ直すフォールバック処理を行っています。この時、プロンプトには”現在の日時は {current_datetime} です。この日時を基準として次の質問に答えてください。\n質問:{query}”という形で、スクリプト実行時のリアルタイムの日時情報が付与されています。これにより、LLMは自身の持つ一般的な知識に加えて、明示的に与えられた最新の日時情報を参照して回答を生成します。そのため、「00:46」や「00:47」といった、スクリプト実行時に近い時刻を回答できています。
2. LLMの持つ一般的な知識とバイアス
LLMは学習データに基づいた一般的な知識を持っています。しかし、その知識はリアルタイムの情報ではありません。
* 「今日は何日?」への回答:
* この質問に対しては、RAGが「この情報には、日付に関する記述は見当たりません。」と回答しています。これはRAGの情報源に日付情報がなかったためと考えられます。そして、フォールバックが行われず、LLMに現在の日時情報を含むプロンプトが渡されなかったため、LLMはRAGの回答をそのまま出力してしまっています。もし、この質問もフォールバックの対象となっていれば、現在の正確な日付を回答できたはずです。
3. 「不十分なフレーズ」によるRAGの判断基準
スクリプト内で定義されているinsufficient_phrasesのリストは、RAGが生成した回答が「役に立たない」と判断するためのキーワードです。
* RAGの回答にこれらのフレーズが含まれていた場合、スクリプトはRAGの回答を破棄し、直接LLMに最新の日時情報を含むプロンプトを渡しています。これにより、LLMはより正確なリアルタイムの時刻情報を提供できています。

まとめ
プロンプトによって回答の正確さが変わる主な原因は、以下の2点に集約されます。
1. RAGが参照する情報源の特性: RAGは事前にインデックス化された情報しか利用できないため、リアルタイムの情報(現在の時刻や日付など)は提供できません。
2. フォールバック時のプロンプトの変化: RAGが役に立たないと判断された場合、スクリプトがLLMに渡すプロンプトにリアルタイムの日時情報が明示的に追加されるため、LLMはその情報に基づいてより正確な回答を生成できます。
したがって、今回のケースでは、スクリプトがフォールバック時に動的にプロンプトを調整していることが、時刻の回答の正確さに大きく寄与していると言えます。RAGが適切にリアルタイム情報を扱えない場合、このようなフォールバック戦略は非常に有効です。

この問題を解決するためにコードを変更

RAGの検索結果に関わらず、日付と時刻に関する質問に対しては、常に最新の日時情報をLLMに伝えるようにコードを変更

変更の概要
主な変更点は以下の通りです。
1. 日付・時刻関連のキーワードの検出: 質問内容が日付や時刻に関連するかどうかを判断するためのキーワードリストを定義します。
2. プロンプトへの日時情報追加の条件変更: RAGの回答が不十分な場合だけでなく、質問が日付や時刻に関連すると判断された場合にも、現在のリアルタイムの日時情報をLLMへのプロンプトに含めるようにします。
これにより、ユーザーが「現在の時刻は?」や「今日の日付は?」と質問した場合、RAGの検索結果に関わらず、スクリプトが取得した最新の日時情報がLLMに渡され、LLMはその情報に基づいて正確な回答を生成できるようになります

import sys
from llama_index.core import StorageContext, load_index_from_storage, Settings
from llama_index.llms.ollama import Ollama
from llama_index.embeddings.huggingface import HuggingFaceEmbedding
from datetime import datetime

# --- 1. LLMと埋め込みモデルの設定 ---
Settings.llm = Ollama(model="elyza:jp8b", request_timeout=360.0)
Settings.embed_model = HuggingFaceEmbedding(model_name="intfloat/multilingual-e5-large")
print("LLMと埋め込みモデルの設定が完了しました。")

# --- 2. インデックスのロード ---
print("インデックスをロード中...")
try:
    persist_dir = "./storage/simple_vs"
    index_id = "miyabe_miyuki"

    storage_context = StorageContext.from_defaults(persist_dir=persist_dir)
    loaded_index = load_index_from_storage(storage_context, index_id=index_id)
    print("インデックスのロードが完了しました。")

    query_engine = loaded_index.as_query_engine(similarity_top_k=3)
    print("クエリエンジンの準備ができました。")

    direct_llm = Settings.llm  # 直接LLM回答用のLLM

    # 日時関連の質問を検出するためのキーワード
    datetime_keywords = ["時刻", "時間", "日付", "何日", "今日", "今"]

    print("\n質問を入力してください(終了するには 'exit' と入力)。")
    while True:
        query = input("あなたの質問: ")

        if query.lower() == 'exit':
            print("プログラムを終了します。"); break

        if not query.strip():
            print("質問を入力してください。"); continue

        print("回答を生成中...")
        try:
            # RAGで情報を検索
            response = query_engine.query(query)

            insufficient_phrases = [
                "含まれていません", "情報は含まれていません", "情報がありません",
                "該当情報がありません", "記述はありません", "見つかりません",
                "特定することができません", "Wikipediaのページ編集用", "ウィキペディアやウィキデータ",
                "記載がありません", "MediaWiki", "閲覧したとしても"
            ]

            # 日時関連の質問かどうかを判定
            is_datetime_query = any(keyword in query for keyword in datetime_keywords)

            # 現在の日時情報を取得
            current_datetime = datetime.now().strftime("%Y年%m月%d日 %H:%M")
            prompt_with_datetime = f"現在の日時は {current_datetime} です。この日時を基準として次の質問に答えてください。\n質問:{query}"

            final_response = ""

            # RAGの回答が不十分な場合、または日時関連の質問の場合に直接LLMで回答
            if (not response.response.strip()) or any(phrase in response.response for phrase in insufficient_phrases) or is_datetime_query:
                print("RAGに該当情報がないか、日時関連の質問のため、直接LLMで回答します。")
                direct_response = direct_llm.complete(prompt_with_datetime)
                final_response = direct_response.text
            else:
                final_response = response.response

            print("\n--- 回答 ---")
            print(final_response)
            print("------------\n")

        except Exception as e:
            print(f"質問処理中にエラーが発生しました: {e}")

except FileNotFoundError:
    print(f"エラー: インデックスデータが見つかりません。'{persist_dir}' を確認してください。");
    sys.exit(1)
except Exception as e:
    print(f"予期せぬエラーが発生しました: {e}");
    sys.exit(1)

とコード変更

現在の課題は、RAGがリアルタイム情報(特に日付と時刻)を提供できないために、LLMが常に正確な「現在」の情報を答えることができない点ですね。これを解決するために、RAGの検索結果に関わらず、日付と時刻に関する質問に対しては、常に最新の日時情報をLLMに伝えるようにコードを変更します。

変更の概要
主な変更点は以下の通りです。
1. 日付・時刻関連のキーワードの検出: 質問内容が日付や時刻に関連するかどうかを判断するためのキーワードリストを定義します。
2. プロンプトへの日時情報追加の条件変更: RAGの回答が不十分な場合だけでなく、質問が日付や時刻に関連すると判断された場合にも、現在のリアルタイムの日時情報をLLMへのプロンプトに含めるようにします。
これにより、ユーザーが「現在の時刻は?」や「今日の日付は?」と質問した場合、RAGの検索結果に関わらず、スクリプトが取得した最新の日時情報がLLMに渡され、LLMはその情報に基づいて正確な回答を生成できるようになります。

変更後のコード


import sys
from llama_index.core import StorageContext, load_index_from_storage, Settings
from llama_index.llms.ollama import Ollama
from llama_index.embeddings.huggingface import HuggingFaceEmbedding
from datetime import datetime

# --- 1. LLMと埋め込みモデルの設定 ---
Settings.llm = Ollama(model="elyza:jp8b", request_timeout=360.0)
Settings.embed_model = HuggingFaceEmbedding(model_name="intfloat/multilingual-e5-large")
print("LLMと埋め込みモデルの設定が完了しました。")

# --- 2. インデックスのロード ---
print("インデックスをロード中...")
try:
    persist_dir = "./storage/simple_vs"
    index_id = "miyabe_miyuki"

    storage_context = StorageContext.from_defaults(persist_dir=persist_dir)
    loaded_index = load_index_from_storage(storage_context, index_id=index_id)
    print("インデックスのロードが完了しました。")

    query_engine = loaded_index.as_query_engine(similarity_top_k=3)
    print("クエリエンジンの準備ができました。")

    direct_llm = Settings.llm  # 直接LLM回答用のLLM

    # 日時関連の質問を検出するためのキーワード
    datetime_keywords = ["時刻", "時間", "日付", "何日", "今日", "今"]

    print("\n質問を入力してください(終了するには 'exit' と入力)。")
    while True:
        query = input("あなたの質問: ")

        if query.lower() == 'exit':
            print("プログラムを終了します。"); break

        if not query.strip():
            print("質問を入力してください。"); continue

        print("回答を生成中...")
        try:
            # RAGで情報を検索
            response = query_engine.query(query)

            insufficient_phrases = [
                "含まれていません", "情報は含まれていません", "情報がありません",
                "該当情報がありません", "記述はありません", "見つかりません",
                "特定することができません", "Wikipediaのページ編集用", "ウィキペディアやウィキデータ",
                "記載がありません", "MediaWiki", "閲覧したとしても"
            ]

            # 日時関連の質問かどうかを判定
            is_datetime_query = any(keyword in query for keyword in datetime_keywords)

            # 現在の日時情報を取得
            current_datetime = datetime.now().strftime("%Y年%m月%d日 %H:%M")
            prompt_with_datetime = f"現在の日時は {current_datetime} です。この日時を基準として次の質問に答えてください。\n質問:{query}"

            final_response = ""

            # RAGの回答が不十分な場合、または日時関連の質問の場合に直接LLMで回答
            if (not response.response.strip()) or any(phrase in response.response for phrase in insufficient_phrases) or is_datetime_query:
                print("RAGに該当情報がないか、日時関連の質問のため、直接LLMで回答します。")
                direct_response = direct_llm.complete(prompt_with_datetime)
                final_response = direct_response.text
            else:
                final_response = response.response

            print("\n--- 回答 ---")
            print(final_response)
            print("------------\n")

        except Exception as e:
            print(f"質問処理中にエラーが発生しました: {e}")

except FileNotFoundError:
    print(f"エラー: インデックスデータが見つかりません。'{persist_dir}' を確認してください。");
    sys.exit(1)
except Exception as e:
    print(f"予期せぬエラーが発生しました: {e}");
    sys.exit(1)

変更点の解説
1. datetime_keywordsリストの追加:

datetime_keywords = ["時刻", "時間", "日付", "何日", "今日", "今"]

このリストは、ユーザーの質問にこれらのキーワードが含まれている場合に、その質問を日時関連の質問として識別するために使用

is_datetime_query変数の追加:

is_datetime_query = any(keyword in query for keyword in datetime_keywords)

ユーザーのqueryの中にdatetime_keywordsリスト内のいずれかのキーワードが含まれているかをチェックし、結果をブール値で保持

直接LLMで回答する条件の変更:

if (not response.response.strip()) or any(phrase in response.response for phrase in insufficient_phrases) or is_datetime_query:
    print("RAGに該当情報がないか、日時関連の質問のため、直接LLMで回答します。")
    direct_response = direct_llm.complete(prompt_with_datetime)
    final_response = direct_response.text
else:
    final_response = response.response

ここが最も重要な変更点です。これまでの条件 (RAGの回答が空、または不十分なフレーズを含む) に加えて、or is_datetime_queryを追加しました。これにより、質問が日時関連であると判断された場合も、RAGの回答内容に関わらず、直接LLMに最新の日時情報付きのプロンプトが送られます

実行すると

LLMと埋め込みモデルの設定が完了しました。
インデックスをロード中...
インデックスのロードが完了しました。
クエリエンジンの準備ができました。

質問を入力してください(終了するには 'exit' と入力)。
あなたの質問: 今日は何日?
回答を生成中...
RAGに該当情報がないか、日時関連の質問のため、直接LLMで回答します。

--- 回答 ---
現在の日付は2025年05月29日なので、今日は29日です。
------------

あなたの質問: 今 今の時間は?
回答を生成中...
RAGに該当情報がないか、日時関連の質問のため、直接LLMで回答します。

--- 回答 ---
現在の日時は2025年05月29日00:59なので、00:59です。
------------

あなたの質問: 今のアメリカの大統領は?
回答を生成中...
RAGに該当情報がないか、日時関連の質問のため、直接LLMで回答します。

--- 回答 ---
2025年05月29日現在、米国の大統領はジョー・バイデン氏です。
------------

というように大統領の名前が違う

これは学習済みデータにもRAGにもないのが原因なので正常動作になる
次は取説と約款のRAG作成

その前に gemma3 4b + faster-whusoer のコードを公開する

chromaについて

chromaについて

「Chroma」は、軽量かつ高速な**ベクトルストア(Vector Store)**で、主にRAG(Retrieval-Augmented Generation)や類似文書検索に使われる

■ Chromaとは
**Chroma(正式名称:ChromaDB)は、テキストや画像などのデータをベクトル(数値の集合)として保存し、後から類似検索(類似性の高いものを検索)**するためのデータベースです。
* 開発元:Chromadb Inc.
* ライセンス:Apache 2.0(オープンソース)
* 特徴:
* 自己完結型(埋め込みデータの保存から検索まで完結)
* 高速・軽量(ローカルでも動作)
* RAGなどのLLMとの相性が良い

■ 主な用途
1. RAG(LLM × ベクトル検索)
* PDFや文書を分割してベクトル化し、Chromaに保存
* クエリと類似する文書をChromaで検索してLLMに渡す
2. 類似文章検索
* 「この文章に似ているものは?」という検索が可能
3. 画像や音声の類似性検索(拡張可能)

■ 保存形式(Chroma + SQLite + Parquet)
Chromaはデフォルトで以下を使います:
* SQLite:メタデータや設定情報を保存
* Parquet:ベクトルそのもの(高次元な数値)を効率的に保存
この構成により、
* ローカルファイルでの軽量運用が可能
* クラッシュしてもデータが残りやすい
* 他ツールと連携しやすい(Parquet形式はPandasやSparkとも互換あり)

pip install chromadb

でインストール

次にテスト

import chromadb
from chromadb.utils.embedding_functions import OpenAIEmbeddingFunction

client = chromadb.Client()
collection = client.create_collection(name="my_collection")

collection.add(
    documents=["東京は日本の首都です", "大阪は関西の中心都市です"],
    ids=["doc1", "doc2"]
)

results = collection.query(
    query_texts=["日本の都市について教えて"],
    n_results=1
)
print(results)

これを
touch db_test.py
でファイル作成し

 python db_test.py

で実行

最初にモデルのダウンロードが行われるため時間がかかる

Llama index RAGのメタデータ保存

Llama index RAGのメタデータ保存

[RAGカスタマイズ] LlamaIndexで情報元のPDFのメタデータを含めた回答を生成する
を参考に行う

社内資料の検索をスムーズにするRAGシステムをLlamaIndexを使用して開発している際にある問題に当たった.
「情報の出所がどのPDFの何ページ目かを知りたい」
営業やカスタマーセンターの方が顧客から質問され,RAGを使用して回答を得て,顧客に回答するという使用方法を想定する場合,間違った情報を伝えてはならない.従って,情報源となるPDFのページを示すことで,簡単にファクトチェックを行えるようになる.
今回の記事では,このRAGシステムを構築する際によく遭遇する問題をLlamaIndexで解決していく
というコンセプトらしい

この中で

from llama_index.core import (
    StorageContext,
    load_index_from_storage,
    get_response_synthesizer
)
from llama_index.core.retrievers import VectorIndexRetriever
from llama_index.core.query_engine import RetrieverQueryEngine
from llama_index.core.postprocessor import SimilarityPostprocessor

# 保存済みのindexを読み込み
index_path = "path/to/storage"
storage_context = StorageContext.from_defaults(persist_dir=index_path)

index = load_index_from_storage(storage_context=storage_context)

# Retrieverを準備
retriever = VectorIndexRetriever(
    index=index,
    similarity_top_k=5
)

# NodePostProcessorの準備
# スコアが0.75以下のNodeを削除
score_filtter_processor = SimilarityPostprocessor(similarity_cutoff=0.75)
node_postprocessors=[score_filtter_processor]

# Synthesizerの作成
response_synthesizer = get_response_synthesizer(
    output_cls=ResponseModel
)

# QueryEngineの作成
query_engine = RetrieverQueryEngine(
    retriever=retriever,
    response_synthesizer=response_synthesizer,
    node_postprocessors=[AddMetadataNodePostprocessor()]
)

output = query_engine.query("質問を入力")


「保存されたインデックス(index)を読み込んでクエリを実行する流れ」を記述したものであり、インデックスを保存する処理(=永続化のための保存処理)は含まれていません

RAGの回答にメタデータを含める
それでは,これらの方法を応用して,RAGで生成する回答に出所となるPDFファイルとそのページを含める方法についてまとめる.大きく3つの手順が必要になる.
1. PDFからDocumentオブジェクトを生成する際に所望のメタデータを含める
2. Retrieverで検索をする際にPostNodeProcessorによりNodeのテキストにメタデータを追加する
3. Synthesizerの出力形式をpydanticで指定し,メタデータが含まれるようにする

Nodeオブジェクトに所望のメタデータを含める
LlamaIndexでは,PDF → Documentオブジェクト → Nodeオブジェクト の順でデータが変換されていく.
DocumentオブジェクトのメタデータはNodeオブジェクトにも継承される.
つまり,PDFのファイルパスやページ数をメタデータとして含める場合は,
PDF → Documentの部分でそれぞれのDocumentオブジェクトにメタデータを持たせる.

LlamaIndexが提供しているSimpleDirectoryReaderでは,PDFをDocumentに変換する際,デフォルトでメタデータを付与してくれる.従って,メタデータを付与するための処理は以下の処理だけで良い.

Chromaについて調べた

llamaindex の保存先でChroma → SQLite + Parquet を勧められたが どのようなものか知りたい

🔷 1. Chroma とは?
Chroma は 高速なベクトルストア(類似検索エンジン) です。以下の特徴があります:
* ベクトル(埋め込み)とメタデータの保存・検索が可能
* シンプルなPython APIで操作可能
* ローカルストレージにも対応(クラウド不要)
* LlamaIndexと統合しやすい
LlamaIndex の ChromaVectorStore 経由で使います。

🔷 2. SQLite とは?
SQLite は 軽量な組み込み型データベース です。特徴:
* ファイル1つで完結(.db)
* インストール不要でPythonに組み込み済み
* 小中規模のアプリに最適
👉 Chromaはメタデータやインデックス情報の保存先としてSQLiteを使えます。

🔷 3. Parquet とは?
Parquet は 列指向のファイルフォーマット(Apache Arrowベース)で、以下の特徴があります:
* 高圧縮&高速読み書き
* PandasやPyArrowなどと連携しやすい
* 機械学習や分析によく使われる形式
👉 Chromaでは、埋め込みベクトル自体をParquetに保存するオプションがあります。

🧩 まとめると:この構成の利点
コンポーネント 役割 保存先
Chroma ベクトル検索・管理 –
SQLite メタデータ・インデックス情報 chroma.sqlite3
Parquet 埋め込みベクトルの保存 chroma.parquet
この構成では、以下のような利点があります:
* ✅ クラウド不要・ローカルで完結
* ✅ 速くて軽い
* ✅ LlamaIndexと連携しやすい
* ✅ データ構造が明確(バックアップ・移行しやすい)

必要なマシンスペック

🖥️ 必要なマシンスペックの目安
✅ 最小スペック(数十〜数百ページのPDFを家庭内検索用途で扱う場合)
項目 推奨値
CPU Intel Core i5 / Apple M1 / Ryzen 5 以上
メモリ(RAM) 8GB以上(できれば16GB)
ストレージ SSD(空き容量10GB程度〜)
OS macOS / Windows / Linux(どれでも可)
Python環境 Python 3.9〜3.11 + pipでパッケージ管理可能なこと
✔ 対応可能なこと
* 家電マニュアル、契約書、学校通知PDFを読み込み
* 自然言語で「何ページに書いてある?」などと検索
* 毎日新しいお知らせを追加

⚙ 中規模スペック(数千ページ、複数人で共有など)
項目 推奨値
CPU Intel Core i7 / Apple M1 Pro / Ryzen 7以上
メモリ(RAM) 16GB〜32GB
ストレージ SSD(空き容量50GB以上推奨)
その他 常時稼働できるようファン静音 or ラズパイサーバなど
✔ 対応可能なこと
* 数千ページ以上のマニュアルや書籍、PDFを対象にした検索
* 高速な応答(数秒以内)で快適な対話
* 音声やチャットでの自然なやり取り

💡 小型構成としてRaspberry Piは使える?
* Raspberry Pi 4(4GB〜8GB RAM)でも動作可能ですが、ベクトル埋め込みの生成が非常に遅くなるため、最初のインデックス構築はPCで行い、完成済みのindexだけをRaspberry Piで読み込むといった構成が理想です。

⚠️ 注意点:Chroma+Parquetの重さについて
* Chroma自体は軽量ですが、埋め込みベクトルの生成(特にHuggingFaceの大きなモデル)にはメモリとCPUを多く使います。
* ベクトルの保存(Parquet)は効率的ですが、読み込み時に全体をメモリにロードする動作があるため、数万文書以上扱うなら32GB RAMが望ましいです。

とりあえずm1macbookAIr 16GBでも動作は可能みたい

事例があるかgpt で検索

ローカルでEmbeddingしてローカルLLMでIndex検索するデモ
が近いけど
これは

構成 モジュール
Embedding(埋め込み) 「intfloat/multilingual-e5-large」をHuggingFaceEmbeddingで利用
LLM 「elyza/Llama-3-ELYZA-JP-8B-q4_k_m.gguf」をLlambaCppで利用
ベクトル検索と類似性検索 Faiss
という構成なのでちょっと違った

続・LlamaIndexを使って独自データをQ&AするだけならOpenAI API使わなくてもいいでない?
これは
* LlamaIndex
llama.cpp
の構成
こっちはデータのロード方法が載っているので参考になるかも

あと

Ruri: 日本語に特化した汎用テキスト埋め込みモデル

これも組み合わせてみる

[RAGカスタマイズ] LlamaIndexで情報元のPDFのメタデータを含めた回答を生成する
が多分一番近い

LlamaIndexのデータ形式に関するまとめ
も面白い

GPTによれば

[RAGカスタマイズ] LlamaIndexで情報元のPDFのメタデータを含めた回答を生成する
にあるように
RAGの回答にメタデータを含める
それでは,これらの方法を応用して,RAGで生成する回答に出所となるPDFファイルとそのページを含める方法についてまとめる.大きく3つの手順が必要になる.
1. PDFからDocumentオブジェクトを生成する際に所望のメタデータを含める
2. Retrieverで検索をする際にPostNodeProcessorによりNodeのテキストにメタデータを追加する
3 Synthesizerの出力形式をpydanticで指定し,メタデータが含まれるようにする

で必要なメタデータの保存も可能

Chroma(ChromaDB)でもメタデータは保存・検索可能です

✅ Chroma におけるメタデータの扱い
ChromaDB では、各ドキュメント(レコード)に対して以下の3種類の情報を保存できます:
項目 内容
ids ドキュメントの一意なID
documents 実際のテキスト内容
metadatas 任意のキー・バリューペア
この metadatas に、PDFファイルのファイルパスやページ番号などを含めることができます。

🔧 例:Chroma にメタデータ付きでデータを追加

collection.add(
    documents=["これは1ページ目です", "これは2ページ目です"],
    ids=["doc1", "doc2"],
    metadatas=[
        {"file_path": "sample.pdf", "page": 1},
        {"file_path": "sample.pdf", "page": 2}
    ]
)

このようにして、LlamaIndex の Document.metadata に相当する情報を Chroma にも明示的に渡せます。

✅ LlamaIndex × Chroma でのメタデータの流れ
LlamaIndex 側で SimpleDirectoryReader を使うと、読み込まれた Document に自動的に以下のようなメタデータが付きます:

print(documents[0].metadata)
# 出力例: {'file_path': './docs/sample.pdf', 'page_label': '1', 'page_number': 1}

このメタデータは、VectorStoreIndex を ChromaVectorStore 経由で作る際に、内部的にNodeオブジェクトにコピーされ、Chromaにも反映されます。
構成例:

from llama_index.core import SimpleDirectoryReader, VectorStoreIndex, StorageContext
from llama_index.vector_stores.chroma import ChromaVectorStore

reader = SimpleDirectoryReader(input_dir="./docs")
documents = reader.load_data()

# Chromaの初期化(永続化あり)
from chromadb.config import Settings
from chromadb import PersistentClient

client = PersistentClient(path="./chroma_db")
chroma_store = ChromaVectorStore(chroma_collection=client.get_or_create_collection("my_collection"))

storage_context = StorageContext.from_defaults(vector_store=chroma_store)
index = VectorStoreIndex.from_documents(documents, storage_context=storage_context)

index.storage_context.persist()

🔍 検索時にメタデータも取得できる?
はい。クエリ結果(Node)から、以下のようにメタデータが取得可能です:

response = index.as_query_engine().query("これは何ページの情報ですか?")
for node in response.source_nodes:
    print(node.metadata)

✅ まとめ
項目 Chroma対応 備考
ファイルパスの保存 ✅ metadatas={“file_path”: …}
ページ番号の保存 ✅ metadatas={“page”: …}
LlamaIndex連携時の自動反映 ✅ Document.metadata → Node.metadata → Chroma に自動伝搬

さらに、「特定のPDFファイル+ページに限定して検索したい」といった用途がある場合、Chromaのフィルター検索も可能です
(ただし query() ではなく query_where() 相当を使う必要あり)。希望があればその例も紹介します。

まずはインストールして進める

RAG

RAG

LlamaIndex」はツール/ライブラリの名前、
「RAG(Retrieval-Augmented Generation)」は手法やアプローチの名前

1. RAGとは
* 正式には Retrieval-Augmented Generation(検索拡張生成)。
* LLM(例:GPT、Llamaなど)に直接すべての情報を記憶させるのではなく、
必要に応じて外部の知識(ドキュメント、データベースなど)を検索し、その結果を使って回答を作成する仕組み。

* 一般的なRAGの流れ:
1. ユーザーから質問を受け取る
2. 質問に関連しそうな情報を検索(情報検索フェーズ)
3. 見つかった情報をもとにLLMが回答を生成(生成フェーズ)
→ RAGは「仕組み」や「考え方」

2. LlamaIndexとは
* RAGの「仕組み」を簡単に構築できるようにするためのPythonライブラリ。

* 役割:
* 文書やデータを「Index」(インデックス化=検索しやすく加工)
* 質問に応じてそのIndexから「検索」
* 検索結果をもとに「LLMにプロンプトして回答生成」
* つまり、LlamaIndexは
* 「データを取り込みやすくする」
* 「賢く検索できるようにする」
* 「LLMにうまく渡して回答させる」
を手伝ってくれるツールです。
→ LlamaIndexは「RAGを実装するためのツール」

| 項目 | 内容 |
| :——— | :———————– |
| RAG | 検索+生成を組み合わせた仕組み・手法 |
| LlamaIndex | RAGを作るためのPythonライブラリ・ツール |

コンセプトは
取扱説明書や契約書を電子化し、音声での質問に答えて、該当するページを表示したい。 また内容をわかりやすく説明する機能も欲しい

【あなたのゴール】
1. 取扱説明書や契約書を電子データ化しておく
2. 音声入力で質問できる
3. 質問に対して
* 該当するページやセクションを画面に表示
* 分かりやすく要約や解説もする

これは技術的には、
「RAG+音声入出力+画面UI」 を組み合わせることで実現できます!

なお
プロトタイプはmacbookairで動作し ページ表示もmacbookairだが 本番環境は画面にページ表示は指定したIPアドレスのラズパイディスプレイを想定。

なお以前動かしたものが
Ollama index チュートリアルを参考に
のメモに残っているので調べた

PDFファイルに関しては

 touch test.py

でファイルを作成

PDFファイルに関しては
Dataフォルダに格納

wget https://www.mhlw.go.jp/content/001018385.pdf -O data/10k/001018385.pdf

でファイルを取得し保存している

なおパッケージインストールは

pip install llama-index-core llama-index-readers-file llama-index-llms-ollama llama-index-embeddings-huggingface
from llama_index.llms.ollama import Ollama
from llama_index.embeddings.huggingface import HuggingFaceEmbedding
from llama_index.core import SimpleDirectoryReader, VectorStoreIndex
from transformers import AutoTokenizer

# 使用モデルを選択します。
llm = Ollama(model="elyza:jp8b", request_timeout=40.0)

# HuggingFace Embedding モデルの初期化
embed_model = HuggingFaceEmbedding(model_name="all-MiniLM-L6-v2")

# Transformers tokenizerの設定
tokenizer = AutoTokenizer.from_pretrained("sentence-transformers/all-MiniLM-L6-v2", clean_up_tokenization_spaces=False)

# PDFファイルの読み込み
reader = SimpleDirectoryReader(input_files=["./data/001018385.pdf"])
data = reader.load_data()
index = VectorStoreIndex.from_documents(data, embed_model=embed_model)

# クエリエンジンの初期化
query_engine = index.as_query_engine(llm=llm, streaming=True, similarity_top_k=3)

# クエリの実行
response = query_engine.query("就業規則では休みは何日もらえますか?日本語で答えて。")
response.print_response_stream()

# ソースノードの確認
for node in response.source_nodes:
    print(f"Text: {node.node.text[:100]}...")
    print(f"Similarity score: {node.score}")  # 'score'を使用

これで動いているのが確認できている

またテキストファイルの場合は

test_txt.py

として

from llama_index.llms.ollama import Ollama
from llama_index.embeddings.huggingface import HuggingFaceEmbedding
from llama_index.core import SimpleDirectoryReader, VectorStoreIndex
from transformers import AutoTokenizer

# 使用モデルを選択します。
llm = Ollama(model="elyza:jp8b", request_timeout=40.0)

# HuggingFace Embedding モデルの初期化
embed_model = HuggingFaceEmbedding(model_name="all-MiniLM-L6-v2")

# Transformers tokenizerの設定
tokenizer = AutoTokenizer.from_pretrained("sentence-transformers/all-MiniLM-L6-v2", clean_up_tokenization_spaces=True)

reader = SimpleDirectoryReader(input_files=["./data/10k/bocchi.txt"])
data = reader.load_data()
index = VectorStoreIndex.from_documents(data, embed_model=embed_model)


# クエリエンジンの初期化
query_engine = index.as_query_engine(llm=llm, streaming=True, similarity_top_k=3)

# ストリーミングでレスポンスを取得
response = query_engine.query(
    "文化祭ライブはどうだったのか**日本語**で簡潔に説明してください。 "
    " page reference after each statement."
)
response.print_response_stream()

で動作確認できている

あとはデータ保存について調べる

GPT、geminiともにデフォルトではメモリにあるため
プログラム終了するとデータは消えてしまう

このため保存先が必要になる

RAGのデータについて調べたが
Gemini GPTともに
メモリがデフォルトでプログラム終了後は内容は消えるため
自分で保存しないとだめ

保存しなければ → 一時的なRAM(セッション内のみ有効)
明示的に保存した場合 → index.save_to_disk() で指定したパス(通常ローカル)
外部ベクトルDB使用時 → 各サービスのストレージ領域(クラウド or DB)

用途により保存方法は異なるため
コンセプトの再確認

家庭内での利用。 妻や子供、ITに詳しくない両親たちが、家電の説明書や契約書の内容をわかりやすく説明するのに使う。
別の用途として、学校からのお知らせのPDFを随時読み込んで緊急時の対応などに対処できるようにしたい

これに対し
家庭内での用途(非技術ユーザーの利用、家電マニュアルや学校のお知らせPDFなどの理解補助)という条件であれば、
保存先は**「ローカルで簡単に使えるが、永続化されており、安定していて高速」**であることが重要
とのこと

GPTとgeminiによる提案はどちらも
家庭内利用に最適な保存先:Chroma(ローカル)

この場合必要なマシンスペックを調べることにする