社外に出せない機密文書をどうAI検索するか?
社内閉域データとPostgreSQL pgvectorによる安全なナレッジ検索(RAG)
「ベテラン社員が退職する前に、過去10年分の見積書・トラブル対処法・社内規程を誰でも3秒で引き出せるようにしたい。でも機密情報を外部AIに渡すわけにはいかない」——そのジレンマを解決する安全な社内AI検索の設計図を解説します。
この記事の要点と4つの章のご案内
| 章構成 | 主なテーマ | 要点と確認ポイント |
|---|---|---|
| 1. 基本と全体像 | 身近なたとえでわかる「RAG(検索拡張生成)」:AIに丸暗記させるのではなく「社内の鍵付き書庫を見て答えさせる」 | AIに社内文書を「丸暗記(追加学習)」させる必要はない: 社内専用データベースから必要な箇所だけをその都度カンニングペーパーとして引き出して答えさせる「RAG(検索拡張生成)」なら、低コストかつ外部漏洩ゼロで明日から運用できます。 |
| 2. 現場での進め方と対策 | 図解でわかる「危険な無料AI利用」と「閉域データベース(db01)× 権限連動RAG」の違い | 「誰が質問したか」によって検索できる範囲をデータベースの鍵で安全な分離: PostgreSQL 19 の行レベルセキュリティ(RLS)により、人事や経営会議の機密書類が一般社員のAI回答に混ざる事故をシステム構造として構造的に遮断します。 |
| 3. 関連する制度とルール | 社内AI検索の使い心地を決める3つのポイント | 必ず「元の書類の何ページ目に書いてあるか」の証拠リンクを添えて回答: 社内資料に書いていないことをAIが勝手に想像して答えるのを防ぎ、人間がワンクリックで原本を確認できる安心設計です。 |
| 4. 詳しい技術仕様としくみ | 現場で陥りやすい落とし穴と回避策(社内AI検索・RAG構築の4大失敗例) | ご安心ください。一般的な個人向け無料チャットAIにファイルを入れると外部学習に使われるリスクがありますが、当社の社内ナレッジ検索(RAG)では、文書データそのものを自社専用の隔離データベース(db01 / PostgreSQL 19)の内部に保管します。AIモデルには「質問に関連する数段落の抜粋」だけを外部学習ゼロ保証の暗号化通信で渡して要約させるため、社外にデータが流出することは一切ありません。 |
- 01. AIに社内文書を「丸暗記(追加学習)」させる必要はない: 社内専用データベースから必要な箇所だけをその都度カンニングペーパーとして引き出して答えさせる「RAG(検索拡張生成)」なら、低コストかつ外部漏洩ゼロで明日から運用できます。
- 02. 「誰が質問したか」によって検索できる範囲をデータベースの鍵で安全な分離: PostgreSQL 19 の行レベルセキュリティ(RLS)により、人事や経営会議の機密書類が一般社員のAI回答に混ざる事故をシステム構造として構造的に遮断します。
- 03. 必ず「元の書類の何ページ目に書いてあるか」の証拠リンクを添えて回答: 社内資料に書いていないことをAIが勝手に想像して答えるのを防ぎ、人間がワンクリックで原本を確認できる安心設計です。
FORCE ROW LEVEL SECURITY、PIIマスキングのRust 2024実装を公開しています。
01. 身近なたとえでわかる「RAG(検索拡張生成)」:AIに丸暗記させるのではなく「社内の鍵付き書庫を見て答えさせる」
「自社の過去のトラブル事例や社内規程をAIに覚えさせるには、何百万円もかけて独自のAIモデルを学習(ファインチューニング)させないといけないのでしょうか?」というご質問をよくいただきます。結論から言うと、AIモデル自体に社内データを丸暗記させる必要はまったくありません。
テストにたとえると、AIに社内の全書類を丸暗記させる(追加学習する)のは大変な時間とお金がかかり、しかも来月ルールが変わったときに古い記憶を消すことができません。それに対して現在主流の「RAG(ラグ:検索拡張生成)」という仕組みは、「優秀な読解力を持つAIに、質問が来た瞬間だけ社内の鍵付き書庫(データベース)から該当するマニュアルのページを開いて渡し、『このページに書いてある事実だけを使って要約してください』と答えさせる(教科書持ち込み可のテスト)」方式です。
この方式なら、新しいマニュアルを1枚追加した瞬間に最新の答えが返るようになり、万一書類を削除すればその瞬間にAIの回答からも構造的に消去されます。
平均 18分 → 3秒
共有フォルダの奥深くから過去の見積書や手順書を探し回っていた平均18分の探索時間が、出典リンク付きの3秒回答に短縮されます。
外部学習 0%(全体遮断)
個人情報(PII)の事前マスキングとゼロデータ保持(Zero Data Retention)構成により、社内機密が外部AIの学習に使われることはありません。
越権漏洩 0件(RLS強制)
PostgreSQL 19 の行レベルセキュリティ(RLS)により、質問者の部署・役職で閲覧できない行はベクトル検索自体から物理的に除外されます。
02. 図解でわかる「危険な無料AI利用」と「閉域データベース(db01)× 権限連動RAG」の違い
社員が質問してから3秒で安全な回答と原本リンクが返ってくるまでの裏側の防壁を図解します。
03. 社内AI検索の使い心地を決める3つのポイント
01. 「型番ピタリ一致」と「意味のふんわり検索」の両方に強いハイブリッド方式
よくあるAI検索ツールは、意味の似た文章を探すのは得意でも、「品番 M8-25B」や「第14条第2項」といった1文字違いの番号を見分けるのが苦手です。当社のシステムは、意味を探すベクトル検索と、文字の一致を探す全文検索を数学的に融合(RRF:逆順位融合)しているため、品番でも話し言葉でも一発で正解にたどり着きます。
02. 古いマニュアルと最新マニュアルが混ざらない「有効期限・世代管理」
社内の共有フォルダには「就業規則_2022.pdf」「就業規則_2024改定案.pdf」「就業規則_最新_最終.pdf」のように似たファイルがいくつも眠っていることがあります。当社のナレッジ基盤では、最新の有効版だけに検索フラグを立て、旧版は履歴アーカイブへ自動移動するため、AIが古いルールを答えてしまう事故を防ぎます。
03. 社内資料に答えがないときは、推測で嘘をつかず「資料に記載がありません」と答える
検索された資料と質問との一致度スコアが基準値に満たない場合は、AIに無理な作文をさせず「現在の社内規程には該当する記載が見当たりません。総務窓口へご確認ください」と回答させます。これにより、AIの知ったかぶり(ハルシネーション)による業務ミスを根絶します。
04. 現場で陥りやすい落とし穴と回避策(社内AI検索・RAG構築の4大失敗例)
「社内チャットボットを作ったけれど、とんちんかんな答えばかり返ってきて誰も使わなくなった」という失敗を防ぐ4つの急所です。
10年前の廃止済み料金表や下書きメモまで全部AIに読ませてしまう
【なぜ危険か】 古い料金表と新しい料金表が両方入っていると、AIはどちらが最新か判断できず、廃止された安い金額をお客様へ案内する原因になります。
【回避策】 いきなり全フォルダを入れるのではなく、まず「現行の規程・よくある質問・直近2年の正式見積」に絞って登録し、版管理(バージョン番号)を付与します。
500文字ごとに機械的に切った結果、見出しと金額表が別々に千切れる
【なぜ危険か】 「Aプランの料金は以下の通りです」という文と、その下の金額表が別々の塊に分断されると、AIはどのプランの金額かわからなくなります。
【回避策】 文字数で機械的に切るのではなく、章・節・見出し階層と表の構造を保ったまま意味のまとまりごとに分割(セマンティック・チャンキング)します。
データベース内に全社の書類を混ぜて入れ、AIへの指示文だけで隠そうとする
【なぜ危険か】 「人事情報は答えないでください」とプロンプトで指示するだけでは、質問の言い回し(プロンプトインジェクション)によって機密が引き出されてしまいます。
【回避策】 AIに渡す前のPostgreSQLデータベース層で FORCE ROW LEVEL SECURITY を有効化し、権限のない行は物理的に読み出せない設計を順守します。
AIがもっともらしい文章だけを返し、元の書類のどこに書いてあるか分からない
【なぜ危険か】 出典がわからないと、スタッフは「本当にこの回答を信じていいのかな?」と不安になり、結局自分で最初からマニュアルを探し直すことになります。
【回避策】 すべての回答に必ず「文書名・章タイトル・ページ番号」のリンクを添え、1クリックで原文の該当箇所が開くように設計します。
05. 御社の「社内ナレッジ検索・情報共有」3項目セルフ診断
該当数:0 / 3 項目社内の日常業務で当てはまるものにチェックを入れてみてください。
06. 理解を深める確認クイズ(解説付き)
Q. 社内の機密文書を検索するAIシステムにおいて、「一般社員が質問したときに、役員専用の機密議事録の内容が誤って回答に含まれてしまう事故」を確実に防ぐ方法はどれでしょうか?
A. AIへのプロンプト(指示文)に「あなたは配慮したAIなので、一般社員には役員の秘密を原則として言わないでください」と書いておく 答えを見る ↓
不正解です(非常に危険です!)。 AIに機密データを渡した上で「言わないでね」と指示するだけでは、質問の工夫次第で簡単に秘密を喋らせてしまうことができます。
B. AIが回答を作る前の「データベース検索の段階」で、PostgreSQLの行レベルセキュリティ(RLS)を使い、質問者の権限で閲覧できる書類しか検索されないように遮断する 答えを見る ↓
正解です! 権限のないデータはそもそもデータベースから1行も読み出されず、AIの目にも触れないようにする(データベースカーネル層での強制アクセス制御)ことこそが、原則として漏洩しないナレッジ検索の鉄則です。
C. 社員全員に「役員向けの質問をするのは禁止」という誓約書を書いてもらい、データは誰でも見られる状態にしておく 答えを見る ↓
不正解です。 悪意がなくても、普通の業務質問をした際にAIが勝手に役員議事録を関連資料として拾ってきてしまうため、システムの仕組み(RLS)で分離することが必須です。
なぜ「意味を数字の矢印(ベクトル)にする」と、違う言葉で書かれた同じ内容が見つかるのか?
従来の検索では、マニュアルに「冷却装置の不具合」と書いてあるとき、社員が「エアコンが冷えない」と検索すると1件もヒットしませんでした。これに対し、現代のベクトル検索(pgvector)は、言葉の意味を数百〜数千次元の「方角(矢印)」に変換して保管します。「冷却装置の不具合」と「エアコンが冷えない」は文字はまったく違いますが、意味の方角がほぼ同じ向きを指すため、コンピュータが「この2つは同じ意味の困りごとだ!」と瞬時に見つけ出すことができるのです。
御社の「社内マニュアル・過去見積・技術資料」を安全に3秒検索化しませんか?
外部学習ゼロ・PostgreSQL 19 行レベル権限管理つきの安全な社内AIナレッジ基盤構築
Reciprocal Rank Fusion(BM25疎検索 × pgvector HNSWコサイン類似度)・PostgreSQL 19 FORCE ROW LEVEL SECURITY ベクトルフィルタリングとPII秘匿化チャンキングのRust 2024実装
本稿では、社外秘文書・医療福祉記録・法務契約書を扱うマルチテナント・ロール階層型RAG基盤(db01.ok.st)の内部設計を詳解し、(1) 語彙一致(BM25)と階層的ナビゲーション小世界グラフ(HNSW)密ベクトル検索を無パラメータ統合する Reciprocal Rank Fusion(RRF)、(2) PostgreSQL 19 カーネル層でプロンプトインジェクションによる越権情報漏洩を物理遮断する FORCE ROW LEVEL SECURITY 述語プッシュダウン、および (3) Rust 2024によるPII自動秘匿化・RRF融合エンジンを定式化する。
1. 【Reciprocal Rank Fusion(RRF)による疎密ハイブリッド検索の正規化統合】 型番・法令条文番号に強い疎ベクトル全文検索スコア BM25(q, d)(値域 [0, ∞))と、意味的言い換えに強い密ベクトル余弦類似度 sim_cos(e_q, e_d) = (e_q · e_d)/(∥e_q∥_2 ∥e_d∥_2)(値域 [-1, 1])は確率分布のスケールが異なるため、単純な線形和ではクエリごとに重みが不安定化する。各検索器 m ∈ {BM25, HNSW} における文書チャンク d の順位 r_m(d) ∈ {1, 2, …} のみを用いる RRF スコア: RRF(d | q) = ∑m ∈ {BM25, HNSW} (w_m)/(k_rrf + r_m(d)) (k_rrf = 60) により、スコア分布の歪みに一切影響されないロバストな上位 K チャンク抽出を実現する。
2. 【PostgreSQL 19 FORCE ROW LEVEL SECURITY による越権検索の数学的消滅】 アプリケーション層の WHERE 句組み立てに依存する認可実装は、バグやSQL/プロンプトインジェクションによるバイパスリスクを伴う。db01.ok.st では全ナレッジチャンク表に対して ALTER TABLE knowledge_chunks ENABLE ROW LEVEL SECURITY; ALTER TABLE knowledge_chunks FORCE ROW LEVEL SECURITY; を強制し、トランザクション開始時に SET LOCAL app.current_tenant_id = '...'; SET LOCAL app.clearance_mask = '...'; をバインドする。これにより、HNSWインデックス走査の各ホップにおいてポリシー述語:
P_RLS(d) ⇔ (d.tenant_id = current_setting('app.current_tenant_id')) ∧ ((d.acl_mask \& user_mask) ≠ 0)
がデータベースカーネル内で強制評価され、権限外チャンクがLLMコンテキストへ混入する確率は厳密に 0 となる。
3. 【埋め込み前の決定的PII秘匿化(Pre-Embedding PII Redaction)】 ベクトル埋め込みベクトル e_d ∈ ℝ^1536 から元テキストの個人情報を逆推定するEmbedding Inversion攻撃を防ぐため、文書をチャンク分割してEmbedding APIへ送信する前段において、電話番号・マイナンバー・メールアドレス・銀行口座番号を決定性有限オートマトンで検出し、型付きプレースホルダ(<PII:PHONE_JP> 等)へ不可逆置換する。
| Subsystem Layer | Configuration / Index Spec | Parameter Value | Security & Latency Guarantee |
|---|---|---|---|
| pgvector HNSW Index | USING hnsw (embedding vector_cosine_ops) |
m = 16, ef_construction = 128, ef_search = 80 | Recall@10 ≥ 0.985, Query ≤ 6.2ms |
| Row-Level Security | PG19 FORCE ROW LEVEL SECURITY |
Tenant UUID + 64-Bit Clearance Bitmask | 0% Cross-Tenant / Cross-Role Leakage |
| Hybrid RRF Merger | BM25 Rank + Cosine Rank Fusion | k_rrf = 60, Top-K = 5 Grounded Chunks | Exact Part-Number & Semantic Match |
| Abstention Gate | Minimum Cosine & RRF Threshold | sim_cos ≥ 0.780 Required to Answer | Deterministic Abstention on Unknowns |
#![forbid(unsafe_code)]
/// Document chunk metadata retrieved from db01 PostgreSQL 19 knowledge store.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct KnowledgeChunkCandidate {
pub chunk_id: u64,
pub tenant_id: u64,
pub required_clearance_mask: u64,
pub is_latest_revision: bool,
pub bm25_rank: Option<u32>, // 1-indexed rank from lexical search
pub dense_rank: Option<u32>, // 1-indexed rank from pgvector HNSW search
pub cosine_similarity_permille: u16,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ScoredChunkResult {
pub chunk_id: u64,
/// Fixed-point RRF score scaled by 1_000_000 for deterministic integer sorting.
pub rrf_score_micros: u64,
pub cosine_similarity_permille: u16,
}
pub const RRF_K_CONSTANT: u64 = 60;
pub const MIN_GROUNDED_SIMILARITY_PERMILLE: u16 = 780;
/// Enforces tenant isolation, role clearance bitmask, revision freshness, and computes
/// deterministic Reciprocal Rank Fusion (RRF) scores without floating-point non-determinism.
pub fn rank_and_filter_authorized_chunks(
request_tenant_id: u64,
user_clearance_mask: u64,
candidates: &[KnowledgeChunkCandidate],
top_k: usize,
) -> Vec<ScoredChunkResult> {
let mut authorized_scored: Vec<ScoredChunkResult> = Vec::new();
for c in candidates {
// Hard security invariant: tenant match, active revision, and RBAC bitmask authorization
if c.tenant_id != request_tenant_id
|| !c.is_latest_revision
|| (c.required_clearance_mask & user_clearance_mask) != c.required_clearance_mask
{
continue;
}
if c.cosine_similarity_permille < MIN_GROUNDED_SIMILARITY_PERMILLE && c.bm25_rank.is_none() {
continue;
}
let mut score_micros: u64 = 0;
if let Some(r_bm25) = c.bm25_rank {
score_micros += 1_000_000 / (RRF_K_CONSTANT + (r_bm25 as u64));
}
if let Some(r_dense) = c.dense_rank {
score_micros += 1_000_000 / (RRF_K_CONSTANT + (r_dense as u64));
}
if score_micros > 0 {
authorized_scored.push(ScoredChunkResult {
chunk_id: c.chunk_id,
rrf_score_micros: score_micros,
cosine_similarity_permille: c.cosine_similarity_permille,
});
}
}
// Sort descending by RRF score, then by cosine similarity, then ascending by chunk_id
authorized_scored.sort_by(|a, b| {
b.rrf_score_micros
.cmp(&a.rrf_score_micros)
.then_with(|| b.cosine_similarity_permille.cmp(&a.cosine_similarity_permille))
.then_with(|| a.chunk_id.cmp(&b.chunk_id))
});
authorized_scored.truncate(top_k);
authorized_scored
}
/// Redacts long contiguous digit sequences (8..=16 digits, e.g., phone/account/ID numbers)
/// prior to vector embedding to prevent PII leakage into embedding space.
pub fn redact_digit_pii_sequences(input: &str) -> String {
let mut out = String::with_capacity(input.len());
let mut digit_run = String::new();
for ch in input.chars() {
if ch.is_ascii_digit() || ch == '-' {
digit_run.push(ch);
} else {
let raw_digits = digit_run.chars().filter(|c| c.is_ascii_digit()).count();
if (8..=16).contains(&raw_digits) {
out.push_str("[PII_REDACTED]");
} else {
out.push_str(&digit_run);
}
digit_run.clear();
out.push(ch);
}
}
let trailing_digits = digit_run.chars().filter(|c| c.is_ascii_digit()).count();
if (8..=16).contains(&trailing_digits) {
out.push_str("[PII_REDACTED]");
} else {
out.push_str(&digit_run);
}
out
}
db01.ok.st(FreeBSD + OpenZFS + PostgreSQL 19 pgvector)上で稼働する当社の閉域RAG基盤は、行レベルセキュリティ(RLS)とRRFハイブリッド検索により、情報漏洩ゼロと高精度な出典提示を両立しています。