RAGのメイン処理

RAGのメイン処理

    # RAG検索
    query_embed = ollama_embed(prompt)
    results = st.session_state.collection.query(
        query_embeddings=[query_embed],
        n_results=2
    )
query_embed = ollama_embed(prompt)

でユーザのプロンプトをベクトル化

session_state.collection.query

を使うことで
chromaDBのメソッドを使うことで
似ている情報をベクトル同士を比べることで似ている情報を得ることが可能になる

この中で

query_embeddings=[query_embed],

でプロンプトを指定して

n_results=2

で上位何個を該当させるか指定できる
今回は上位2つまで

このときの結果は

    # {
    #     "ids":
    #     "documents": [["doc1", "doc2"]]
    #     "distances": [[XXX, XXX]]
    # }

というように返ってくる
Documents はテキスト内容
Distancesはベクトルの数値
なので必要なのは documentsの値になるので
これがあるかで判定すればいい

なので
Documentsの0番目のインデックスを取得すれば
最初のリストが取得できる

あとは中身を joinで結合すればいいので

    if results["documents"]:
        context_text = "\n".join(results["documents"][0])

というようにして
変数に結果を格納する

次に、RAG部分のプロンプトの作成

        rag_prompt = f"""
        以下は関連ドキュメントの抜粋です。
        {context_text}
        この情報を参考に以下の質問に答えてください。
        {prompt}
        """

こうすることで
質問を入力すれば、事前に参照するプロンプトが既に組み込まれているので
簡単にDB参照の機能が追加された状態で質問が構築される

もし、documentsがない、つまり該当する知識がないのなら

        final_user_prompt = rag_prompt
    else:
        final_user_prompt = prompt

というようにすれば分岐処理になって
RAGなしのプロンプトが実行されるようになる

あとは

    st.session_state.messages.append({"role": "user", "content": final_user_prompt})

で情報を履歴として保持するようにする

あとは
システムプロンプトを入れて過去の履歴を追加する

    if system_prompt.strip():
        messages = [{"role": "system", "content": system_prompt}] + st.session_state.messages
    else:
        messages = st.session_state.messages
    

これで RAGを使ったmessages にプロンプトが格納される

それを ollamaに渡す

    # LLMの返答を表示
    with st.chat_message("assistant"):
        placeholder = st.empty()
        stream_response = ""
        stream = client.chat.completions.create(
            model=model,
            messages=messages,
            temperature=temperature,
            stream=True
        )
        for chunk in stream:
            stream_response += chunk.choices[0].delta.content
            placeholder.write(stream_response)

これで streamlitで回答を表示
回答内容を保存もしている

ファイルをベクトル化してDBへ保存

ファイルをベクトル化してDBへ保存

if st.sidebar.button("インデックス作成"):
    for file in uploaded_files:
        text = load_word_document(file)
        chunks = split_text(text)
        for i, chunk in enumerate(chunks):
            embed = ollama_embed(chunk)
            st.session_state.collection.add(
                documents=[chunk],
                embeddings=,
                ids=[f"{file.name}_{i}"]
            )
    st.sidebar.success("インデックス作成完了")

によって
ボタンを押したら処理が完了する

if st.sidebar.button("インデックス作成"):

はボタンを押したら、という意味

uploaded_files:

は自作のメソッドで、ファイルアップロードしたときの結果が入っているので

for file in uploaded_files:

でfor ループで file へ代入していく

# ワードファイルのアップロード
uploaded_files = st.sidebar.file_uploader(
    "Wordファイルをアップロード(.docx)",
    type=["docx"],
    accept_multiple_files=True
)

がそのメソッド

# Wordファイルを読み込む関数
def load_word_document(file):
    return "\n".join(para.text for para in Document(file).paragraphs)

も自作のメソッドで

 text = load_word_document(file)

によってアップされたwordファイルの中身を textへ代入

テキストの分割は

# テキスト分割関数
def split_text(text):
    chunk_size = 200
    overlap = 50
    chunks = []
    start = 0
    while start < len(text):
        end = start + chunk_size
        chunks.append(text[start:end])
        start += chunk_size - overlap
    return chunks

で行っているので

chunks = split_text(text)

でチャンクに分割したものを chunks へ格納

for i, chunk in enumerate(chunks):

でインデックスと一緒に代入していく

そして自作したベクトル化関数

# Ollamaからインストールしたモデルを使ったベクトル化関数
def ollama_embed(text):
    r = requests.post(
        "http://localhost:12000/api/embeddings",
        json={"model": "nomic-embed-text", "prompt": text}
    )
    data = r.json()
    return data["embedding"]

これを使い

embed = ollama_embed(chunk)

でベクトル化したものを変数に格納

            st.session_state.collection.add(
                documents=[chunk],
                embeddings=,
                ids=[f"{file.name}_{i}"]
            )


