ウェブアプリにjsmigemoを導入するチュートリアル 

漢字を含む大量の項目から自分の選びたい項目を選ぶには、漢字変換という入力動作が必要になるため、時間を要します。
Migemoは漢字変換を必要とせず、ローマ字で入力するだけで項目の絞り込みが可能となる検索手法です。
このMigemoをJavaScriptで実装したjsmigemoをウェブアプリに導入し、快適な検索を実現しましょう。

このチュートリアルでは、jsmigemoの導入について説明します。

## 今回作るウェブアプリ

全国の地方自治体には自治体コードとよばれるコードが振られています。
今回は、自治体名を入力して、自治体コードを表示するウェブアプリを作成します。
自治体名の中には、読み方の難しい漢字もあります。
Migemoは正しい読み方が分からなくても、自治体名の一部の漢字を入力すればよいので、
今回のチュートリアルにはぴったりです。

ウェブアプリのフレームワークはVue.jsを利用します。

## 1. Vue.jsプロジェクトのセットアップ

まず、Vue.jsのプロジェクトを作成します。
プロジェクト名は、jsmigemo-demoとします。

```
npm create vite@latest jsmigemo-demo -- --template vue-ts
cd jsmigemo-demo
npm install
npm run dev
```

すると、以下のようなメッセージが表示されます。

```
  vite v2.9.13 dev server running at:
  > Local: http://localhost:3000/
  > Network: use `--host` to expose
  ready in 445ms.
```

ウェブブラウザで`http://localhost:3000/`にアクセスしてみましょう。
無事、Vue.jsで作成された画面を見れたでしょうか。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/105381/2079869f-c16f-7e29-f951-a13bef2329ec.png)


## 2. 自治体コードの入手

地方自治体コードの一覧は総務省のウェブサイトから入手できます。

https://www.soumu.go.jp/denshijiti/code.html

「都道府県コード及び市区町村コード」のExcelファイルをダウンロードし、
Excelで開きます。
1行目のヘッダー以外の行をコピーし、publicディレクトリ内にCSVとして`data.csv`という名前で保存します。

```public/csv:data.csv
010006,北海道,,ﾎｯｶｲﾄﾞｳ,
011002,北海道,札幌市,ﾎｯｶｲﾄﾞｳ,ｻｯﾎﾟﾛｼ
012025,北海道,函館市,ﾎｯｶｲﾄﾞｳ,ﾊｺﾀﾞﾃｼ
012033,北海道,小樽市,ﾎｯｶｲﾄﾞｳ,ｵﾀﾙｼ
...
```

## 3. 自治体コードの絞り込み検索

自治体コードを表示する前に、画面を整理しておきます。
まず、App.vueファイルはHelloWorldコンポーネントを読み込んで表示するのみにしておきます。

```vue:src/App.js
<script setup lang="ts">
// This starter template is using Vue 3 <script setup> SFCs
// Check out https://vuejs.org/api/sfc-script-setup.html#script-setup
import HelloWorld from './components/HelloWorld.vue'
</script>

<template>
  <HelloWorld />
</template>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>
```

次に、HelloWorld.vueです。
publicディレクトリに配置したdata.csvを読み込み、表示します。
画面のテキスト入力ボックスに、絞り込みたい文字列を入力すると、その文字列を含む行のみが表示されます。

```vue:src/components/HelloWorld.vue
<script lang="ts">
import { defineComponent } from 'vue'

export default defineComponent({
  data() {
    return {
      query: "",
      items: [] as string[],
    }
  },
  mounted() {
    fetch('data.csv')
    .then(e => e.text())
    .then(e => {
      this.items= e.split('\n')
    })
  },
  computed: {
    filteredItems(): string[] {
      return this.items.filter(e=>e.indexOf(this.query)!==-1)
    }
  }
})
</script>

<template>
  <h1>全国地方公共団体コード検索</h1>
  <input v-model="query" />
  <ul>
    <li v-for="item in filteredItems" :key="item">
    {{item}}
    </li>
  </ul>
</template>
```

## 4. Migemoを導入

ここでjsmigemoをインストールします。

```
npm install jsmigemo
```

加えて、Migemo用の辞書ファイルを入手します。
ここでは、yet-another-migemo-dictを利用します。
yet-another-migemo-dictは、制限の緩いライセンスの辞書から生成されたMigemo用の辞書であり、商用製品にも組み込みやすくなっています。

以下のウェブサイトから、最新の`migemo-compact-dict.zip`をダウンロードし、展開した`migemo-compact-dict`をpublicディレクトリに配置して下さい。

https://github.com/oguna/yet-another-migemo-dict/releases


それでは、Vueコンポーネント上でjsmigemoを使ってみましょう。

まず、jsmigemoのクラスをインポートします。
ついでにmarkRawメソッドもインポートしておきます。（理由は後述）

```diff_javascript
-import { defineComponent } from 'vue'
+import { defineComponent, markRaw } from 'vue'
+import { CompactDictionary, Migemo } from 'jsmigemo'
```

次に、コンポーネントにMigemoオブジェクトを管理する変数を作ります。

```diff_javascript
  data() {
    return {
      query: "",
      items: [] as string[],
+     migemo: null as null|Migemo,
    }
  },
```

