AI Dev Lab
AkinFind

AkinFind ができるまで — ブラウザ内で完結する embeddings 意味検索

多言語の文章埋め込みモデル (Xenova/multilingual-e5-small) を transformers.js の CDN ESM で side-load し、 各文を正規化ベクトル化。 cosine 類似度で意味検索と似ている文ペア検出を全て端末内で行う設計記録。

AkinFind は、文章を貼るだけで「意味の近い文」を探せる検索ツールです。多言語の文埋め込みモデルをブラウザ内で動かすので、言葉が違っても意味で引っかかります(「昼ごはん」で「ランチ」が出る)。文章はどこにも送りません。

#🎯 キーワード検索では拾えないもの

Ctrl+F は「文字が一致するか」しか見ません。だから

  • 「昼ごはん」で検索しても「ランチ」は出ない
  • 「解約したい」で検索しても「退会の方法」は出ない
  • 英語のメモと日本語のメモは互いに見つからない

議事録やメモが溜まると、この取りこぼしが効いてきます。意味で引ける検索があれば解決しますが、たいていは文章をどこかのサーバーに送る必要があります。仕事のメモを外に出せない場面は多い。

そこでモデルごとブラウザに持ってくる構成にしました。

#🧭 文埋め込みとコサイン類似度

やっていることは 2 段です。

  1. 各文をモデルに通して**ベクトル(数百次元の数列)**にする
  2. ベクトル同士のコサイン類似度で近さを測る

モデルは Xenova/multilingual-e5-small を transformers.js 経由で使っています。多言語対応なので、日本語と英語をまたいだ検索ができます。

ts
const r = await extractor(prefix + t, { pooling: "mean", normalize: true })
out.push(Array.from(r.data as Float32Array))

pooling: "mean" は、トークンごとのベクトルを平均して 1 文 1 ベクトルにする指定です。normalize: true で長さ 1 に揃えます。

正規化しておくと、コサイン類似度が単なる内積になります。

ts
export function cosine(a: number[], b: number[]): number {
  let s = 0
  for (let i = 0; i < n; i++) s += a[i] * b[i]
  return s
}

除算も平方根もありません。正規化を推論側に押し付けることで、比較側が掛けて足すだけになります。

#🔤 e5 モデルの接頭辞

このモデルには作法があります。検索する側の文には query: 、検索される側には passage: を付ける必要があります。

ts
const prefix = kind === "query" ? "query: " : "passage: "
const r = await extractor(prefix + t, ...)

これは e5 系モデルが学習時にそう訓練されているためで、付けないと精度が明確に落ちます。モデル固有の使い方を守るという、地味だけど効く部分でした。ドキュメントを読まずに使うと気づけません。

#📊 類似度を「体感に合う数字」に伸ばす

実装して最初に困ったのがこれです。e5 の類似度は、まったく関係ない文同士でも 0.7 くらい出ます。関係あるものは 0.85〜0.95。つまり実際に使われる幅は 0.7〜1.0 の狭い範囲に密集しています。

これをそのまま「類似度 72%」と出すと、無関係な文が「7 割似ている」ことになって意味が伝わりません。

ts
export function simPct(cos: number): number {
  // e5 の類似度は概ね 0.7〜1.0 に密集するので 0.7→0%, 1.0→100% に伸張して体感に合わせる
  const stretched = (cos - 0.7) / 0.3
  return Math.round(Math.max(0, Math.min(1, stretched)) * 100)
}

0.7 を 0%、1.0 を 100% に線形に引き伸ばすだけです。数学的には元の値のほうが正しいのですが、利用者が判断に使う数字としてはこちらが正しいと判断しました。

こういう変換は「ごまかし」になりかねないので、コメントで理由を明記し、変換していること自体を隠さないようにしています。

#🔍 2 つのモード

意味で検索 — 入力した文に近いものを、リストから順に出す。 似ている文を検出 — リスト内の全ペアを比較して、近いもの同士を見つける。重複や言い換えの発見に使えます。

後者は総当たりなので O(N²) ですが、ベクトルは既に手元にあるので掛けて足すだけです。数百件なら一瞬で終わります。重いのは推論(1 回だけ)で、比較は軽い ── この非対称性が使い方を決めました。

#😓 苦労したところ

