目次
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章 休暇規定 > 年次有給休暇」)を加えます。
ベクトルデータベースを使う
段落が多い場合、毎回すべてのベクトルと照合すると時間がかかり、再起動するたびに再計算が必要です。ベクトルデータベースに保存して検索する方法に切り替えられます。
| ツール | 特徴 |
|---|---|
| Chroma | Pythonで最も手軽に始められ、ローカルフォルダーに保存できます |
| SQLite + sqlite-vec | 単一ファイルで、小規模なプロジェクトに適しています |
| PostgreSQL + pgvector | PostgreSQLをすでに使っている場合の第一候補です |
| Qdrant、Milvus | 大量のデータや独立したサービス向けです |
ハイブリッド検索
意味検索は、製品型番や人名などの完全一致検索が苦手です。キーワード検索(BM25)と意味検索を組み合わせると、通常はより良い結果が得られます。
コンテキスト長に注意する
複数の段落をプロンプトに入れるときは、全体の長さがモデルのコンテキスト長を超えないようにします。超えると、前のほうの内容が切り捨てられます。必要に応じて num_ctx を大きくしてください。環境変数と設定を参照してください。
自分でコードを書きたくない場合
Open WebUIにはドキュメントのアップロードとナレッジベース機能が組み込まれており、PDFやWordファイルを直接アップロードして、ドキュメントに基づいてモデルに回答させることができます。
更新日 2026年10月12日