一、まず検索システムという対象を定義しておく
「検索+生成」だけでは検索システムにならない
エンジニアリングでいちばん間違えやすいのは、検索システムの実装を「ベクトリ検索の結果を大きな言語モデルで要約すること」だとみなすことです。デモの段階ならこれは十分に完成して見えます。ユーザーが質問を入力し、システムがいくつかのパッセージを見つけ、モデルに渡して自然な答えを生成するところまでは問題なく動きます。しかし実際にスケールさせると、三つの場所で同時に崩壊します。
一つ目の崩壊点は召回(リコール)です。生成モデルがどれほど強くても、消化できるのは検索レイヤが渡してきた数百件の内容に過ぎません。召回段階で正しい文書が外れてしまえば、その先の生成処理をどれだけ詰めても、誤った材料をもとに答えを組み立てているだけになります。しかもその誤りは滑らかで、直接エラーを返すより遥かに追いにくいのです。二つ目の崩壊点はレイテンシです。生成は直列的で並列化できず、遅延のテールが非常に重い処理で、所要時間は出力長とモデル規模の両方によって決まります。検索システムの P95 レイテンシ目標を数百ミリ秒まで圧縮した場合、何らかの形式のオンライン生成は必ずこのバジェットを押し壊します。三つ目の崩壊点は信頼性です。ユーザーが検索に来るのは、通常、何らかの事実を確認したいからです。出典も参照先もタイムスタンプもない回答は、内容が正しくあっても、ユーザーの目には捏造と大差ありません。
五つの層の分担と、それぞれの責務の境界
完成した検索システムは五つの層に分解できます。オフラインインデックス構築層は、生のコーパスを検索できる構造へ変換する担当で、ユーザーリクエストのパス上には置かれません。ここで決まるのは「見つけてもらえるかどうか」の上限です。オンライン検索層(召回層)は、数千万件の文書から数百件の候補をミリ秒単位で絞り込む担当で、「候補集合の質の上限」を決めます。リランキング層は候補に対してより細かく、かつより高コストな関連性の判定を行い、「最終順位の精度」を決めます。生成層は並べ替えられた材料を自然な言葉の回答にまとめる担当で、「答えの表現品質と信頼性」を決めます。結果提示層はユーザーが何を見るか、つまり要約カード、回答本文、引用リスト、関連クエリを決める担当で、「情報が本当に消費されたか」を決めます。
この五層のあいだでいちばん重要なエンジニアリング上の合意は、上位層は下位層の能力不足を補えるが、その代償としてコストとレイテンシが非線形に増えていく ということです。たとえば召回が弱いときは、より強いリランキングモデルとより長いコンテキストで埋められます。すると200ミリ秒のリクエストが8秒に化けます。逆に召回が十分よければ、リランキングが扱う候補集合はごく小さくなり、生成も少数の高信頼度材料に絞って自信を持って実行できます。アーキテクチャ設計の中心的な作業は、各層に必要な能力を「ちょうど足りる」水準まで圧縮することにあります。
レイテンシバジェットをどう配分するか
レイテンシバジェットはコードを書く前に切り分けておく必要があります。あとからだと各环节で「少し余計に使う」ことが積み上がり、最後に総量が破綻するからです。以下は高頻度検索シナリオでの典型的な配分例です。生成は任意なので、事前計算済みの答えにヒットした場合や構造化されたリストをそのまま返す場合は、生成分のバジェットを召回とリランキング側にまるごと振り向けられます。
| 層 | P50 バジェット | P99 バジェット | 超過時のフォールバック動作 |
|---|---|---|---|
| クエリ理解 | 15 ms | 60 ms | 書き換えをスキップし、元クエリをそのまま使う |
| マルチレーン召回 | 40 ms | 120 ms | 最も遅いレーンを落とし、フェールバックのレーンを残す |
| リランキング | 80 ms | 200 ms | 粗いスコアのまま単純にソートする |
| 生成 | 600 ms | 2500 ms | トップ10件のタイトルと要約を返す形へ切り替える |
| 表示とネットワーク | 40 ms | 100 ms | 初期画面だけストリーミングし、残りはバックグラウンドで補う |
この表で本当に価値があるのは数字そのものではなく、最後の列です。各环节について「バジェットを使い切ったらどうするか」を事前に考え抜いておく必要があります。エンジニアリングの実践では、フォールバック戦略を事前に設計しておらず、障害が起きてから初めて考え出すと、そのままエラーになることが多く、そのまま何も返せない状態になります。一方、慎重に設計したフォールバック経路なら、過負荷時でも使える結果を返せます。フォールバック自体も独立してテストすべきコードパスであり、普段あまり実行されないぶんだけ、主経路より壊れやすい傾向があります。
さらに、バジェット配分とトラフィック形状の関係にも注意が必要です。検索トラフィックには明確なピークと谷があり、リランキングや生成といった重い計算の环节は、ピーク時にボトルネックになりやすいものです。よく使われるのはトラフィックの水位に応じてバジェットを動的に調整する手法です。水位が低いときは全量のリランキングと生成を行い、水位が高いときはまず召回の結果を返してユーザーが読み始められるようにし、同時にバックグラウンドでリランキングと生成の結果を補って、ストリーミングで埋めていきます。こうすればユーザーから見たレイテンシは初期画面の召回段階に固定され、重い処理はユーザーに知覚されない非同期タスクに落とせます。
二、オフラインインデックス構築層:正引き・逆引き・ベクトル
転置インデックスという現実的な姿
転置インデックス(inverted index)は、検索システムの中で最も古くて、しかも置き換えが効かない部品です。中心になる構造は「単語から文書リストへの対応表」で、ヒットしたあとに本文・タイトル・タイムスタンプといった元のデータを取り出すために、正引きインデックス(forward index、文書から元フィールドへの対応)を使います。正引きと逆引きはセットとしての一組です。逆引きは候補集合を高速に絞り込み、正引きは完全な情報を補完します。この組み合わせは成熟した検索エンジンほぼすべてに存在し、ベクトル検索に置き換わったことはありません。
実装上の現実的な要点は、転置インデックスについて補足しておきます。第一にトークナイゼーションと正規化です。日本語のシーンでは分割の粒度の選び方が召回品質を直接左右します。粗い分割は召回の範囲が広い一方でノイズが多く、細かい分割は精度が高い一方で、切り方を誤ると完全に召回できなくなります。実務では「粗く切り、細かく直す」組み合わせがよく使われます。インデックス側にはやや粗めの分割を保存し、クエリ側で重要な部分だけ精密にマッチングするわけです。第二に、ストップワードと高頻度語の枝刈りです。「の」「怎么」「是什么」のような単語はほとんどの文書に出現するので、インデックスから取り除けば転置テーブルの容量とクエリコストが显著に減ります。第三にフィールドの重みです。タイトルヒット、タグヒット、本文ヒットでは関連性への寄与が通常まったく違うため、転置テーブルをフィールドごとに分けて保存するか、スコア付けの段階で異なる飽和係数を使う必要があります。
ベクトルインデックスの構造選択
dense retrieval は文書とクエリを同じベクトル空間に写像し、近似最近傍(Approximate Nearest Neighbor、ANN)インデックスを使うことで準線形の検索速度を得ます。インデックス構造の選択は召回率とレイテンシのバランスに直接影響します。ただし、このバランスは千万件規模のコーパスと億件規模のコーパスではまったく違います。
百万件程度までなら、グラフインデックス(proximity graph ベースの方法など)は召回率とレイテンシの両面で、転置型のベクトルインデックスより有利です。代償は構築が遅いこと、メモリ消費が大きいこと、そして文書を一件更新するたびにグラフへ入れ直す必要があることです。億件規模になると、グラフインデックスのメモリと構築コストは受け入れられなくなり、転置型の量子化方式(浮動小数点ベクトルを低ビット幅の整数に量子化し、転置インデックスで引く方式)が現実的な選択肢になります。レイテンシはわずかに高くなりますが、メモリ消費は元のベクトルの一小部分まで抑えられ、エンジニアリングの複雑さも運用コストもはるかに低くなります。実際の性能差は次元数、量子化ビット幅、フィルタ条件、ハードウェアに強く依存するため、汎用的な結論は存在せず、自分のデータとハードウェアで実測する必要があります。
インデックス構築のスループットは第一級の要件
多くのチームが気にしているのはクエリのレイテンシだけですが、インデックス構築のスループットで本番展開のあとにつまずきます。ベクトルインデックスの構築コストは、おおよそ文書数とベクトル次元の積に比例し、グラフインデックスではエッジの維持コストが更に積み重なります。構築フェーズはレジューム(checkpoint)に対応していなければ、一度の失敗で振り出しからやり直しになります。並列化にも対応していなければ、単機のスループットがコンテンツ取り込みのボトルネックになります。
#增量索引构建的基本骨架(伪代码)
def build_delta(docs, base_index, changed_ids):
#1.只处理发生变更的文档,先在基线索引里删除旧向量
for doc_id in changed_ids:
base_index.delete(doc_id)
#2.批量计算新向量,批量写入,避免逐条提交
vectors = embed_in_batches([d for d in docs if d.id in changed_ids], batch_size=256)
base_index.upsert_bulk(docs, vectors)
#3.写完后原子切换可见性标记,避免读到半成品
base_index.commit_generation(new_generation)
#构建必须可重入:任何一步中断后重跑都能得到一致结果
この骨格には見落とされがちで、しかも極めて重要な設計点が三つあります。第一に削除は書き込みより前に実行しなければならないことです。そうしないと古いベクトルが残り、インクリメンタル更新の場面では「一つの文書に二つの内容」が残る隠れたデータエラーになります。第二にバッチコミットです。一件ずつの書き込みは、バッチ書き込みより一到二桁もスループットが低く終わります。第三に世代の切り替え(generation)メカニズムです。クエリ側が読むのは常に完全な一つの世代であり、構築が途中で中断されても「インデックスに本文はあるのにベクトルがない」という穴あきの状態にはなりません。この三点は単体スクリプトなら省略できますが、常時稼働するオンラインシステムでは必須です。
三、オンライン検索層:召回のエンジニアリング上のトレードオフ
疎検索:BM25 とその派生系
BM25 は今も検索システムにおける最も強い疎検索のベースラインであり、これは度重なる実運用の で検証されてきました。中心にある考え方は、単語の出現頻度は大きいほど寄与するが上限があり、逆文書頻度(Inverse Document Frequency、IDF)が語の識別力を測り、文書長の正規化によって長い文書が持つ有利さを抑える、というものです。BM25 のパラメータは単純で、学習が不要で、説明可能で、単一文書のスコア計算がグローバル状態に依存しません。正確な一致が必要な场景、電話番号や型番のような短い文字列の検索が必要な場面では、ほぼ代替がありません。
BM25 の派生系は、主に三つの方向で工夫されています。第一はクエリ側の重み付けです。クエリ内の語を意図に応じた重みで差异化し、タイトル語と修飾語に違う重みを与えるといった具合です。第二は文書側のフィールド単位スコア付けで、BM25 を複数フィールド版に分解してから線形または非線形に組み合わせます。第三は意味レベルの信号の導入です。ナレッジグラフや事前学習済み言語モデルが生むエンティティリンクの結果でクエリ語に重みをつけ、「苹果」のような曖昧な語が文脈の中でより適切な IDF 推定を得られるようにします。
dense retrieval:デュアルエンコーダー構造と学習目標
dense retrieval の主流な構造はデュアルエンコーダー(Dual Encoder)です。クエリと文書をそれぞれ一つのエンコーダーで同じ空間に写像し、ベクトル類似度を関連性の判断基準にします。利点はクエリベクトルをオフラインで事前計算でき、クエリ時は文書ベクトルストアに対する近似最近傍検索が一回で済むため、レイテンシを制御できることです。欠点も明確で、エンコード時に query と document がお互いを見られないことを「表現の独立性」と呼びます。語レベルマッチング能力が本質的に弱まり、特に厳密な位置合わせが必要な場面では明確にスコアが落ちます。
学習目標の選択は最終的な効果に大きな影響があります。もっとも素朴な対比学習はサンプルペアだけを使うため、退行しやすくなります。ハードネガティブ(Hard Negative、つまり検索結果では上位に出たが実際は無関係な文書)を導入するのは効果改善に決定的に効きます。モデルが混同しやすい境界領域をちょうどカバーできるからです。さらに、訓練中に原文の語レベルマッチング信号を一部混ぜたマルチタスク制約を入れると、デュアルエンコーダーが厳密マッチングに対して鈍くなる問題をある程度 緩和できます。これらはメカニズム層の合意として確立した方向ですが、どの組み合わせが効くかはコーパスの構成とアノテーション品質に依存するので、自分のシーンで検証する必要があります。
マルチレーンの召回をどう融合するか
成熟した検索システムが単一レーンの召回に賭けることはありません。典型的な構成はこうです。疎レーンが厳密な一致を担い、dense レーンが意味の一般化を担い、リライトレーンが表現の拡張を担い、グラフ構造のレーンがエンティティと多ホップの関連を担い、最後に行動ログに基づく人気や協調レーンが加わります。各レーンが自分の候補集合とスコアを返し、そのあと融合層に入ります。
融合のもっとも素朴な方法はスコアを正規化してから重み付き和を取ることです。ただしここには見落としやすい落とし穴があります。レーンによってスコアの分布が大きく異なり、疎レーンのスコアは通常ある範囲に集中するのに対し、ベクトルレーンのコサイン類似度は別の範囲に集中します。単純に重み付けすると、あるレーンが事実上潰されてしまいます。安全な方法は、まず分位数正規化や統計量に基づく標準化を行ってから重み付けを行うことです。さらに安全な方法としては、少量の人手アノテーション付きクエリで融合モデルを学習させ、各レーンの相対的な重みをモデル自身に学ばせる方法があります。融合におけるもう一つの重要な判断はクォータ(Quota)です。融合の前に各レーンが貢献できる候補数を制限し、召回能力の高いレーンが他のレーンの候補を押し出してしまい、マルチレーン召回がシングルレーンに退行するのを防ぎます。
#多路召回的融合与配额(伪代码)
def retrieve_fused(query, plan, k=200):
#plan:每一路的名称、权重、配额、是否启用、截止时间
pools = []
for lane in plan.lanes:
if not lane.enabled or lane.over_budget():
continue
cands = lane.search(query, top_k=lane.quota, timeout=lane.timeout)
pools.append((lane.name, lane.weight, normalize_by_rank(cands)))
#分位数归一化后加权,避免不同路的分数尺度互相压制
merged = defaultdict(float)
for name, weight, cands in pools:
for rank, (doc_id, _) in enumerate(cands):
merged[doc_id] += weight * (1.0 / math.log2(rank + 2))
return sorted(merged.items(), key=lambda x: -x[1])[:k]
このコードで最も注目に値するのはアルゴリズムの詳細ではなく、over_budget() という判定と timeout という引数です。ここに表われているのは、よく見落とされているアーキテクチャ原則、すなわちオンライン検索は部分的な失敗を許さなければならないということです。ベクトルインデックスが時々揺れ、疎インデックスのあるシャードがタイムアウトしても、それがリクエスト全体を失敗させるべきではなく、そのレーンの寄与が欠ける形で現れ、残りのレーンとフォールバック戦略で補う必要があります。また、名前の逆数でスコアを付けるやり方も、一つの事実を示しています。召回段階では「順位」という順序情報のほうが「絶対スコア」より信頼できるということです。スコアの尺度の差に自然に応えやすいからです。
従来の検索レーンをフォールバックとして残す理由
このレーンは多くのチームの新しいシステムでは削られます。意味検索があれば をのほぼすべてをカバーできそうだからです。しかし、このレーンが担う責務は意味検索とはまったく異なり、意味検索が構造的に不得手なタスクがいくつかあります。
一つ目は正確な識別子の検索です。ユーザーが注文番号、バージョン番号、メールの断片、エラーコードを引くとき、これらのトークンは意味ベクトルにほぼ判別力がありません。訓練コーパスの中で自然な言語として出てこないからです。このとき語レベルマッチングはより正確であるだけでなく、事実上唯一の実行可能な経路になります。二つ目は希少なエンティティの長尾部分です。意味モデルは高頻度概念をよく理解しますが、低頻度の固有名詞を理解できるかは、事前学習やファインチューニングのデータに現れたかどうか次第です。一方、語レベルマッチングはこの影響を受けません。三つ目はリアルタイム性です。ユーザーがたった今起きている出来事を検索したとき、新規に書き込まれたインデックスはまだベクトルを生成していないものの、転置インデックスなら書き込みと同時に見えるようになります。四つ目は説明可能性です。語ヒットのハイライトは関連性の根拠としてユーザーに説明しやすく、誤り修正システムにとっても最も信頼できるフィードバック源になります。
したがって従来の検索レーンを残すコストは非常に低く、得られるものはシステム全体のテール側の失敗率を桁違いに下げることです。これは検索システムの中で私が「ほぼ最適化で消されない」と考える設計点のひとつです。
召回率とレイテンシのトレードオフ
召回段階の核心的な緊張関係は、「与えられたミリ秒バジェットの中で候補集合をどれだけ完全に作れるか」という点です。調整可能なレバーのうち、影響の大きい順に並べると次のようになります。
一つ目はインデックス構造のパラメータです。近似最近傍の検索幅が召回率とレイテンシのトレードオフを直接決めるもので、幅を一段広げることで得られる召回率の改善は、低い召回率の領域ではとても割に合います。一方、高い召回率の領域では限界効果が急速に薄まります。
二つ目はレーンごとの候補数のクォータです。小さすぎると本来関連する文書が切れてしまい、大きすぎると融合とリランキングのコストが跳ね上がります。三つ目は二段召回を使うかどうかです。まず非常に低コストなフィルタで範囲を縮めてから、精密な検索を行います。四つ目は文書長の切り詰めです。ベクトル検索では先頭のパラグラフだけをエンコードすれば文書レベルの関連性は判断できることが多く、全体をエンコードするとコストは倍増します。
強調しておきたいのは、ここでいう召回率を「モデルが関係 と判定と思っているか」で判断してはいけないということです。必ずアノテーション済みデータセット上の正解ラベルで計算します。実用的な手順は、まず広めの候補集合で理論上の上界を出し、そこから設定を一つずつ削りながら曲线を見て、どこから明らかに急勾配になるかを確認することです。そこがレイテンシバジェット内での最適な打ち切り点のことがほとんどです。
四、クエリ理解層:過小評価されている効果の置き場
意図分類とクエリルーティング
クエリ理解(Query Understanding)は、検索システムの中で投資対効率の高い層です。ユーザー入力が同じ数十語であっても、意図はまったく違うことがあります。特定のページを見つけたい、概念を理解したい、エラーを解決したい、二つの選択肢を比較したい、動向を追いたい。これらで望ましい検索のしかたすら異なります。ナビゲーション型の意図は厳密な一致で唯一の答えを素早く出す必要があり、ブラウジング型は多様性が重要で、探索型は字面の一致ではなく意味の一般化が必要です。
意図分類の価値は、系統にラベルを振ることではなく、処理経路を分けられることにあります。たとえばナビゲーション型と判定できれば、リランキングを飛ばして最高スコアのページを直接取り出してハイライトできます。鮮度重視の意図と判定できれば、時間フィルタをソフト制約からハード制約に変えられます。比較型の意図なら、クエリを複数のサブクエリに分解して別々に召回してから統合できます。意図ルーティングをうまく做到的ときの効果は、リランキングモデルのサイズを一段階上げるより明瞭に見えることがよくあります。
クエリ書き換えと拡張
クエリ書き換え(Query Rewriting)にはいくつかの方向があります。誤り修正(入力をユーザーが本当に言いたかった表現に直すこと)、正規化(意味のない修飾語を外し、口語表現を用語にそろえること)、拡張(元クエリに同義語・上位語・下位語・関連エンティティを足すこと)、分割と統合(複合クエリをサブクエリに割る、あるいは複数の節を一つの検索単位にまとめること)です。
書き換えの最大のリスクはバイアスを持ち込むことです。拡張語と本来の意味の結びつきが弱いと、書き換え後のクエリが召回を誤った方向へ誘導します。しかもこの種の誤りは最終結果の中でユーザーには「検索の間違い」と認識されにくい。ユーザーから見えるのは「このシステム馬鹿だ」だけです。そのため実践では保守的な書き換え方針が好まれます。確信度が十分に高いときだけ拡張し、元クエリも独立した召回入力として一本のレーンとして残し、融合層で元クエリと書き換えクエリの結果を合流させます。書き換えで元クエリを完全に置き換えるのではなく、並行させる。この原則はほとんどの場面でより堅牢です。
鮮度の判定
鮮度判定(Freshness)は、単純に新しい順に並べるのではなく、時間を第一級の信号として扱うことです。鮮度の信号は三つあります。クエリ本文に含まれる時間表現(「最新」「2024年」「先週」など)、クエリが関係するエンティティそのものの変化速度(ソフトウェアのバージョン番号、株価、天気といったエンティティは本質的に変化する)、そして文書自体のメタデータの更新特徴です。この信号の扱い方は一律に時間減衰係数を掛けるのではなく、層別にするべきです。時間の制約が明示されたクエリはハードフィルタあるいは強い重みとして扱い、エンティティの時間変化には適度ななめらかな減衰を使い、安定した知識(数学の定理、歴史的事実)には時間ペナルティを一切加えません。
時間減衰関数のパラメータは非常に敏感です。減衰が速すぎると、まさに重要なのに少し古い文書まで抑えてしまい、減衰が遅すぎると古くてもキーワードの一致が極端に高い文書が前に来てしまいます。堅牢な方法は減衰パラメータを試せる変数として扱い、クエリの種類ごとに個別に調整することです。グローバルな一つのパラメータで全てを賄うのではなく。
複数ターンのコンテキスト継承
複数ターンの難点は「前の発言を覚えていられるか」ではありません。この発言のどの成分が継承で、どの成分が置き換えかを判定することです。ユーザーが先に「A製品の電池持ち」と尋ね、続いて「じゃあBは?」と聞いた場合、二つ目のクエリの指示対象は暗黙です。先に「A製品の電池持ち」と尋ね、続いて「充電にどのくらいかかる」と聞いた場合、二つ目のクエリの主語は製品ではなく充電になります。実行可能な判定方法は、クエリそのものの意味的完全性を見ることです。単独で明確なクエリとして解釈できるものは継承を使わず、外部情報がないと解釈できないものだけが継承を発動させます。
コンテキスト継承の実装は浅い層と深い層の二つに分けられます。浅い層はクエリの連結で、直前のターンのクエリ語を連結して普通の検索を一度実行します。深い層は書き換えで、モデルに会話履歴を渡して自己完結した完全なクエリを生成させます。後者の品質は明らかに優れています。指示語の解決を正しく扱えるからです。ただし同時にエンジニアリング上の制約も生まれます。モデル呼び出しのレイテンシが入り、書き換えの誤りがそのまま検索結果を汚染します。妥協案としては、まずルールで継承が要るかどうかを判定し、必要なときだけモデルを呼ぶようにし、書き換え結果についても元の入力をフォールバック用の召回経路として残しておくことです。
スペリングと同義語(synonym)
この二つの問題は基礎的に見えますが、実装を誤ると観測できるだけの長尾トラフィックを直接奪います。スペリング修正では「必ず直す」場合と「直してはいけない」場合を区別する必要があります。明白な入力エラー(キー入力ミス、ローマ字と漢字の混在入力、日本語と中国語の混在入力)は直すべきです。一方、固有名詞、ブランド名、人名、型番の「風合わない綴り」はそれが正しいことが多く、直さないより直すほうが悪い結果になります。判断材料は通常そのトークンのコーパス内での出現頻度と、そのユーザーの過去の挙動であり、単純な編集距離の閾値ではありません。
同義語処理の実用的な原則はこうです。同義拡張はインデックス側に入れてもクエリ側に入れても構いませんが、両方に同時に行ってはいけません。そうすると重複カウントと重みの不均衡が生じます。インデックス側で拡張する方式は長尾クエリに親切です。コストは構築時に一度払うだけだからです。クエリ側で拡張する方式は新語やホットワードに親切です。インデックスの再構築を待つ必要がないからです。両者は語の更新頻度で層分けして組み合わせられます。
五、リランキング層:精度とコストの境界線
cross-encoder が解決したこと
召回段階のデュアルエンコーダーには避けられない欠陥があります。クエリと文書はエンコード時にお互いを見えません。スコアリング関数の入力は独立した二つのベクトルなので、「この単語がこの文脈で重要か」をモデル化できないのです。cross-encoder は逆にクエリと文書を連結してモデルに渡し、各層の注意層で直接に両者の相互作用を見られるようにすることで、粒度の細かい関連性判断を下します。代償は、クエリ文書ペアごとに完全なフォワード計算が要ることです。事前計算できず、近似最近傍で加速できず、レイテンシは候補集合の大きさに正比例します。
この特性が cross-encoder の立ち位置を決めます。すでに縮小済みの候補集合にしか使えません。粗い段階で百万件から千件へ、リランキングで千件から数十件へ落とす。この数層の積の効果は全体の召回品質を一段上げると同時に、レイテンシバジェットの主な消費箇所にもなります。
全量をリランキングできない理由
大雑把に計算してみましょう。中規模な cross-encoder がクエリ文書ペア一つに対してフォワード計算一回を行うコストは、通常のハードウェアでは通常ミリ秒の桁です。千件の候補を全量リランキングすれば、この層だけで秒単位になります。万件なら十几秒です。GPUを並列に使ってスループットを十倍にしても、オンラインサービスの待ち行列とスケジューリングのコストによって P99 は制御を失います。
もう一段深い問題があります。リランキングの限界的な効果は非線形に逓減します。粗い段階で一位の候補について、リランキングが解くとべきはその誤判定を正すことです。一方、五百位の候補は、関連性が高くてもユーザーにとっては一画面内の位置の差にすぎません。
カスケード reranking:階層化と適応制御
単一の reranking モデルですべての候補をカバーできないなら、最も現実的なのはカスケード(Cascade)です。第一層は軽量な reranking モデルで大きめの候補集合を処理します。蒸留で得られたもの、層数を切り詰めた言語モデル、関連度のために専門的に訓練した分類器など、形は何でも構いませんが、十分に信頼できる並び順を出せることだけが条件です。第二層では高コストの強力なモデルで上位数十件の候補だけを担当します。第三層は極端な場合にだけ介入し、例えば最も関連性の高い数件の候補について細粒度の関連度判定を行ったり、チャンク単位のマッチングを生成したりします。
| カスケード層 | 候補規模 | モデルの形 | 担う役割 |
|---|---|---|---|
| 粗い recall | 10⁵~10⁶ | sparse とベクトル索引 | 高 recall、ノイズは許容する |
| 軽量 reranking | 500~2000 | 蒸留した cross-encoder か、低層で打ち切ったもの | ノイズを潰し、信頼できる頭部を作る |
| 強い reranking | 30~100 | 完全な cross-encoder | 頭部の精密ソート、最終順序を決める |
| 精判定 | 5~10 | 細粒度マッチングまたはチャンク単位の判定 | チャンクの特定、関連チャンクの抽出 |
表の候補規模は目安の桁数にすぎません。実際の境界は注釈データセット上の曲線から決めるべきです。具体的にどこで切るかは、各層のモデルの実際の識別能力、上のレイテンシ予算、そして「上位の何件かは必ず当たらなければならない」というビジネス側の硬い要件に依存します。
カスケードの上に検討に値する強化方法が二つあります。一つは適応型カスケードです。安価な判別器で現在のクエリの難易度を先に判定し、簡単なクエリ(ナビゲーション型など)は reranking をスキップし、難しいクエリだけが完全な経路を通るようにします。これで平均コストを下げつつ品質の損失は小さくできます。二つは蒸留によるアライメントです。軽量 reranking モデルの出力が、強い reranking モデルの出力分布に近づくように学習させ、より安価な形態で強いモデルの効果に追いつかせます。結果としてカスケードの第一層をより信頼できるものにできます。
reranking 以外のシグナル
関連度による reranking が最終順序を決める唯一の要因ではありません。最終的な並びは通常、複数目標のブレンドです。テキストの関連度、コンテンツ品質(出所の権威性、新鮮度、構造の完全さ)、ユーザーパーソナライズ(過去の関心、現在のセッション)、そしてビジネス目標(クリック率予測、コンバージョン予測)です。これらのシグナル間の重みはオンライン実験で決める必要があります。「関連度優先」という並びをハードコードすると、実際には関連度が埋もれてしまいます。
踏みやすい落とし穴が一つあります。ビジネスシグナルを強いすぎる状態にすることです。クリック率モデルが十分に強くなると、ユーザーが最もクリックしそうな内容を先頭に並べることを学びますが、これは「ユーザーが最も必要としている内容」とは系統的にずれています。煽り的なタイトルといった項目は、クリック率では常に有利です。対策はビジネスシグナルに上限を設け、クリックそのものだけでなく「クリック後に読み進めたか」のような負のフィードバックシグナルもモデルに入れることです。
六、生成フェーズ:幻覚と引用の追跡
幻覚はどこから来るのか
検索シーンで生成モデルが幻覚を起こす理由は複数あります。まとめて議論すると、优化の方向を誤ります。
第一の類型は検索が空振りしている場合の幻覚です。検索が関連情報を渡していないため、モデルはパラメータ内の記憶しか拠り所にできません。パラメータ記憶は長尾と鮮度の面で信頼できません。この種の幻覚の根本原因は召回側にあり、正解はプロンプトを書き換えることではなく、まず召回を直すことです。第二の類型は制約が機能していない場合の幻覚です。検索は正しい材料を渡したのに、モデルが生成の過程でそれを無視してしまう。多くは、コンテキストが長すぎて中間の情報が希釈されること(いわゆる forgetting in the middle)、あるいは指示が「材料だけに基づいて回答せよ」と明示していないことが原因です。第三は統合型の幻覚です。材料自体が断片的で、矛盾しているか、あるいは曖昧であり、モデルが要約の段階で独自の取捨と補完を行っています。
この三つの類型はまったく違う対処を要します。エンジニアリング上で最も価値があるのは、次の原則です。「解なし」を合法な答えの状態として扱う。検索の品質が閾値を下回ったとき、システムは明示的に「十分に関連する内容が見つかりませんでした」と返し、再書き換えの方向も提示すべきです。モデルに無理に答えを捻り出させるのではありません。不知道と言える検索システムのほうが、絶対に拒絶しない代わりに、誤った内容を平然と書き出すシステムより、はるかに有用です。
引用の追跡を、単なる飾りにしたくないために
引用(Citation)は実践の場でしばしば飾りとして作られます。モデルに「引用番号を出せ」と頼むと、モデルはその番号を出します。しかしその番号に対応する文書も、その文も、実はまったく関係ない。ユーザーがクリックして一致しないことに気づき、それ以来引用を信頼しなくなる。引用のしくみが機能不全に陥る典型的な経路であり、引用そのものがない場合よりも悪い結果です。
引用を信頼できるものにするには、生成の過程を検証可能な粒度に制約することが鍵です。現実的なやり方はいくつかあります。一つは文や段落単位の引用で、出力されるすべての文を具体的な入力チャンクに紐づけ、その紐づけ関係を生成時に強制的に作らせる(生成後に推測するのではなく)ことです。一つは構造化された事実テンプレートで、答えを「結論文+証拠チャンク」の組み合わせとしてレンダリングし、モデルは与えられたチャンクの中での抽出と書き換えだけを担当して、自由な構成はさせないことです。もう一つは生成後の検証で、各引用について独立に含意判断(Entailment)を行い、その文が本当に引用した材料から導けるかを調べ、通ったものだけを表示し、通らなかったものは引用なしの出力に降格させます。
#引用对齐与校验的流程(伪代码)
def generate_with_citations(query, materials, k=5):
context = build_context(materials[:k])
sentences = llm_generate(query, context, require_span_refs=True)
verified = []
for sent in sentences:
ref_ids = sent.span_refs
#1.引用必须指向真实存在的片段
ref_ids = [r for r in ref_ids if r in context.span_index]
#2.句子必须真的能从该片段推出,而不是编号存在即可
if not ref_ids:
verified.append(sent.without_refs())
continue
ok = entailment_check(sent.text, context.spans[ref_ids[0]].text)
if ok:
verified.append(sent.with_refs(ref_ids))
else:
#校验不过不静默丢弃,降级为无引用并保留内部告警
verified.append(sent.without_refs())
metrics.inc("citation_rejected")
return verified
このフローには強調すべき細部がいくつかあります。引用番号は存在性の検証が必須です。モデルは存在しない番号を非常に自然に生成しがちで、長いコンテキストの中ではほぼ避けられないからです。検証は番号の有効性ではなく含意判断に基づいていなければなりません。後者のほうが本来ユーザーが気にしていることです。検証を通らなかったものを黙って捨てると、システムは正しい内容をこっそり失います。正解はテキストを残したまま引用なしへ降格させ、同時にメトリクスを記録することです。そうすれば情報を失わずに、引用のしくみの健全性も観測できます。
プロダクトの観点から見ると、引用はクリック可能、位置特定可能、ハイライト可能でなければなりません。文書全体の先頭へ飛ぶリンクを見せる価値は低く、ユーザーが本当に欲しいのは、その文を支えている段落へ直接ジャンプすることです。この機能はフロント側の問題に見えますが、実際にはインデックス層が安定したチャンク識別子とチャンクのオフセットを保持しているかに依存しています。したがってチャンク粒度の分割はオフライン段階で済ませなければならず、クエリのたびにその場で分割してはいけません。
生成のレイテンシとコスト
生成フェーズの所要時間は出力長と強く相関します。長い出力は、複雑な答えを正確に表現するうえで前提となるものです。ここには構造的な矛盾があります。生成を必要とする問題ほど答えが長くなり、レイテンシが高くなり、降級が発動する確率も大きくなります。緩和の方向は主に三つです。一つはストリーミング出力で、初回バイトのレイテンシを最小限に抑え、ユーザーにまず骨組みだけを見せること。二つはオンデマンド展開で、まず結論の文と要点リストを出し、証拠の細部はユーザーが展開するかさらに問いかけた時点で補うこと。三つはルーティングで、簡単な問題には短い出力戦略を、複雑な問題にのみ長い出力を通すことです。
コストの観点から見ると、生成は大規模な検索システムの中で、検索全体のコストを上回りうる唯一のコンポーネントです。よく使われる最適化手段は三つです。モデル階層ルーティング(小さなモデルで簡単なクエリの大部分を処理する)、コンテキストの切り詰め(一定量を送り込むのではなく、本当に関連するチャンクだけを送る)、そして高頻度クエリに対する答えのキャッシュです。これらの手段の効果はかなり大きい一方で、一貫性の課題も持ちます。キャッシュされた答えが最新のデータと食い違う可能性があるため、新鮮性が関わる答えには短い有効期限を設定しなければならず、その失効ポリシーはインデックスの鮮度と揃えておく必要があります。
七、インデックスの鮮度と一貫性
増分インデックスを正しく扱うには
増分インデックス(Incremental Indexing)の核心的な課題は「書き込めるか」ではなく、「書き込んだあとに不整合が生じないか」です。主なリスクは三つあります。
一つ目は部分書き込みです。ドキュメントのベクトルは書き込まれたものの順引きフィールドがまだ更新されていないと、クエリのときに不完全なデータを掴んでしまいます。対策は世代切り替え、またはダブルライトバッファです。まず見えないステージング領域で全フィールドの書き込みをすべて完了させ、それが済んでから不可分(atomic)に可視状態へ切り替えます。二つ目は削除の不同期です。元データが削除されたのにインデックス側から削除されないと、すでに存在しない内容まで召回されてしまいます。権限やコンプライアンスの文脈ではこれは深刻な問題です。削除イベントは書き込みイベントと同じ処理パイプラインを通す必要があり、削除処理は再試行可能かつ検証可能でなければなりません。三つ目は一部のドキュメントにおける複数バージョンの問題です。ドキュメント更新の途中で旧バージョンが検索され、ユーザーが新バージョンを期待していると、再現の難しい散発的な問題になります。通常はドキュメントのバージョン番号を読み取り時に検証することで解きます。
フルリビルドの本当の代償
フルリビルド(Full Rebuild)はすべての不整合を解いてくれる特効薬のように見えますが、代償は過小評価されています。コストには、ソースデータ全体の読み込み、再トークナイゼーションと表の構築(sparse)、再エンコードとグラフ構築(dense)、ストレージへの書き込み、そして切り替え期間中の二重ストレージ占有が含まれます。sparse インデックスであればフルリビルドは数分で終わる程度です。ベクトルインデックスでは再構築時間がデータ量と次元の積に比例し、しかもグラフインデックスの構築は直列的で、並列化による高速化が困難です。より厄介なのは切り替えの瞬間のサービスへの影響です。新旧インデックスを併存させるとストレージのピーク使用量が倍増するか、切り替えのときに短時間の書き込みブロックが発生します。
現実的なのは混在戦略です。sparse インデックスはフルリビルドに増分修正を加える(コストが低く、inverted index の増分書き込み能力が十分あるため)、ベクトルインデックスは増分主体に定期的なフルリビルドを併用する(増分で蓄積したフラグメントと品質のドリフトを除去するため)、そしてフルリビルドはトラフィックの谷の時間帯に実行します。この組み合わせは「インデックスは常に絶対的に一貫している」ことを追うより現実的で、後者は経済的に実行不可能だからです。
一貫性ウィンドウはどう決めるか
一貫性ウィンドウとは、ソースデータが変更されてから検索で可視になるまでの時間です。この指標はサイト全体で一つの基準とするのではなく、業務シーンごとに段階分けるべきです。サポートのチケットや注文状態といった社内データは分単位、秒単位の可視性が要求されます。公開コンテンツは時間単位でも許容されることがあります。アーカイブデータや静的なナレッジベースなら日単位でも構いません。段階分けをしたら、異なるデータ領域を独立したインデックスインスタンスに分け、それぞれ独自の更新頻度と保持ポリシーを設定します。グローバルに一貫したインデックスを無理に作るより、構成はずっと単純になり、コストも安くなります。
さらに特別扱いすべき場合があります。ユーザー自身のコンテンツです。投稿した直後に検索しても自分のものが出てこない——これは利用体験にとってかなり不快な問題です。しかも「投稿直後のコンテンツはユーザー識別子でそのまま通す」というバイパスを使えば、全チェーンのインデックスが揃うまで待つことなく解決できます。ただしこのバイパスは必ず権限フィルタと組み合わせなければなりません。さもなくば権限外アクセスへのバックドアになります。
八、非機能要件:マルチテナンシー、権限、コスト
マルチテナント分離の三つの粒度
マルチテナンシー(Multi-tenancy)の分離粒度は三つあり、それぞれ実装コストがまったく異なります。
最も粗いのはインデックス単位の分離で、テナントごとに独立したインデックスインスタンスを持ちます。分離はもっとも徹底しています。データが物理的に共有されないからです。実装ももっとも単純で、権限フィルタに特別な仕組みは不要です。代償はリソース利用率の低さです。小さなテナントのインデックスは決して規模効果が得られず、テナント数が百万単位になるとインスタンス管理の負荷は受け止められません。
中間がコレクション単位の分離です。複数のテナントがインデックスを共有しますが、各テナントのデータにはインデックス内でテナント識別用のフィールドを持たせ、クエリのときにフィルタ条件として使います。リソース利用率が高く、実装も複雑ではないので、もっとも一般的な方式です。代償は、情報の漏えいを防ぐにはフィルタをインデックス層まで降下させなければならないことです。全件のスコアリングのあとにフィルタすると、関係のない情報が存在することを漏らしてしまいます。
最も細かいのはドキュメント単位またはフィールド単位の分離です。インデックス構造は共有しますが、結果を返す前に一件ずつ権限を検証します。権限ルールが複雑な場合(たとえばロール、グループ、行・列レベルのルール)には避けられませんが、性能上の問題をインデックス層からアプリケーション層へ押し出すことになり、本番環境のボトルネックになりがちです。
実践上の判断基準ははっきりしています。テナント数が少なく使用量の差が大きいならインデックス単位の分離、テナント数が多く使用量が均等ならコレクション単位の分離、そして権限ルール自体が複雑な場合(これはエンタープライズでは常态です)は、コレクション単位の分離の上に正確な検証を一層載せ、部门や事業ラインごとにハイブリッドにします。
権限フィルタは検索層まで降下させる必要がある
これは検索システムの中で安全事故を起こしやすい、もっとも厄介な個所です。よくあるけれど誤ったやり方は、検索時にフィルタせず、まず結果を取り出してから権限ロジックで見えない部分を落とすというものです。このやり方には三つの問題があります。第一に、情報の存在そのものが漏れることです。ユーザーは応答時間や件数の変化から、見えないコンテンツが存在すると推測できてしまいます。第二に、検索の計算量が無駄になります。見えている結果を十件返すために、候補を数百件取り出して一件ずつ検証する必要があります。第三に、近似最近傍のシーンではそもそも成立しないことです。フィルタがベクトル検索のあとに行われると、ユーザーが recall できる結果はごく数件しかなく、件数が揃わないまま終わることがあります。
正解は、権限条件をベクトル検索の後段の処理としてではなく、フィルタ(Filter)として検索層へ降下させることです。近似最近傍のインデックスは通常、条件付きのフィルタクエリをネイティブにサポートしており、実装には「先にフィルタして検索する」「先に検索してからフィルタする」「両者のハイブリッド」の三パターンがあります。三つ目が最も最適です。まずフィルタ条件つきの検索を一度行い、フィルタ後に残る候補が足りなければ、もう一度より広い条件で検索してからフィルタすることを繰り返し、件数が条件に合うまで幅を調整していきます。sparse の経路も同様に、inverted index の読み取り時にフィルタ対象のフィールドで短絡できます。全件を抜いてからフィルタするより格段に効率的です。
権限フィルタには見落とされがちな点が二つあります。第一に、フィルタ条件はリクエストレベルで信頼されなければならず、ユーザー入力から直接取ってはならないということです。これは典型的な injection の攻撃面です。第二に、権限の変更とインデックスの更新との間には遅延があり、権限を取り消したばかりのドキュメントでもインデックスには古い識別子が残っています。対策は「権限バージョン番号」の水位表を維持し、リクエストにユーザーの権限バージョンを持たせることです。バージョンが古いリクエストは、検証を一度走らせて昇格させる経路を通します。
大規模インデックスのストレージとコスト
ストレージコストは数千万件以下では通常問題になりませんが、億単位になると主要な矛盾になります。押さえておきたいのはいくつかの積です。ドキュメント数 × ベクトル次元 × 1 次元あたりのバイト数で元のベクトル容量になり、inverted index の容量はコーパスの平均 token 数とドキュメント数で決まります。そして順引き(forward)ストレージの容量はフィールドの総量で決まります。順引きが三つのうちでもっとも大きいのは、そこが原文を保持しているからです。しかも冗長なコピーが通常複数併存しています。
ストレージコストを下げる一般的な手段を効果対比順に並べると、ベクトル量子化(float を低精度の整数に圧縮する、もっとも直接的な手段)、コールド/ホット階層化(低頻度でアクセスされるデータを、より安価で容量の大きいストレージへ移す)、順引きフィールドのオンデマンドロード(高頻度でアクセスされるフィールドだけを保持し、低頻度のフィールドはオブジェクトストレージへ)、そしてドキュメント本文と検索に必要なメタ情報の分離(検索にはインデックスと要約さえあればよく、本文は必要に応じてメインストレージから読み込む)です。これらを組み合わせた効果は通常かなり明確です。ベクトル部分の割合は全体のストレージの半分にも満たないことが多く、空間の大半は順引きと本文のほうにあるからです。
よく過小評価されるコストがもう一つ、キャッシュです。クエリ単位の結果キャッシュも、ドキュメント単位のメタデータキャッシュもメモリを要します。検索システムでは、この両方のヒット率は通常かなり高い。キャッシュを設計するときは失効条件を明確にしておく必要があります。結果キャッシュの失効はインデックスの世代変更によって起こり、ドキュメントキャッシュの失効はドキュメントの更新によって起こります。明確な失効のトリガーがないとユーザーは古い内容を見ることになり、この種の問題はテスト環境ではまず表面化しません。
信頼性と障害ドメイン
検索システムは典型的な「局所障害、全体不可用」のシステムです。ある経路の recall がタイムアウトしても、見た目には品質の低下というだけですが、降格ロジックが満たされていなければ、全体が 500 になります。したがって信頼性設計の要点は、各 recall 経路に独立したタイムアウトとサーキットブレーカーを設けることで、単一経路の障害がその経路の寄与だけに留まるようにすることです。サーキットブレーカーの発動条件はエラー率だけを見れば足りず、レイテンシの分布も見る必要があります。レイテンシ分布の長い尾が滞留を生み、最終的に雪崩に化ける一方で、その時点のエラー率はまだ正常なままということがあるからです。
九、評価体系:オフライン指標とオンライン指標の乖離
オフライン指標はそれぞれ何を測るか
召回率(Recall@k)は正しいドキュメントが候補セットに含まれたかどうかを測り、召回層の網羅性を反映します。reranking や生成を改善してもこの値は動きません。平均逆順位(Mean Reciprocal Rank、MRR)は最初に見つかる正解が何番目に現れるかを測り、recall より頭部を重視しますが、二位以降の分布はまったく見ません。nDCG(Normalized Discounted Cumulative Gain)は位置による重みづけに加えて、複数の関連結果の関連度グレードも同時に考慮するため、ソート品質をもっとも総合的に評価できるオフライン指標です。
この三つの指標は別々の問いに答えるもので、これらで判断すると結論も違ってきます。たとえば生成の品質を改善した場合、この三つのオフライン指標はどれも動きませんが、オンラインでは顕著な改善が見られることがあります。逆に、reranking モデルのオフライン指標が二三点上がっても、オンラインでは何も変わらないことがあります。理由は、これらの指標がすべて「ユーザーは最後まで丁寧に読み、関連度で判断する」と仮定しているからです。しかし実際のユーザーの行動はクリック、走査、離脱であり、並びの意味づけがまったく違います。
乖離の具体的な現れ方
オフラインとオンラインの乖離は通常、いくつかの形で現れます。第一にアノテーション分布の偏りです。アノテーションセットのクエリはすべて丁寧に構成されたもので、オンラインの実トラフィックにおける長尾の割合はこれをはるかに上回ります。結果として、オフライン最適化による効果は、ユーザーがほとんど触れない部分集合に集中してしまいます。第二にクリックバイアスです。ユーザーは上位にしかクリックしないため、クリックログから作った訓練シグナル自体が位置バイアスを含み、その結果 CTR モデルがバイアスを引き継ぎて拡大します。第三に「正解が候補セットに入っていない」場合です。ユーザーの本当の意図は複合的で、正解ドキュメントが一つだけでは回答になりません。アノテーションも一つのドキュメントしか付けていないため、オンラインのユーザーは依然として満足しません。第四に、レイテンシが完全に無視される点です。すべてのオフライン評価はレイテンシを数えません。レイテンシを 200 ミリ秒から 2 秒に引き上げた案でも、オフライン指標の上では依然として「勝ち」ます。
A/B test の設計で押さえるべき点
検索システムの A/B test には、いくつか特に注意すべき特殊点があります。
トラフィック分割はクエリではなくユーザー単位でなければなりません。検索には強い重複クエリ現象があるため、クエリ単位で分けると同一ユーザーが二つの版を見てしまい、混同が生じ、結果の帰属も難しくなります。
実験期間は完全な利用サイクルを覆う必要があります。曜日内の行動パターンは平日と休日で大きく異なります。二日間の実験だけでは、長期的な結論と真逆の結果が出てしまいます。祝日や大型イベントの汚染期間も避けるべきです。
指標は複合的でなければなりません。妥当な組み合わせはこうです。成功(ユーザーが最初の結果を見てすぐに戻らないこと)をガードレール指標に、有効クリック率やクリック後の滞在時間を主指標に、人工満足度サンプリングを較正指標にします。ガードレール指標が基準を満たさない場合はロールアウトしません。このルールは実験開始前に決めておき、事後的に都合のよい解釈を許さないようにするのです。
サンプルサイズは事前に見積もってください。クリック率を例にすると、ベースライン値が小さな改善を検出するために必要なサンプル数を決めます。ベースラインが高いほど必要なサンプルは大きくなります。高クリック率の検索シーンでは、数パーセントの差でも統計的有意性に到達するには相当のトラフィックを要します。つまり、表面上は些細な変更でも短期のうちには検証できない、ということです。これ自体が重要なアーキテクチャ上の制約になります。長い実験でしか確認できない変更は、コアな経路に置くべきではないのです。
軽視できないガードレール指標
主指標とガードレール指標に加えて、A/B test ではいくつかの「安全型」指標を固定で監視すべきです。それ自体が改善する必要はありませんが、悪化すればシステムに問題があることを示すからです。P95 と P99 のレイテンシ、検索の失敗率(タイムアウト、サーキットブレーカー、降格のトリガー比率を含む)、無結果率、そして引用検証の拒否率です。これらの指標はアーキテクチャ変更の安全網を構成し、事後的な振り返りよりずっと有効です。
十、2026 年のいくつかの進展方向
ハイブリッド検索の融合はさらに進んでいる
sparse と dense の融合は、初期のころは結果単位のスコア加重でしたが、いまはより細かい複数の粒度まで進んでいます。興味深い方向がいくつかあります。一つは term 単位の融合です。二つの独立した並びに対して加重するのではなく、同じ scoring function のなかで term マッチングのシグナルとベクトル類似度を二つの特徴量として明示的に融合し、両者の相対的な重みを reranking モデル自身に学ばせることです。二つは sparse 検索を inverted index から学習可能な sparse 表現へ置き換えることです。sparse 性が単語分割の規則から生まれるのではなく学習から得られるようになり、正確な一致を保ったまま単語分割の誤りを避けられます。三つは end-to-end 学習で、sparse、dense、reranking を一つの結合した目的関数の中で最適化し、各層の出力分布が互いに分離可能にすることです。この三つのルートの成果の大きさはコーパスの性質に強く依存します。語彙のゆらぎが大きいシーンほど、term 単位の融合と学習可能な sparse の効果は大きくなります。
Agentic Search の実際の効果と請求書
モデルに検索の回数も検索対象も自分で決めさせるのは、いまもっとも変化の速い方向のひとつです。その魅力は直接的で、単一回の検索では明示的に表現された意味しかマッチングできない一方、複雑な問題は分解が要るからです。まず実体を尋ね、次に関係を尋ね、次に反例を尋ね、失敗したあとに別の検索方式へ切り替える必要があることまであります。これが「検索」を「調査」へ引き上げる決定的な能力です。
ただし代償はしっかり数えておく必要があり、二本立てで考える必要があります。
一つ目はレイテンシです。多輪検索は厳密に直列で、各ラウンド少なくとも一回のモデル推論と一回の検索を含みます。輪数が増えればレイテンシはほぼ線形に伸びます。数百ミリ秒を目標とする検索エントリでは、二、三ラウンドで既に予算を超えます。このため Agentic Search はデフォルトモードではなく「ディープモード」と位置づけるべきです。ユーザーが自分で数十秒待てるエントリへ切り替えるようにします。
二つ目はコストと制御不能性です。輪数をモデルが自主的に決めるため、コストの上限は確実な保証を失い、病的なクエリが大量の無効検索を引き起こす可能性があります。しかも各ラウンドは一つの意思決定の機会であり、意思決定の質が最終的な効果を決め、誤った決定は連鎖的に増幅されます。エンジニアリング上必要な制約は三つです。ラウンド数に硬い上限を置くこと、各ラウンドに検索クォータを与えること、そして各ラウンドの間で「現在の情報は十分か」を明示的に判定し、無意味な検索の継続を避けることです。さらにフォールバックも用意しておく必要があります。ラウンドを使い切ったときは、既存の材料から不確実性を付記した回答を生成し、情報が十分であるかのように振る舞わないことです。
| モード | レイテンシの桁 | コストの桁 | 向いている問題 |
|---|---|---|---|
| 単一回のハイブリッド検索 | 百ミリ秒級 | 低 | 事実型のクエリ、ナビゲーション、正確一致 |
| クエリ書き換え+単一回の検索 | サブ秒級 | 中 | 口語表現、別名、多輪の指示 |
| 生成型サマリー | 秒単位 | 中高 | 総覧型、総合的な判断が必要なもの |
| Agentic 多輪 | 十秒級またはそれ以上 | 高、上限なし | 複合的な問題、比較や多角的な検証が必要なもの |
この表の重要な情報は具体的な数字ではなく、四つのモードのレイテンシとコストが桁をまたがっているという点です。したがってこれらは同じインターフェースの中の分岐ではなく、異なるプロダクトのエントリポイントでなければなりません。大半のトラフィックは前の二つの枠へ流して、後の二つは本当にそれを必要とするユーザーに残すべきです。これは全体をコストに収める上でもっとも有効な手段でもあります。
semantic cache と類似クエリの再利用
semantic cache(Semantic Cache)は、ベクトル類似度で新しいクエリと過去のクエリが意味的に等価かどうかを判定し、その結果を再利用します。重複率の高いシーン(サポート、社内のナレッジベース、話題イベント)での効果は非常に大きく,この種のシーンでは大量のクエリが意味的に同義だからです。ただしリスクも同様に明確です。
一つは鮮度のリスクです。あるイベントに関する答えをキャッシュ했고、そのイベントがその後進展した、キャッシュヒットは古い内容を返してしまいます。キャッシュには失効時間とインデックスの世代を結び付け、インデックス更新時に能動的に失効させなければなりません。二つに権限のリスクがあります。ユーザーごとにクエリの意味は等価でも権限が異なるため、結果をそのまま再利用すると権限外アクセスになります。キャッシュキーにはユーザーの権限コンテキストを含めるか、いっそ権限を伴わない公開クエリだけに絞るのが安全です。第三にエラーの伝播リスクです。誤った答えがキャッシュされると継続的にヒットし、訂正のコストが非常に高くなります。したがって高い確信度で判定された結果だけをキャッシュに入れるべきです。
実践でもっとも穏当なのは、semantic cache を答えの本文ではなく結果タイトルの層に置くことです。同じクエリの top の結果リストの再利用はもっと安全であり、生成された答えは毎回計算し直すので、鮮度とエラー伝播のリスク範囲ははるかに小さくなります。
検索と Agent の境界
Agent(自律エージェント)が新しいエントリポイントになってくると、「検索はどのような形にすべきか」という問いが繰り返し提起されます。使える判断基準は、インターフェースが何を約束しているかを見ることです。
もしインターフェースが「権威ある答えを提示し、追跡できるようにする」と約束しているなら、それは検索システムであり、厳密な召回、レイテンシ予算、引用検証、評価体系を持たなければなりません。もしそのインターフェースが「複数ステップの操作と判断を必要とするタスクを完了させる」と約束しているなら、それは Agent システムであり、出力には副作用があります。一連のアクション、一度の意思決定、一つの成果物でありうるからです。
両者のエンジニアリング上の重点は異なります。検索システムの中心的な矛盾はレイテンシと品質のトレードオフであり、Agent システムの中心的な矛盾は自律性と信頼性のトレードオフです。したがってより合理的なアーキテクチャは、二つのシステムが共存して相互に呼び出す構造です。検索システムが一つのツールとして Agent から使われ、Agent には構造化され、追跡可能で、レイテンシの保証を持つ結果インターフェースを公開します。Agent は分解と決定を担当し、検索は取得と検証を担当します。検索システム自体に多輪の自律的な決定を背負わせると、Agent の柔軟性も検索の信頼性も失われます。逆に Agent から直接元のデータソースへアクセスさせると、測定可能性が失われます。この境界は、2026 年になってもなお成り立つエンジニアリング上の共通認識だと考えています。
十一、落とし込み可能なリファレンス実装
以下のコードは、これまで議論してきた各層を一つのリクエストフローにつなぎ合わせたものです。実行可能なプロダクションコードではなく、鍵となる意思決定ポイントを明示的に書き出した骨格です。各層のタイムアウト、降格、並列化の方法、そして信頼度の閾値をすべてコードに書いています。まさにここが、アーキテクチャが実際に落とし込まれたときに意見が分かれるポイントだからです。
def search_pipeline(req):
#1.查询理解层:意图、时效、改写,各自独立超时,失败可跳过
intent = classify_intent(req.q, timeout=15ms)
fresh = detect_freshness(req.q, intent)
variants = [req.q]
if intent.rewrite_needed:
rw = llm_rewrite(req.q, history=req.history, timeout=60ms)
#原查询与改写并行召回,而不是替换
variants.extend(rw.queries)
#2.召回层:各路并行,按计划分配配额与超时
plan = build_plan(intent) #每路的开关、配额、权重来自配置
pools = parallel_collect([
sparse_search(req.q, plan.sparse, filter=acl_filter(req)),
vector_search(req.q, plan.vector, filter=acl_filter(req)),
sparse_search(variants[1:], plan.expansion, filter=acl_filter(req)),
hot_search(req, plan.behavior, filter=acl_filter(req)),
], timeout=120ms)
#3.融合层:按配额截断后做分位数归一化加权
candidates = fuse(pools, quota=plan.quota, target=1000)
#4.置信度闸门:这一层决定生成是否会引入幻觉
quality = judge_quality(candidates, intent)
if quality.score < THRESHOLD[intent.type]:
#明确返回无解,而不是让模型硬答
return no_answer(retry_hint=quality.hint, candidates=candidates[:5])
#5.重排层:轻量层处理千级,强模型处理头部,自适应跳过简单查询
if intent.simple:
ranked = candidates[:10]
else:
coarse = light_rerank(req.q, candidates[:800], timeout=200ms)
ranked = strong_rerank(req.q, coarse[:80], timeout=180ms)
ranked = diversify(ranked) #去掉同源近重复
#6.呈现:多档延迟预算,按预算选交付形态
if budget.remaining() < GENERATION_BUDGET:
return render_list(ranked[:10])
if budget.remaining() < GENERATION_BUDGET * 0.5:
return render_summary_with_stub(ranked) #首屏流式
return render_full_answer(req, ranked, budget=budget)
この骨格の中には単独で指摘する価値のある五つの点があり、本文のすべての結論がここに集約されます。
一つ目は手順 1 の、書き換えの結果が元のクエリと並行するものであり、置き換えではないという点です。書き換えはバイアスを持ち込むので、元の経路を残すのが唯一のフォールバックです。二つ目は手順 2 の、各 recall 経路が同じ権限フィルタ条件を携えているという点です。フィルタはインデックス層まで降下させ、後段のフィルタによる情報漏えいや候補不足を避けています。三つ目は手順 4 の信頼度ゲートで、「検索が見つからなかった」場合と「見つかったが質が不十分」な場合の両方を、正当な無解答のレスポンスに変えてしまいます。これが生成の幻覚を抑えるもっとも効果的な単一の設計です。四つ目は手順 5 の適応的なスキップで、簡単なクエリは reranking の予算を消費しません。これが直接にカスケード reranking の平均コストを決めます。五つ目は手順 6 の、残りの予算に応じて納品形態を選ぶという設計で、まず完全な答えを作ってからタイムアウトで切り落とすのではなく、納品形態そのものを先に選ぶ設計です。
スケジュール上の推奨順序
ゼロから構築するなら、経験上の優先順位はこのようになります。まず recall 層のハイブリッド検索です(sparse と dense を各一路、加えて従来型のあいまい検索をフォールバックとして一路)。正解率をまず許容できる水準に乗せます。次に軽量 reranking を入れ、蒸留で強いモデルにアライメントします。この段階の効果が通常もっとも大きくなります。その上にクエリ理解を重ねます(まず意図と鮮度の識別からで、書き換えは後で構いません)。そして評価体系です。この手順は精密なチューニングより必ず先で、そうしないと変更が改善なのか改悪なのか判断できません。次に引用検証と生成です。その際、信頼度ゲートを生成より前に置いてください。最後に Agentic のディープモードや semantic cache といった最適化です。
この順序の論理はこうです。前半の手順は効果が確定的で、リスクが低く、オフライン指標で検証できます。後半の手順は効果がシーンに依存しオンライン実験を要するため、早めに投資すると評価できないものを作ってしまいます。
十二、おわりに
AI 検索システムのアーキテクチャ判断は、実質的には四つの互いに衝突する要求のあいだでバランス点を探すことにあります。recall の網羅性、レイテンシの予算、生成の信頼性、そしてコストの制御可能性です。この四つが同時に最適になることはありえないので、アーキテクチャ設計の中心的な作業は「どんなシーンで何を犠牲にするか」を明示することに尽きます。
文書全体を貫く判断をまとめると、こうです。どの層にも降格の経路を用意する。過負荷時の挙動は、ピーク性能よりもアーキテクチャの質をよく語ります。recall の段階で従来の検索経路を残すコストは非常に低く、効果は非常に大きいので、最適化で消してはいけません。権限フィルタは検索層まで降下させなければなりません。これは最適化の問題ではなく安全性の問題です。引用は独立した検証を通って初めて成立します。番号が存在することと、引用が成り立つことは別です。評価体系は精密なチューニングより先に用意してください。さもないとすべての最適化が盲飛びになります。レイテンシ予算はコードの一行目より前に切り分け、各段階について予算を超えた場合の対処を事前に決めておくことです。
最後に一点強調しておきます。検索システムの品質の差がアルゴリズム自体の先端さから来ることはまれで、多くは工学的ディテールの完成度から来ます。インデックス書き込みの一貫性、降格経路の実装、評価セットの品質、実験期間の十分さです。この種の作業に近道はなく、より強いモデルへ差し替えることで代替することもできません。同じ技術スキームでも、チームが違えば実際の効果が大きく違うのはこのためです。
十三、見落とされがちな工学的ディテール
長いドキュメントの扱い方がシステムの上限を決める
ドキュメントの分割(Chunking)の影響範囲は直観をはるかに超えています。インデックスの容量、recall の粒度、生成の証拠の完全さ、引用の特定可能性に同時に影響します。変更が一度で、結果がすべての層に現れる、稀有な意思決定点のひとつです。よく使われる方法は、固定長での分割、意味の境界での分割、そして構造に沿った分割(見出し階層、節、リスト項目)です。
固定長はもっとも実装が簡単ですが、まとまった議論を途中から断ち切ってしまい、両方のチャンクがコンテキストを失い、reranking モデルにも生成モデルもその関連性を判断できません。意味の境界で分割する(自然段落、見出し)ほうが妥当な選択です。段落そのものが、著者が表現した最小の完全な単位だからです。代償はチャンクの長さが不揃いになることで、長い段落はベクトルエンコードの有効範囲を超える可能性があり、超長分割をもう一段重ねる必要があります。
より投資に値するのは、チャンクに構造情報を持たせることです。所属ドキュメント ID、見出しのパス(h2/h3 の階層チェーン)、ドキュメント内での順序、そして前後のチャンクへの参照。これらのメタデータには三つの用途があります。reranking の特徴量がチャンクのドキュメント内での位置を感知できるようにします(チャンクがドキュメントの冒頭に現れるのは、末尾に現れるより内容を代表するからです)。生成の引用が完全なパスを表示できるようにします(「ドキュメントタイトル › 二階層見出し › 三階層見出し」は、孤立した一文よりはるかに役に立ちます)。そして結果の重複排除を、チャンク単位ではなくドキュメント単位でできるようにします。
チャンクのオーバーラップは、内容の種別ごとに決めるべき問題です。技術ドキュメントや Q&A の組では、各段落がそれ自体で完結しているため、オーバーラップは小さく、ゼロでもかまいません。叙述的なテキストや長い議論では、段落をまたぐ情報を保つために一定量のオーバーラップが必要です。オーバーラップ率を上げるとインデックスの容量と recall 時の重複候補率がそのまま押し上がるので、グローバルな一つの値ではなく、ドキュメント種別ごとに設定すべきです。
重複排除と多様性
ベクトル検索には有名な性質があります。高得点の結果はたいてい同一ドキュメントの隣接する複数のチャンクからもたらされます。これは生成フェーズではよいこと(材料が連続する)ですが、結果提示では悪いこと(ユーザーがほぼ同じ結果十条を見る)です。したがって結果提示の前に重複排除の工程が必須です。
重複排除の強さはシーンごとに分ける必要があります。ユーザーが欲しいのが総覧であるなら、同源のチャンクを複数残すことで生成により完全な材料を与えられます。ユーザーが欲しいのが一つのドキュメントの所在であるなら、ドキュメント単位で重複排除し、各ドキュメントではもっとも関連するチャンクだけを見せるのが正解です。実装上は二層の重複排除を同時に行うほうが堅実です。まずドキュメント ID によるハードな重複排除で単一ドキュメントの比率を制限し、次に残った候補に対して意味的な近重複検出を行います(チャンク間の類似度が閾値を超えたら一つだけ残す)。後者は追加の類似度計算が一度必要になりますが、この時点では候補数がすでに小さいため、コストは許容できます。
無結果の扱いは、有結果の扱いより設計する価値がある
無結果のシーンは、検索システムの中でもっとも過小評価されている部分です。発生頻度は高くありませんが、ユーザーがシステム全体に対して抱く信頼度を直接左右します。ユーザーは「検索できない」ことを異常とは受け取らず、システムが壊れているか、収録されていないと考えるからです。
妥当な対処は三層に分かれます。第一層は recall の拡張です。現在のすべての経路が空を返したとき、制約を緩めて再試行します。厳しすぎる時間フィルタを外す、ベクトル検索の類似度閾値を緩める、クエリの低重みの修飾語を外す、意図識別で得た強いフィルタ条件を無効化する、といった具合です。第二層は書き換えです。それでも解が見つからないときは、クエリをより単純な部分に分解して別々に検索するか、ベクトル検索で意味的にもっとも近い利用可能なコンテンツを探します。第三層でようやく明示的な通知になります。ただし「見つかりませんでした」と一言で済ませるのではなく、具体的に実行できる提案を添える必要があります。候補となる代替語、条件を緩める具体的な案、あるいは関連する人気のコンテンツなどです。
無結果の比率そのものは、継続的に監視すべき指標です。比率の急上昇は通常、ユーザーのクエリが変わったのではなく、インデックス側で問題が起こったことを意味します(構築の失敗、世代切り替えの停止)。同時に、これはとても良いヘルスチェックのシグナルにもなります。
段階リリースとインデックスの互換性
インデックス構造の変更(トークナイザの差し替え、ベクトルモデルの差し替え、チャンク分割方針の変更)は、新旧インデックスが共存できないことを意味します。再構築のウィンドウをゼロにすることは不可能です。エンジニアリングで実行可能なのは、ダブルライトとダブルリードの組み合わせです。並列のインデックスインスタンスを新しく作り、内容を両方へ同時に書き込み、読みのトラフィックを比率で新旧に配分し、段階ごとに両者の結果差分を比較し、差分が安定したところで新インデックスへ切り替えて古い方を廃棄します。
このプロセスでの重要な制約は、インデックスの世代識別子が結果まで貫通している必要があるという点です。reranking と生成の評価はインデックスバージョンごとにグループ化して集計する必要があり、そうしないと新旧インデックスの効果が混ざって、信頼できる結論が出ません。もう一つの制約は、クエリ理解層がインデックス形式について置いている前提です。たとえばクエリ側の分割はインデックス側と一致していなければならず、トークナイザを替えるとこの前提は散在する多くの場所で破れます。したがって分割の呼び出しを一つのモジュールに収斂させておくのは、この種の変更を一箇所の変更に収めるためです。
コストの可観測性
検索システムのコスト分布はしばしば直観と逆です。トラフィック比率がもっとも高い一部のクエリが、消費するコストの割合は極めて小さいままかもしれません。一方、少数の複雑なクエリが大部分を消費します。コストを管理可能にするには、クエリの軸でコストを分解して記録しなければなりません。最低限、意図ごとのリクエスト比率、各 recall 経路の候補数、reranking が実際に処理した候補数、生成の入出力 token 数、そしてキャッシュヒット率です。
この一連のデータがあって初めて、「この意図はもっと安い検索モードへ切り替える」といった根拠のある判断ができます。このデータがない場合、コスト最適化は全体のパラメータ調整でしかなく、通常は高価値なユーザーを損ないながら、得られる効果は目立ったものになりません。
十四、検索から答えまでのエビデンスチェーン
エビデンスチェーンは引用より重要
引用が解決するのは「答えのどの一文かをクリックして検証できるか」という問題です。けれどユーザーが本当に信頼したいのは、一本の完整なエビデンスチェーンです。ユーザーの質問から始まり、クエリ理解がどう解釈され、検索がどんな材料を見つけ、材料がどう絞り込まれ並べ替えられ、最終的な答えのどの文がそれぞれどこから来たのか、という流れです。この鎖のどこか一つでも隠されますと、ユーザーは結論しか見せず、その結論の信頼性を判断できません。
この鎖を明示することには具体的な実装上の価値があります。エラーを特定できるからです。ユーザーが誤った答えを報告したとき、システムは「今回の検索の理解が X で、Y 件の候補を recall し、正しいドキュメントはその中の Z 番目にあり、除外された理由は W です」と答えられます。この鎖がなければ、原因究明は人手による再現しかなく、検索システムのクエリ分布は極端に長い尾を持つため、再現率はきわめて低くなります。原因究明の能力をアーキテクチャに組み込むのは、事後にログを足すよりずっと安いのです。
具体的に記録へ落とす項目は、元のクエリ、意図判定の結果、鮮度のパラメータ、各経路の recall 件数とクォータ、ブレンド後の top 順位、各候補が reranking でどれだけスコアを動かされたか(reranking で大きく下げられた候補は、しばしば問題の手がかりです)、最終的にコンテキストへ選ばれたチャンクのリスト、そして答えの各文の引用バインディングと検証結果です。これらの記録はストレージコストを抑えるため一部の比率のリクエストでサンプリングしても構いませんが、無結果のリクエストと信頼度の低いリクエストは全量記録すべきです。情報密度がもっとも高いからです。
チャンク単位のエビデンス特定
引用を本当に使えるものにするには、特定の精度はドキュメントではなくチャンク単位まで必要です。実行可能な設計として、生成の段階でドキュメント識別子ではなくチャンク識別子を出力させるものがあります。あとからチャンクからドキュメントへの対応表を引いて、クリック可能なリンクを復元します。このやり方の追加の利点は、チャンク単位のハイライトを支えられることです。ユーザーが引用をクリックすると、その文を支えている段落がそのまま表示され、長いドキュメントの中で自分で検索する必要がなくなります。「引用」を形式上の記号から実用的な機能へ変える、決定的な一歩です。
チャンク識別子の安定性も考えておく必要があります。チャンク分割の方針が変わって識別子がすべて変わると、歴史的な引用はみな無効になります。実行可能なのは、識別子の基盤を「ドキュメント ID+連番」ではなく「ドキュメント ID+コンテンツハッシュ」にすることです。そうすれば再分割しても、同じ内容のチャンクは依然としてマッチし、少なくとも内容の類似度によって旧引用を新チャンクへ移行させることができます。
十五、評価セット構築のコスト
評価セットは検索システムでもっとも希少な資産
検索システムのあらゆる判断は評価セットに依存しますが、信頼できる評価セットを構築するコストはコードを書くよりずっと高昂です。実用最小限の評価セットには三つのものが必要です。実際の分布に沿ったクエリの集合、人手でアノテーションした関連度の判断、そして安定した階層化の方式(少なくとも意図と難易度で分ける必要があります。分けない合計スコアは最適化をほとんど指導できません)。
クエリの入手元としてもっとも実用的なのは人が作るのではなく、オンラインのログからサンプリングすることです。人が作ったクエリは「妥当で、答えのある」側に強く偏り、実際のトラフィックの長尾の割合はずっと高いままです。より重要なのは、ログのクエリにはコンテキスト情報(誰が、いつ、どこから検索したか)が付いていることです。その情報が正解の判断基準を決めるので、同じクエリでもユーザーによって正解が異なることがあります。
アノテーション工程でもっとも問題になりやすいのは粒度の不揃いです。アノテーターに「このテキストはクエリに関連しているか」という二択を投げると、回答者の相互一致性は一般に必ずしも理想的水準にはなりません。より有効な設問の形は、並び替え済みの候補リストを示し、回答者に順序を調整してもらうものです(関連度の選好判断)。この作業は人間にとってより自然で、実際の並び替えのニーズにも近づくからです。具体的なアノテーション方法と一致性チェックの方法はツールの実装に依存し、コストと品質のトレードオフに汎用的な答えはありません。
階層別の評価は合計スコアより有用
各種のクエリを含む評価セットの合計スコアには、ほとんど指導価値がありません。合計が十ポイント上がったとしても、ある種類のクエリが大きく改善して他の種類がわずかに後退した結果にすぎない場合が多いからです。本当に役に立つのは階層別の結果です。意図別(ナビゲーション型、事実型、探索型、比較型)に指標を見ること、難易度別(上位のクエリ、長尾のクエリ、新興のクエリ)に指標を見ることです。
階層別の結果があれば、优化の方向は明確になります。たとえばナビゲーション型の recall 率が九割近く、探索型が六割にとどまっているなら、問題は全体の recall ではなく意味の一般化にあります。上位のクエリは調子が悪い一方で長尾は良好、というなら、問題は reranking の上位部分の精度にあります。これらの判断は合計スコアからは導けませんし、次のラウンドでどの層を触るべきか直接に決めます。
オンラインのシグナルで弱教師データを作る
人手によるアノテーションセットは、いつだって小さく、モデルの反復を動かすには足りません。もう一つの道はオンライン行動から弱教師シグナルを作ることです。ユーザーがある結果をクリックしたなら、少なくとも彼にとって関連があるとみなせます。クリック後すぐ戻らなかったなら、内容品質はまずまずです。より上位に並ぶ結果をスキップしたなら、その結果は関連していない可能性があります。これらは reranking モデルの学習に使えますが、位置バイアスを扱う必要があります。もっとも直接的な対策は上位数か所だけのクリックとスキップのパターンを信用し、明示的にナビゲーション型とわかるクリック(クリック後すぐ戻ってそのまま対象ページに入るもの)は別としてモデル化することです。
弱教師セットは人手のアノテーションセットの代わりにはなりません。両者の用途は異なります。弱教師セットは量が大きくカバレッジが広いのでモデルの学習に適し、人手のアノテーションセットは小さいが正確なので、譲れない評価の基準に適します。片方だけを残すと、この両者の間にバイアスができても気づけません。
十六、品質回帰とオンライン運用
設定はコード、そして設定は品質のパラメータ
検索システムには効果を決めるパラメータが大量に存在します。各 recall 経路のクォータ、ブレンドの重み、鮮度の減衰係数、reranking の段数、重複排除の閾値、意図ごとの閾値ゲート、キャッシュの有効期限です。これらがコードや各所の設定センターに散らばっていると、「先週品質が悪くなったのはなぜなのか」といった問いに答えられなくなります。したがってこれらをバージョン管理された一つの設定へ収斂させ、設定変更のたびに追跡可能なリリース記録を紐づけることが、検索システム運用の最初のエンジニアリング業務です。
設定をひとまとめにして初めて、パラメータを走査する形での回帰検証ができます。評価セットを固定し、パラメータを一つずつ妥当な範囲で取值し、階層ごとの指標の変化を観察し、結果を一覧の表にまとめます。この手順の価値は最適なパラメータを見つけることだけではありません。各パラメータの感度を把握することのほうが重要です。わずかに調整しただけで効果が大きく変わるパラメータ(変更を厳密に管理しなければならない)と、広い範囲で動かしてもほとんど反応しないパラメータ(ビジネス側に自由に調整させてもよい)の区別です。パラメータ感度という考え方は実践上の価値が厳しく過小評価されており、チームがどこにレビュー労力を投じるべきかを直接に決めます。
常駐監視すべきヘルス指標
先ほど触れたガードレール指標に加えて、次のグループの指標の異常は通常、ユーザーのクレームの前に問題を発見できます。単独で挙げておく価値があります。
インデックス側で監視するのは、各 recall 経路の空結果の比率(ある経路が突然ゼロになるなら、通常は構築かサービスの異常を意味します)、インデックスの世代遅れ、そしてインデックス構築の失敗率と再試行回数です。recall 側で監視するのは候補数分布のドリフトで、分布が突然縮むなら、ある種類のクエリが気づかれない失敗に該当しているということです。生成側で監視するのは無解答率、引用検証の拒否率、そして生成の平均出力長です。提示側で監視するのは結果のクリック位置の分布です。
このグループの指標に共通する特徴は「品質の指標ではなくシステム状態の指標」であることです。異常はほぼ常に効果の揺らぎではなく、具体的な故障を指すため、アラートの精度は高くなります。これに対して、関連度の指標の短期的な揺らぎは故障なのかノイズなのかの判断が難しく、アラート価値はむしろ低くなります。この二つのシグナルは分けて監視し、混ぜてはいけません。
障害訓練
検索システムの降格経路は日常まったく実行されないため、能動的に訓練しなければなりません。訓練に値するシーンとしては、ベクトルインデックスサービスが完全に利用不能、ある recall 経路のレイテンシが急騰して全体がタイムアウトする、reranking モデルのタイムアウトが大量に発生する、生成サービスにレート制限がかかる、インデックス構築タスクが長時間ハングして世代が遅れる、そして権限サービスの異常でフィルタ条件が得られない、といったものがあります。
どのシーンの訓練にも、明確な答えを一つ置いておかなければなりません。ユーザーには何が見えるのか、そしてその結果は受け入れてよいのか。特に重要なのは最後のシーンです。権限サービスの異常時のデフォルト動作は「許可」ではなく「拒否」でなければなりません。そしてこの種のデフォルト値は、日常のほぼ到達しない分岐にあるため、コードで逆に書かれてしまうことがよくあります。訓練の価値は、まさにこの日常到達しない分岐を実際に一度通すことにあります。
十七、アーキテクチャ選択の判断基準
どの規模にどの複雑さが要るか
検索アーキテクチャの複雑さは規模と釣り合わせるべきで、ここでは大まかな境目の目安を示します。コーパスが十万ドキュメント以内なら、単一プロセスにメモリ上のインデックスとメモリ上のベクトルテーブルを足すだけで足り、sharding も世代切り替えも独立したクエリ理解サービスも不要です。百万の桁になったら、インデックスは永続化と sharding が必要になり、ベクトル検索には近似最近傍の構造を導入し、reranking には独立デプロイした軽量モデルが必要になります。億の桁では、ここまでのすべての機構が必須になり、さらにストレージ階層化、ホット/コールド分離、独立したインデックス構築クラスタが加わります。
この境目の意味は、ある規模を越えると一度に複数の問題を同時に解決する必要がある、という点です。その規模に達する前にこれらの機構をあらかじめ実装しても、払うった保守コストがチームを長く引きずります。中小規模の段階では、誰よりも愚直な案で次の境目まで進めるのが、通常は正しいエンジニアリング上の判断です。
自社開発と調達の境界
検索関連のコンポーネント(エンジン、ベクトルデータベース、reranking モデル、生成モデル)には、いまや実用可能なオープンソースの案と商用サービスが両方あります。判断の根拠は四つの問いに置くべきです。データをドメイン外に出してよいか、性能指標が硬い要求を満たすか、反復の速度が足りるか、そして総所有コストが本当に低いか。
このうち「反復の速度」は過小評価されがちなのに、しばしば決定的な要素になります。検索システムの効果差は、大量の細かなチューニング(分割の方針、重み、閾値、学習データの組み立て)から出てきます。どれも高い頻度での反復を要します。選んだ案が変更のたびに長い手続きを踏むか外部のスケジュールに従うなら、それらのチューニング上の利点は享受できず、最終的な効果は自由に修正できる案のほうがむしろ劣ります。総所有コストの計算には、エンジニアリングの人力と反復速度の損失を含めなければならず、ライセンス料とマシンコストだけを計算すると、誤った結論に到達しがちです。
もう一点補足しておきます。某一層を外部の案に委ねることは、その層の理解を捨てることではないということです。recall、reranking、生成という三層の具体的な実装は案ごとに大きく違います。内部の仕組みを理解していなければ、調子が悪いときに問題がどの層にあるのか、パラメータが悪いのか案が合わないのか、判断できません。最終的に問題を解くのではなく、ベンダーを入れ替えることばかりになります。各層の仕組みへの理解を保つことが、最適化の余地を残す前提になります。