コンテンツへスキップ

社内RAGで「他部署の資料が見えてしまう」問題の解き方

POSII COLUMN / Django × AI 実装 #1

社内RAGで「他部署の資料が見えてしまう」
——本番直前に必ず起きる事故と、その防ぎ方

ベクトル検索は「意味が近いか」しか見ていません。見ていい人かどうかは、誰も判定していない

社内向けのRAGは、だいたい本番直前で同じ壁にぶつかります。「検索したら、他部署の資料が出てきた」。デモの段階では誰も気づきません。少人数のテストデータでは、そもそも見てはいけない文書が入っていないからです。全社のデータを入れた瞬間に露見します。

これは実装のミスではなく、ベクトル検索という仕組みそのものの性質です。放置できる種類のバグではないので、設計の段階で織り込む必要があります。この記事では、何が起きるのか、なぜ普通の感覚では防げないのか、そしてPostgreSQL+pgvectorでどう実装するかを順に書きます。

何が起きるのか

たとえば社員が「今期の評価基準を教えて」と聞いたとします。RAGは質問を意味のベクトルに変換し、社内文書の中から意味の近いものを上位5件取ってきて、それを根拠にAIが回答を作ります。

このとき上位に来るのは、人事部が持っている個人別の査定シートかもしれません。「評価基準」という質問と、意味的にはこれ以上ないほど近いからです。

検索は、正しく動いています。
意味は完全に近い。それが問題なのです。

ベクトル検索がやっているのは「質問のベクトルと文書のベクトルの距離を測って、近い順に並べる」ことだけです。この計算のどこにも、閲覧権限という概念は存在しません。

なぜ、普通のWeb開発の感覚だと防げないのか

通常のデータベース検索と、ベクトル検索では処理の順番が違います。

  通常のDB検索 ベクトル検索
処理の順番 条件で絞る → 並べる 全件との距離を測る → 近い順にk件
権限の扱い WHERE句に自然に書ける 書く場所が用意されていない
該当なしのとき 0件が返る 0件にならない。遠くても必ずk件返る

最後の行が厄介です。通常の検索なら「条件に合うものがない」は0件で表現されますが、ベクトル検索はどんなに的外れでも必ずk件返してきます。「近いものがなかった」という状態が存在しないので、無関係な文書が堂々と根拠として使われます。

Djangoで作っていると、この違いは特に見えにくくなります。ORMのクエリセットには filter() があるので「後から絞ればいい」と思ってしまう。ところが後から絞ると、次の節で書くとおり別の問題が起きます。

防ぎ方は3つ。ただし2つは条件次第で破綻する

① 後フィルタ:検索してから、権限のないものを落とす

いちばん実装が楽な方法です。5件取ってきて、そのうち見ていいものだけ残す。多くのチュートリアルがこの形になっています。

ですが、これは本番で確実に壊れます。5件すべてが他部署の文書だった場合、残るのは0件。AIは根拠なしで答えることになります。

さらに悪い副作用があります。「権限のある人には答えが出て、権限のない人には無言が返る」という挙動は、その文書が存在すること自体を教えてしまいます。人事の資料について聞いたときだけ回答が急に薄くなる、という形で外から推測できてしまう。情報を隠したつもりが、存在を漏らしています。

件数を稼ごうとして k を 5 から 50 に増やす対処もよく見ますが、これは問題を先送りしているだけです。データが増えれば50件も全部落ちます。しかも毎回50件ぶんの距離計算をすることになり、遅くなります。

② インデックス分離:部署ごとにテーブルを分ける

安全性はいちばん高い方法です。人事は人事のインデックス、営業は営業のインデックス。物理的に混ざりません。

ただし、横断検索ができなくなります。社内RAGを入れる動機はたいてい「部署をまたいで探せるようにしたい」なので、目的と正面から衝突します。加えて、

  • 兼務している社員をどう扱うか(両方のインデックスを検索して結果をマージする=スコアが比較できない)
  • 全社共有の就業規則をどこに置くか(全インデックスに複製する=更新漏れが起きる)

という運用の問題が必ず出てきます。部署数が少なく、かつ横断の必要がない場合に限って有効です。

