中級 22 分RAG

ChromaDB と Ollama を使用したローカル RAG:チュートリアル Python

ChromaDB、Ollama、Python を使ってローカルで RAG を構築するには、3 つの要素を組み合わせます。データをディスクに永続保存するベクトルストア(ChromaDB)、テキストのチャンクをベクトルに変換する埋め込みモデル(Ollama 経由の nomic-embed-text)、そして検索で見つかった文章をもとに回答するチャット LLM です。API キーは不要で、データが外部に漏れることもありません。本ガイドでは、未加工の PDF から、出典を引用するチャットボットを 22 分で作成するまでの手順を紹介します。

著者 Mohamed Meguedmi·更新 2026-08-27·Windows・macOS・Linuxでテスト済み

#なぜローカルRAGにこのスタックを採用するのか

多くのRAGチュートリアルは、LangChainやLlamaIndexから始まります。これらのフレームワークは強力ですが、内部で何が起きているかが見えにくくなります。ここでは、依存ライブラリを3つだけ使い、パイプラインを自分で書きます。各ステップを理解し、後で何を最適化すべきかも分かるようになります。

ChromaDB
オープンソースのベクトルストア、純粋なPythonで構成、永続モード(SQLite + HNSWインデックス)を内蔵。サーバーを起動する必要はありません。
Ollama
埋め込みモデル(nomic-embed-text)とチャット LLM(Qwen 3.5、Granite 4.2、Gemma 4)の両方を提供します。localhost:11434 上の単一の HTTP エンドポイント。
ネイティブPython
いくつかの関数だけで、フレームワークは使いません。必要になれば後からLangChainを組み込めますが、始める段階では不要です。
i
得られるもの
PDFが入ったフォルダを読み込み、PDFをチャンクに分割してChromaDBでインデックスを作成し、引用付きでフランス語の回答を返す、約150行のPythonスクリプトです。すべてローカルで実行され、外部へのリクエストは一切発生しません。

#前提条件

ローカルコパイロットキット

このガイドでモデルの導入まで、キットでエディタ内でコードを書くコパイロットの導入まで進められます。

  • 永久に利用できるオンラインスペース
  • PDF + ファイル
  • 生涯アップデート
Python 3.10+
ChromaDBは3.10以上が必要です。python --version で確認してください。
Ollamaがインストールされ、起動されている
デーモンはデフォルトで http://localhost:11434 で接続を待ち受けます。初めて設定する場合は、まずOllamaのインストールガイドに従ってください。
8 GB の RAM
16 GBあれば余裕があります。9BのチャットモデルはQ4で約6 GB、埋め込みモデルは約300 MBのメモリを使用します。
GPUは必須ではありません
CPUでも推論は動作しますが、速度は遅くなります。大規模なコーパスを取り込む際は、6 GB以上のメモリを搭載したGPUがあると、埋め込みの計算を大幅に高速化できます。

#1. ChromaDBをインストールし、Ollamaを準備する

クリーンな仮想環境を作成し、必要な3つのライブラリをインストールし、Ollama側でモデルをダウンロードします。

Python環境
python -m venv .venv
source .venv/bin/activate  # sous Windows : .venv\Scripts\activate
pip install chromadb ollama pypdf

3つのパッケージ:ベクトルストア用のchromadb、公式Pythonクライアント用のollama、PDFの読み取り用のpypdf。これだけです。

Ollama のモデル
ollama pull nomic-embed-text
ollama pull qwen3.5:9b

nomic-embed-text は137Mのパラメータを持つ、マルチ言語の埋め込みモデルで、次元768のベクトルを生成します。軽量で高速であり、フランス語に優れています。Qwen 3.5 9B(6.6 GB、256kのコンテキスト、マルチ言語、Apache 2.0)はチャットの最終段階で使用されます。2026年では8 GBがデフォルト設定です。granite4.2:8b(より節約)またはgemma4:12bに置き換えることも可能です。コードの変更は一切必要ありません。

→
Ollama が応答することを確認する
curl http://localhost:11434/api/tags を実行するだけで、モデルの一覧が表示されるはずです。何も表示されない場合は、デーモンが起動していません。別のターミナルで ollama serve を実行してください。

#2. エンベディングモデルの設定

埋め込みとは、テキストの意味を表すベクトルです。意味が近い2つのテキストは、ベクトルも近くなります。これがRAGの仕組みの核です。質問の埋め込みに最も似た埋め込みを持つチャンクを検索します。

embed.py — 簡単なテスト
import ollama

resp = ollama.embeddings(
    model="nomic-embed-text",
    prompt="Le contrat est résilié de plein droit en cas de manquement grave."
)

vec = resp["embedding"]
print(f"Dimension du vecteur : {len(vec)}")
print(f"5 premières valeurs : {vec[:5]}")

「Dimension du vecteur : 768」と表示されるはずです。model not foundというエラーで失敗する場合は、ollama pull nomic-embed-textが実行されていないことが原因です。

