▍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 →
ShadowTwin

立体パズルの別解を作る:同じ影と最少の積み木

ShadowTwinの開発記録。3方向の図から別々の最少配置を2つ探す仕組み、5問の独立全探索、スマホの横並び比較、保存・再編集と入力検査を解説します。実照明の計算や能力診断ではありません。

read log →
SlitCard

スリットアニメーションを作る:4コマの合成と透明原稿の設計

SlitCardの開発記録。4コマの帯を交互に合成し、すき間と黒帯で別のコマを見せる仕組みを解説。同縮尺の台紙・透明シートSVG、50mm確認線、手動プレビュー・初期非表示、再編集と実出力検査を整理します。

read log →
WindowLight

日当たりシミュレーションを作る:窓の光を床へ投影する幾何モデル

WindowLightの開発記録。太陽位置近似から窓の4隅を床へ投影し、凸多角形を部屋で切り取る設計を解説。国立天文台の16時刻との限定照合、大気差・低高度・実際の採光との違い、SVGと設定の再編集を整理します。

read log →
GridLights

電源構成の比較を作る:割合と実量を100個の電球で読み解く

電源構成比較GridLightsの開発記録。12か国の年次発電データを最大剰余法で100個の電球に変換し、割合とTWhを比較する設計を解説。欠損を0補完しない7分類、予想と実値、出典付きSVGと実データ検証を整理します。

read log →
SoundLoom

音のお絵かきを作る:実WAVから絵を測り直す設計

音のお絵かきの開発記録。描画を正弦波のPCMとWAVへ変換し、保存音声の周波数をGoertzel法で測って比較する仕組みを解説。音量・測定窓・量子化の限界と、編集・離脱時の古い結果を抑止する検証を整理します。

read log →
MazeLoom

宝探し迷路を作る:寄り道まで含めた最短ルートの探索

宝探し迷路の開発記録。通路を木として生成し、現在地と3つの宝の取得状態を組み合わせて最短ルートを探す仕組みを解説。問題コードの版固定、画面と解答付きPDFの一致、手数と印刷の検証を整理します。

read log →