③ 事前フィルタ:検索の条件そのものに権限を入れる(本命)

文書の側に「この文書を見ていいグループ」を持たせ、距離を測る前に候補を絞ります。絞った母集団の中で近い順にk件取るので、件数が減りません。存在推定も起きません。

後から落とすのではなく、
最初から見えている集合を変える。

実装は少し面倒ですが、本番に出すつもりなら選択肢はこれだけです。以下、具体的に書きます。

PostgreSQL + pgvector での実装

マネージドのベクトルDBを使わず、すでに使っているPostgreSQLに pgvector を入れる構成を前提にします。社内RAGではデータを外に出さないことが要件になりがちなので、この構成が選ばれる場面は多いはずです。

文書に「見ていいグループ」を配列で持たせる

権限をユーザー単位で持つと組み合わせが爆発するので、グループ(部署・役職・プロジェクト)のID配列で持ちます。

# models.py
from django.contrib.postgres.fields import ArrayField
from django.contrib.postgres.indexes import GinIndex
from pgvector.django import VectorField, HnswIndex

class DocumentChunk(models.Model):
    document   = models.ForeignKey(Document, on_delete=models.CASCADE)
    body       = models.TextField()
    embedding  = VectorField(dimensions=384)   # e5-small

    # 見ることを許可されたグループID。ここが権限の実体
    allowed_groups = ArrayField(models.IntegerField(), default=list)

    class Meta:
        indexes = [
            GinIndex(fields=["allowed_groups"]),
            HnswIndex(
                name="chunk_emb_hnsw",
                fields=["embedding"],
                m=16, ef_construction=64,
                opclasses=["vector_cosine_ops"],
            ),
        ]

配列の重なりを判定する && 演算子は GINインデックスが効きます。ここを張り忘れると全件走査になり、絞り込みのたびに遅くなります。

検索は「絞ってから測る」

# services/search.py
from pgvector.django import CosineDistance

def search(query_vec, user, k=5):
    group_ids = list(user.groups.values_list("id", flat=True))
    return (
        DocumentChunk.objects
        .filter(allowed_groups__overlap=group_ids)   # ← 先に絞る
        .annotate(dist=CosineDistance("embedding", query_vec))
        .order_by("dist")[:k]
    )

要点は filter()annotate() より前に書くことです。順番を入れ替えても同じSQLになるように見えますが、意図が読めなくなるので必ずこの順で書きます。

権限の取得をこの関数の中に閉じ込めるのが重要です。呼び出し側が group_ids を渡す設計にすると、いずれ「管理画面用だから全部見せたい」といって空配列や特権フラグを渡すコードが生まれ、そこから漏れます。検索関数は必ず user を受け取り、権限は内部で解決する。

ここで一度ハマります:絞り込むと、件数が足りなくなる

事前フィルタを入れると、今度は検索結果が k 件に届かないという現象が起きます。原因はHNSWインデックスの仕組みです。

HNSWは全件を調べる代わりに、グラフを辿って「近そうな候補」を ef_search 件ぶんだけ集めてきます。権限による絞り込みは、その候補が集まった後に適用されます。集めた候補のほとんどが権限外だった場合、残るのはごくわずかです。

対処は2つあります。

  1. pgvector 0.8.0 以降の反復スキャンを使う(推奨)。候補が足りなければ自動的に探索を続けてくれます。
    SET hnsw.iterative_scan = relaxed_order;
  2. hnsw.ef_search を上げる。単純ですが、絞り込みの強さに応じて必要な値が変わるので、上げ幅の根拠が持てません。
なお、絞り込みが非常に強い(対象が数十件しかない等)ときは、PostgreSQLがHNSWインデックスを使わず全件走査に切り替えることがあります。これは不具合ではなく、そのほうが速くて正確だからです。EXPLAIN ANALYZE で Index Scan が消えても慌てないでください。

実装しても、まだ3つの経路から漏れます

検索に権限を入れれば終わり、ではありません。ここから先が、実際に事故になる部分です。

① チャンクの切り方で権限が混ざる

長い文書は分割してベクトル化します。このとき、1つの文書の中に権限の異なる情報が同居していると守れません。

