GrooveSeek

Semantic search over a Markdown knowledge base, served over MCP.

View the Project on GitHub alphabet-h/grooveseek

22. embedding を provider 境界の後ろに置き、既定は FastEmbed のままにする

背景と課題

v1.12.0 まで、embedding の推論とは FastEmbed の具体的なラッパのことだった。 コマンド・設定・索引の互換検査はどれも ModelChoice の言葉で話していた — GrooveSeek がダウンロードしてプロセス内で動かせる、2 つのローカル BGE モデルの enum である。 それ以外にベクトルを作れるものは無く、索引が使えるかを決める場所はどこも、 その enum に聞いて決めていた。

運用者から、既に動かしている embedding サービス (ローカルの推論サーバや、 ホスティングされたもの) を GrooveSeek から使いたい、ただし GrooveSeek が vendor ごとの作法を覚えるのではなく、という要望が出た (issue #312)。 そうしたサービスの多くは OpenAI が公開した POST /v1/embeddings と同じ形の リクエストを話すので、クライアント 1 つで多くを覆える。

これを「variant を 1 つ足す」以上の話にしている事情が 2 つある。外部の endpoint は 索引するすべての chunk と、すべての queryを受け取る — 知識ベースの本文がプロセスの外へ、 場合によってはマシンの外へ出る。そして索引は、どのモデルがベクトルを作ったかを記録し、 別のモデルの下では開くことを拒否する。したがって「モデル」を識別するものが、 GrooveSeek の制御下に無いサービスまで覆わなければならなくなる。

この記録が答えるのは、GrooveSeek と embedding の実装の継ぎ目をどこに置くか、 そこを通って作られた索引を何で識別するか、そして外向きの embedding を誰が有効にしてよいかである。

判断の軸

検討した選択肢

  1. document と query を別の呼び出しに分けた、小さな非公開の provider trait。 FastEmbed をその既定の実装にし、vendor に依存しない OpenAI 互換の HTTP クライアントを 2 つ目の実装にする。 — 採った案。

  2. FastEmbed の具体型を残し、外部モデルを ModelChoice の variant として足す。却下。 コマンドと設定の配線、互換検査は既に ModelChoice に依存していた (PR #314 がリファクタの理由として挙げたもの)。しかもリモートのモデルは この enum では記述できない — 決まった次元も、キャッシュ用のディレクトリも、 ダウンロードも無い。variant を足すたびに、1 つの vendor のモデル一覧が、 索引を開くかどうかを決める場所へ広がっていく。

  3. sidecar を同梱・管理する、または vendor の SDK に依存する (Python のサーバ、MLX、Jina のクライアント)。却下。issue #312 の non-goals である。 GrooveSeek が別プロセスの lifecycle (起動・死活・再起動・更新) を持つことになり、 vendor 固有のリクエストフィールドや task の規約が増えていく。 サービスは運用者が既に動かしている。GrooveSeek に要るのはそこへ届くことだけである。

  4. endpoint を probe してモデルと次元を知る。却下。 索引を開くことがネットワークの往復に依存するようになる: 起動と Config::validate が決定的でなくなり、止まっているサービスが設定の誤りに見え、 索引を開く前から本文や probe のリクエストがマシンの外へ出る。 代わりに運用者が dimension を宣言し、GrooveSeek はすべての応答をそれと照合する。

    覆る条件: ユーザの本文を含まない metadata 呼び出しでモデルの識別と次元を返す protocol が、頼れるほど広く実装されたとき。

  5. GrooveSeek が見つけたどの config の [embedding] も受け入れる。却下。 カレントディレクトリやその git root で見つかった config は、そのリポジトリ・ アーカイブ・共有ドライブを書いた誰かが置いたものかもしれない。それを受け入れれば、 ディスク上で見つかったファイルが、索引した本文とすべての query を、 自分が名指すアドレスへ送らせられる。これは [parsers] に R5 がある理由そのものであり (ADR-0016)、 同じ境界を R7 としてここにも適用する。

  6. endpoint を identity に含める。却下。hostname・port・scheme の変更や、 負荷分散された 2 台のサーバが、ベクトルは変わっていないのに索引の全面的な 作り直しを強いることになる。しかも endpoint の文字列は、その裏に実際にデプロイされている モデルを識別しない — 同じ URL での再デプロイはどちらにしても見えない。 移すたびに作り直しを払い、何の保証も得られない。

    覆る条件: 応答に含まれるモデルの fingerprint のように、embedding 空間を サーバ側が識別する値が得られ、GrooveSeek が URL の代わりにそれを記録できるようになったとき。

決定

embedding の推論は非公開の EmbeddingProvider trait の後ろに置く。FastEmbed は 既定の実装のまま残し、2 つ目の実装は POST /v1/embeddings を話す vendor 非依存のクライアントとする。

結果と代償

参考