tech-book-labs
外部統合 · 最終検証 2026-07-26 · @huggingface/transformers 4.2.0 · 初公開 2026-07-26

Transformers.js でブラウザ完結の意味検索を作る

@huggingface/transformers の feature-extraction pipeline で文章を embedding(意味ベクトル)に変換し、cosine 類似度でランキングする意味検索をサーバー推論ゼロで実装するパターン。pipeline の初期化と dtype(量子化)の選択、モデルのキャッシュ挙動、日本語対応モデルの選び方とサイズの現実、UI をブロックしない読み込み設計まで、クリックでモデルを取得する触れる demo 付きで整理する実装メモ。

transformers-js embedding semantic-search browser-ml onnx huggingface

検証日: 2026-07-26

使用バージョン: @huggingface/transformers@4.2.0

対象: キーワード一致(部分文字列 / BM25)では拾えない「言い換え・意図」で検索したい人。サーバー API を増やさずにサイト内検索を賢くしたい人

Transformers.js(Hugging Face 公式の JS 版 Transformers。ONNX Runtime を内蔵し、モデルをブラウザ内で推論する)で、意味検索(semantic search — 文字の一致ではなく embedding の近さで探す検索)をブラウザ完結で作る。クエリはどこにも送信されず、ホスティングは静的ファイルのみ。動く demo 付き。

触って試す

「モデルを読み込む」でモデル(約 23MB)を取得すると検索できる。例えば “work with time and calendars” と入れると、time / calendar という単語を含まない “parse and format dates” の文が最上位に来る — これがキーワード検索との違い。

1. 全体像 — 検索が動くまでの 3 段階

① モデル DL(初回のみ、以降ブラウザ cache)
② corpus(検索対象の文書)を embedding 化して配列で保持
③ クエリを embedding 化 → 全 corpus と cosine 類似度 → 降順ソート

計算量の主役は ① と ②。③ のクエリ 1 回分は数十 ms なので、体感は「初回ロードだけ重く、検索は即時」になる。この構造から UI 設計が決まる: ①② はユーザ操作(クリック)を起点に進捗を見せながら実行し、③ は同期的に見せてよい

2. 最小サンプル(pipeline → embedding → cosine)

pnpm add @huggingface/transformers@4.2.0

次のコードは feature-extraction pipeline(文章 → ベクトルの変換器)を作り、2 つの文の類似度を出す:

import { pipeline } from "@huggingface/transformers";

// 初回はモデルを Hugging Face Hub から DL(以降は Cache Storage から)
const extractor = await pipeline(
  "feature-extraction",
  "Xenova/all-MiniLM-L6-v2",
  { dtype: "q8" },          // 8bit 量子化(約 23MB)。省略時はモデル既定
);

// pooling: "mean" で「文全体で 1 本のベクトル」に、normalize で長さを 1 に
const out = await extractor(
  ["How to parse dates", "work with calendars"],
  { pooling: "mean", normalize: true },
);

// out.dims = [2, 384]。normalize 済みなので内積 = cosine 類似度
const [a, b] = [out.data.slice(0, 384), out.data.slice(384)];
const score = a.reduce((s: number, v: number, i: number) => s + v * b[i], 0);

ポイント:

  • pooling: "mean"normalize: true をセットで付ける。付けないとトークンごとのベクトル([文数, トークン数, 384])が返ってきて、次元が合わず cosine が計算できない
  • normalize: true 済みなら cosine 類似度はただの内積になり、自前実装が 3 行で済む
  • 返り値は Tensor。out.data が flat な Float32Arrayout.dims が形状。文ごとに slice で切り出す

3. dtype(量子化)の選択とモデルサイズ

dtype はモデルの重みの精度で、ダウンロードサイズと精度のトレードオフを決める:

dtypeサイズ(MiniLM-L6 の場合)用途
fp32~90MB精度基準。ブラウザ配信には重い
q8~23MB既定候補。embedding 用途では体感差がほぼない
q4~14MB生成系 LLM 向け。embedding では劣化が見えることがある

