RAGシステムにおけるコンテキスト検索の課題
エンタープライズ向けのチャットボットやAIアシスタントを構築すること自体は難しくありません。真の課題は回答の精度にあります。大規模言語モデル(LLM)は文章生成に優れている反面、参照可能なファクトチェック用ドキュメントが不足していると、平然と誤情報を作り出してしまう(ハルシネーションを起こす)傾向があります。
その明確な解決策がRAG(Retrieval-Augmented Generation:検索拡張生成)です。関連ドキュメントを検索し、プロンプトとしてLLMにコンテキストを与えます。しかし、適切なコンテキストを過不足なく抽出するのは決して容易ではありません。リトリーバー(検索エンジン層)が誤った情報や不十分な情報を取得してしまうと、後続の処理パイプライン全体が無意味な回答を出力することになります。
AI向けデータ検索における3つのアプローチ
システムエンジニアは通常、次の3つのアプローチを比較・検討します。
- 1. キーワード検索(Keyword Search / BM25):ElasticsearchやPostgreSQLのFull-Text Searchなどでお馴染みの手法です。単語の出現頻度(改良版TF-IDF)に基づいてスコアリングを行います。
- 2. セマンティック・ベクトル検索(Dense Vector Search):テキストを768次元や1536次元のベクトルに変換します。データベースがコサイン類似度や内積(Dot Product)を計算し、キーワードが完全に一致しなくても類義・同義のテキストを抽出します。
- 3. ハイブリッド検索(Hybrid Search)とGenerative Searchの組み合わせ:同一クエリに対してBM25とベクトル検索を並行して実行します。リランキングを行って最も関連性の高いテキストを抽出した後、データベース自身がLLMへコンテキストを直接送信し、その場で回答を生成・要約します。
各ソリューションの実用性比較
キーワード検索(BM25)
BM25は特定・固有の文字列処理に極めて強力です。例えば注文番号DH-2024-9981やエラーコードERR_CONNECTION_RESET、PostgreSQLのような固有名詞を検索する場合、BM25は100%精度の高い結果を返します。
しかし、ユーザーが自然言語で表現した途端に限界が現れます。試しに「メモリオーバーフローの直し方」と検索すると、BM25は文字面が一致しないという理由だけで、「Out of Memory(OOM)障害のトラブルシューティング」と書かれたドキュメントを見落としてしまいます。
セマンティック・ベクトル検索(Dense Vector Search)
ベクトル検索は、BM25の弱点を正確にカバーします。深い意味関係を把握できるため、「年末の賞与」という質問に対して「第4四半期インセンティブ規定」といったドキュメントを容易に関連付けることができます。
しかしながら、ベクトル埋め込み(Embeddings)にはテキストの意味を過度に平滑化(Smoothing)してしまう傾向があります。ユーザーが特定のソフトウェアバージョンv2.14.3-hotfixやIPアドレス192.168.1.105などを厳密に探している場合、ベクトル検索ではその完全一致文字列を含むドキュメントが2ページ目や3ページ目まで沈んでしまうことが珍しくありません。
Weaviateにおけるハイブリッド検索とGenerative Search
これら双方を組み合わせることが、本番環境において最も現実的な選択肢となります。社内の多くのベンチマーク測定において、ハイブリッド検索は単一の手法のみを使用した場合と比べ、Recall@10スコアが15%〜25%向上するという結果が得られています。
Weaviateでは、alpha重みパラメータ(0.0〜1.0)によってこのバランスを調整します。システムログの調査ならalphaを下げてBM25寄りに設定し、社内FAQのQ&Aならalphaを上げてベクトル寄りに調整します。
さらに魅力的なのは、Generative Search機能によってWeaviateを軽量なRAGステーションとして活用できる点です。単一のクエリを発行するだけで、システムが関連検索を行うと同時にLLMを呼び出して回答を要約・生成します。これにより、バックエンドに存在していた何十行もの煩雑なグルーコード(Glue Code)を大幅に削減できます。
Docker上でWeaviateを動かす理由
データ規模が数百万件を超える場合、ベクターデータベースをセルフホストすることが最も手っ取り早いコスト削減策となります。Pineconeなどでクエリ従量課金の高額な請求に頭を悩ませる代わりに、4 vCPU・16GB RAMのVPS上でWeaviateを運用すれば、月額わずか数十ドルにコストを抑えられます。
WeaviateはGo言語で記述されており、メモリ効率が高く、高速なHNSWインデックスをサポートしています。このサービスをDockerコンテナとしてパッケージ化することで、ローカルの開発環境からステージングクラスターまで一貫した環境を即座に構築できます。
実践:DockerとPythonによるWeaviate環境の構築
ステップ1: docker-compose.ymlの作成
プロジェクト用のディレクトリを新規作成します。
mkdir weaviate-docker && cd weaviate-docker
touch docker-compose.yml
docker-compose.ymlを開き、以下の構成を記述します。ここでは自動ベクトル化を行うtext2vec-openaiモジュールと、LLM呼び出しを行うgenerative-openaiモジュールをあらかじめ有効化しておきます。
services:
weaviate:
image: cr.weaviate.io/semitechnologies/weaviate:1.26.1
command:
- --host
- 0.0.0.0
- --port
- '8080'
- --scheme
- http
ports:
- "8080:8080"
- "50051:50051"
restart: on-failure:0
environment:
QUERY_DEFAULTS_LIMIT: 25
AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED: 'true'
PERSISTENCE_DATA_PATH: '/var/lib/weaviate'
DEFAULT_VECTORIZER_MODULE: 'text2vec-openai'
ENABLE_MODULES: 'text2vec-openai,generative-openai'
CLUSTER_HOSTNAME: 'node1'
volumes:
- weaviate_data:/var/lib/weaviate
volumes:
weaviate_data:
バックグラウンドでコンテナを起動します。
docker compose up -d
ポート8080はREST API用、ポート50051はgRPC用として使用されます。サービスが正常に稼働しているかを簡単なcurlコマンドで確認します。
curl -s http://localhost:8080/v1/meta | grep version
ステップ2: Python Client v4のインストール
WeaviateのSDKバージョンv4では、データの読み書き通信が全面的にgRPCプロトコルへと刷新されました。これにより、HTTP RESTを採用していた従来のv3と比較して、バッチインポートの速度が2〜3倍向上しています。
pip install -U "weaviate-client>=4.7.0"
ステップ3: スキーマの定義とサンプルデータの投入
import_data.pyを作成します。このスクリプトではCollection(SQLにおけるテーブルに相当)を初期化し、技術ドキュメントのサンプルレコードを登録します。
import os
import weaviate
from weaviate.classes.config import Configure, Property, DataType
# Weaviateがベクトルの自動生成や回答生成で使用するOpenAI APIキーを定義
OPENAI_KEY = os.environ.get("OPENAI_API_KEY", "sk-your-openai-api-key")
client = weaviate.connect_to_local(
headers={"X-OpenAI-Api-Key": OPENAI_KEY}
)
try:
# 再テスト時のために既存のコレクションをクリア
if client.collections.exists("ArticleGuide"):
client.collections.delete("ArticleGuide")
# 埋め込みモデルと生成モデルを指定してコレクションを作成
articles = client.collections.create(
name="ArticleGuide",
vectorizer_config=Configure.Vectorizer.text2vec_openai(model="text-embedding-3-small"),
generative_config=Configure.Generative.openai(model="gpt-4o-mini"),
properties=[
Property(name="title", data_type=DataType.TEXT),
Property(name="content", data_type=DataType.TEXT),
Property(name="errorCode", data_type=DataType.TEXT),
]
)
sample_data = [
{
"title": "KubernetesクラスタにおけるOOM障害の対処",
"content": "コンテナがメモリ制限(memory limit)を超過すると、Linuxカーネルはノードのリソースを保護するためにOOM Killer経由でSIGKILLシグナルを送信します。",
"errorCode": "ERR-OOM-137"
},
{
"title": "PostgreSQLの接続負荷を軽減するPgBouncerの設定",
"content": "Postgresのアイドル状態の接続は、1つあたり2〜10MBのRAMを消費することがあります。PgBouncerを導入してコネクションプーリングを行うことで、メモリ効率を劇的に改善できます。",
"errorCode": "ERR-DB-503"
},
{
"title": "AWS IAMアクセス権限のベストプラクティス",
"content": "最小権限の原則(Least Privilege)を適用し、各サービスアカウントのアクセス範囲を制限します。安易にAdministratorAccessを付与するのは避けてください。",
"errorCode": "ERR-SEC-401"
}
]
# スループットを最大化するため動的バッチ処理(dynamic batching)を使用してデータを投入
with articles.batch.dynamic() as batch:
for item in sample_data:
batch.add_object(properties=item)
print("Weaviateへのサンプルデータの投入が正常に完了しました!")
finally:
client.close()
ステップ4: ハイブリッド検索の動作検証
test_hybrid_search.pyを作成し、正確なキーワード一致と文脈上の類似性がどのように組み合わさるかを検証します。
import os
import weaviate
client = weaviate.connect_to_local(
headers={"X-OpenAI-Api-Key": os.environ.get("OPENAI_API_KEY", "sk-your-openai-api-key")}
)
try:
articles = client.collections.get("ArticleGuide")
# 自然言語表現('メモリ不足でPodが落ちた')と正確なエラーコード('ERR-OOM-137')を混在させたクエリ
query_text = "メモリ不足でPodが落ちた ERR-OOM-137"
# alpha = 0.5: BM25に50%、ベクトル検索に50%の比重を均等に配分
response = articles.query.hybrid(
query=query_text,
alpha=0.5,
limit=2
)
print(f"検索クエリ: '{query_text}'\n")
for obj in response.objects:
print(f"[+] タイトル : {obj.properties['title']}")
print(f" エラーコード: {obj.properties['errorCode']}")
print(f" 内容 : {obj.properties['content']}\n")
finally:
client.close()
ステップ5: Generative Search(データベース内RAG)の実践
一般的なアーキテクチャでは、検索を実行して取得したテキストリストを受け取り、プロンプトテンプレートに文字列として結合した上で、改めてOpenAIへリクエストを送信する必要があります。Weaviateはこの一連のサイクルを1つのコマンドに集約しています。
本機能には2つのモードが用意されています。
- Single Prompt:取得した各レコードを個別に処理します(各ドキュメントの翻訳や個別要約などに適しています)。
- Grouped Task:上位k件の検索結果すべてを共通のコンテキストとしてまとめ、LLMが総合的に分析・生成を行います(典型的なRAGの質問応答パターンです)。
test_generative_search.pyを作成します。
import os
import weaviate
client = weaviate.connect_to_local(
headers={"X-OpenAI-Api-Key": os.environ.get("OPENAI_API_KEY", "sk-your-openai-api-key")}
)
try:
articles = client.collections.get("ArticleGuide")
# ハイブリッド検索を実行し、抽出されたコンテキストをそのままgpt-4o-miniで分析
response = articles.generate.hybrid(
query="メモリ不足によるコンテナ停止",
alpha=0.6,
limit=2,
grouped_task="提供されたドキュメントに基づいて、エラーの原因を説明し、DevOpsエンジニア向けの簡潔なトラブルシューティング手順を案内してください:"
)
print("=== 抽出されたドキュメント ===")
for obj in response.objects:
print(f"* {obj.properties['title']} ({obj.properties['errorCode']})")
print("\n=== LLMによる統合レスポンス ===")
print(response.generated)
finally:
client.close()
本番運用における最適化のプラクティス
- 匿名アクセスの無効化:
AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED: 'true'の設定は、ローカル環境での検証時のみ安全です。本番環境に移行する際は、データベース全体の漏洩リスクを防ぐため、静的APIキーまたはOAuth2/OIDCによる認証を必ず有効化してください。 - 業務ユースケースに応じたalpha値の調整:まずは
alpha = 0.5を基準値としてスタートすることをおすすめします。データセットに固有のエラーコード、製品SKU、専門的な医療用語などが多く含まれる場合は、alphaを0.3〜0.4に下げてBM25の重みを強化します。逆に、多様な自然言語を扱う対話型チャットボットを構築する場合は、0.7〜0.8に設定することで、より柔軟で自然な検索結果が得られます。 - 完全オンプレミス環境の実現:OpenAIに依存することは必須ではありません。
docker-compose.ymlにtext2vec-transformersやtext2vec-ollamaサービスを追加することで、埋め込み処理および推論プロセスをすべてローカル環境内で完結させることが可能です。これにより、社内機密データを外部のインターネットへ送信することなく安全に運用できます。

