1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

EmbeddingGemma 2の索引設計:モデル・前処理・次元をfingerprintで固定する

1
Posted at

EmbeddingGemma 2を検索へ組み込むときは、保存するベクトルに「どのモデルと前処理で、どの実行条件から作ったか」を結び付けたい。同じ長さの数値配列でも、生成条件が違えば、そのまま比較してよいとは限らないからだ。

この記事では、モデル・processor・runtime・dtype・次元・正規化・プロンプト方針をfingerprintへまとめ、取り込み側と検索側で一致を確認する小さな実装を示す。これは本記事の設計案であり、EmbeddingGemma 2の公式API仕様ではない。

実行した範囲は、純Pythonで手入力した合成ベクトルの契約検査まで。 実モデルの取得・推論、日本語検索精度、Macの実RAMや速度は測定していない。

公開された機能と、索引が持つ責任

Googleは2026年10月7日01:00 JSTの公式発表で、EmbeddingGemma 2を公開した。テキスト・コード・画像・音声・動画を検索用ベクトルへ変換するモデルであり、このモデル単体が回答文章を生成するわけではない。公式発表、開発者ガイド

公式カードの標準出力は768次元で、512・256・128次元への短縮を案内している。短縮した後はL2正規化し、検索文と文書で同じ次元を使う。128次元については、多媒体の品質低下を伴うため用途ごとの確認が必要と説明されている。固定版の公式モデルカード

たとえばAIニュースラボで、写真・会議音声・文書を検索対象にする設計を考える。入力をベクトルへ変換した後は、索引側に媒体名を渡すだけで互換性が確立するわけではない。画像の切り出し、音声の前処理、文書の分割、タスク指定を、どの設定で行ったかも管理したい。

これは想定する処理の図で、写真や録音を実際に検索した結果ではない。文書の閲覧権限や対象版による絞り込みは、ベクトルの類似度と別に適用する。

索引へ保存する契約

本例は、次の情報からSHA-256のfingerprintを作る。

項目 固定するもの 変更時の扱い
model revision モデル名に加え、実際のcheckpoint revisionや配布物の識別子 別索引へ再埋め込み
processor revision tokenizer、pooling、媒体処理と設定の版 別索引へ再埋め込み
runtime revision 使用した実装・backend・バージョン 別索引へ再埋め込み
dtype 重み・演算の精度、量子化のrecipeなど 別索引へ再埋め込み
dimension 最終的に保存する次元数 別索引へ再埋め込み
normalization 次元短縮の後でL2正規化する手順の版 手順を合わせて再埋め込み
prompt policy query用とdocument用の指示、タイトル付与、媒体の並べ方の組 方針を合わせて再埋め込み

queryとdocumentの本文や指示を同じ文字列にする、という意味ではない。用途に応じた両方の指示を一組の方針として固定し、その方針の識別子を一致させる。

processor revisionには、コードのcommitだけでなく、画像サイズや音声設定など実際に使った設定も含める。表の文字列を手作業で付ける場合でも、本番では設定ファイルや配布物のhashに対応させる。元資料の内容hash、区間・ページ・分割位置は、資料ごとの別メタデータとして持つ。

ここでは「契約が変わったら混ぜない」という保守的な運用を選んだ。実装の更新で出力が変わらない場合もあるが、同一索引への混在を許す例外はこのコードでは扱わない。互換性の検証を別途行ってから、運用として判断する。

また、fingerprintは自己申告された設定の一致を確認する値だ。コードを実行した実体や前処理の正しさまで証明するものではなく、署名や改ざん防止の仕組みも持たない。

長さ以外の失敗も入口で止める

次元数だけを見ていると、NaNやInfinityを含む配列、ゼロベクトル、別の実行条件で作った配列が通り得る。公式カードもfloat16ではNaNや気付きにくい劣化が起こり得るとして、推論にbfloat16またはfloat32を指定している。量子化済みの配布物を使う場合は、その変換とruntimeの条件を別に確認する。公式モデルカードのNumerical Precision

有限な値を持っていても、正しい意味のベクトルであるとは限らない。この入口検査が確かめるのは、比較を始めるための必要条件である。

実装では、取り込み・queryの両方で次を確認する。

  1. fingerprintが索引に保存した契約と一致する。
  2. ベクトルの長さが宣言した最終次元と一致する。
  3. 全要素が有限な数値であり、ノルムが有限でゼロでない。
  4. L2正規化した後に内積を取る。

短い配列をゼロ埋めしたり、長い配列を入口で勝手に切り捨てたりはしない。次元短縮は埋め込みを作る側で実施し、完成した最終次元を受け取る。この例の3次元は挙動を見せるための合成例で、EmbeddingGemma 2の対応出力次元ではない。

純Pythonで再現する最小実装

Python 3.9以降の標準ライブラリだけを使う。以下をembedding_contract.pyとして保存する。モデル・SDK・APIキーは不要。

"""Synthetic-vector index contract; no model, SDK, download, or inference."""
from dataclasses import asdict, dataclass, replace
import hashlib
import json
import math
from numbers import Real


@dataclass(frozen=True)
class Contract:
    model_revision: str
    processor_revision: str
    runtime_revision: str
    dtype: str
    dimension: int
    normalization: str = "truncate-then-l2-v1"
    prompt_policy: str = "toy-query-doc-policy-v1"

    def fingerprint(self):
        if (isinstance(self.dimension, bool)
                or not isinstance(self.dimension, int)
                or self.dimension <= 0):
            raise ValueError("invalid dimension")
        if self.normalization != "truncate-then-l2-v1":
            raise ValueError("unsupported normalization")
        fields = asdict(self)
        if any(not isinstance(v, str) or not v.strip()
               for k, v in fields.items() if k != "dimension"):
            raise ValueError("empty contract field")
        data = json.dumps(fields, sort_keys=True, separators=(",", ":"))
        return hashlib.sha256(data.encode("utf-8")).hexdigest()