初回のモデルダウンロード。 数十 MB を落とすので、何も出ないまま待たせると離脱します。進捗のコールバックを受け取って画面に出し、一度読めばキャッシュされることを明記しました。

ts
onProgress({ kind: "running", message: "意味を 計算中…" })

ダウンロード中と推論中を別のメッセージにしているのは、どちらで待っているかが分かるだけで体感が変わるからです。

1 件ずつ推論している。 バッチにまとめたほうが速いのですが、途中経過を出せなくなります。件数が多いときに「何件目を処理中」が出ないと固まったように見えるので、ここは速度より進捗を優先しました。

「似ている」の基準を利用者が決められない。 閾値を固定すると、用途によって多すぎたり少なすぎたりします。結果を類似度順に並べて、どこで切るかは見て判断してもらう形にしました。

#🔭 今後の拡張

  • ファイル(テキスト・Markdown)の読み込み
  • ベクトルを端末内に保存して、次回は推論を省く
  • クラスタリング(似た文をグループにまとめる)
  • より大きいモデルの選択(精度と速度のトレードオフ)

#💡 このサービスから言えること

モデルをブラウザに持ってくると、「送れないデータ」に対して使えるようになります。 精度はサーバー側の大きいモデルに劣りますが、そもそも使えるかどうかが変わります。議事録や個人のメモは、まさにその領域でした。

そしてモデルが返す数値をそのまま見せるのが正しいとは限らない、というのも学びでした。0.7〜1.0 に密集する類似度は、機械にとっては十分な情報でも、人が判断に使う数字ではありません。どう見せるかまでが実装だと思っています。

[ ./next_action ]

読んだら、 AkinFind を実際に動かす。

この開発ログは AkinFind をどう作ったかの記録です。 読み終わったらそのままサービス本体へ戻って、 実物で価値を確かめてください。

[ ./related_logs ]

関連する開発ログ

all logs →
ToonCast

ToonCast ができるまで — AnimeGANv2 をブラウザで動かす

AnimeGANv2 の小さな ONNX (約9MB) を onnxruntime-web (単一スレッド WASM=COOP/COEP不要、 color-revive で承認済みライブラリの再利用) で実行。 512x512・[-1,1] 正規化で推論し、 結果を元解像度に戻して表示する設計記録。 写真は端末内処理。

read log →
ColorRevive

ColorRevive ができるまで — onnxruntime-web で白黒写真をカラー化

DeOldify の量子化 ONNX を onnxruntime-web (CDN side-load・単一スレッド WASM=COOP/COEP不要) で実行。 256x256 でモデル推論し、 輝度は元写真・色だけ AI を YCbCr で再合成して輪郭を保つ設計記録。 写真は端末内処理。

read log →
PhotoTwin

PhotoTwin ができるまで — CLIP画像埋め込みで似た写真を見つける

CLIP (Xenova/clip-vit-base-patch32) の image-feature-extraction を transformers.js の CDN ESM で side-load し、 各写真を正規化ベクトル化。 cosine 類似度で重複・似た写真をブラウザ内で検出する設計記録 (新ライブラリ追加なし=what-cam と同じ CLIP の再利用)。

read log →
WhatCam

WhatCam ができるまで — CLIP のゼロショット画像分類をブラウザで動かす

CLIP (Xenova/clip-vit-base-patch32) を transformers.js の CDN ESM で side-load し、 写真と候補ラベルの近さをブラウザ内で計算。 日本語ラベルを英語プロンプトに変換し、 図鑑と自由入力の両モードで「これ何?」を判定する設計記録。

read log →
DepthCast

DepthCast ができるまで — 1枚の写真をAIの深度推定で立体にする

Depth Anything (transformers.js) を CDN ESM で side-load し、 1枚の写真から深度マップを推定。 WebGL2 フラグメントシェーダで深度に比例した視差 (iterative backward parallax) を作り、 赤青アナグリフ / WebM 書き出しまで端末内で完結させた設計記録。

read log →
BioWave

BioWave ができるまで — バイオリズムを正弦波で描く

バイオリズム占いの開発ログ。 生年月日からの経過日数をもとに身体 23 日・感情 28 日・知性 33 日の正弦波で調子を出し、 ゼロ交差を要注意日として拾い、 2 人の誕生日差から相性を計算するまでを記録しています。

read log →