ハイブリッド検索の4段パイプライン — ACLフィルタ、バイナリ粗探索、int8リランク、RRF融合
Ask機能の裏側: 検索品質と権限安全性を両立させるretrieval基盤の設計
Key Takeaways
- 検索はACLフィルタ→バイナリ粗探索(200件)→int8リランク(50件)→RRF融合(20件)の4段パイプライン
- ACLはスコアリング前に述語適用するfail closed設計で、権限リークを構造的に排除する
- RRFはk=60・keyword重み0.85/vector重み1.0で、スコア正規化なしに複数retrieverを統合する
- 前提が欠けると自動的に単純な方式へフォールバックし、フラグ有効化が常に安全
課題: 「ベクトル検索だけ」では足りない
RAGの検索品質は、単一のretrieverでは頭打ちになります。dense検索(埋め込み)は言い換えに強い一方で固有名詞や記号に弱く、keyword検索(全文検索)はその逆です。さらにマルチテナント環境では、検索結果に他人のドキュメントが混ざる権限リークが最悪の障害になります。
MarianのAskパイプラインはこの2つを同時に解決するため、検索を4つのステージに分解しています。
クエリ
│
├─ 1. ACLフィルタ … スコアリング前に不可視IDを除外(fail closed)
├─ 2. 粗探索(coarse) … バイナリ符号のHamming距離で上位200件
├─ 3. リランク(rerank) … int8ドット積で上位50件に絞る
└─ 4. RRF融合 … dense / keyword / graph のランキングを統合 → 20件設計原則: ACLは「後でフィルタ」ではなく「最初に遮断」。スコアリングに入る前に不可視IDを落とせば、どのステージにもリークの余地がない。
ステージ1: ACLを唯一のチョークポイントにする
アクセス制御は private / space / org / public の4段階の可視性で表現され、判定は純粋関数 canViewResource() に集約されています。デフォルトは拒否(fail closed)です。
export function canViewResource(resource: ResourceAcl, viewer: Viewer): boolean {
const visibility = resource.visibility ?? 'private'
if (visibility === 'public') return true
if (resource.ownerId === viewer.userId) return true
if (resource.allowUserIds?.includes(viewer.userId)) return true
if (visibility === 'org')
return resource.orgId != null && viewer.orgId != null && resource.orgId === viewer.orgId
if (visibility === 'space') return intersects(resource.spaceIds, viewer.spaceIds)
return false // deny by default
}実行時には全リソースに対する判定結果を Set に事前計算し、(id) => boolean の述語にコンパイルします。以降のステージはこの述語をO(1)で参照するだけです。重要なのは、この述語が粗探索・リランク・融合のすべての入力に適用されることです。「上位N件を取ってから権限で間引く」方式だと、間引いた分だけ結果が減って品質が劣化するうえ、Nの取り方次第でリークも起きます。
ステージ2-3: 2段階の量子化検索
粗探索はfp32ベクトルではなく1bit量子化したバイナリ符号のHamming距離で行います(詳細は別記事「バイナリ量子化」で扱います)。768次元が96バイトになるため、大量の候補をメモリ常駐で走査できます。粗探索で200件に絞った後、int8量子化ベクトルのドット積で50件にリランクします。
| ステージ | 表現 | 距離 | 候補数(デフォルト) |
|---|---|---|---|
| 粗探索 | binary (1bit/dim, 96B) | Hamming | 200 |
| リランク | int8 (+scale) | dot product | 50 |
| 融合後 | — | RRFスコア | 20 |
ステージ4: RRF — スコアを混ぜず、順位を混ぜる
dense・keyword・graphという性質の異なるretrieverの結果を統合するとき、素朴に「スコアの重み付き和」を取ると失敗します。コサイン類似度とBM25スコアはスケールも分布も違うため、正規化のチューニングが地獄になるからです。MarianはReciprocal Rank Fusion(RRF)を採用し、スコアではなく順位だけを使います。
// lib/marian-search/fusion.ts
export function reciprocalRankFusion(lists: readonly RankedList[], options: RrfOptions = {}) {
const k = options.k ?? 60 // RRF論文由来の平滑化定数
// 各リストの各idについて: contribution = weight / (k + rank)
// 全リストで合算し、スコア → ソース数 → id辞書順でソート
}k=60はオリジナルのRRF論文の推奨値をそのまま使っています。kが小さいと1位と2位の差が極端になり、大きいと順位差が均されます。Marianではさらにリストごとの重みを持たせており、現在の設定はkeyword 0.85 / vector 1.0です。dense側をわずかに優先しつつ、固有名詞クエリではkeywordリストの上位が融合結果を支配できるバランスを取っています。
RRFの利点は「キャリブレーション不要」に尽きる。retrieverを足すときに必要なのは順位リストと重み1つだけで、スコア分布の分析が要らない。
グレースフルデグラデーション: フラグの裏で常に動く保険
この4段パイプラインは ASK_HYBRID=1 のフィーチャーフラグでオプトインです。さらに、有効時でも前提が欠けていれば自動的に1つ前の方式へ畳み込まれます。
- RAPTORツリーが未構築 → 2段階量子化検索へフォールバック
- バイナリ符号が未バックフィル → 単段のIVFFlat(fp32コサイン)へフォールバック
- ハイブリッド無効 → 従来のpgvectorブレンド(変更なし)
フォールバックチェーンの終端(IVFFlat単段)は常に利用可能なので、新機能のフラグを有効化してもAskが壊れることはないという不変条件が成り立ちます。検索基盤のような足回りでは、この「有効化が安全」という性質がロールアウト速度を決めます。
コンテキスト組み立ての上限値
検索で20件まで絞った後、LLMに渡すコンテキストは文字数ベースの上限で刈り込みます。実際の定数は次のとおりです。
| 定数 | 値 | 意味 |
|---|---|---|
| MAX_TEXT_FILES | 12 | コンテキストに入れる最大ファイル数 |
| MAX_TOTAL_CHARS | 12,000 | コンテキスト合計文字数 |
| PER_FILE_CHARS | 2,400 | 1ファイルあたりの上限 |
| MAX_SOURCES | 6 | 引用として提示する最大ソース数 |
| MAX_MEMORY_RECORDS | 8 | 併載するメモリレコード数 |
PER_FILE_CHARSで1ファイルの支配を防ぎ、MAX_TOTAL_CHARSで全体を抑える二重の予算制御です。「検索は広く、コンテキストは狭く」が原則で、絞り込みの責任は融合までのランキングに負わせます。
まとめ
Marianのハイブリッド検索は、(1)ACL述語をスコアリング前の唯一のチョークポイントにする、(2)量子化2段階で候補を安価に絞る、(3)RRFで順位だけを融合しキャリブレーションを排除する、(4)前提が欠けたら自動で前の方式に畳む、という4つの決定で構成されています。どれも単体では地味ですが、組み合わせると「権限安全で、壊れず、retrieverを足しやすい」検索基盤になります。
FAQ
- なぜスコアの重み付き和ではなくRRF(Reciprocal Rank Fusion)を使うのか?
- コサイン類似度とkeyword検索のスコアはスケールも分布も異なり、重み付き和は正規化のチューニングが必要になるためです。RRFは各retrieverの順位だけを使い、weight/(k+rank)を合算します(Marianはk=60、keyword重み0.85、vector重み1.0)。retrieverを追加しても順位リストを渡すだけで済みます。
- マルチテナント環境での権限リークをどう防いでいるのか?
- ACL述語(private/space/org/publicの4段階、デフォルト拒否)をスコアリング前に適用し、不可視IDは粗探索・リランク・融合のどのステージにも入らない設計です。「上位N件取得後にフィルタ」方式と違い、品質劣化もリークの余地も構造的に発生しません。
- ASK_HYBRIDフラグを有効にして検索が壊れるリスクはないのか?
- フォールバックチェーンがあるため壊れません。RAPTORツリーがなければ2段階量子化検索へ、バイナリ符号がなければ単段IVFFlat(fp32コサイン)へ自動で畳み込まれます。終端のIVFFlatは常に利用可能なので、フラグ有効化は安全な操作として設計されています。