定義
テキストアンカリング(text anchoring) とは、ウェブ文書の特定範囲を後から再び見つけられるように座標を保存することである。堅牢な(robust) アンカリングは、その座標を一つではなく性質の異なる複数の手がかりとして残し、復元時に各手がかりへ重みを付けて候補を選び、確信が足りなければ付けないという方式を指す。
なぜ必要か
ハイライトやメモを保存する最も単純な方法は、DOM パスと offset を書くことである。
// 次回訪問時にほぼ確実に壊れる保存方式
{ path: "body > article > p:nth-child(3)", start: 42, end: 58 }
広告が一つ挿入され、段落の順序が変わり、フレームワークが一覧を再描画すれば、同じ座標がまったく別の文字を指す。
逆に引用文だけを保存すると、同じ表現が文書に複数回現れたときどれか分からず、誤字修正や助詞の変更ひとつで完全一致が失敗する。
そのため W3C Web Annotation モデルは selector を複数まとめて保存する設計になっている。一つが壊れても残りが支え、複数の手がかりが同じ地点を指せば確信が上がる。そして多くのドメインでは見当違いの場所に付くことのほうが、付かないことより悪いという原則がしきい値設計を左右する。
仕組み
手順 1: 文書を一つの正規化文字列にする
DOM を走査して Unicode NFC に統一し、連続する空白を一つにまとめて文書全体を単一の文字列にする。同時に、正規化後の位置から元のテキストノードと offset への対応を保存する。
// 空白の判定はノード単位ではなく文書単位で行う
// <p>グーグル<b>ラボ</b></p> → "グーグルラボ" (間に空白が入ってはいけない)
SCRIPT・STYLE・hidden・aria-hidden の要素は除外する。画面に出ていない文字が混ざると利用者が選んだ文とインデックスがずれる。
手順 2: 保存時に複数の selector を一緒に残す
{
quote: { exact: "確信が足りなければ止める", prefix: "…前 96 文字…", suffix: "…後 96 文字…" },
position: { start: 10432, end: 10444 } // 正規化文字列を基準にした位置
}
要素境界をテキスト境界へ移し、両端の空白を整えてから保存する。
手順 3: 復元は 3 段の候補探索と重み付きスコア
| 段階 | 方法 | スコアの性質 |
|---|---|---|
| 完全一致 | 正規化文字列から exact を検索 | 最も高い重み |
| 文脈一致 | 前後文脈の一部(例: 32 文字)で近傍候補を探索 | 中程度の重み |
| あいまい一致 | トークン窓を転がして類似度(Dice 係数など)で探索 | 上限を低く設定し自動適用から除外 |
各候補のスコアを 0〜1 に正規化したうえで、判定を三つに分ける。
- 最高スコアが下限未満 → 未解決として何も付けない。
- 1 位と 2 位のスコア差が小さい → 候補があってもあいまいとして扱う。これは確信ではなく競合状況を見る指標なので、しきい値とは別に必要になる。
- 上限以上のときだけ自動でハイライトする。その間の区間はマーカーのみ残し強調はしない。
手順 4: 復元後に再検証する
生成した Range の内容を再度正規化し、元の引用文と照合する。食い違えば失敗として扱う。最後の安全網である。
実務での適用
しきい値は名前付き定数として一箇所にまとめる。ドメインごとに調整すべき値なので、散らばると手を入れられなくなる。
export const CONFIDENCE = {
LOW: 0.78, // これ未満は未解決
HIGH: 0.9, // これ以上だけ自動適用
AMBIGUITY_GAP: 0.08, // 1位と2位の差がこれより小さければあいまい
} as const;
キャプチャの時点も同じくらい重要である。利用者がマウスを離した瞬間にアンカーを計算し、値として保持する。
document.addEventListener("mouseup", () => {
const selection = window.getSelection();
if (!selection || selection.isCollapsed) return;
pendingAnchor = captureAnchor(selection.getRangeAt(0)); // その場で値に固定
});
保存ボタンのクリックまで Range オブジェクトを保持していると、その間にホストページが再描画されテキストノードが差し替わり、保存が静かに失敗する。
トレードオフ
手がかりを複数保存すると容量が増え、キャプチャのコストも上がる。前後 96 文字ずつの文脈を持てば、注釈ひとつで数百バイトになる。
文脈の長さは双方向のトレードオフである。短ければ繰り返しの多い区間であいまいになり、長ければ文書のわずかな修正で文脈一致が壊れる。
あいまい一致は復元率を目に見えて上げるが、誤付着のリスクを同時に持ち込む。スコア上限を設けて自動適用から外すのが現実的な妥協点になる。
使うべきでない場合
- 自分が管理する静的文書。 安定した ID を振れるなら、そのほうがはるかに安く正確である。
- 誤った付着が致命的なドメイン(法的署名、医療記録など)であいまい一致を自動適用すること。未解決のままにして人が確認する。
- 訪問のたびに文書全体が生成し直される画面。 アンカーではなくデータモデル側に識別子を置くべきである。
よくある間違い
- 位置 offset を単独で信頼する。 文書が 1 文字変わるだけで全部ずれる。加点用途に留める。
- ノード単位で空白を正規化する。 インラインタグで分割された語の間に存在しない空白が生まれ、完全一致が失敗する。
- あいまい性の間隔を設けない。 しきい値だけでは 0.91 と 0.90 の候補から根拠なく前者を選ぶことになる。
- 非表示要素をインデックスに含める。 利用者が見たことのない文字が混ざって offset がずれる。
- 選択直後ではなく保存時点でアンカーを計算する。 ホストページの再描画一回で静かに失敗する。
- 再検証の手順を省略する。 スコアは高いが実際には別の区間を指している場合を、最後に取り除く手段がなくなる。
関連概念
- browser-extension-message-based-rpc — 復元結果を他の実行空間の画面へ届ける上位の経路
- test-oracle — 「確信が足りなければ答えずに止める」という同じ原則の別の適用