embedding モデルは q8 から試すのが妥当。取得したモデルは ブラウザの Cache Storage に保存され、2 回目以降はネットワークに出ない(DevTools → Application → Cache Storage → transformers-cache で確認できる)。

4. 日本語で使うには — モデル選びとサイズの現実

demo の all-MiniLM-L6-v2 は英語向け。日本語クエリを扱うには多言語モデルに替えるだけでコードは同じだが、サイズが跳ね上がることは知っておく:

モデルq8 サイズ日本語
Xenova/all-MiniLM-L6-v2~23MB
Xenova/multilingual-e5-small~118MB◎ 100+ 言語
Xenova/paraphrase-multilingual-MiniLM-L12-v2~120MB

多言語モデルが大きいのは語彙(トークン語彙 25 万件)の embedding 行列が支配的なため。100MB 超を訪問者に配るかは用途次第で、サイズを正面から表示して opt-in にする(demo と同じくクリックで取得・進捗表示)のが誠実な設計。なお multilingual-e5-* 系は学習時の形式に合わせて、corpus 側に passage: 、クエリ側に query: の接頭辞を付けると精度が上がる(モデルカード記載の仕様)。

さらに品質を追う場合(蒸留・自前 fine-tune・ONNX Runtime 直叩き)は、ONNX Runtime Web + 蒸留の記事がこの先の工程を扱っている。

5. UI をブロックしない読み込み設計

モデル DL と corpus embedding 化は数秒〜数十秒かかりうるので、demo では次の形にしている:

  • ページ表示時には何も読み込まない。ボタンクリックで開始(サイズを明示)
  • progress_callback で DL 進捗(%)を表示する:
const extractor = await pipeline("feature-extraction", MODEL_ID, {
  dtype: "q8",
  progress_callback: (p) => {
    if (p.status === "progress") setProgress(Math.round(p.progress ?? 0));
  },
});
  • corpus の embedding は一度計算したら Float32Array の配列で保持し、検索ごとに再計算しない

corpus が数千件を超えるなら、embedding 化自体をビルド時(Node で同じ Transformers.js が動く)に済ませて JSON/バイナリで配信し、ブラウザではクエリ 1 本だけ推論する構成に切り替える — 訪問者の計算量が「corpus 全体」から「クエリ 1 本」に落ちる。

つまずいたポイント

  • 結果の次元が [1, 7, 384] になって cosine が合わない: pooling: "mean" の付け忘れ。トークンごとのベクトルが返っている
  • progress_callback が何度も 100% を報告する: コールバックはファイル単位(model / tokenizer / config)で呼ばれる。p.file を見て .onnx のときだけ進捗バーに反映する
  • スコアが全部 0.9 以上で差がつかない: normalize 忘れ + 短文同士だと起きやすい。normalize: true を確認し、それでも近い場合はモデルを 1 段大きいものに(MiniLM は 384 次元で表現力に上限がある)
  • ビルドツールで dev は動くのに本番 build で WASM が見つからない: bundler の設定によっては onnxruntime の .wasm が拾われない。Transformers.js は既定で WASM を CDN(jsDelivr)から読むので通常は問題ないが、CSP で外部ドメインを塞いでいるサイトでは env.backends.onnx.wasm.wasmPaths を自サイトのパスに向けて .wasm を自前配置する
  • Module not found: fs 系の警告: Node 兼用ライブラリなので bundler が Node 組み込みを解決しようとする。実行時には使われないので、bundler 側で fs/path を external/empty に落とす(Vite は既定で問題なし)

向くケース / 向かないケース

判定
サイト内検索の「言い換え対応」(数百〜数千件)◎ 本命。静的ホスティングだけで完結
プライバシー要件が厳しい検索(入力を外に出せない)◎ 推論が端末内で閉じる
数万件以上の corpus△ 総当たり cosine が線形に効く。近似最近傍(HNSW 等)の導入を検討
最高精度の検索(商用検索エンジン級)✖ サーバー側の大型 embedding モデル + rerank に軍配

関連書籍

tech-book.net 検索

ISBN 9784295020318

詳細を tech-book.net で見る
tech-book.net 検索

ISBN 9784297132064

詳細を tech-book.net で見る