0
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ChromaDB + Ollama で作る高機能ローカルRAGシステム

Posted at

ChromaDB + Ollama で作る高機能ローカルRAGシステム

はじめに

この記事は、Claudeによって作成されました。
このコードの大部分はGeminiによって生成されたもので、筆者はその動作検証と最終的な調整を担当しました。
この記事では、ChromaDBとOllamaを組み合わせて、完全にローカル環境で動作するRAG(Retrieval-Augmented Generation)システムを構築する方法をご紹介します。
このシステムは、ご自身のPC上でサーバーを起動して利用するものです
スクリーンショット 2025-07-14 232125.png

特徴

  • 🔍 高性能ベクトル検索: ChromaDBによる高速な類似度検索
  • 🤖 ローカルLLM: Ollamaで複数のモデルをサポート
  • 📄 文書管理: 複数ファイルのアップロードと管理
  • 🌐 Webインターフェース: 直感的なチャットUI
  • 🔧 設定可能: 埋め込みモデル、検索件数、コレクション管理

必要な環境

前提条件

  • Python 3.8以上
  • Node.js(開発時のみ)
  • 十分なメモリ(4GB以上推奨)

必要なサービス

  1. ChromaDB サーバー (ポート: 8001)
  2. Ollama サーバー (ポート: 11434)
  3. Pythonバックエンド (ポート: 5000)

インストール手順

1. ChromaDBのセットアップ

# ChromaDBのインストール
pip install chromadb

# ChromaDBサーバーの起動
chroma run --host 0.0.0.0 --port 8001 --path ./chroma_db

2. Ollamaのセットアップ

# Ollamaのインストール(公式サイトから)
# https://ollama.ai/

# モデルのダウンロード
ollama pull llama3.2:latest
ollama pull llama3.2:3b
ollama pull llama3.2:1b

# Ollamaサーバーの起動
ollama serve

3. Pythonバックエンドのセットアップ

# 必要なパッケージのインストール
pip install flask flask-cors sentence-transformers chromadb requests

システム構成

バックエンドサーバー (Python + Flask)

Faiss.py

import os
# [WinError 1114] PyTorchのDLL初期化エラーを回避するための設定
os.environ['KMP_DUPLICATE_LIB_OK']='TRUE'

import json
import requests
import chromadb
import traceback
from flask import Flask, request, jsonify, Response
from flask_cors import CORS
from sentence_transformers import SentenceTransformer

# --------------------------------------------------------------------------
# 1. Flaskアプリケーションの初期設定
# --------------------------------------------------------------------------
app = Flask(__name__)
CORS(app) # CORSをapp定義の直後に適用

# Sentence Transformerモデルのキャッシュディレクトリ設定
os.environ['SENTENCE_TRANSFORMERS_HOME'] = './st_models'

# ChromaDBクライアントの初期化
try:
    db_client = chromadb.PersistentClient(path="./chroma_db")
except Exception as e:
    print(f"Error initializing ChromaDB: {e}")
    db_client = None

# 埋め込みモデルのキャッシュ
embedding_models = {}

def get_embedding_model(model_name="all-MiniLM-L6-v2"):
    """埋め込みモデルをキャッシュから取得またはロードする"""
    if model_name not in embedding_models:
        print(f"Loading embedding model: {model_name}")
        try:
            embedding_models[model_name] = SentenceTransformer(model_name)
        except Exception as e:
            print(f"Error loading embedding model {model_name}: {e}")
            return None
    return embedding_models[model_name]

# --------------------------------------------------------------------------
# 2. ステータス確認用API
# --------------------------------------------------------------------------
@app.route('/health', methods=['GET'])
def health_check():
    return jsonify({"status": "ok"}), 200

@app.route('/chromadb/status', methods=['GET'])
def chromadb_status():
    if not db_client: return jsonify({"status": "error", "detail": "ChromaDB client not initialized"}), 500
    try:
        db_client.heartbeat()
        return jsonify({"status": "ok"}), 200
    except Exception as e:
        return jsonify({"status": "error", "detail": str(e)}), 500

@app.route('/ollama/status', methods=['GET'])
def ollama_status():
    """Ollamaサーバーの接続状況とモデルリストを確認"""
    try:
        response = requests.get('http://127.0.0.1:11434/api/tags', timeout=5)
        response.raise_for_status()
        models_data = response.json().get('models', [])
        return jsonify({"connected": True, "models": models_data}), 200
    except requests.exceptions.RequestException as e:
        return jsonify({"connected": False, "detail": "Failed to connect to Ollama server."}), 500
    except Exception as e:
        return jsonify({"connected": False, "detail": str(e)}), 500