documents=[chunk],は元々の文章
embeddings=,はベクトル化したもの
ids=[f”{file.name}_{i}”]はアップロードしたファイル名とインデックス番号
となる

これでアップしたword ファイルの内容をベクトル化しDBへ保存する仕組みができた

次はインプットした情報をベクトル変換し
そのベクトルと近い情報をDBから撮ってきて回答を得る仕組みを作る

RAGで使う wordファイルのアップローダ作成

RAGで使う wordファイルのアップローダ作成

# ワードファイルのアップロード
uploaded_files = st.sidebar.file_uploader(
    "Wordファイルをアップロード(.docx)",
    type=["docx"],
    accept_multiple_files=True
)

これで

type=["docx"],

の設定で拡張子が docx のもののみ受け付ける

  accept_multiple_files=True

これで複数ファイルのアップロードが可能

uploaded_files =

とすることで機能を変数に格納できる

RAGのためのテキスト分割関数

RAGのためのテキスト分割関数

文章となる文字列をチャンクという塊に分割し
分けたものをベクトル化してDBに入れる

このためチャンクに分ける分割関数を作成する

# テキスト分割関数
def split_text(text):
    chunk_size = 200
    overlap = 50
    chunks = []
    start = 0
    while start < len(text):
        end = start + chunk_size
        chunks.append(text[start:end])
        start += chunk_size - overlap
    return chunks

以下解説

chunk_size = 200

チャンクサイズの設定

overlap = 50

塊の許容範囲の設定

これにより1000文字なら
最初の200文字をチャンクにして
次のチャンクは151〜350が1つのチャンクになる

Overlapは重なりの意味なので
50被っているようになる

    start = 0
    while start < len(text):


Startがテキストの文字数より小さい間はループを続ける

end = start + chunk_size

が終了地点の設定
つまりチャンクサイズの200より上になれば終わり

 chunks.append(text[start:end])

で文字を

 chunks = []

append(text[start:end])

によって追加していく

そして

  start += chunk_size - overlap

で次のスタート地点の設定

chunk_size - overlap

の計算で
200−50
となって
150から開始地点となる

最後に

   return chunks

で分割されたチャンクを返すようにして処理が終わる

RAGのためのPDF内容抽出

RAGのためのPDF内容抽出

以下のPDFファイルを読んで、ページごとに内容をテキストに変換してください。 
**重要なルール:** - 各ページのテキストの先頭に、必ず以下の形式でページ番号を記載してください。 例: `--- ページ 1 ---` - テキストは、元のドキュメントのレイアウト(段落、箇条書き、表など)をできるだけ維持し、読みやすい形式で出力してください。 - 特に表は、Markdownの表形式などで構造を明確にしてください。 - PDF内に画像が含まれている場合、その画像に書かれている文字や説明もテキストに含めてください。 - PDF全体の内容を省略せず、全て抽出してください。 **抽出されたテキストの例:** --- ページ 1 --- [ページ1のテキスト] --- ページ 2 --- [ページ2のテキスト] ... --- ページ N --- [ページNのテキスト]

としたけど

ご提供いただいたPDFファイルから、指定された形式(ページ番号の付与、レイアウトの維持、表のMarkdown変換など)でテキストを抽出するご依頼ですね。
残念ながら、私の内部ツールは、PDFファイルから画像や複雑なレイアウトを読み取り、ご指定の形式に変換する機能は備えておりません。UnstructuredやGemini APIといったOCRに特化した外部サービスを利用しない限り、高精度な抽出や構造化されたテキストの出力は困難です。
以前ご案内した通り、PDFから高精度なテキスト抽出を行い、RAGシステムで利用するためには、以下のような外部ツールをご自身でご利用いただくのが最も確実です。
推奨される方法:
1. GeminiのWeb UIを利用して、PDFをアップロードします。
2. 以前ご案内した以下のプロンプトをGeminiに送信して、PDFのテキスト抽出と整形を依頼します。

以下のPDFファイルを読んで、ページごとに内容をテキストに変換してください。 **重要なルール:** - 各ページのテキストの先頭に、必ず以下の形式でページ番号を記載してください。 例: `--- ページ 1 ---` - テキストは、元のドキュメントのレイアウト(段落、箇条書き、表など)をできるだけ維持し、読みやすい形式で出力してください。 - 特に表は、Markdownの表形式などで構造を明確にしてください。 - PDF内に画像が含まれている場合、その画像に書かれている文字や説明もテキストに含めてください。 - PDF全体の内容を省略せず、全て抽出してください。

となるので
これで実験する

Gemini 2.5 flash では処理する時に同じメッセージが出るだけ
Gemini 2.5 pro にすると画面に憑依される

なお gemini によれば
割して保存したほうがRAGの性能を最大限に引き出すことができる

