LlamaIndex + pgvector:日本語・タイ語ビジネス文書に対応したRAGの本番運用
RAGのデモはたいていうまく動く。しかし、本番環境に展開されたRAGシステムの多くは失敗する——静かに、高コストで、デバッグの難しい形で。
simpliDoc(Simplicoが開発した多言語対応AIドキュメントインテリジェンス基盤)の構築を通じ、私たちはプロトタイプと、タイ語・日本語・英語の企業文書を同時処理できる本番システムとの違いを体験しました。本記事では、実際にデプロイした内容を共有します:技術スタック、テストを経て採用した設定値、遭遇した障害パターン、そしてその解決策です。
このスタックを選んだ理由
SimplicoではLlamaIndexをオーケストレーション層、pgvector(PostgreSQL拡張)をベクトルストアとして採用しています。他の選択肢ではなく、この2つを組み合わせた理由は以下のとおりです。
- LlamaIndexは多言語埋め込みをネイティブサポートし、トークン化の挙動が根本的に異なるタイ語・日本語・英語を横断するチャンキング戦略を適切に処理できます。
- pgvectorはPostgreSQLの内部で動作するため、ベクトルデータをビジネスデータと同じデータベースに格納できます。追加インフラ不要、同期の複雑さなし、別サービスの運用負担なし。個人情報保護法(PIPL)やMETIのAIガバナンスガイドラインに基づき厳格なデータレジデンシーを求める日本企業にとって、すべてをオンプレミスの単一Postgresインスタンスに収めることは、コンプライアンス上の大きな優位性です。
システムアーキテクチャ
flowchart TD
A["ドキュメントアップロード\n(PDF / DOCX / TXT)"] --> B["LlamaIndex\nDocument parser"]
B --> C["言語検出\n(langdetect)"]
C --> D["multilingual-e5-large\n埋め込みモデル"]
D --> E["pgvector\n(PostgreSQL)"]
F["ユーザーの質問"] --> G["FastAPI\nRAGエンドポイント"]
G --> D
G --> E
E --> H["Top-k検索\n(コサイン類似度)"]
H --> I["Claude API\n回答生成"]
I --> J["ストリーミングレスポンス\n(SSE)"]
ステップ1:PostgreSQL + pgvector のセットアップ
-- pgvector拡張を有効化
CREATE EXTENSION IF NOT EXISTS vector;
-- ドキュメントチャンクテーブル
CREATE TABLE document_chunks (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
doc_id UUID NOT NULL,
chunk_index INTEGER NOT NULL,
content TEXT NOT NULL,
language VARCHAR(10), -- 'th', 'ja', 'zh', 'en'
embedding VECTOR(1024), -- multilingual-e5-largeの出力次元数
metadata JSONB,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- 高速近似最近傍探索のためのHNSWインデックス
-- 50k件超では必須:なければクエリが~8msから4秒超に
CREATE INDEX ON document_chunks
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
本番運用のヒント: HNSWインデックスはリリース前に作成してください。後から追加するのは避けるべきです。私たちは80kチャンクの状態でリリース後に追加するという失敗をしました。インデックス構築に4時間かかり、その間クエリのレイテンシが増大しました。
ステップ2:埋め込みモデル
HuggingFaceのmultilingual-e5-large(1024次元)を使用しています。タイ語・日本語・簡体字中国語・英語を単一モデルで処理でき、言語ごとのモデル管理が不要です。
from llama_index.embeddings.huggingface import HuggingFaceEmbedding
embed_model = HuggingFaceEmbedding(
model_name="intfloat/multilingual-e5-large",
max_length=512,
device="cpu", # GPUがあれば利用可。本番はCPU 4コアVMで運用
)
本番環境でのスループット: 4コアCPU VMで約60チャンク/秒。200ページのPDFを約600チャンクに分割した場合、取り込み処理は約10秒で完了します。
ステップ3:チャンキング戦略
ほとんどのRAGプロジェクトが失敗するのはここです。日本語とタイ語には単語境界のスペースがないため、文字数ベースのチャンキングは英語の場合とまったく異なる結果をもたらします。
from llama_index.core.node_parser import SentenceSplitter
splitter = SentenceSplitter(
chunk_size=400, # 文字数(トークン数ではない)
chunk_overlap=80, # 20%のオーバーラップ
paragraph_separator="\n\n",
)
検証結果と400/80を採用した理由:
| チャンクサイズ | オーバーラップ | 結果 |
|---|---|---|
| 256 / 50 | 小さすぎ | タイ語文が途中で分断、コンテキスト欠落 |
| 512 / 100 | 中程度 | 英語は良好、タイ語/日本語は断片化が残る |
| 400 / 80 | 採用値 | 3言語すべてで最良の検索品質 |
| 800 / 160 | 大きすぎ | 検索品質は良好、pgvectorのコサイン類似度の識別力が低下 |
調整前(チャンクサイズ256、日本語契約書テキスト):
取得チャンク: "...利率は年率15パーセントとし、借主が返済を怠った場合に..."
回答: 利率は年15%です。
欠落: 延滞条件が次のチャンクにあり取得されなかった
調整後(チャンクサイズ400、同一文書):
取得チャンク: "...利率は年率15パーセントとし、借主が30日を超えて返済を怠った場合に適用されます..."
回答: 利率は年15%で、返済が30日超延滞した場合に適用されます。
ステップ4:インジェストパイプライン
import asyncio
from llama_index.core import Document, VectorStoreIndex
from llama_index.vector_stores.postgres import PGVectorStore
async def ingest_document(file_path: str, doc_id: str, language: str):
with open(file_path, "rb") as f:
raw_text = extract_text(f) # PDF/DOCXパーサー
doc = Document(
text=raw_text,
metadata={"doc_id": doc_id, "language": language}
)
vector_store = PGVectorStore.from_params(
database="simplidoc",
host="localhost",
port=5432,
user="simplidoc_user",
password=os.environ["DB_PASSWORD"],
table_name="document_chunks",
embed_dim=1024,
)
index = VectorStoreIndex.from_documents(
[doc],
embed_model=embed_model,
transformations=[splitter],
vector_store=vector_store,
)
return index
ステップ5:FastAPI RAGエンドポイント
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
from anthropic import Anthropic
app = FastAPI()
client = Anthropic()
@app.post("/query")
async def query_documents(request: QueryRequest):
query_engine = index.as_query_engine(
similarity_top_k=5,
streaming=True,
)
retrieval = await query_engine.aretrieve(request.query)
context = "\n\n---\n\n".join([node.text for node in retrieval])
system_prompt = """あなたは企業向けビジネス文書のドキュメントアシスタントです。
提供されたコンテキストのみに基づいて質問に回答してください。
コンテキストに答えが含まれていない場合は、明確にその旨を伝えてください。
質問と同じ言語で回答してください。"""
async def stream_response():
with client.messages.stream(
model="claude-sonnet-4-20250514",
max_tokens=1000,
system=system_prompt,
messages=[{
"role": "user",
"content": f"コンテキスト:\n{context}\n\n質問: {request.query}"
}]
) as stream:
for text in stream.text_stream:
yield f"data: {text}\n\n"
return StreamingResponse(stream_response(), media_type="text/event-stream")
本番環境で遭遇した障害パターン
1. 埋め込みモデルのコールドスタート(初回クエリで12秒の遅延)
multilingual-e5-largeモデルは初回使用時にRAMへロードされます。コールドVMでは、各セッションの最初のクエリで12秒の遅延が発生しました。
解決策:起動時にモデルをウォームアップします。
@app.on_event("startup")
async def warm_embedding_model():
_ = embed_model.get_text_embedding("warmup")
2. 日本語PDFのテキスト抽出が文字化け
一部の日本語PDFは非標準のフォントエンコーディングを使用しており、PyPDF2では文字化けが発生しました。日本語文書にはpdfplumberを使用してください。
import pdfplumber
def extract_pdf(path: str) -> str:
with pdfplumber.open(path) as pdf:
return "\n".join(page.extract_text() or "" for page in pdf.pages)
3. 低しきい値でpgvectorが無関係なチャンクを返す
当初はコサイン類似度0.5超のチャンクをすべて返していました。曖昧な質問に対して0.55〜0.65のスコアで無関係なチャンクが含まれることがありました。
解決策:しきい値を引き上げ、リランキングステップを追加します。
retrieval = await query_engine.aretrieve(request.query)
filtered = [n for n in retrieval if n.score >= 0.72]
コストとパフォーマンス
4コア / 8GB RAM の単一VM(タイのクラウドプロバイダーで月額約¥4,500相当)での実績:
| 指標 | 値 |
|---|---|
| 平均クエリレイテンシ(ウォーム時) | 1.8秒(エンドツーエンド) |
| 埋め込みスループット | 約60チャンク/秒 |
| pgvector検索(HNSW、200kチャンク) | 約8ms |
| クエリあたりのClaude APIコスト | 約¥0.18(当社利用量水準) |
| インデックス済みドキュメント数 | タイ語/日本語/英語で12,000件超 |
データレジデンシーについて
個人情報保護法に基づく日本企業のデプロイメントでは:すべての埋め込みとドキュメントコンテンツはPostgreSQLインスタンス内に留まります。multilingual-e5-largeはローカルで動作するため、外部の埋め込みAPIにはドキュメントコンテンツは送信されません。Claude APIに送信されるのは、回答生成のために検索されたコンテキスト(文書全体ではなく)のみです。
METI AIガバナンスガイドラインへの対応として:すべてのAI処理ステップを文書化し、回答生成に使用されたコンテキストをログとして保存することを推奨します。これにより説明可能性の要件を満たすことができます。
より厳格なデータレジデンシーが必要な場合は、回答生成ステップのClaude APIをローカルホストのモデルに置き換えることも可能です。
関連記事
- Building a Modern Cybersecurity Monitoring & Response System
- Understanding Wazuh: Architecture, Use Cases, and Real-World Applications
企業ドキュメント向けの多言語RAGシステムが必要ですか?Simplicoへお問い合わせください——タイ・日本・グローバルのお客様向けに構築実績があります。
Get in Touch with us
Related Posts
- simpliShop:受注生産・多言語対応のタイ向けECプラットフォーム
- ERPプロジェクトが失敗する理由と成功のための実践的アプローチ
- Payment APIにおけるIdempotencyとは何か
- Agentic AI × SOCワークフロー:プレイブックを超えた自律防御【2026年版ガイド】
- SOCをゼロから構築する:Wazuh + IRIS-web 現場レポート
- ECと基幹システムの二重入力をなくす:受注から仕訳までの自動化アーキテクチャ
- SIerのブラックボックスから脱却する:オープンソースで構築する中小企業向けSOCアーキテクチャ
- リサイクル工場管理システム:日本のリサイクル事業者が見えないところで損をしている理由
- エネルギー管理ソフトウェアのROI:電気代を15〜40%削減できる理由
- Wazuh + オープンソースで構築する軽量SOC:実践ガイド(2026年版)
- ECサイトとERPを正しく連携する方法:実践ガイド(2026年版)
- AI コーディングアシスタントが実際に使うツールとは?(Claude Code・Codex CLI・Aider)
- 燃費を本気で改善する:高負荷・低回転走行の物理学
- タイ産ドリアン・青果物デポ向け倉庫管理システム(WMS)— ERP連携・輸出書類自動化
- 現代のドリアン集荷場:手書き台帳をやめて、システムでビジネスを掌握する
- AI System Reverse Engineering:AIでレガシーソフトウェアシステムを理解する(Architecture・Code・Data)
- 人間の優位性:AIが代替できないソフトウェア開発サービス
- ゼロからOCPPへ:ホワイトラベルEV充電プラットフォームの構築
- Wazuh Decoders & Rules: 欠けていたメンタルモデル
- 製造現場向けリアルタイムOEE管理システムの構築