# --------------------------------------------------------------------------
# 3. コレクション管理API
# --------------------------------------------------------------------------
@app.route('/collections', methods=['GET', 'OPTIONS'])
def list_collections():
    if request.method == 'OPTIONS': return Response(status=200)
    if not db_client: return jsonify({"detail": "ChromaDB not available"}), 500
    collections = db_client.list_collections()
    return jsonify([{"name": c.name} for c in collections]), 200

@app.route('/collections', methods=['POST'])
def create_collection():
    if not db_client: return jsonify({"detail": "ChromaDB not available"}), 500
    data = request.get_json()
    name = data.get('name')
    if not name: return jsonify({"detail": "Collection name is required"}), 400
    try:
        db_client.get_or_create_collection(name=name)
        return jsonify({"message": f"Collection '{name}' created successfully."}), 201
    except Exception as e:
        return jsonify({"detail": str(e)}), 500

@app.route('/collections/<string:collection_name>', methods=['DELETE', 'OPTIONS'])
def delete_collection(collection_name):
    if request.method == 'OPTIONS': return Response(status=200)
    if not db_client: return jsonify({"detail": "ChromaDB not available"}), 500
    try:
        db_client.delete_collection(name=collection_name)
        return jsonify({"message": f"Collection '{collection_name}' deleted."}), 200
    except Exception as e:
        return jsonify({"detail": str(e)}), 500

@app.route('/collections/<string:collection_name>/documents', methods=['GET', 'OPTIONS'])
def get_documents_in_collection(collection_name):
    if request.method == 'OPTIONS': return Response(status=200)
    if not db_client: return jsonify({"detail": "ChromaDB not available"}), 500
    try:
        collection = db_client.get_collection(name=collection_name)
        data = collection.get(include=['metadatas'])
        
        documents = []
        if data and data['ids']:
            unique_filenames = set()
            for metadata in data['metadatas']:
                filename = metadata.get('filename')
                if filename and filename not in unique_filenames:
                    documents.append({"id": filename})
                    unique_filenames.add(filename)
        
        return jsonify({"documents": documents, "count": collection.count()}), 200
    except ValueError:
        return jsonify({"documents": [], "count": 0, "detail": "Collection not found"}), 200
    except Exception as e:
        return jsonify({"detail": str(e)}), 500

# --------------------------------------------------------------------------
# 4. 文書アップロードとチャットAPI
# --------------------------------------------------------------------------
@app.route('/upload', methods=['POST', 'OPTIONS'])
def upload_file():
    if request.method == 'OPTIONS': return Response(status=200)
    if not db_client: return jsonify({"detail": "ChromaDB not available"}), 500
    if 'file' not in request.files: return jsonify({"detail": "No file part"}), 400
    
    file = request.files['file']
    collection_name = request.form.get('collection_name', 'default')
    
    if file.filename == '': return jsonify({"detail": "No selected file"}), 400

    try:
        text = file.read().decode('utf-8')
        chunk_size = 500
        chunks = [text[i:i+chunk_size] for i in range(0, len(text), chunk_size)]
        
        collection = db_client.get_or_create_collection(name=collection_name)
        model = get_embedding_model()
        if not model: return jsonify({"detail": "Embedding model not available"}), 500

        embeddings = model.encode(chunks).tolist()
        ids = [f"{file.filename}:{i}" for i in range(len(chunks))]
        metadatas = [{"filename": file.filename, "chunk": i} for i in range(len(chunks))]

        collection.add(embeddings=embeddings, documents=chunks, metadatas=metadatas, ids=ids)
        
        return jsonify({"message": "File processed successfully", "chunks": len(chunks)}), 200
    except Exception as e:
        return jsonify({"detail": str(e)}), 500

