chromaDB

ローカルLLMを使ってRAGが導入された本格的なチャットボットを作ろう!ローカルLLM実行ツールであるOllamaとPythonを使ってローカル環境にいくら実行しても無料のAI環境を作ろう!

で word ファイルをRAGにしているので参考になるかも
chromaDBも取り扱っているし

https://ollama.com/library/nomic-embed-text
でollamaでベクトル化するモデルをインストールする

ollama pull nomic-embed-text

でインストール

次にchromaDBのインストール

pip install chromadb

なお
ChromaDBでは、テキストデータを直接ベクトルストアに保存することもできる
ChromaDBをローカルファイルで使う

も参考に

そしてドキュメントファイルをインストールして使えるようにする
python-docxと request もインストールする

pip install python-docx  

python-docx でWordファイルを操作する
も参考に

とりあえず
OpenAI のAPIなので
後でOllamaに書き換えて行う

import streamlit as st
from openai import OpenAI
import chromadb
from docx import Document
import requests


# ChromaDBの設定
DB_DIR = "./chroma_db"
chroma_client = chromadb.PersistentClient(path=DB_DIR)

if "collection" not in st.session_state:
    st.session_state.collection = chroma_client.get_or_create_collection(
        name="local_docs"
    )

# 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"]

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

# テキスト分割関数
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

## サイドバーの設定 ##
st.set_page_config(page_title="Local LLM Chat")

st.sidebar.title("設定")
model = st.sidebar.text_input("モデル名", value="llama3.1:8b")
temperature = st.sidebar.slider("temperature", 0.0, 2.0, 0.3, 0.1)
system_prompt = st.sidebar.text_area(
    "System Prompt",
    "あなたは有能なアシスタントです。日本語で回答してください",
)

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

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("インデックス作成完了")

# タイトル
st.title("Local LLM Chat")

# 会話の履歴を保管
if "messages" not in st.session_state:
    st.session_state.messages = []

# 会話の履歴をリセットするボタン
if st.sidebar.button("会話をリセット"):
    st.session_state.messages = []

# 会話の履歴を表示
for m in st.session_state.messages:
    with st.chat_message(m["role"]):
        st.write(m["content"])


prompt = st.chat_input("メッセージを入力")

client = OpenAI(
    api_key="ollama",   
    base_url="http://localhost:12000/v1"
)

if prompt:

    # ユーザーのプロンプトを表示
    with st.chat_message("user"):
        st.write(prompt)

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

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

    if results["documents"]:
        context_text = "\n".join(results["documents"][0])
        rag_prompt = f"""
        以下は関連ドキュメントの抜粋です。
        {context_text}
        この情報を参考に以下の質問に答えてください。
        {prompt}
        """
        final_user_prompt = rag_prompt
    else:
        final_user_prompt = prompt

    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
    
    # 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)

    # 会話の履歴を保存
    
    st.session_state.messages.append({"role": "assistant", "content": stream_response})

がコードの内容

以下は解説
まず最初にchromaDBの設定をする

DB_DIR = "./chroma_db"


この指定フォルダにDBを保存する

chroma_client = chromadb.PersistentClient(path=DB_DIR)

でchromaDBのDBをセット

この2つで使う準備ができる

if "collection" not in st.session_state: st.session_state.collection = chroma_client.get_or_create_collection( name="local_docs" )

でDBにテーブルがなければ作成

ここから必要になる機能を作成する
作成する機能は
ベクトル化関数
Wordファイルを読み込む関数
テキスト分割関数
がRAG必要

# 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"]

この解説

  r = requests.post(
        "http://localhost:12000/api/embeddings",

の部分は local の Ollamaへのリクエスト
これで処理をする

json={"model": "nomic-embed-text", "prompt": text}

の部分は
使用するモデルの指定、今回はnomic-embed-textをベクトル化するモデルとして指定
プロンプトでtext変数を指定

data = r.json()

で返ってきたレスポンスを辞書型へ変換している

return data["embedding"]

で辞書型の中に
Embedding ば埋め込みベクトルなので
これを取り出して返している

もしdataの中を知りたいのなら
Printなどで表示すれば中にembeddingがあるのがわかる

次にwordファイルを読み込む関数

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

この解説

from docx import Document

で使えることになる doucment を使うことで簡単に処理ができる

Document(file).paragraphs)

