見出し画像

今更!?ローカルでRAG環境を作ってみる

RAG(Retrieval-Augmented Generation)という言葉自体は、
もはや「今更?」と言われてもおかしくないほど一般的になってきました。

一方で、
・ローカル環境で
・できるだけ手軽に
・環境構築で詰まらずに
RAGを試せる情報は、意外とまとまっていないと感じています。

本記事では、Docker と Dev Containers を使って、
ローカルで動作する RAG 環境を構築してみた内容をまとめます。
※本記事では CPU 動作を前提としています。モデルサイズやマシンスペックによっては推論速度が大きく変わる点に注意してください。


ローカル環境で動かす

構成について

できるだけシンプルかつローカルで完結する構成を選びました。
Dockerベースで環境を構築し、Dev Containers 上で動作する構成としています。

Dev Containers は Docker コンテナを前提とした開発環境の仕組みで、
VS Code などのエディタ上から、コンテナ内の環境をそのまま開発環境として扱えます。

構築環境はこちら
・Python 3.11 + UV
・ollama + bge-m3 / qwen2.5:7b
・PostgreSQL + pgvector
構築時に、pgvector拡張機能の有効化と、ローカルLLMのモデルダウンロードを行っています

Docker 構成図

```mermaid
flowchart LR
    subgraph Host["ホストOS"]
      VSCode[VS Code]
      Docker[Docker Engine]
    end

    VSCode -->|Dev Containers| App

    subgraph Compose["docker-compose (devcontainer-network)"]
      App[app<br/>Python / uv / RAG実行]
      DB[(db<br/>PostgreSQL + pgvector)]
      Ollama[ollama<br/>Ollama server]
    end

    Docker --> Compose

    App -->|POSTGRES_HOST=db| DB
    App -->|OLLAMA_HOST=http://ollama:11434| Ollama

    App -.depends_on.-> DB
    App -.depends_on.-> Ollama
```

本記事の環境は docker-compose で 3 つのサービスを起動します。
・app: Dev Container として開く開発用コンテナ(Python/uv/RAGの実行)
・db: PostgreSQL(pgvector入り)。起動時に init-pgvector.sql を実行して拡張機能等を初期化
・ollama: ローカルLLM用の Ollama サーバー。起動時に ollama-init.sh を実行してモデルを事前ダウンロード

app は環境変数(POSTGRES_HOST / OLLAMA_HOST など)で db / ollama に接続し、
3サービスは devcontainer-network 上で名前解決(db, ollama)して通信します。

RAG構成図
RAGは以下のように動作します

```mermaid
flowchart LR
    User[質問]

    subgraph App["app(RAGアプリ)"]
      R[Retriever]
    end

    subgraph Ollama["ollama"]
      E[Embedding<br/>bge-m3]
      L[LLM<br/>qwen2.5:7b]
    end

    subgraph DB["db"]
      V[(Vector DB<br/>pgvector)]
    end

    Docs[ドキュメント] -->|Embedding| E --> V

    User --> R
    R -->|検索| V
    V -->|関連文書| R
    R -->|コンテキスト| L
    User --> L

```

環境構築

このRAG環境を動かすために、以下のソフトウェアが必要となりますので事前にインストールください。

Docker Desktop
Visual Studio Code
Dev Containers拡張機能

環境のダウンロード

以下の環境からコードをダウンロードします。

git clone https://github.com/tsux-com/RAG-example.git

環境の設定

Githubからダウンロードをしたリポジトリを、VSCodeで開きます。
Ctrl+Shift+Pを選択し、メニューから「Dev Container: Reopen in Container」を選択して、Docker環境を構築します。
初回時はローカルLLMのモデル情報もダウンロードするため、5~15分ほどかかります

Dev Container: Reopen in Container

以下のコマンドを入力し、modelsにqwen2.5:7bbge-m3:latestが登録されていたら完了です。

curl http://ollama:11434/api/tags | jq