@app.route('/chat', methods=['POST', 'OPTIONS'])
def chat():
    if request.method == 'OPTIONS': return Response(status=200)
    
    if not db_client: return jsonify({"detail": "ChromaDB not available"}), 500

    data = request.get_json()
    query = data.get('query')
    model_name = data.get('model', 'llama3.2:latest')
    collection_name = data.get('collection', 'default')
    embedding_model_name = data.get('embedding_model', 'all-MiniLM-L6-v2')
    top_k = data.get('top_k', 3)

    if not query: return jsonify({"detail": "Query is required"}), 400

    def generate_response():
        try:
            embedding_model = get_embedding_model(embedding_model_name)
            if not embedding_model:
                yield f"data: {json.dumps({'error': 'Embedding model not available'})}\n\n"
                return

            query_embedding = embedding_model.encode([query]).tolist()
            
            try:
                collection = db_client.get_collection(name=collection_name)
                results = collection.query(query_embeddings=query_embedding, n_results=top_k)
                context = "\n---\n".join(results['documents'][0])
                yield f"data: {json.dumps({'search_results': True})}\n\n"
            except Exception:
                context = "利用可能な参考情報はありません。"

            prompt = f"参考情報に厳密に基づいて、ユーザーの質問に日本語で回答してください。参考情報に答えがない場合は、「情報が見つかりません」と回答してください。\n\n### 参考情報:\n{context}\n\n### ユーザーの質問:\n{query}"
            
            payload = {
                "model": model_name,
                "messages": [{'role': 'user', 'content': prompt}],
                "stream": True
            }
            
            with requests.post('http://127.0.0.1:11434/api/chat', json=payload, stream=True) as r:
                r.raise_for_status()
                for line in r.iter_lines():
                    if line:
                        decoded_line = json.loads(line.decode('utf-8'))
                        if 'content' in decoded_line.get('message', {}):
                            content_data = {"content": decoded_line['message']['content']}
                            yield f"data: {json.dumps(content_data)}\n\n"
        
        except Exception as e:
            traceback.print_exc()
            yield f"data: {json.dumps({'error': str(e)})}\n\n"

    return Response(generate_response(), mimetype='text/event-stream')

if __name__ == '__main__':
    get_embedding_model()
    app.run(host='0.0.0.0', port=5000, debug=True, use_reloader=False)

フロントエンド(HTML + JavaScript)

ソースコードはGitHubリポジトリ内の RAG3.html をご参照ください

フロントエンドは、モダンなWebUIを提供するHTMLファイルで構成されています。主な機能:

  • リアルタイムチャット: ストリーミング応答対応
  • 文書管理: ファイルアップロード、コレクション管理
  • 設定パネル: モデル選択、検索パラメータ調整
  • 接続状況監視: 各サービスの状態をリアルタイム表示

使用方法

1. サーバー起動

# ターミナル1: ChromaDBサーバー
chroma run --host 0.0.0.0 --port 8001 --path ./chroma_db

# ターミナル2: Ollamaサーバー
ollama serve

# ターミナル3: Pythonバックエンド
python Faiss.py

スクリーンショット 2025-07-14 231551.png
スクリーンショット 2025-07-14 231751.png

2. Webインターフェースの利用

  1. ブラウザで http://localhost:5000 にアクセス
  2. 接続状況を確認(ChromaDB、Ollama共に緑色になることを確認)
  3. 新しいコレクションを作成
  4. 文書をアップロード(.txt, .md, .pdf, .docx対応)
  5. チャットで質問を開始

3. 設定のカスタマイズ

  • LLMモデル: Llama 3.2, Gemma, Mistralなど
  • 埋め込みモデル: all-MiniLM-L6-v2, all-mpnet-base-v2など
  • 検索件数: 3, 5, 10件から選択
  • コレクション: 複数のコレクションで文書を分類管理

技術的な特徴

RAGパイプライン

  1. 文書の前処理: アップロードされた文書を500文字ごとにチャンク分割
  2. ベクトル化: Sentence Transformersで埋め込みベクトルを生成
  3. インデックス保存: ChromaDBに永続化して保存
  4. 検索: ユーザーの質問をベクトル化し、類似度検索を実行
  5. 生成: 検索結果を文脈としてOllamaのLLMで回答を生成

パフォーマンス最適化

  • モデルキャッシュ: 埋め込みモデルをメモリ上でキャッシュ
  • ストリーミング応答: Server-Sent Eventsでリアルタイム応答
  • 永続化: ChromaDBによる高速な永続化ストレージ

トラブルシューティング

よくある問題と解決方法

  1. ChromaDB接続エラー

    # ポート8001が使用中でないか確認
    netstat -ano | findstr :8001
    
  2. Ollama接続エラー

    # Ollamaサービスの状態確認
    ollama list
    
  3. PyTorchエラー

    # 環境変数設定
    os.environ['KMP_DUPLICATE_LIB_OK']='TRUE'
    

拡張可能性

今後の改良点

  • PDFパーサー: より高度な文書解析
  • マルチモーダル: 画像や音声への対応
  • スケーラビリティ: 複数ユーザー対応
  • 認証機能: セキュリティ機能の追加

まとめ

このシステムは、完全にローカル環境で動作するRAGシステムとして、以下の利点を提供します:

  • プライバシー: データが外部に送信されない
  • カスタマイズ性: 各コンポーネントを自由に調整可能
  • コスト効率: APIコストが発生しない
  • オフライン対応: インターネット接続不要

企業内での文書検索システムや、個人の知識管理システムとして活用できます。
ソースコードはこちら (GitHub):https://github.com/ishikawamasahito/RAG3.html

参考資料


この記事が参考になりましたら、ぜひ⭐をお願いします!

0
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?