i
なぜnomic-embed-textを使用するのか
FRベンチマーク(MTEB-fr)において、nomic-embed-textは200Mパラメータ未満のモデルでトップ5にランクインしています。純フランス語の処理に関しては、mxbai-embed-largeがしばしば優れますが、670Mのサイズを必要とします。nomicは品質と速度のバランスが取れた優れた選択肢であり、初回導入に適しています。

#3. フランス語PDFの取り込み

インジェストは 3 つのことをします。PDF のページを読み取り、テキストを適切なサイズのチャンクに分割し、各チャンクとその埋め込みを ChromaDB に永続モードで保存します。

ingest.py
import os
import chromadb
import ollama
from pypdf import PdfReader

client = chromadb.PersistentClient(path="./chroma_db")
collection = client.get_or_create_collection(name="docs")

def chunk_text(text, size=800, overlap=100):
    chunks = []
    start = 0
    while start < len(text):
        end = min(start + size, len(text))
        chunks.append(text[start:end])
        start += size - overlap
    return chunks

def ingest_pdf(path):
    reader = PdfReader(path)
    name = os.path.basename(path)
    for page_num, page in enumerate(reader.pages):
        text = page.extract_text() or ""
        for i, chunk in enumerate(chunk_text(text)):
            emb = ollama.embeddings(
                model="nomic-embed-text",
                prompt=chunk
            )["embedding"]
            collection.add(
                ids=[f"{name}-p{page_num}-c{i}"],
                embeddings=[emb],
                documents=[chunk],
                metadatas=[{"source": name, "page": page_num + 1}],
            )
    print(f"OK : {name} ingéré ({len(reader.pages)} pages)")

if __name__ == "__main__":
    for f in os.listdir("./pdfs"):
        if f.endswith(".pdf"):
            ingest_pdf(f"./pdfs/{f}")

チャンカーはテキストを800文字のブロックに分割し、隣接するブロック間で100文字を重複させます。これは、小さすぎて文脈が不足することも、大きすぎて重要な情報が埋もれることもない、出発点となる設定です。情報密度が非常に高い法律関連の内容では500文字に減らし、余白が多くゆったりとした構成の技術マニュアルでは1200文字に増やしてください。

→
ChromaDBの永続化モード
PersistentClient(path="./chroma_db")は、再起動後も残るディレクトリを作成します。SQLiteにはメタデータを、HNSWインデックスにはベクトルを保存します。サーバーを起動する必要も、Dockerを使う必要もありません。後からクライアント/サーバーモードに移行するには、HttpClientに置き換えるだけです。

ドキュメントが入ったフォルダ ./pdfs/ を対象に、データの取り込みを開始してください:

データ取り込みを開始する
mkdir -p pdfs
# placez vos PDF dans ./pdfs/
python ingest.py
!
スキャンされたPDF=テキストなし
pypdfが抽出できるのは、PDFにテキストとして含まれている文字だけです。PDFがスキャン画像の場合、extract_text()は空の結果を返します。その場合は、データを取り込む前にOCR処理が必要です。Tesseract、またはOllama経由で利用するQwen 3.5 9Bのようなマルチモーダルの視覚モデルを使います。

#4. ChromaDBにおけるtop-k検索

チャンクをインデックス化した後の検索では、まず質問を埋め込みベクトルに変換し、次にコサイン距離が最も近いk個のベクトルをChromaに要求します。チャンクが10万個あっても、検索は瞬時に完了します。

search.py
import chromadb
import ollama

client = chromadb.PersistentClient(path="./chroma_db")
collection = client.get_collection(name="docs")

def search(question, k=4):
    q_emb = ollama.embeddings(
        model="nomic-embed-text",
        prompt=question
    )["embedding"]
    results = collection.query(
        query_embeddings=[q_emb],
        n_results=k,
    )
    chunks = results["documents"][0]
    metas = results["metadatas"][0]
    return list(zip(chunks, metas))

if __name__ == "__main__":
    hits = search("Quelles sont les conditions de résiliation ?")
    for chunk, meta in hits:
        print(f"[{meta['source']} p.{meta['page']}]")
        print(chunk[:200], "...\n")

k=4は適切なデフォルト値です。小さすぎると関連するコンテキストを取りこぼし、大きすぎるとLLMがノイズに埋もれ、コンテキストウィンドウの上限を超えてしまいます。対象が非常に絞られた質問ならk=2で十分です。横断的な質問では6に上げてください。

#5. 引用付きチャットループ

ここからは各処理を組み合わせます。関連するチャンクを検索し、そのコンテキストを使ってプロンプトを作成し、Ollama経由でQwen 3.5に送信して、モデルに出典を引用するよう求めます。

chat.py
import ollama
from search import search

SYSTEM = """Tu es un assistant qui répond uniquement à partir du CONTEXTE fourni.
Si la réponse n'est pas dans le contexte, dis-le clairement.
Cite tes sources entre crochets sous la forme [source.pdf p.X]."""

