目次

Ollamaのテキスト埋め込みとRAG入門:ドキュメントに基づいてモデルに回答させる

言語モデルは、あなたの会社の規則、製品マニュアル、個人のメモを知りません。**RAG(Retrieval-Augmented Generation、検索拡張生成)**では、まずドキュメントから質問に関連する段落を見つけ、その段落と質問を一緒にモデルへ渡して、資料に基づいて回答させます。

質問 ──▶ 最も関連するドキュメントの段落を探す ──▶ 「以下の資料に基づいて回答してください:……」 ──▶ モデル ──▶ 回答

「関連する段落」を見つける鍵となる技術が、テキスト埋め込み(embedding)です。

テキスト埋め込みとは

埋め込みモデルは、テキストを数値の列(ベクトル)に変換します。意味が近いテキストは、ベクトルも近くなります。使われている単語がまったく異なっていても同じです。

  • 「休暇を申請するにはどうすればよいですか?」
  • 「休みを取るにはどう申し出ればよいですか?」

この2つのベクトルは非常に近くなるため、キーワード検索ではできない「意味検索」が可能になります。

埋め込みモデルをダウンロードする

ollama pull bge-m3            # 多言語対応、日本語を含むアジア言語に強い
ollama pull nomic-embed-text  # 軽量、主に英語向け

埋め込みモデルはチャットには使えず、ベクトルの生成専用です。

埋め込みを生成する

curl http://localhost:11434/api/embed -d '{
  "model": "bge-m3",
  "input": ["休暇を申請するにはどうすればよいですか?", "休みを取るにはどう申し出ればよいですか?"]
}'
import ollama

result = ollama.embed(model="bge-m3", input=["休暇を申請するにはどうすればよいですか?", "休みを取るにはどう申し出ればよいですか?"])
vectors = result.embeddings
print(len(vectors), len(vectors[0]))  # 2件、それぞれ1024次元

input には単一の文字列か文字列のリストを渡せます。1件ずつ送るより、まとめて処理するほうがずっと速くなります。

類似度を計算する

最もよく使われるのはコサイン類似度です。値は -1 から 1 の範囲で、1に近いほど類似しています。

import math


def cosine(a, b):
    dot = sum(x * y for x, y in zip(a, b))
    return dot / (math.sqrt(sum(x * x for x in a)) * math.sqrt(sum(y * y for y in b)))


print(cosine(vectors[0], vectors[1]))

実装:ローカルナレッジベースのQ&A

会社の規則をまとめた handbook.txt があるとします。プログラム全体は次のとおりです。

import math
import ollama

EMBED_MODEL = "bge-m3"
CHAT_MODEL = "qwen3:8b"


def cosine(a, b):
    dot = sum(x * y for x, y in zip(a, b))
    return dot / (math.sqrt(sum(x * x for x in a)) * math.sqrt(sum(y * y for y in b)))


# 1. ドキュメントを読み込み、段落に分割する(空行で区切る)
text = open("handbook.txt", encoding="utf-8").read()
chunks = [c.strip() for c in text.split("\n\n") if c.strip()]

# 2. 各段落の埋め込みを生成する(1回だけ実行し、保存して再利用できる)
chunk_vectors = ollama.embed(model=EMBED_MODEL, input=chunks).embeddings


def ask(question: str, top_k: int = 3) -> str:
    # 3. 質問に最も近い段落を見つける
    q_vec = ollama.embed(model=EMBED_MODEL, input=question).embeddings[0]
    ranked = sorted(zip(chunks, chunk_vectors), key=lambda p: cosine(q_vec, p[1]), reverse=True)
    context = "\n\n".join(c for c, _ in ranked[:top_k])

    # 4. 段落と質問を一緒にモデルへ渡す
    response = ollama.chat(
        model=CHAT_MODEL,
        messages=[
            {
                "role": "system",
                "content": "あなたは社内アシスタントです。提供された資料だけに基づいて、日本語で回答してください。"
                "資料に答えがない場合は、「資料に関連情報がありません」と回答し、推測しないでください。",
            },
            {"role": "user", "content": f"資料:\n{context}\n\n質問:{question}"},
        ],
        options={"temperature": 0.2},
    )
    return response.message.content


while True:
    q = input("\n質問:").strip()
    if not q:
        break
    print(ask(q))

これで、最も基本的ながら実用可能なRAGシステムができました。すべてのデータはローカルに保存されます。

改善方法

チャンク分割(chunking)

チャンクが大きすぎると無関係な内容が混ざり、小さすぎると文脈が失われます。一般的な方法は次のとおりです。

  • 1つのチャンクを約300~800文字にし、隣接するチャンクを50~100文字重ねます。
  • 固定文字数で分割するより、見出しや段落などドキュメントの構成に沿って分割します。
  • 各チャンクの先頭に、該当する見出し(例:「第3章 休暇規定 > 年次有給休暇」)を加えます。

ベクトルデータベースを使う

段落が多い場合、毎回すべてのベクトルと照合すると時間がかかり、再起動するたびに再計算が必要です。ベクトルデータベースに保存して検索する方法に切り替えられます。

ツール特徴
ChromaPythonで最も手軽に始められ、ローカルフォルダーに保存できます
SQLite + sqlite-vec単一ファイルで、小規模なプロジェクトに適しています
PostgreSQL + pgvectorPostgreSQLをすでに使っている場合の第一候補です
Qdrant、Milvus大量のデータや独立したサービス向けです

ハイブリッド検索

意味検索は、製品型番や人名などの完全一致検索が苦手です。キーワード検索(BM25)と意味検索を組み合わせると、通常はより良い結果が得られます。

コンテキスト長に注意する

複数の段落をプロンプトに入れるときは、全体の長さがモデルのコンテキスト長を超えないようにします。超えると、前のほうの内容が切り捨てられます。必要に応じて num_ctx を大きくしてください。環境変数と設定を参照してください。

自分でコードを書きたくない場合

Open WebUIにはドキュメントのアップロードとナレッジベース機能が組み込まれており、PDFやWordファイルを直接アップロードして、ドキュメントに基づいてモデルに回答させることができます。

更新日 2026年10月12日