def unit_vector(values, dimension):
    values = tuple(values)
    if len(values) != dimension:
        raise ValueError("vector length mismatch")
    if any(isinstance(x, bool) or not isinstance(x, Real) for x in values):
        raise ValueError("non-finite or non-numeric vector")
    try:
        values = tuple(float(x) for x in values)
    except (OverflowError, ValueError):
        raise ValueError("non-finite or non-numeric vector") from None
    if any(not math.isfinite(x) for x in values):
        raise ValueError("non-finite or non-numeric vector")
    norm = math.hypot(*values)
    if not math.isfinite(norm) or norm == 0:
        raise ValueError("zero or non-finite norm")
    return tuple(float(x / norm) for x in values)


class Index:
    def __init__(self, contract):
        self.contract = contract
        self.fingerprint = contract.fingerprint()
        self.documents = {}

    def _validate(self, values, contract):
        if contract.fingerprint() != self.fingerprint:
            raise ValueError("contract mismatch")
        return unit_vector(values, self.contract.dimension)

    def add(self, document_id, values, contract):
        if (not isinstance(document_id, str) or not document_id.strip()
                or document_id in self.documents):
            raise ValueError("empty or duplicate document id")
        vector = self._validate(values, contract)
        self.documents[document_id] = vector

    def search(self, values, contract, k=2):
        if isinstance(k, bool) or not isinstance(k, int) or k <= 0:
            raise ValueError("invalid k")
        query = self._validate(values, contract)
        scores = [(key, math.fsum(a * b for a, b in zip(query, vector)))
                  for key, vector in self.documents.items()]
        return sorted(scores, key=lambda item: (-item[1], item[0]))[:k]


def main():
    contract = Contract("toy-model-r1", "toy-processor-r1", "python-toy-r1",
                        "python-float", 3)
    index = Index(contract)
    index.add("photo-dog", [0.8, 0.6, 0.0], contract)
    index.add("audio-dog", [0.98, 0.1, 0.0], contract)
    print("rank:", [key for key, _ in index.search([1.0, 0.0, 0.0], contract)])
    try:
        index.search([1.0, 0.0, 0.0], replace(contract, dtype="toy-4bit"))
    except ValueError as error:
        print("rejected:", error)


if __name__ == "__main__":
    main()

math.hypotでノルムを求め、各要素を単位長へそろえる。正規化したベクトル同士の内積をスコアとして使い、同点では資料IDを使って順番を固定している。同点時のID順は再現性のための規則で、関連性の優劣ではない。

コード末尾のphoto-dogとaudio-dogは資料IDの仮名であり、配列は手入力した数値だ。犬の写真や鳴き声から生成したベクトルではない。

python3 embedding_contract.py

この実行で得た出力は次の通り。

rank: ['audio-dog', 'photo-dog']
rejected: contract mismatch

2件の合成資料の順位を返した後、dtypeだけを変更したqueryを拒否する。同じ3要素の配列でも、別の契約を通さないことを確認できる。

2026年10月7日、macOS 27.0.1 / Apple Silicon / CPython 3.9.6で実行した。追加の境界検査37件も通過した。model・processor・runtime・dtype・次元・prompt方針の差を取り込みとqueryの双方で拒否し、NaN・Infinity・ゼロ・短すぎる/長すぎる配列を拒否する。浮動小数点数へ変換できない大きな整数も拒否し、失敗した取り込みが既存資料を増やさないことを確認した。これは契約コードの検査結果であり、実モデルの品質や実行速度の測定ではない。

実際のruntimeへ接続するときに追加するもの

確認日2026年10月7日時点で、Ollama v0.40.0は正式releaseとして公開されている。固定runnerが振り分ける媒体は画像と音声で、EmbeddingGemma 2の全媒体対応をそのままOllamaの対応範囲とみなせない。固定modelの入力上限は8192トークン。使うtower・入力方法・長さの扱いをその経路で確認する。Ollama v0.40.0、固定runner、固定model

MLX-VLMはEmbeddingGemma 2対応を開発本流へ統合したが、確認時の正式配布は0.7.6。固定main内の0.7.7表記と正式配布を分けて扱う。実モデル用のMLX-VLM経路にはPython 3.10以上などの依存があり、上のPython 3.9で動く合成例とは必要環境が異なる。統合PR、正式release、固定pyproject

実際に接続する際は、runtimeが返したベクトルと、ロードした配布物・processor・設定の記録を組にして、この境界へ渡す。768→256の変更、媒体の前処理変更、量子化版への移行は、別索引を作って同じ資料を再処理し、検索品質を比較してから切り替える。

ベクトルの数が減ることや、配布ファイルが小さくなることは、必要RAMやレイテンシの実測とは別である。モデルのライセンス表記とモデルカードが案内する利用方針も確認してから運用へ組み込む。公式モデルカード

この設計でそろえられるのは「比較してよいと宣言した条件」と入力の数値上の健全性まで。次に必要なのは、実際の日本語queryと正解資料を用いた検索評価だ。契約が一致していることと、欲しい資料が上位に来ることを別々に確認する。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?