def ask(question):
    hits = search(question, k=4)
    context = "\n\n".join(
        f"[{m['source']} p.{m['page']}]\n{c}" for c, m in hits
    )
    prompt = f"CONTEXTE :\n{context}\n\nQUESTION : {question}"
    resp = ollama.chat(
        model="qwen3.5:9b",
        messages=[
            {"role": "system", "content": SYSTEM},
            {"role": "user", "content": prompt},
        ],
        options={"temperature": 0.2, "num_ctx": 8192},
    )
    return resp["message"]["content"]

if __name__ == "__main__":
    while True:
        q = input("\nQuestion (vide pour quitter) > ").strip()
        if not q:
            break
        print("\n" + ask(q))

重要な3つの詳細があります。第一に、temperature=0.2です。創造的な回答ではなく、事実ベースの回答を求めています。第二に、num_ctx=8192です。Ollamaのデフォルトウィンドウ(2048)は、800文字のチャンクを4つ注入すると短すぎます。第三に、システムプロンプトは、モデルが幻覚を起こす代わりに「わかりません」と言うことを強制します。これはRAGにおける幻覚防止の主な安全策です。

→
ストリーミングによるより良いユーザー体験
ollama.chatをollama.chat(..., stream=True)に置き換え、応答を順に処理して、届いたトークンから表示してください。このコードを実際のインターフェース(FastAPI + WebSocket、またはStreamlit)に組み込む際には、これが非常に重要です。

#6. 具体例:契約書を扱う法務チャットボット

ある事務所が、PDF 形式の業務委託契約書200件について質問したいとします。上記の構成を使えば、1時間もかからずに、次のような質問に答えるアシスタントを用意できます。

よくある質問
「契約終了後の競業避止期間が12か月を超える条項を含む契約はどれですか?」
何が起こるか
質問の埋め込みを使って、意味的に近いキーワード(競業避止、契約終了後、期間)を含むチャンクを検索します。Qwen 3.5はこの4つの箇所を読み、関連するファイル名を添えて回答します。
プライバシーの保証
データが端末の外に出ることはありません。APIキーも不要です。テレメトリもありません。これが、ローカルRAGとOpenAIのラッパーを区別する点です。
!
知っておくべき限界
基本的なRAGは、対象を絞った質問(「条項Xとは何か」)にはうまく答えられますが、集計を求める質問(「Xを含む契約はいくつあるか」)は苦手です。後者には、複数の段階に分けてデータベースに問い合わせるエージェントか、GraphRAGが必要です。それはまた別の話です。

#トラブルシューティング

ChromaDBへのデータ取り込みが遅い
ボトルネックはほとんどがOllamaへのembedding呼び出しです。nomic-embed-textがGPU上で動作しているかをollama psで確認してください。CPUでは約50チャンク/秒、GPUでは約500チャンク/秒です。
「model not found」
Ollamaがnomic-embed-textを見つけられません。ollama pull nomic-embed-textを再実行し、ollama listで確認してください。
出典を捏造する回答
9Bモデルは今でも時折ハルシネーションを起こします。十分なVRAMがあるなら、mistral-small(24B、約14GB、フランス語で非常に優れています)またはqwen3.8:27bに切り替えてください。あるいは、ChromaDBの後段にリランカー(クロスエンコーダー)を追加して、偽陽性を除外してください。
フランス語での埋め込み品質が低い
nomic-embed-textは多言語対応ですが、フランス語のみのコンテンツには最適ではありません。法律や医療に関するコンテンツには、Solon-embeddings-large-0.1またはbge-m3を試してください(Ollamaではなく、sentence-transformersで読み込みます)。
ChromaDBのサイズが際限なく増える
各リインデックス処理により重複が発生します。PDFを再インポートする前に、collection.delete(where={"source": name}) を実行して以前のチャンクを削除してください。

#さらに詳しく

これで、正常に動作するRAGができました。さらに発展させるために、次に取り組むとよい項目は以下のとおりです。

フランス語向けの埋め込みモデルを比較する
当サイトのガイド「フランス語向けのおすすめ埋め込みモデル」では、フランス語コンテンツを使って BGE、E5、Solon、nomic を比較しています。
チャンク分割を改善する
「チャンキング戦略」では、意味に基づく分割、Markdown の見出しによる分割、段落ごとの分割を詳しく解説しています。こうした工夫が、精度の向上に最も大きくつながることがよくあります。
rerankerを追加
「パイプラインにリランカーを追加する」:Chromaの後にクロスエンコーダーを配置することで、関連性が15%向上します。次に進むステップとして自然な選択です。
ハイブリッド検索
「BM25+ベクトルのハイブリッド検索」は、語句に基づく検索と意味に基づく検索を組み合わせたもので、専門用語や固有名詞が多い場合には不可欠です。
このガイドは役に立ちましたか?

ご意見、誤りのご指摘、補足はありますか?ぜひお知らせください。皆さんにとってより良いガイドにするために役立ちます。