{
  "models": [
    {
      "name": "qwen2.5:7b",
      "model": "qwen2.5:7b",
      "modified_at": "2026-01-19T05:27:51.667945795Z",
      "size": 4683087332,
      "digest": "845dbda0ea48ed749caafd9e6037047aa19acfcfd82e704d7ca97d631a0b697e",
      "details": {
        "parent_model": "",
        "format": "gguf",
        "family": "qwen2",
        "families": [
          "qwen2"
        ],
        "parameter_size": "7.6B",
        "quantization_level": "Q4_K_M"
      }
    },
    {
      "name": "bge-m3:latest",
      "model": "bge-m3:latest",
      "modified_at": "2026-01-19T05:25:47.117436373Z",
      "size": 1157672605,
      "digest": "7907646426070047a77226ac3e684fbbe8410524f7b4a74d02837e43f2146bab",
      "details": {
        "parent_model": "",
        "format": "gguf",
        "family": "bert",
        "families": [
          "bert"
        ],
        "parameter_size": "566.70M",
        "quantization_level": "F16"
      }
    }
  ]
}

利用するには?

OpenAIのライブラリを使用し、OpenAIのAPIを利用するのと同じ方法でアクセスが可能です。
アクセス時のURLとしてbase_urlを"http://ollama:11434/v1"api_keyを"ollama" と設定してください。

from openai import OpenAI


# Ollamaサーバーに接続(OpenAI API互換モード)
client = OpenAI(
    base_url="http://ollama:11434/v1",  # Ollama のOpenAI互換エンドポイント
    api_key="ollama"  # ダミーキー(必須だが、Ollamaでは検証されない)
)


# ========================================
# 1. Embeddings(ベクトル化)
# ========================================

text = "機械学習はデータからパターンを学習する技術です"

embedding_response = client.embeddings.create(
    model="mxbai-embed-large",
    input=text
)

embedding = embedding_response.data[0].embedding
print(f"テキスト: {text}")
print(f"ベクトル次元: {len(embedding)}")
print(f"最初の5要素: {embedding[:5]}")

# ========================================
# 2. Chat Completions(チャット)
# ========================================

response = client.chat.completions.create(
    model="qwen2.5:7b",
    messages=[
        {
            "role": "system",
            "content": "あなたは親切なAIアシスタントです。"
        },
        {
            "role": "user",
            "content": "Pythonの特徴を3つ教えてください"
        }
    ],
    temperature=0.7,
    max_tokens=500
)

print(f"質問: Pythonの特徴を3つ教えてください")
print(f"回答: {response.choices[0].message.content}")

サンプルのRAGを動かしてみよう

サンプルの環境設定

サンプルコードは、Pythonで作成しました。
動作させるため、以下のコマンドで環境を作ります。
コマンドを実行すると、使用されるライブラリ等がインストールされます

cd example
uv sync

RAG環境の構築

次に、RAGの環境を作ります
初回実行時にDBにテーブルがない場合は、テーブルの作成、ベクトルデータの登録を行います
元データは、example/rag_learning/docフォルダ内の昔話を使用しています

cd example/rag_learning
uv run main.py

RAGデータの登録について
exampler/rag_learning/setup.pydocフォルダ内の昔話をベクトルデータ化してデータベースに登録するために、以下の処理を行っています。

・テーブルの作成
・物語のチャンクに分割(150文字で分割、オーバーラップ75文字)
・チャンクのベクトル化(bge-m3を使用)
・テーブルへ登録

実際に検索する

初期登録が完了すると、以下の4つの方法でRAG検索できます。

キーワード検索  example/rag_learing/search_keyword.py
 キーワードに含まれる文字を直接検索します。
ベクトル検索  example/rag_learing/search_vector.py
 キーワードをベクトル化し、検索します。
ハイブリッド検索  example/rag_learing/search_hybrid.py
 ベクトル検索とキーワード検索を組み合わせ
 重み:ベクトル50% + キーワード50%)