結論から言うと、分割して保存したほうがRAGの性能を最大限に引き出すことができます。
以下にその理由と、具体的な保存方法について説明します。

分割して保存すべき理由

RAG(Retrieval-Augmented Generation)は、質問に関連する情報をデータベースから検索し、その情報をもとに回答を生成する仕組みです。このプロセスにおいて、元のドキュメントをどのように分割して保存するか(チャンク化)が非常に重要になります。
1. 検索精度の向上:
* 質問に最も関連する小さなチャンクを正確に特定できるようになります。
* もしドキュメント全体を一つの大きなテキストとして保存した場合、検索結果にノイズ(質問と関係ない情報)が多く含まれてしまい、関連性の低い情報に基づいて回答を生成してしまうリスクが高まります。
2. 生成される回答の質の向上:
* LLM(大規模言語モデル)は、与えられた情報(コンテキスト)の量が増えすぎると、重要な情報を見落としたり、まとまりのない回答を生成したりすることがあります(コンテキスト過多の問題)。
* 適切なサイズに分割することで、LLMに与える情報を質問に関連する部分だけに絞り込むことができ、より正確で簡潔な回答を引き出すことができます。
3. コストと処理速度の最適化:
* 大きなテキストを処理するには、より多くの計算リソースと時間がかかります。
* 分割することで、検索と生成のプロセスが効率化され、全体の処理速度が向上し、APIコストの削減にもつながります。

具体的な保存方法

PDFファイルから抽出したテキストをRAG用に保存する際は、以下のような方法が考えられます。

1. ページごとに分割して保存する

最もシンプルで効果的な方法です。抽出したテキストを、元のページごとに異なるテキストファイルとして保存します。
保存方法の例:
* page_01.txt
* page_02.txt
* page_03.txt
* …
この方法では、質問に関連する情報がどのページにあるか検索し、そのページ全体のテキストをLLMに渡すことができます。ただし、1ページに非常に多くの情報が含まれている場合、さらに小さな単位に分割することも検討が必要です。

2. セクションや段落ごとに分割して保存する

ページの区切りに関係なく、内容のまとまり(見出し、段落、箇条書きなど)ごとに分割して保存する方法です。
保存方法の例:
* section_契約概要.txt
* section_保障内容.txt
* section_保険料の支払い.txt
* section_よくある質問.txt
* …
この方法の利点は、より意味のある単位で情報を分割できることです。例えば、「保険料の支払いについて」という質問に対しては、「保険料の支払い」のセクションを直接検索して利用できます。

3. チャンクサイズを指定して分割する(高度な方法)

テキストを固定の文字数や単語数で区切り、オーバーラップ(重複)を持たせてチャンク化する方法です。
* 例えば、「文字数500文字、重複200文字」といったルールで分割します。
* この方法は、検索エンジンがより多くの関連情報を捉えるのに役立ちます。
この方法は、特に長大なドキュメントや、特定のテーマが複数のページにまたがっている場合に有効です。

まとめ

まずは「ページごとに分割して保存する」方法から始めるのがおすすめです。
そして、RAGの利用を始める際には、まず抽出したテキストをデータベース(ベクトルデータベースなど)に格納し、そのデータベースに対して質問を投げることになります。質問に最も関連性の高いテキストチャンクが検索され、LLMに渡されて回答が生成されるという流れです。

ということで
プロンプトを

PDFファイルを読み込み、その内容をページごとにテキストに変換し、指定された形式でファイルに保存してください。
重要なルール:
* 各ページのテキストの先頭に、必ず以下の形式でページ番号を記載してください。例: --- ページ 1 ---
* テキストは、元のドキュメントのレイアウト(段落、箇条書き、表など)をできるだけ維持し、読みやすい形式で出力してください。
* 特に表は、Markdownの表形式などで構造を明確にしてください。
* PDF内に画像が含まれている場合、その画像に書かれている文字や説明もテキストに含めてください。
* PDF全体の内容を省略せず、全て抽出してください。
* 抽出したテキストは、ページごとに分割して、それぞれ異なるファイルとして保存してください。
* ファイル名は「page_ページ番号.txt」としてください(例: page_1.txt)。
* 出力は、保存されたファイルとその場所を示すメッセージのみにしてください。

というように変更しファイルに保存するようにする

しかし処理する約款ページは270Pあるため、分割して実験することにする

次に
Echo 証券の内容の抽出

最初にJSONで抽出を試したが、いくつかの要素が抜けている
これをテキストファイルで抽出すると
ほぼ全て抽出できる
ただし、⭕️❌での記載部分は反映されない

このため一度テキストで抽出し修正することにする

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
しかし保険証券のように少し複雑になると途端に精度が下がる

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に変換する際,デフォルトでメタデータを付与してくれる.従って,メタデータを付与するための処理は以下の処理だけで良い.

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(ローカル)

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