典型は議事録です。前半は全社共有でよい決定事項、後半は特定メンバーだけの人事の話。文書単位で権限を付けていると、どちらかに倒すしかなくなります。

権限は、文書ではなくチャンクに付ける。
そして「混在する文書は分割する」という運用を先に決める。

② 会話履歴に残り、次の質問で再利用される

マルチターンの対話にすると、過去のやりとりを文脈として渡すのが普通です。ところが過去の回答には、そのとき参照した文書の内容が含まれています

権限が変わったあと(異動した、プロジェクトを抜けた)に古い履歴を読み込むと、いま見てはいけない情報が、履歴経由で現在の回答に入り込みます。履歴にも参照時点の権限スコープを記録し、変わっていたら読み込まない、という判定が要ります。

③ 「引用しないで」とプロンプトに書いても意味がない

これがいちばん誤解されている点です。「機密文書は引用しないでください」とシステムプロンプトに書けば安全になる、という発想がありますが、渡した時点で、すでに漏れています

LLMは渡された文書を読んで回答を作ります。直接引用しなくても、内容は要約や言い換えの形で出てきます。プロンプトによる禁止は対策ではなく、お願いです。権限のない文書は、そもそもLLMに渡さない。ここは実装で担保するしかありません。

「何を渡したか」の記録を残す

社内で運用する以上、事故は前提です。問題は起きたときに「何が誰に漏れたか」に答えられるかどうかで、ここが答えられないシステムは、たいてい一度の事故で止められます。

最低限、質問ごとに次を残します。

記録する項目 なぜ必要か
質問した人 / 日時 影響範囲の特定
そのとき解決した権限グループ 権限設定の誤りだったのかを切り分ける
LLMに渡したチャンクのID これが本体。回答文だけ残しても何が漏れたかは分からない
回答文 実際に出力された内容の確認

チャンクのIDさえ残っていれば、後から「この文書を参照した質問」を逆引きできます。回答テキストだけを保存しているログは、事故のときにほぼ役に立ちません。

権限は、後から足せない

ここまで書いたことの結論はひとつです。

権限モデル → チャンク設計 → インデックス設計 → 検索
この順番でしか作れない。

逆順に作ると、つまり「まず動くRAGを作ってから権限を足そう」とすると、チャンクの切り直しと再ベクトル化が発生します。文書量が数万件を超えていれば、それだけで数日かかる作業です。しかもその間、システムは止まります。

デモを作るだけなら権限は要りません。ですが社内に出すつもりがあるなら、最初のスプリントで権限モデルを決めてください。これが、この記事でいちばん伝えたいことです。

まとめ

  • ベクトル検索は意味の近さしか見ていない。権限という概念が計算に存在しない
  • 検索してから落とす「後フィルタ」は、件数が消えるうえに文書の存在を漏らす
  • 正解は事前フィルタ。グループID配列+GINインデックスで、距離を測る前に絞る
  • 絞ると件数が足りなくなるので、pgvectorの反復スキャンを有効にする
  • チャンクの混在・会話履歴・プロンプト任せ、の3経路が残る
  • LLMに渡したチャンクIDのログがないと、事故のときに何も説明できない
  • 権限は後付けできない。最初に決める

POSIIでは、社内データを外に出さない構成でのRAG構築と、権限設計を含む要件整理をお手伝いしています。「PoCまでは動いたが、本番に出す段になって権限で止まっている」という段階のご相談が最も多い領域です。

ご相談はこちらから

最後までお読みいただき、心より感謝申し上げます。

大河原潤

大河原 潤

AI開発専門家

ブーム以前からAI研究に携わる、本物の専門家。「AIに使われる」のではなく、「AIを使いこなす」確かな技術力を提供します。

【アカデミックな裏付け】

  • カリフォルニア大学リバーサイド校 博士前期課程修了(研究分野:測度論、経路積分)
  • アメリカ数学会のジャーナルに論文発表

【社会的に認められた専門性】

  • AI関連書籍:『誤解だらけの人工知能』(2018年)、『AI×Web3の未来』(2023年)
  • プログラミング専門書:実務的な技術書を2冊出版(確かな実装力の証明)
  • 100社以上のAI導入コンサルティング実績、特許売却経験あり