そして、コンポーネント初期化時（`mounted()`）に、辞書を読み込み、Migemoオブジェクトを初期化します。
ここで、`markRaw`メソッドで、Migemoオブジェクトをプロキシに変換されないようにしましょう。
プロキシに変換されると、状態の変化を通知してくれます。
しかし、このチュートリアルではMigemoオブジェクトの状態を変化させないので、通知は不要です。
プロキシへの変換をスキップすることで、処理時間とメモリを削減します。

```diff_javascript
  mounted() {
    fetch('data.csv')
    .then(e => e.text())
    .then(e => {
      this.items= e.split('\n')
    })
+   fetch('migemo-compact-dict')
+   .then(e => e.arrayBuffer())
+   .then(e => {
+     const dict = new CompactDictionary(e)
+     const migemo = new Migemo()
+     migemo.setDict(dict)
+     this.migemo = markRaw(migemo)
+   })
  },
```

一度、ブラウザで実行してみて、エラーがないことを確認してみましょう。

## 5. Migemoで絞り込む

filteredItemsメソッドを変更します。

まず、Migemoオブジェクトの生成は辞書ファイルを読み込んでからですので、時間がかかります。
そのため、migemoがnullなら絞り込みせずitemsを返すようにします。
migemoが初期化されnullでなければ、queryメソッドに入力された文字列を渡して、正規表現を生成してもらいます。
RegExpクラスで`g`フラグを指定しなければ、一致を探索するカーソルが移動してしまいます。
`exec`メソッドの結果、nullが返ってこなければ正規表現で一致したので、filteredItemsの結果として返します。


```diff_javascript
  computed: {
    filteredItems(): string[] {
-     return this.items.filter(e=>e.indexOf(this.query)!==-1)
+     if (this.migemo) {
+       const regex = new RegExp(this.migemo!.query(this.query), "g")
+       return this.items.filter(e=>regex.exec(e)!==null)
+     } else {
+       return this.items
+     }
    }
  }
```

以下に全体のコードを載せます。

```vue:src/components/HelloWorld.vue
<script lang="ts">
import { defineComponent, markRaw } from 'vue'
import { CompactDictionary, Migemo } from 'jsmigemo'

export default defineComponent({
  data() {
    return {
      query: "",
      items: [] as string[],
      migemo: null as null|Migemo,
    }
  },
  mounted() {
    fetch('data.csv')
    .then(e => e.text())
    .then(e => {
      this.items= e.split('\n')
    })
    fetch('migemo-compact-dict')
    .then(e => e.arrayBuffer())
    .then(e => {
      const dict = new CompactDictionary(e)
      const migemo = new Migemo()
      migemo.setDict(dict)
      this.migemo = markRaw(migemo)
    })
  },
  computed: {
    filteredItems(): string[] {
      if (this.migemo) {
        const regex = new RegExp(this.migemo!.query(this.query), "g")
        return this.items.filter(e=>regex.exec(e)!==null)
      } else {
        return this.items
      }
    }
  }
})
</script>

<template>
  <h1>全国地方公共団体コード検索</h1>
  <input v-model="query" />
  <ul>
    <li v-for="item in filteredItems" :key="item">
    {{item}}
    </li>
  </ul>
</template>
```

それでは、実際にMigemo検索を試してみましょう。

検索する自治体は「吹田市」です。よく、「ふきたし」と誤読されることで有名です。

「fuki」と入力すると、3自治体まで絞れました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/105381/68649ab0-fae7-c63e-8e7b-4ffa0c342f12.png)

続けて、空白の後「ta」と入力すると、無事に「吹田市」のみに絞れました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/105381/32b35aef-7423-39d7-4ae9-ad971242f8e5.png)

漢字変換の手間をかけずに大量の項目の中から素早く目的の項目を絞ることができました。
しかも、正しい漢字の読み方をしらなくても問題ありません。

## 6. ヒット部分をハイライト

Migemo検索により項目のフィルタリングを実現しましたが、これでは入力した文字列が項目のどの部分にヒットしたのか分かりません。
検索に続けて入力するためには、ヒットした部分のハイライトが必要です。

filteredItemsを以下のように書き換え、ヒットした部分をspanタグで囲むようにします。

```javascript
  computed: {
    filteredItems(): string[] {
      if (this.migemo && this.query) {
        const regex = new RegExp(this.migemo!.query(this.query), "g")
        return this.items.map(e=> {
          const r = regex.exec(e)
          if (r) {
            return e.substring(0, r.index) + "<span class='highlight'>" + e.substring(r.index, r.index+r[0].length) + "</span>" + e.substring(r.index + r[0].length)
          }
          return ""
        }).filter(e => e!=="")
      } else {
        return this.items
      }
    }
  }
```

HelloWorld.vueのtemplateおよびstyleを以下のようにし、ハイライトしましょう。

```vue
<template>
  <h1>全国地方公共団体コード検索</h1>
  <input v-model="query" />
  <ul>
    <li v-for="item in filteredItems" :key="item" v-html="item">
    </li>
  </ul>
</template>

<style>
span.highlight {
  background-color: pink;
}
</style>
```

無事、ハイライトされました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/105381/ccbb0ac2-0fbf-c8dc-9ff8-0af401297331.png)

「吹田市」を絞るためには、ハイライトされた「吹」の次の文字の「田」にヒットさせる文字列を入力すれば良いことがわかります。
そこで、検索文字列の「fuki」のあとに、空白と「ta」を入れれば、「吹田市」に項目を絞れるのです。

## まとめ

Migemoにより漢字変換という手間を削減できるため、項目選択が早くなります。
ぜひ、Migemoを導入してみてください。