によりファイルの段落情報がリストになる

これを for で取り出していく

for para in Document(file).paragraphs

とすれば para に格納していく

そしてその結果を受け取る時に

"\n".join(para.text

としておくことで
Joinで ¥nを指定することで改行されて結合されていく

chromaDB + streamlit + elyza:jp8b

import streamlit as st
from openai import OpenAI
import chromadb
from docx import Document
import requests


# ChromaDBの設定
DB_DIR = "./chroma_db"
chroma_client = chromadb.PersistentClient(path=DB_DIR)

if "collection" not in st.session_state:
    st.session_state.collection = chroma_client.get_or_create_collection(
        name="local_docs"
    )

# 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"]

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

# テキスト分割関数
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

## サイドバーの設定 ##
st.set_page_config(page_title="Local LLM Chat")

st.sidebar.title("設定")
model = st.sidebar.text_input("モデル名", value="llama3.1:8b")
temperature = st.sidebar.slider("temperature", 0.0, 2.0, 0.3, 0.1)
system_prompt = st.sidebar.text_area(
    "System Prompt",
    "あなたは有能なアシスタントです。日本語で回答してください",
)

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

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("インデックス作成完了")

# タイトル
st.title("Local LLM Chat")

# 会話の履歴を保管
if "messages" not in st.session_state:
    st.session_state.messages = []

# 会話の履歴をリセットするボタン
if st.sidebar.button("会話をリセット"):
    st.session_state.messages = []

# 会話の履歴を表示
for m in st.session_state.messages:
    with st.chat_message(m["role"]):
        st.write(m["content"])


prompt = st.chat_input("メッセージを入力")

client = OpenAI(
    api_key="ollama",   
    base_url="http://localhost:12000/v1"
)

if prompt:

    # ユーザーのプロンプトを表示
    with st.chat_message("user"):
        st.write(prompt)

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

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

    if results["documents"]:
        context_text = "\n".join(results["documents"][0])
        rag_prompt = f"""
        以下は関連ドキュメントの抜粋です。
        {context_text}
        この情報を参考に以下の質問に答えてください。
        {prompt}
        """
        final_user_prompt = rag_prompt
    else:
        final_user_prompt = prompt

    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
    
    # 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)

    # 会話の履歴を保存
    
    st.session_state.messages.append({"role": "assistant", "content": stream_response})

のコードを
OpenAI のAPIではなく
Ollama のelyza:jp8bを使用するようにコードを変更

openai の依存を削除し、/api/chat(ストリーミング)を requests で直接呼び出し。
モデル既定値を elyza:jp8b に変更。
Ollama のベースURL をサイドバーで指定可能(既定: http://localhost:11434)。埋め込みも同じURLを使用。
温度などは options 経由で送信。
ストリーミングは1行ごとの JSON を読み、message.content を随時描画。

import streamlit as st
import chromadb
from docx import Document
import requests
import json

# -----------------------------
# 共通設定
# -----------------------------
st.set_page_config(page_title="Local LLM Chat")

st.sidebar.title("設定")
ollama_base_url = st.sidebar.text_input("Ollama ベースURL", value="http://localhost:11434")
model = st.sidebar.text_input("モデル名", value="elyza:jp8b")  # 既定を elyza:jp8b に
temperature = st.sidebar.slider("temperature", 0.0, 2.0, 0.3, 0.1)
system_prompt = st.sidebar.text_area(
    "System Prompt",
    "あなたは有能なアシスタントです。日本語で回答してください",
)

# -----------------------------
# ChromaDB の設定
# -----------------------------
DB_DIR = "./chroma_db"
chroma_client = chromadb.PersistentClient(path=DB_DIR)

if "collection" not in st.session_state:
    st.session_state.collection = chroma_client.get_or_create_collection(
        name="local_docs"
    )

# -----------------------------
# Ollama API ユーティリティ
# -----------------------------
def ollama_embed(text: str):
    """Ollama の /api/embeddings を使ってベクトルを取得"""
    url = f"{ollama_base_url}/api/embeddings"
    r = requests.post(
        url,
        json={"model": "nomic-embed-text", "prompt": text},
        timeout=60,
    )
    r.raise_for_status()
    data = r.json()
    return data["embedding"]

def ollama_chat_stream(messages, model_name: str, temperature: float = 0.3):
    """
    Ollama の /api/chat ストリームを逐次 yield するジェネレータ。
    messages: [{"role":"system|user|assistant", "content":"..."}]
    """
    url = f"{ollama_base_url}/api/chat"
    payload = {
        "model": model_name,
        "messages": messages,
        "stream": True,
        "options": {
            "temperature": float(temperature)
        }
    }
    with requests.post(url, json=payload, stream=True) as r:
        r.raise_for_status()
        for line in r.iter_lines(decode_unicode=True):
            if not line:
                continue
            # 各行は JSON。末尾で {"done":true} が来る
            try:
                obj = json.loads(line)
            except json.JSONDecodeError:
                continue
            if obj.get("done"):
                break
            msg = obj.get("message", {})
            delta = msg.get("content", "")
            if delta:
                yield delta

# -----------------------------
# Word 読み込み & チャンク
# -----------------------------
def load_word_document(file):
    return "\n".join(para.text for para in Document(file).paragraphs)

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

# -----------------------------
# UI: アップロード → インデックス
# -----------------------------
uploaded_files = st.sidebar.file_uploader(
    "Wordファイルをアップロード(.docx)",
    type=["docx"],
    accept_multiple_files=True
)

if st.sidebar.button("インデックス作成"):
    try:
        for file in uploaded_files or []:
            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("インデックス作成完了")
    except Exception as e:
        st.sidebar.error(f"インデックス作成でエラー: {e}")

# -----------------------------
# 会話領域
# -----------------------------
st.title("Local LLM Chat")

if "messages" not in st.session_state:
    st.session_state.messages = []

# リセットボタン
if st.sidebar.button("会話をリセット"):
    st.session_state.messages = []

# 既存履歴の表示
for m in st.session_state.messages:
    with st.chat_message(m["role"]):
        st.write(m["content"])

# 入力
prompt = st.chat_input("メッセージを入力")

if prompt:
    # ユーザーの入力を表示
    with st.chat_message("user"):
        st.write(prompt)

    # --- RAG: 近傍検索 ---
    try:
        query_embed = ollama_embed(prompt)
        results = st.session_state.collection.query(
            query_embeddings=[query_embed],
            n_results=2
        )
    except Exception as e:
        results = {"documents": []}
        st.warning(f"RAG検索でエラーが発生しました: {e}")

    # ドキュメントの結合
    if results.get("documents"):
        context_text = "\n".join(results["documents"][0])
        rag_prompt = f"""
以下は関連ドキュメントの抜粋です。
{context_text}

この情報を参考に以下の質問に答えてください。
{prompt}
"""
        final_user_prompt = rag_prompt
    else:
        final_user_prompt = prompt

    # セッション履歴にユーザー発話(RAG統合後)を追加
    st.session_state.messages.append({"role": "user", "content": final_user_prompt})

    # Ollama へ投げる message 配列を組み立て
    chat_messages = []
    if system_prompt.strip():
        chat_messages.append({"role": "system", "content": system_prompt})
    # 既存履歴をそのまま渡す(role は "user"/"assistant")
    chat_messages.extend(st.session_state.messages)

    # --- ストリーミング応答 ---
    with st.chat_message("assistant"):
        placeholder = st.empty()
        stream_response = ""
        try:
            for delta in ollama_chat_stream(chat_messages, model, temperature):
                stream_response += delta
                placeholder.write(stream_response)
        except Exception as e:
            st.error(f"Ollama応答でエラーが発生しました: {e}")

    # 履歴へ保存
    st.session_state.messages.append({"role": "assistant", "content": stream_response})


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

で実行

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

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() 相当を使う必要あり)。希望があればその例も紹介します。

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