ハイブリッド検索(重みづけ)example/rag_learing/search_optimal.py
 ベクトル検索とキーワード検索を組み合わせ
 重み:ベクトル20% + キーワード80%)   

今回は、 example/rag_learing/utils.pyのextract_keywordsで入力文字をキーワードに分割してキーワード判定を行っています。

ハイブリッド検索(重みづけ)時の検索例)
各検索法を選んだあと、入力文字をいれて情報を検索することができます
検索結果をもとにLLMを利用して要約することも可能です(ローカルだと遅いので任意としてます)

検索方式を選択してください (1-4)、または 'q' で終了: 4

検索クエリを入力してください: 蟹が死ぬ物語は?

🔍 ベクトル検索: 「蟹が死ぬ物語は?」
   クエリをベクトル化中...
   取得件数: 5件
   [1] ❌ 猿蟹合戦 (類似度: 0.623)
   [2] ❌ 猿蟹合戦 (類似度: 0.610)
   [3] ❌ 猿蟹合戦 (類似度: 0.567)
   [4] ❌ 猿蟹合戦 (類似度: 0.548)
   [5] ❌ 猿蟹合戦 (類似度: 0.545)

============================================================
📋 検索結果(ベクトル検索):
------------------------------------------------------------

[1] 猿蟹合戦 (sarukani.txt)
    スコア: 0.623
    ました。すると、猿は「ほら、やるよ」と言って、まだ青くて硬い柿を蟹に投げつけました。青い柿は蟹の甲羅に当たって、とても痛かったです。「痛い!ひどいよ! 」と蟹が叫びました。猿は「わはは」と笑いながら、...

[2] 猿蟹合戦 (sarukani.txt)
    スコア: 0.610
    れてしまいました。可哀想な蟹は、そのまま死んでしまったのです。蟹には小さな子供たちがいました。子蟹たちは泣きながら、母蟹の仇を討つことを決心しました。 「お母さんの仇を取ろう」と子蟹たちは話し合いまし...

[3] 猿蟹合戦 (sarukani.txt)
    スコア: 0.567
    昔々、ある森に一匹の蟹が住んでいました。蟹は毎日川で魚を捕まえたり、森を歩き回ったりして暮らしていました。ある日、蟹は道端で大きなおにぎりを見つけました。「これは運がいいぞ」と蟹は喜んで、おにぎりを拾...

[4] 猿蟹合戦 (sarukani.txt)
    スコア: 0.548
    になった猿が土間に出ると、牛の糞で滑って転んでしまいました。転んだ猿の上に、天井から蜂が降りてきて、何度も何度も刺しました。「痛い!痛い!許してくれ! 」と猿は泣き叫びました。最後に、屋根の上から臼が...

[5] 猿蟹合戦 (sarukani.txt)
    スコア: 0.545
    いました。蟹は「本当かい?ありがとう」と喜びました。猿は素早く木に登りました。そして、木の上で甘く熟した柿を見つけると、自分でどんどん食べ始めました。 「美味しい、美味しい」と猿は夢中で食べました。下...

題材が悪いせいか、鬼が島と、竜宮城のベクトル差が少なく、結果が振るわなくていろいろ苦労しました・・

まとめ

ローカルLLMでも、動作するものが驚くほど手軽に組むことができました
サンプルのRAG検索の処理は、ほぼ基本的な動作のみです。
キーワード検索や、スコアのマッチ度などを自分なりに変えていくと理解も深まるかなと思います。

EmbeddingやLLMのモデルを変えてみるのも良いですね。
Embeddingモデルでも検索の精度が大きく結果が変わるのが面白かったです。
ぜひチャレンジしてみてください!

著者 : 田中 雅也 (痛快株式会社)
痛快師として日夜よくわからない活動をしています。


痛快技術株式会社について

痛快技術株式会社 : 

当社は、技術の力で社会の問題を解決することを目的として設立されました。私たちは、技術の楽しさを広め、社会のニーズに合わせた製品を開発し、幸せな未来を実現することを使命としています。

いいなと思ったら応援しよう!