SharePointの特定フォルダーに置かれた資料を自動で取り込み、Azure OpenAIのRAGで検索・回答したい。しかしサンプル(azure-search-openai-demo)だけではSharePoint連携や文書権限の反映が不足し、実運用で詰まりがちです。この記事では「取り込みの橋渡し」と「権限トリミング」を前提に、設計パターンと実装の勘所を具体的に解説します。
なぜ「SharePoint→RAG→Azure AI Search」がサンプルだけでは完結しないのか
RAG(Retrieval Augmented Generation)は大きく分けて、次の2つの工程で成り立ちます。
- 取り込み(Ingestion):データを集めて、分割(チャンク化)し、埋め込み(ベクトル)を作り、検索基盤(Azure AI Search)へ登録する
- 検索・回答(Query):ユーザーの質問に対して、検索(ベクトル検索+フィルター)で候補を取り、LLMで回答を生成する
多くのサンプルは「ローカルファイル」や「Blob Storage」など、取り込み元が比較的単純な前提で作られています。そのため、SharePointを“取り込み元として直結する”部分や、SharePointのフォルダー/文書レベル権限を検索結果へ反映する部分は、別途設計・実装が必要になるケースがほとんどです。
| 領域 | やること | サンプルで不足しがちな点 |
|---|---|---|
| データ取得 | SharePointからファイルを列挙・差分検知・ダウンロード | SharePoint接続・認証・差分同期の仕組み |
| 解析 | 本文抽出、OCR、表やPDFの扱い、チャンク分割 | 業務文書の“実データ”に耐える前処理設計 |
| 登録 | 埋め込み生成、Azure AI Searchへベクトル+メタデータ登録 | スキーマ設計(ACL含む)と更新/削除の運用 |
| 権限 | SharePointの閲覧権限(ACL)を検索・回答に反映 | 権限トリミング(フィルター)実装と安全性 |
目指すべき全体アーキテクチャ(設計の正解パターン)
結論から言うと、SharePoint連携は「橋渡し(取り込み)」を別で用意するのが前提です。おすすめは、取り込みを次のように分割して考えることです。
構成要素と役割
| コンポーネント | 役割 | Azureでの実装候補 |
|---|---|---|
| 取得レイヤー | SharePointからファイル取得・差分検知・削除検知 | Azure Functions / Logic Apps / ADF |
| 中継ストレージ(任意) | 生ファイル保管、再処理、監査、バックアップ | Blob Storage / ADLS Gen2 |
| 前処理レイヤー | 本文抽出、チャンク化、メタデータ整形、ACL抽出 | Functions / Container Apps / AKS |
| ベクトル化 | 埋め込み生成(Azure OpenAI Embeddings) | Azure OpenAI |
| 検索基盤 | ベクトル+全文+フィルター検索、権限トリミング | Azure AI Search |
| アプリ層 | 認証、ユーザー属性/所属取得、検索フィルター付与、回答生成 | App Service / Static Web Apps + API / Functions |
データフロー(取り込み)
- SharePointの特定フォルダー(ドキュメントライブラリ)からファイル一覧を取得
- 更新日時やETag等で差分を判定し、変更があったファイルだけダウンロード
- 本文抽出(Office/PDF/画像など)→チャンク分割(検索に耐える単位へ)
- チャンクごとに埋め込み(ベクトル)を生成
- Azure AI Searchへ「チャンク本文+ベクトル+メタデータ+ACL」を登録
- 削除・移動・リネームを検知したら、Search側の該当ドキュメントも同期
データフロー(検索・回答)
- ユーザーがMicrosoft Entra ID(旧Azure AD)でサインイン
- サーバー側でユーザーID/グループ所属を確定(クライアント申告は信用しない)
- Azure AI Searchへ「ベクトル検索+ACLフィルター」を付けて問い合わせ
- 許可されたチャンクだけが検索結果として返る
- その結果のみを根拠としてAzure OpenAIが回答生成(RAG)
課題1:SharePointのファイルを自動取得→解析→Azure AI Searchへベクトル格納する
この課題の本質は「SharePointは“取り込み元”として独自の認証・差分同期・権限制御を持つ」点です。したがって、サンプルにSharePoint直結が無いなら、橋渡しを作るのが前提になります。
方法A:SharePoint APIでファイル取得→取り込み処理へ渡す(直結型)
SharePoint Onlineの場合、実装の現場ではMicrosoft Graph API(またはSharePoint REST API)でファイルを取得し、RAGの取り込みパイプラインへ流す形が定番です。
実装イメージ
- 認証:Entra IDにアプリ登録し、アプリケーション権限(app-only)または委任権限(delegated)でアクセス
- 対象フォルダー特定:サイトID、ドライブ(文書ライブラリ)ID、フォルダーパス(またはItem ID)で対象を絞る
- 差分取得:更新日時/ETag/Delta Query等で「変わったものだけ」処理
- ダウンロード:ファイルのコンテンツを取得し、本文抽出へ渡す
おすすめの「最小で堅い」取り込み設計
直結型で最初につまずきやすいのは、安定運用(再試行・重複・欠落防止)です。以下の構成にすると運用が安定します。
| 観点 | 推奨 | 理由 |
|---|---|---|
| 起動方式 | タイマー+差分同期(ポーリング)を基本、余裕があれば通知(Webhook)併用 | Webhookは便利だが再送・取りこぼし対策が必要。まずは差分同期が堅い |
| 処理単位 | 「ファイル」ではなく「チャンク」をIDで管理 | 再処理時に同一チャンクを重複登録しないため |
| 冪等性 | ファイルのバージョン/ETag/ハッシュをキーにして、同一内容はスキップ | 無駄な埋め込み生成(コスト)を減らし、インデックスも汚れない |
| 再試行 | キュー(Storage Queue/Service Bus)で“取得”と“解析/登録”を分離 | SharePoint側の一時エラーやレート制限があっても復旧しやすい |
| 監査 | 処理ログ+最終同期時刻(ウォーターマーク)を永続化 | 「いつ何を取り込んだか」を説明できる |
処理ステップ例(擬似コード)
WordPress貼り付けで読みやすいよう、概念だけを載せます。
// 1) SharePointから差分リスト取得
changedItems = listChangedItems(folderId, watermark)
// 2) 変更分だけ処理キューへ投入
for item in changedItems:
enqueue({ itemId, driveId, siteId, eTag, lastModified })
// 3) ワーカー側でダウンロード→抽出→チャンク→埋め込み→Search登録
msg = dequeue()
fileBytes = downloadFile(msg.itemId)
text = extractText(fileBytes) // PDF/Office/OCRなど
chunks = chunk(text, strategy="semantic")
acl = fetchAcl(msg.itemId) // allowedUsers/allowedGroupsを作る
vectors = embed(chunks) // Azure OpenAI Embeddings
upsertToSearch(chunks, vectors, acl, metadata)
補助ツール(OSS)の位置づけ
SharePointからファイルをpullするOSSとして、例としてkoltyakov/sppullのようなツールが話題に上がることがあります。こうしたOSSは「素早く試せる」一方で、企業利用では次を確認してください。
- 認証方式(証明書認証に対応しているか、トークン更新が安定しているか)
- 差分同期(増分取得)をどう実現するか
- 失敗時の再試行・冪等性・ログ(監査証跡)
- ライセンスと保守(運用で止まったときに誰が直すか)
実運用のRAGは「止まらないETL」が重要なので、OSSは検証・移行の足がかりとして使い、最終的にはAzure Functions/ADFなどの運用基盤に寄せる判断が多いです。
方法B:Azure Data FactoryでSharePoint→Blob/ADLSへ複製してから処理(分離型)
運用しやすさ重視なら、SharePointから一度ストレージへ複製して、以後はBlob/ADLSを取り込み元にする設計が強力です。特に複数システムが同じデータを参照する場合、SharePointへの直撃を減らせます。
なぜADF(複製)を挟むと楽になるのか
- 責務分離:SharePoint接続はADFに任せ、RAG側は「ストレージ→解析→Search登録」に集中できる
- 再処理が簡単:同じファイルを何度でも再解析できる(モデル変更・チャンク戦略変更時に便利)
- 監査性:取り込んだ原本(生ファイル)を保持できる
- スケール:解析ワーカーを増やしてもSharePoint側への負荷を抑えられる
方法Aと方法Bの比較
| 比較項目 | 方法A:API直結 | 方法B:ADFで複製 |
|---|---|---|
| 構築スピード | 早い(最短で動く) | やや工程が増える |
| 運用のしやすさ | 設計次第。差分/再試行を自作しがち | 高い(データ基盤として再利用しやすい) |
| SharePoint負荷 | 取り込み頻度が高いと負荷が増える | 複製タイミングに集約できる |
| 再処理(モデル変更など) | 原本再取得が必要になりやすい | ストレージ上で再処理しやすい |
| 権限(ACL)の扱い | 同時に取得しやすい | ACLは別ルートで取得・同期する設計が必要 |
ポイントは最後の行です。方法Bはファイル複製が楽になる一方、SharePointの文書権限(ACL)を“複製だけで完全に引き継ぐ”のは難しいので、ACLは別途取得してSearchインデックスへ保持する設計を最初から組み込みます。
Azure AI Searchに登録するインデックス設計(ベクトル+メタデータ)
RAGで後悔しやすいのが、インデックス設計を「本文+ベクトルだけ」で始めてしまうことです。SharePoint連携では、あとから必要になるメタデータが多いので、最初に枠を作るのがおすすめです。
最低限入れておきたいフィールド
| フィールド例 | 用途 | 設計の勘所 |
|---|---|---|
| id | 一意キー | 「ファイルID+チャンク番号」など、冪等性を担保できる形にする |
| content | チャンク本文 | 後段の回答生成に使う。前処理で不要なノイズを減らす |
| contentVector | 埋め込みベクトル | 同一モデル/次元で統一。モデル変更時の再生成計画も用意 |
| title | 検索結果表示 | ファイル名だけでなく、文書タイトル抽出ができるとUXが上がる |
| sourceUrl | 原本リンク | SharePointのURLを保持し、回答根拠として提示できる |
| siteId / driveId / itemId | トレーサビリティ | SharePoint側の参照キーを保持して、更新/削除同期に使う |
| lastModified / eTag | 差分処理 | 同一ファイルの更新判定、取り込みのウォーターマークに利用 |
| allowedUsers / allowedGroups | 権限トリミング | 後述。ここを最初から入れておくと手戻りが激減 |
| classification(任意) | 機密度ラベル | 「社外秘」などがあるなら、インデックス分割の判断材料になる |
チャンク分割(分割/埋め込み作成)の現場ノウハウ
SharePointに置かれる業務文書は、単純なテキストだけでなく「表」「箇条書き」「議事録」「設計書」などが混在します。検索精度とコストのバランスを取りやすい指針は次の通りです。
- チャンクは“意味のまとまり”を優先:段落・見出し・箇条書き単位を崩しすぎない
- 大きすぎるチャンクは避ける:回答根拠がぼやけ、無関係な文まで一緒に拾う
- 小さすぎるチャンクも避ける:文脈が不足し、誤答や曖昧回答が増える
- タイトルや見出しをチャンクに付与:本文の前に「見出し」を付けて埋め込むと検索が強くなることが多い
- 表は“行の意味”で再構成:CSV化や「列名:値」の文章化で、検索に乗りやすくする
埋め込みコストが気になる場合は、「変更があったファイルだけ再埋め込み」と、「同一チャンクの重複排除」が最優先の最適化ポイントです。
課題2:SharePointのフォルダー/文書レベル権限をRAGの検索・回答に反映する(権限トリミング)
ここがSharePoint連携RAGの最重要ポイントです。どれだけ検索精度が高くても、見えてはいけない文書が検索結果に混ざった時点でアウトです。RAGでの「権限トリミング」は、基本的に次の設計で実現します。
- 取り込み時:SharePointからACL(許可ユーザー/許可グループ)情報も取得し、Azure AI Searchのインデックスに保持する
- 検索時:サーバー側でユーザーのID/所属グループを確定し、Azure AI Searchへフィルター条件として付与する
権限トリミングの基本形(インデックスにACLを持つ)
インデックス設計としては、次のようなフィールドを用意するのが定石です。
| フィールド | 型のイメージ | 入れる値 |
|---|---|---|
| allowedUsers | 文字列配列(コレクション) | 閲覧可能なユーザーID(例:Entra IDのobjectId等) |
| allowedGroups | 文字列配列(コレクション) | 閲覧可能なグループID(例:Entra IDグループobjectId等) |
| isPublic(任意) | 真偽値 | 全社員公開など、ACL判定を省略できる文書 |
そして検索時に、ユーザーのID/グループに基づくフィルターを必ず付与します(ベクトル検索と併用)。
フィルター条件の考え方(例)
(isPublic eq true)
or allowedUsers/any(u: u eq '{userObjectId}')
or allowedGroups/any(g: g eq '{groupObjectId_1}' or g eq '{groupObjectId_2}')
これにより、ベクトル検索で“近い文章”を探しつつ、ACLで“見てよい文章だけ”に絞ることができます。RAGの回答に使われる根拠も、このフィルターを通過したチャンクだけになります。
「クライアント任せにしない」が絶対条件
権限トリミングが破綻する典型例が、フロントエンド(ブラウザやアプリ)から「私はこのグループです」と送らせる設計です。これは改ざんされます。
必ず、サーバー側で以下を確定します。
- ログインユーザーの一意ID(トークンから取得)
- 所属グループ(トークンのグループクレーム、またはサーバーがGraph等で照会した結果)
この情報をもとに、Searchへのクエリにフィルターを付けて初めて「安全な検索」になります。
グループ所属の取得:実務でハマりやすいポイント
ユーザーの所属グループを毎回APIで取りに行くと遅くなりがちです。一方で、トークンにグループが常に載るとも限りません。現実的な落としどころは次の通りです。
| 方式 | メリット | 注意点 |
|---|---|---|
| トークンのグループクレームを使う | 高速、追加の照会が少ない | グループ数が多いと省略(overage)されることがある |
| サーバーでGraph等を照会して所属を取得 | 確実、粒度の制御ができる | レート制限・遅延・キャッシュ戦略が必要 |
| ハイブリッド(キャッシュ併用) | 確実性と速度のバランスが良い | キャッシュの期限・無効化(退職/異動)設計が重要 |
特に異動や権限変更が多い組織では、キャッシュの持ちすぎが情報漏えいの原因になり得ます。短めの有効期限、または権限変更イベントを起点にした無効化(難しければ短TTL)を検討してください。
ACLを取り込み時にどう取得するか(SharePointの難所)
SharePointの権限は「サイト全体」「ライブラリ」「フォルダー」「ファイル」で継承・例外があり、最終的に“そのアイテムに誰がアクセスできるか”を確定するには、情報の正規化が必要です。
実装方針は大きく2つです。
| 方針 | 内容 | 向いているケース |
|---|---|---|
| 許可プリンシパルをそのまま保持 | 許可ユーザーID/許可グループIDを列挙してSearchへ入れる | まず安全に始めたい、権限構造が比較的単純 |
| グループ展開(メンバー展開)して保持 | グループをユーザーへ展開してallowedUsersに寄せる | クエリ側を簡単にしたいが、ユーザー数増で更新負荷が上がる |
一般には、最初は「グループIDも含めて保持→検索時にユーザーのグループでフィルター」が現実的です。グループ展開は、権限変更のたびに大量更新が発生しがちで、運用コストが跳ね上がります。
権限トリミングを前提にした「おすすめ実装パターン」3選
パターン1:API直結+キュー分離(小さく始めて拡張しやすい)
- 取得:Azure Functions(タイマー)でSharePoint差分→キュー投入
- 解析:Functions/Container Appsで本文抽出・チャンク化・埋め込み
- 登録:Azure AI Searchへアップサート(ACL含む)
- 検索:アプリサーバーがユーザー属性を確定し、フィルター付きでSearchへ
最小構成で動かしやすく、後から規模に応じてワーカーを増やせます。
パターン2:ADFで複製+ストレージ起点の取り込み(運用と再処理に強い)
- 複製:ADFでSharePoint→Blob/ADLS
- 解析:Blobイベント(またはスケジュール)で取り込みパイプライン起動
- ACL:別ジョブでSharePointから権限情報を同期し、Searchへ反映
「データ基盤」を作る発想で、RAG以外の用途にも展開しやすいです。
パターン3:インデックス分割(サイト別/機密度別)+最小ACL(高度だが強い)
- 機密度や部門サイト単位でAzure AI Searchのインデックスを分割
- アプリ側でユーザー属性に応じて検索対象インデックスを切り替える
- 各インデックス内では、簡易ACL(全社/部門)程度に留める
権限モデルが複雑すぎる場合、“検索対象を先に分ける”と安全性と性能の両方を取りやすくなります。
実運用で必ず考えるべき運用設計(更新・削除・移動・版管理)
SharePoint連携RAGは、初回取り込みが動いて終わりではありません。日々の運用で「検索結果が古い」「消したのに出てくる」が起きると信頼を失います。
更新・削除を同期するためのチェックリスト
| イベント | 必要な対応 | 実装のコツ |
|---|---|---|
| ファイル更新 | 該当ファイルのチャンクを再生成し、Searchを更新 | ETag/ハッシュで変更検知し、同一ならスキップ |
| ファイル削除 | Searchから該当ドキュメントを削除 | itemId等で逆引きできるようにしておく |
| 移動/リネーム | sourceUrl/パス等メタデータ更新 | URLだけ変わるケースがある。参照キーはID中心に |
| 権限変更 | ACLフィールドを更新 | 本文が変わらなくても権限は変わる。ACL同期を別ジョブにするのも有効 |
| 例外権限(継承ブレイク) | 例外を正しく反映 | 「フォルダーだけ例外」などがあるので、ACL算出ロジックを明確化 |
よくある落とし穴と対策(SharePoint×Azure OpenAI RAGの現場目線)
落とし穴:取り込みが「全件再処理」になってコストが爆発する
対策:差分同期(ウォーターマーク)+ETag/ハッシュによる冪等化。チャンクID設計を最初に固める。
落とし穴:権限トリミングを後付けしてインデックス作り直しになる
対策:初期段階でも「allowedUsers/allowedGroups」はスキーマに入れておく。中身が空でも枠があるだけで手戻りが減る。
落とし穴:ACL取得が重くて取り込みが遅い
対策:本文更新とACL更新を分離し、ACLは“変更があった時だけ更新”できる仕組みを用意する。必要ならキャッシュとバッチ更新を組み合わせる。
落とし穴:回答がそれっぽいが、根拠が薄い(幻覚が混ざる)
対策:検索で返ったチャンクのみを根拠に回答させる、引用(参照リンク)を表示する、検索結果が少ないときは「不明」と言えるプロンプト/ガードレールを入れる。
実装を進めるための「具体的な次の一手」
ここまでの内容を、最短でプロトタイプに落とすなら次の順番がおすすめです。
- 対象フォルダーを固定:まずは1つのライブラリ/フォルダーに絞る
- 方法A(API直結)で差分取り込みを作る:更新検知→ダウンロード→本文抽出→Search登録まで通す
- インデックスにACLフィールドを追加:最初は全社公開(isPublic=true)でも良いので枠を作る
- 検索時にフィルターを必ず付与:まずは「公開のみ」→次に「ユーザー/グループ」へ拡張
- 運用要件が見えたら方法B(ADF複製)へ寄せる:再処理や監査が必要になったタイミングで段階的に強化
SharePoint連携RAGは、最初から“完璧な権限モデル”を目指すより、安全性(漏えい防止)を最優先に、段階的に精度と運用性を高めるほうが成功しやすいです。
まとめ:SharePoint×Azure OpenAI(RAG)を成功させる鍵
- SharePoint連携は、サンプルだけで完結しない前提で「橋渡し(取り込み)」を設計する
- 取り込みは差分同期+冪等性が命。全件再処理は避ける
- Azure AI Searchのスキーマには、早い段階でACL(allowedUsers/allowedGroups)を入れておく
- 権限トリミングは検索時フィルターで実現し、ユーザー属性はサーバー側で確定する
- 運用要件が出たら、ADF複製やインデックス分割などで運用性・安全性を段階的に強化する

コメント