Search & Retrieval12 min readMarian Engineering

ハイブリッド検索の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)Hamming200
リランクint8 (+scale)dot product50
融合後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_FILES12コンテキストに入れる最大ファイル数
MAX_TOTAL_CHARS12,000コンテキスト合計文字数
PER_FILE_CHARS2,4001ファイルあたりの上限
MAX_SOURCES6引用として提示する最大ソース数
MAX_MEMORY_RECORDS8併載するメモリレコード数

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は常に利用可能なので、フラグ有効化は安全な操作として設計されています。

Related Articles