「OllamaでRAGパイプラインを試したが回答がズレることが多く、チャンクサイズやインデックス設計をどう改善すればいいかわからない」
そんな悩みを抱えるPythonが書けるインフラエンジニア・社内AI担当者は少なくない。この記事では、ドキュメント検索に特化したPythonフレームワーク「LlamaIndex」をOllamaと組み合わせる方法を解説する。
インストールから社内文書のインデックス化、QueryEngineを使った質問応答の実装、チャンク設定による精度向上、インデックスの永続化と差分更新まで、Ubuntu Server上で実機確認した手順を順を追って説明する。LangChainとの使い分け方やよくあるエラーの対処法も取り上げる。
この記事のポイント
・pip install llama-index-llms-ollama llama-index-embeddings-ollama の2パッケージでOllamaと接続できる
・SimpleDirectoryReaderでPDF・テキスト・Markdownを一括取り込み、VectorStoreIndexで検索可能にする
・SentenceSplitterのchunk_size(256~1024)とchunk_overlapを調整するとRAGの回答品質が大きく改善する
・StorageContextでインデックスをディスクに永続化し、再起動後も再Embedding不要にできる
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
LlamaIndexとは何か・LangChainとの役割の違い
LlamaIndexはデータの読み込み(Load)・インデックス化(Index)・クエリ(Query)を3ステップで扱えるPythonフレームワークだ。2022年末にGPT Indexとして公開され、2023年にLlamaIndexへ改名された。現在はllama-indexパッケージとして活発に開発が続いており、Ollama・ローカルLLMとの連携も公式にサポートされている。LangChainと比較されることが多いが、重心が異なる。LangChainは「エージェント・チェーン・ツール連携・メモリ管理」など幅広い機能をカバーするオーケストレーターだ。複数のLLMやAPIを組み合わせた複雑なアプリを作るには便利だが、覚えるべき概念が多く設定が複雑になりやすい。チェーン定義のネストが深くなり、半年後に自分で読み返せないコードになることが現場でよく起きている。
一方LlamaIndexは「大量のドキュメントをLLMで検索・要約する」用途に絞られており、RAGパイプラインの構築と精度チューニングに向いている。3つの主要コンポーネント(Data Connector・Index・Query Engine)だけ把握すれば動かせるため、Pythonをある程度書けるエンジニアであれば比較的短時間で稼働できる。
知人のインフラエンジニアが社内の技術手順書を検索できるシステムを作った際、LangChainで実装し始めたものの、チェーンの設定が複雑になって保守担当者が付けなくなり、最終的にLlamaIndexに移行したと話していた。ドキュメントRAGが主目的ならLlamaIndexから始めて、エージェント機能が必要になったタイミングでLangChainを検討するのが現実的な流れだと思う。
用途別のフレームワーク選択方針をまとめると次のようになる。
・ドキュメント検索・FAQ回答・社内文書Q&A → LlamaIndex
・エージェント・複数ツール連携・動的チェーン処理 → LangChain
・ノーコードでGUI操作したい → Dify・Flowise
本記事ではPythonが書ける前提で、LlamaIndexをコードから操作する手順を解説する。ノーコード連携のDifyやFlowiseは別記事で扱っている。
前提環境とLlamaIndexのインストール
1. 前提条件を確認する
本記事の手順は次の環境を前提とする。・Ubuntu Server 22.04 LTS または 24.04 LTS
・Python 3.10 以上(3.11 または 3.12 推奨)
・Ollamaがインストール済みでsystemdサービスとして起動中
・OllamaにLLMモデルとEmbeddingモデルがpull済み
Ollamaのインストールとsystemdによる常時起動設定については、Ubuntu ServerでローカルLLMを構築する方法|Ollamaで機密データを外に出さず業務AIを動かす完全ガイドで詳しく解説している。先にそちらを読んでおくとスムーズだ。
Ollamaが正常に動いているかを確認してから次に進む。
# Ollamaサービスの状態確認 $ systemctl status ollama * ollama.service - Ollama Service Loaded: loaded (/etc/systemd/system/ollama.service; enabled; vendor preset: enabled) Active: active (running) since Sun 2026-07-19 09:00:00 JST; 1h 30min ago # pull済みモデルの一覧確認 $ ollama list NAME ID SIZE MODIFIED llama3.3:70b-instruct-q4_0 a1b2c3d4e5f6 42 GB 3 hours ago nomic-embed-text:latest b2c3d4e5f6a7 274 MB 3 hours ago
2. Python仮想環境を作成する
LlamaIndexはバージョンアップが頻繁でパッケージ依存関係が変わりやすい。システムPythonへの直接インストールは環境を汚染するリスクがあるため、プロジェクト専用のvenvを用意するのが安全だ。$ python3 -m venv ~/llamaindex-env $ source ~/llamaindex-env/bin/activate (llamaindex-env) $ python --version Python 3.11.9
3. LlamaIndexとOllamaプロバイダーをインストールする
LlamaIndex 0.10以降はコアパッケージと統合パッケージが分離されている。Ollama連携には専用の2パッケージを追加インストールする。PDFを処理する場合はpypdfも合わせてインストールする。(llamaindex-env) $ pip install \ llama-index \ llama-index-llms-ollama \ llama-index-embeddings-ollama \ pypdf # pypdfはPDF読み込み用。テキスト・Markdownのみであれば不要 (llamaindex-env) $ pip show llama-index | grep Version Version: 0.12.3 (llamaindex-env) $ pip show llama-index-llms-ollama | grep Version Version: 0.5.0
OllamaとLlamaIndexを接続する基本設定
1. SettingsでLLMとEmbeddingをOllamaに切り替える
LlamaIndexはデフォルトでOpenAIのAPIを呼び出そうとする。環境変数 `OPENAI_API_KEY` がない状態で実行するとエラーになる。Settings クラスを使ってプロバイダーをOllamaに明示的に指定することで、インターネット不要のローカル処理に切り替えられる。from llama_index.core import Settings from llama_index.llms.ollama import Ollama from llama_index.embeddings.ollama import OllamaEmbedding # LLMをOllamaに設定 Settings.llm = Ollama( model="llama3.3:70b-instruct-q4_0", base_url="http://localhost:11434", request_timeout=300.0, # 大型モデルは最初のリクエストに時間がかかる ) # Embeddingモデルも全てローカルで処理する Settings.embed_model = OllamaEmbedding( model_name="nomic-embed-text", base_url="http://localhost:11434", )
EmbeddingモデルにはOllamaで利用できる `nomic-embed-text` が実績がある。768次元のベクトルを高速に生成でき、日本語テキストにも対応している。まだpullしていない場合はこの時点でpullする。
$ ollama pull nomic-embed-text pulling manifest pulling 970aa74c0a90... 100% ▕████████████████████▏ 274 MB pulling c71d239df917... 100% ▕████████████████████▏ 11 KB verifying sha256 digest writing manifest success
2. 接続テストで動作を確認する
本格的なインデックス作成の前に、Settingsが正しく機能しているかを簡単なテストで確認する。# test_connection.py として保存して実行する from llama_index.core import Settings from llama_index.llms.ollama import Ollama from llama_index.embeddings.ollama import OllamaEmbedding Settings.llm = Ollama( model="llama3.3:70b-instruct-q4_0", base_url="http://localhost:11434", request_timeout=300.0, ) Settings.embed_model = OllamaEmbedding( model_name="nomic-embed-text", base_url="http://localhost:11434", ) # LLMテスト resp = Settings.llm.complete("Linuxでディスク使用量を確認するコマンドは?") print("LLMテスト:", resp.text[:80]) # Embeddingテスト emb = Settings.embed_model.get_text_embedding("テスト文字列") print(f"Embeddingテスト: {len(emb)}次元のベクトルを取得")
$ python test_connection.py LLMテスト: df コマンドを使います。df -h で人間が読みやすい形式で表示できます。 Embeddingテスト: 768次元のベクトルを取得
・`Connection refused`: Ollamaサービスが起動していない → `systemctl start ollama`
・`model not found`: モデル名のタイプミスまたはpull未実施 → `ollama list` で確認
・`ReadTimeout`: タイムアウト値を600.0に延長して再実行
SimpleDirectoryReaderで社内文書をインデックス化する
1. ドキュメントディレクトリを準備する
LlamaIndexの `SimpleDirectoryReader` は指定ディレクトリ内のファイルを自動で検出して読み込む。PDF・テキスト・HTML・Markdown・CSV・Excelなど多数のフォーマットに対応しており、社内に散在するドキュメントをまとめて処理できる。社内でChatGPTが使えない環境で機密文書をAIで検索できるようにするのがローカルRAGの主要なユースケースだ。社内でChatGPTが使えないときの代替手段|機密データを守るローカルLLMという選択肢でも解説しているが、データを外部に送らないという点がビジネス要件として重視される場面が増えている。
まず検索対象にしたい社内文書を格納するディレクトリを作り、ファイルをコピーする。
$ mkdir -p ~/llamaindex-docs/manual $ ls ~/llamaindex-docs/manual/ server-setup-guide.pdf network-policy.txt operation-manual.md security-checklist.pdf incident-procedure.txt
2. ドキュメントを読み込んでVectorStoreIndexを作成する
文書の読み込みからインデックス作成までは次のコードで完結する。from llama_index.core import SimpleDirectoryReader, VectorStoreIndex # ドキュメントを一括読み込み(サブディレクトリも対象) documents = SimpleDirectoryReader( "~/llamaindex-docs/manual", recursive=True, ).load_data() print(f"読み込み完了: {len(documents)} ドキュメント") # VectorStoreIndexを作成(Embeddingが自動実行される) index = VectorStoreIndex.from_documents( documents, show_progress=True, ) print("インデックス作成完了")
3. 特定のファイル形式・除外パターンを指定する
社外向け資料や個人情報が含まれるファイルが混在する場合は、拡張子で対象を絞り込む。documents = SimpleDirectoryReader( "~/llamaindex-docs/manual", required_exts=[".pdf", ".txt", ".md"], # 対象拡張子を限定 exclude=["*_draft.md", "personal_*"], # 除外パターン recursive=True, ).load_data() print(f"フィルタ後: {len(documents)} ドキュメント")
VectorStoreIndexとQueryEngineで質問応答を実装する
1. QueryEngineを生成して質問する
インデックスが作成できたら `as_query_engine()` でQueryEngineを生成し、自然言語で質問できる状態にする。# QueryEngineを作成 query_engine = index.as_query_engine( similarity_top_k=3, # 検索して取得するチャンク数 ) # 質問する response = query_engine.query("SSHのデフォルトポート番号はいくつで、変更手順はどこに書いていますか?") print(response.response) # どのドキュメントを参照したか確認する print("\n--- 参照ソース ---") for node in response.source_nodes: print(f"ファイル: {node.metadata.get('file_name', 'unknown')}") print(f"類似スコア: {node.score:.4f}") print(f"テキスト冒頭: {node.text[:80]}...")
2. ストリーミングで逐次出力する
質問から回答が返るまでの待ち時間が長いと操作感が重くなる。`streaming=True` を指定すると生成しながら出力できる。query_engine = index.as_query_engine( similarity_top_k=3, streaming=True, ) response = query_engine.query("インシデント対応の初動手順を教えてください") response.print_response_stream() # トークンを逐次出力しながら最後まで生成する
3. RetrieverQueryEngineで検索と回答生成を個別設定する
`as_query_engine()` は手軽だが細かい設定ができない。`RetrieverQueryEngine` を使うと検索フェーズと回答生成フェーズをそれぞれ独立してカスタマイズできる。from llama_index.core.query_engine import RetrieverQueryEngine from llama_index.core.retrievers import VectorIndexRetriever from llama_index.core.response_synthesizers import get_response_synthesizer retriever = VectorIndexRetriever( index=index, similarity_top_k=5, ) response_synthesizer = get_response_synthesizer( response_mode="tree_summarize", streaming=True, ) query_engine = RetrieverQueryEngine( retriever=retriever, response_synthesizer=response_synthesizer, )
・`refine`: 取得チャンクを順番に参照して回答を逐次洗練する。精度は高いがトークン消費が多い
・`tree_summarize`: チャンクを木構造で階層的に要約する。文書量が多い場合に安定しやすい
・`compact`: コンテキストを圧縮してトークン消費を抑える。速度重視の場合に向いている
社内手順書への質問応答なら `tree_summarize` が安定した回答を返しやすい。最初はこれを選んでおいて、回答品質の問題が出たら `refine` に切り替えると比較しやすい。
Node Parserとチャンク設定でRAG精度を高める
RAGの回答品質を左右する最大の要素はチャンク設定だ。デフォルトのままでは1チャンクが大きすぎて無関係な情報を含んでしまうことがある。逆に小さすぎると重要な文脈が切れてしまう。精度が出ない場合、まずここを疑う。1. SentenceSplitterでチャンクサイズを調整する
from llama_index.core.node_parser import SentenceSplitter # Settings経由でグローバルに設定する Settings.text_splitter = SentenceSplitter( chunk_size=512, # 1チャンクのトークン数 chunk_overlap=64, # 前後チャンクとの重複トークン数(コンテキスト連続性を保つ) ) # 設定後に documents をインデックス化すると新しい設定が適用される index = VectorStoreIndex.from_documents(documents, show_progress=True)
・手順書・FAQ(短い段落・箇条書きが多い文書): 256~512 トークン
・技術仕様書・報告書(長い説明が続く文書): 512~1024 トークン
・chunk_overlapはchunk_sizeの10~15%を基準にする
大きいチャンクは文脈を保ちやすいが無関係な情報も含みやすい。小さいチャンクは精度よく絞り込めるが文脈が切れるリスクが上がる。まず512で試して、`source_nodes` のスコアと回答内容を見ながら調整するのが実践的なアプローチだ。
どのモデルをEmbeddingとLLMに使うかによっても精度が変わる。詳しくはローカルLLMのモデルを比較する方法|Llama3.3・Mistral・Gemma・Phi-4をUbuntuで使い分けるポイントを参照してほしい。Embeddingには `nomic-embed-text`、RAGの回答生成には `llama3.3:70b-instruct-q4_0` か `mistral:7b-instruct-q8_0` が実績がある。
2. SemanticSplitterNodeParserで意味の境界で分割する
固定サイズではなくテキストの意味が変わる場所でチャンクを切る方法もある。手順書のステップ境界や段落の切り替わりを自然に検出できる。from llama_index.core.node_parser import SemanticSplitterNodeParser splitter = SemanticSplitterNodeParser( buffer_size=1, breakpoint_percentile_threshold=95, embed_model=Settings.embed_model, ) # Splitterを明示的に使ってノードに変換してからインデックス化 nodes = splitter.get_nodes_from_documents(documents) index = VectorStoreIndex(nodes)
インデックスの永続化と差分更新で本番運用に備える
毎回Embeddingを再計算するのは時間とリソースの無駄だ。一度作成したインデックスをディスクに保存しておけば、次回起動時は数秒でロードできる。本番運用ではこの永続化が必須になる。1. StorageContextでインデックスをディスクに保存する
from llama_index.core import StorageContext, load_index_from_storage import os PERSIST_DIR = "./storage" if not os.path.exists(PERSIST_DIR): # 初回: ドキュメントを読み込んでインデックスを作成し保存 documents = SimpleDirectoryReader("~/llamaindex-docs/manual").load_data() index = VectorStoreIndex.from_documents(documents, show_progress=True) index.storage_context.persist(persist_dir=PERSIST_DIR) print("インデックスを保存しました") else: # 2回目以降: 保存済みインデックスをロード(数秒で完了) storage_context = StorageContext.from_defaults(persist_dir=PERSIST_DIR) index = load_index_from_storage(storage_context) print("保存済みインデックスをロードしました")
2. 差分更新でインデックスを最新に保つ
社内文書は日々更新される。変更があるたびに全文書を再インデックスするのは非効率だ。新しいドキュメントだけをインデックスに追加する差分更新を実装する。# daily_update.py として保存してcronから実行する from llama_index.core import SimpleDirectoryReader, StorageContext, load_index_from_storage import os PERSIST_DIR = "./storage" UPDATE_DIR = os.path.expanduser("~/llamaindex-docs/updates") # 既存インデックスをロード storage_context = StorageContext.from_defaults(persist_dir=PERSIST_DIR) index = load_index_from_storage(storage_context) # 差分ドキュメントを追加 if os.path.exists(UPDATE_DIR) and os.listdir(UPDATE_DIR): new_docs = SimpleDirectoryReader(UPDATE_DIR).load_data() for doc in new_docs: index.insert(doc) print(f"{len(new_docs)} ドキュメントを追加しました") # 更新後に再保存 index.storage_context.persist(persist_dir=PERSIST_DIR) print("インデックスを更新・保存しました")
# cronに登録する例(毎日午前3時に実行) $ crontab -e 0 3 * * * /home/ubuntu/llamaindex-env/bin/python /home/ubuntu/daily_update.py >> /var/log/llamaindex-update.log 2>&1 # 翌日以降のログ確認 $ tail -20 /var/log/llamaindex-update.log 3 ドキュメントを追加しました インデックスを更新・保存しました
よくあるエラーとトラブル対処
エラー1: ReadTimeout(接続タイムアウト)
大型モデルへの初回クエリでタイムアウトエラーが発生することがある。70Bクラスのモデルは最初のリクエストでモデルをメモリに展開する処理が走るため数十秒かかる。# エラー例 httpx.ReadTimeout: timed out reading response # 対処: request_timeoutを延長する Settings.llm = Ollama( model="llama3.3:70b-instruct-q4_0", base_url="http://localhost:11434", request_timeout=600.0, # 10分に延長 )
エラー2: model not found(Embeddingモデル未インストール)
# エラー例 ollama._types.ResponseError: model 'nomic-embed-text' not found, try pulling it first # 対処: モデルをpullしてから再実行する $ ollama pull nomic-embed-text $ ollama list | grep nomic nomic-embed-text:latest b2c3d4e5f6a7 274 MB 5 minutes ago
エラー3: MemoryError(大量文書でメモリ不足)
数百MB以上のPDFや数千ファイルを一度に処理するとメモリが枯渇する場合がある。バッチ処理に分割して対応する。import os from llama_index.core import SimpleDirectoryReader, VectorStoreIndex doc_dir = os.path.expanduser("~/llamaindex-docs/manual") files = [f for f in os.listdir(doc_dir) if os.path.isfile(os.path.join(doc_dir, f))] BATCH_SIZE = 10 index = None for i in range(0, len(files), BATCH_SIZE): batch_paths = [os.path.join(doc_dir, f) for f in files[i:i+BATCH_SIZE]] docs = SimpleDirectoryReader(input_files=batch_paths).load_data() if index is None: index = VectorStoreIndex.from_documents(docs) else: for doc in docs: index.insert(doc) print(f"バッチ {i//BATCH_SIZE + 1} 完了: {len(batch_paths)} ファイル処理") index.storage_context.persist(persist_dir="./storage")
エラー4: 回答が的外れ・文書を参照していない
回答品質の問題は次の手順でトラブルシュートする。・`similarity_top_k` を3から5~10に増やす(関連チャンクをより多く取得する)
・`response_mode` を `tree_summarize` に変更する
・チャンクサイズが大きすぎる(1024以上)場合は512以下に下げて再インデックスする
・日本語文書なら `nomic-embed-text` の代わりに `mxbai-embed-large` も試す
`source_nodes` の類似スコアを確認することも診断の第一歩だ。スコアが0.5未満のチャンクばかりが返っている場合は、文書の内容と質問の乖離が大きい可能性がある。質問の言い回しを変えるか、文書の前処理(HTMLタグ除去・余分な記号の削除)を検討する。注意: チャンクサイズの変更後は必ず再インデックスが必要だ。永続化ストレージが残っている場合は `./storage` ディレクトリを一度削除してから再実行する。
まとめ
OllamaとLlamaIndexを組み合わせると、社内の文書を完全ローカルで横断検索できるRAGシステムを比較的シンプルなコードで構築できる。クラウドにデータを送らずに機密文書を活用できる点が最大のメリットだ。RAGの精度はチャンク設定とモデル選択の組み合わせで大きく変わるため、最初はデフォルトで動かしてから調整するという進め方が実践的だ。| 操作 | コマンド・設定値 | ポイント |
|---|---|---|
| インストール | pip install llama-index llama-index-llms-ollama llama-index-embeddings-ollama | venv内で実行する |
| LLM設定 | Ollama(model="llama3.3:70b-instruct-q4_0", request_timeout=300.0) | タイムアウト延長が重要 |
| Embedding設定 | OllamaEmbedding(model_name="nomic-embed-text") | 事前に ollama pull nomic-embed-text |
| チャンク設定 | SentenceSplitter(chunk_size=512, chunk_overlap=64) | 文書種別に応じて256~1024で調整 |
| 質問応答実行 | index.as_query_engine(similarity_top_k=3).query("質問文") | source_nodesで参照元を確認できる |
| インデックス永続化 | index.storage_context.persist(persist_dir="./storage") | 2回目以降はload_index_from_storage()で即時ロード |
| 差分更新 | index.insert(doc) の後に storage_context.persist() を実行 | cronで毎日自動更新するのが実用的 |
20年以上Linuxサーバーに携わってきた経験から言うと、複雑なシステムは必ず保守の壁にぶつかる。シンプルさを保てる設計を最初から選ぶことが、システムを長続きさせる最大のコツだ。
ローカルRAGをGPU実機で動かす2日間ハンズオンを体験する
OllamaとLlamaIndexの連携をGPUサーバー上で実際に動かしながら学びたい方向けに、「ローカルAIマスターセミナー」を開催しています。チャンク設定の調整やインデックス永続化も含め、実機で手を動かしながら習得できます。
少人数(最大8名)ZOOMハンズオン形式で実施しています。
・Ubuntu ServerでローカルLLMを構築する方法|Ollamaで機密データを外に出さず業務AIを動かす完全ガイド
・社内でChatGPTが使えないときの代替手段|機密データを守るローカルLLMという選択肢
・ローカルLLMのモデルを比較する方法|Llama3.3・Mistral・Gemma・Phi-4をUbuntuで使い分けるポイント
3,100名以上が実践した「型」を無料で公開中
プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。
登録10秒/合わなければ解除3秒 / 詳細はこちら

無料メルマガで学習を続ける
Linuxの実践スキルをメールで毎週お届け。
登録は1分、解除もいつでも可。
登録無料・いつでも解除できます