はじめに
インターンの事前勉強で「社内のデータベースやメールを参照して回答するチャットボット」を開発することになり、Azureを使ってRAG(Retrieval-Augmented-Generation)システムを構築した。
まずはAzure SDKとDjangoを使ってRAGの仕組みを実装し、データの流れを完全に理解した上で、次のステップとしてLangGraphによるエージェント化を目指した。
本記事では、その開発過程で直面した「Azure AI Searchのインデックス設計の落とし穴」や「検索ロジックの使い分け」について共有する。
技術スタック
- Frontend: Next.js (TypeScript)
- Backend: Django (Python3.13)
- LLM: Azure OpenAI Service (GPT-3.5 Turbo)
- Vector DB: Azure AI Search
- Data Source: Azure Cosmos DB
- Authentication: Microsoft Entra ID
アーキテクチャの概要
- データ投入: PythonスクリプトでデータをCosmos DBに投入し、インデクサーを使ってAzure AI Searchに自動同期。
- 検索 (Retrieval): ユーザーの質問をDjangoで受け取り、Azure AI Searchに対してハイブリッド検索を実行。
- 生成 (Generation): 検索結果をコンテキストとしてAzure OpenAIに渡し、自然な回答を生成。
実装のポイントと苦労した点
1. インデックス設計と「見えない不整合」との戦い
RAGの肝となるのは、Azure AI Searchのインデックス設計である。
開発中、インデクサーは成功しているのに「検索結果が0件になる」「フィールドがnullになる」という問題に何度も直面した。
学んだこと:
-
アナライザー設定: 日本語データを検索する場合、
Edm.Stringフィールドには必ずja.microsoftを設定しないと、単語区切りがうまくいかずヒットしない。 -
フィールド属性:
Searchable(検索可能)とRetrievable(取得可能)のチェック忘れもやらかしてた。これらはインデックス作成後に変更できないため、作り直しになるからこれだけに数時間かかった。
2. 「一番年上の人は?」にどう答えるか(検索ロジックの分岐)
当初はベクトル検索だけで全て解決しようとしたが、「一番年上の人は誰?」といった比較や集計が必要な質問には、ベクトル検索だけでは正しく答えられないことが判明した。
解決策:
Django側で「質問の意図」を簡易的に判別し、検索クエリを動的に切り替えるロジックを実装した。
# views.py
if "一番年上" in user_query:
results = search_client.search(
search_text="*",
order_by=["age desc"],
top=1
)
else:
results = search_client.search(
search_text=user_query,
vector_queries=[vector_query],
top=3
)
このように、「非構造化データの検索」と「構造化データの検索」を使い分けることで、回答精度を向上させた。
3. ベクトル次元数の罠
Azure OpenAIの埋め込みモデル(Embedding)と、AI Searchのインデックス設定の不一致にも悩まされた。
-
text-embedding-3-small: 1536次元 -
text-embedding-3-large: 3072次元
この次元数が食い違っていると、InvalidRequestParameter エラーが発生するため、モデルの選定とインデックス定義はセットで管理する必要があった。
次のステップ:LangChain / LangGraph へ
SDKを使って手書きで実装したことで、RAGの裏側で何が起きているか(認証、ベクトル化、検索クエリの組み立て)を深く理解できた。
しかし、機能が増えるにつれて views.py の if/else 分岐が複雑になってきた。
- 「期限が近い見積もりを教えて」→ 日付フィルター検索
- 「〇〇の在庫は?」→ 在庫DB検索
- それ以外 → 通常のRAG
このような「条件に応じたツールの使い分け」や「複雑なワークフロー」を管理するために、次は LangChain、特に LangGraph の導入を進めている。
これについてもいつか記事にできたらいいと思う。
まとめ
- RAGの構築は、単にライブラリを繋ぐだけでなく、インデックスのスキーマ設計やデータパイプラインが重要。
- SDKでの実装は大変だが、エラーハンドリングや認証の仕組みを理解する上で非常に勉強になる。
- 複雑な分岐が必要になったタイミングこそが、LangChain/LangGraphを導入するベき。