Tom Select の fuzzy 検索は「ＣＤ」と「CD」を区別しない？全角・半角が混在する日本語でも快適に使えた話

## 🏁 はじめに

多くの選択肢から検索・選択できる UI として人気のある [Tom Select](https://tom-select.js.org/)。  
今回はその **fuzzy検索（曖昧検索）** を使っていて、思いがけず便利な挙動に気づいたのでシェアします。

キーワードはズバリ…

> **全角と半角の「CD」は区別されない？**

日本語環境でありがちな“全角・半角混在問題”。  
その悩みを軽減してくれるかもしれない、Tom Select × Fuse.js の組み合わせに注目です。

---

## 🔧 使用ライブラリ

- **Tom Select v2.2.2**  
  CDN:  
  ```html
  <script src="https://cdn.jsdelivr.net/npm/tom-select@2.2.2/dist/js/tom-select.complete.min.js"></script>
  ```

- **Fuse.js（内部で使用）**  
  Tom Select の `fuzzy検索` はこのライブラリに依存しています。

---

## 🧪 検証内容：全角・半角でどうなる？

以下のように「全角ＣＤ」「半角CD」が混在したリストを `Tom Select` に表示し、  
fuzzy検索で **「CD」（半角）** を入力したときの挙動を確認しました。

### 🔹 HTMLコード（抜粋）

```html
<select id="fuzzySelect">
  <option value="1509">ABザクタンクＣＤ01</option>
  <option value="1510">ＡＢザクタンクCD02</option>
  <option value="1511">ABザクタンクＣＤ03</option>
  <option value="1512">ＡＢザクタンクCD04</option>
</select>
```

### 🔹 JavaScript（Tom Select 初期化）

```js
new TomSelect("#fuzzySelect", {
  maxItems: 1,
  allowEmptyOption: true,
  placeholder: "あいまい検索で探す",
  onInitialize: function() {
    this.clear();
  }
});
```

### 🔹 ソースコード全体
<!-- open属性なし -->
<details><summary>サンプルコードはここを見てください</summary>

```html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>Tom Select - 前方一致 & あいまい検索</title>
  <link href="https://cdn.jsdelivr.net/npm/tom-select@2.2.2/dist/css/tom-select.css" rel="stylesheet">
  <style>
    body { font-family: sans-serif; margin: 2em; }
    label { font-weight: bold; display: block; margin-top: 1.5em; }
    select { width: 100%; max-width: 400px; }
  </style>
</head>
<body>
  <h2>Tom Select 検索タイプ別サンプル（初期未選択）</h2>

  <!-- ▼ 前方一致検索（ドライバー） -->
  <label for="prefixSelect">🚚 前方一致検索（パイロット）</label>
  <select id="prefixSelect" placeholder="前方一致で検索">
    <option value="" disabled selected hidden>-- 選択してください --</option>
    <option value="80">123455970 連邦一郎</option>
    <option value="81">123452482 ティターンズ太郎</option>
    <option value="82">123455157 連邦二郎</option>
    <option value="83">123455796 ティターンズ三平</option>
    <option value="1117">123453000 テストパイロット</option>
    <option value="1133">Zeon0101 ジオン一郎</option>
    <option value="1134">Zeon0102 ジオン二郎</option>
    <option value="1135">Zeon0103 ジオン三郎</option>
    <option value="1136">Zeon0104 ジオン四郎</option>
    <option value="1137">Zeon0105 ジオン五郎</option>
  </select>

  <!-- ▼ あいまい検索（車両） -->
  <label for="fuzzySelect">🚗 あいまい検索（モビルスーツ）</label>
  <select id="fuzzySelect" placeholder="あいまい検索で探す">
    <option value="" disabled selected hidden>-- 選択してください --</option>
    <option value="73">グフ99901</option>
    <option value="74">グフ99902</option>
    <option value="75">グフ99903</option>
    <option value="76">グフ99904</option>
    <option value="413">ＡＢガンタンクCD</option>
    <option value="1509">ABザクタンクＣＤ01</option>
    <option value="1510">ＡＢザクタンクCD02</option>
    <option value="1511">ABザクタンクＣＤ03</option>
    <option value="1512">ＡＢザクタンクCD04</option>
    <option value="1513">ABザクタンクＣＤ05</option>
  </select>

  <script src="https://cdn.jsdelivr.net/npm/tom-select@2.2.2/dist/js/tom-select.complete.min.js"></script>
  <script>
    // ▼ 前方一致検索（ドライバー）
    new TomSelect("#prefixSelect", {
      maxItems: 1,
      placeholder: "前方一致で検索",
      allowEmptyOption: true,
      score: function(search) {
        return function(item) {
          const label = item.text.toLowerCase();
          const keyword = search.toLowerCase();
          return label.startsWith(keyword) ? 1 : 0;
        };
      },
      onInitialize: function() {
        this.clear();
      }
    });

    // ▼ あいまい検索（車両）
    new TomSelect("#fuzzySelect", {
      maxItems: 1,
      allowEmptyOption: true,
      placeholder: "あいまい検索で探す",
      onInitialize: function() {
        this.clear();
      }
    });
  </script>
</body>
</html>
```
</details>

---

## 📷 実際の動作結果

半角「CD」と入力すると、**全角「ＣＤ」を含む項目もヒット**してくれました！

> えっ、Fuse.js って全角と半角を区別するんじゃないの？

と思って調べてみたら…

---

## 🤔 全角・半角の区別について調べてみた

### ✔ Fuse.js は原則として「全角と半角を区別」する

Fuse.js 単体で以下のような比較を行った場合、 `"CD"` と `"ＣＤ"` は別物扱いになります。

```js
const list = [{ name: "ＡＢＣＤ" }, { name: "ABCD" }];
const fuse = new Fuse(list, { keys: ["name"] });

fuse.search("CD"); // 全角「ＣＤ」はヒットしない
```

---

## 🧠 仮説

じゃあなぜ Tom Select でヒットしたのか…？

### 💡 推測される理由

- `threshold` の設定（ゆるめ）によって、**多少の違いは曖昧一致として許容されている**
- Unicode正規化は明示的にされていないが、**視覚的に近い文字列を「近い」と評価**している可能性
- Fuse.js の `includeScore` によるスコア判定が「類似」と判断

---

## ✅ まとめ

| 比較内容 | 結果 | 補足 |
|----------|------|------|
| 半角「CD」 vs 全角「ＣＤ」 | ✅ ヒット | Tom Select + Fuse.js の fuzzy検索はスコア的に「近い」と判断 |
| 明示的な normalize() | ❌ 使用していない | それでもヒットしている点が面白い |
| ユーザー体験的には？ | 🟢 優しい！ | 日本語混在環境でも安心して使える！

---

## 📝 おわりに

今回のように「ちょっと不思議な挙動」に出会ったとき、  
それを試して調べて、推測するって…やっぱり楽しいですね！

「Tom Select の fuzzy 検索って全角・半角にもけっこう寛容だな」と感じたので、  
日本語UIの改善や、ユーザーの入力ミスを吸収したい場面ではぜひ活用してみてください！