エイエイレトリック

なぐりがき

# Next.js で TinySegmenter と kuromoji.js を実行する

以前、 Next.js で作成したページを versel にデプロイしました。

eieito.hatenablog.com

せっかくなので、 javascript を使った NLP を色々試しています。 コードは private repository で管理しているので、ブログで実装を紹介します。

ちなみに Next.jsのチュートリアル に従ってコードは TypeScript 化したので、 紹介するコードは TypeScript で書いています。また、コードは一部抜粋しています。

TinySegmenter

手始めに form に入力したテキストに対して TinySegmenter を実行するページを作りました。

実際のページ: https://shihono-nextjs-test.vercel.app/misc/tiny-segmenter

表示部分としては、 form の onSubmit に textarea のテキストに対し、TinySegmenter を実行する関数 handlerTokenizer を定義しています。

  return (
    <Layout>
      <article>
        <section>
          <form onSubmit={handleTokenizer}>
            <label htmlFor="body">Text</label>
            <textarea
              name="body"
              id="text"
              defaultValue="ここにテキストを入力してください。"
              ref={ref}
            ></textarea>
            <button type="submit">Submit</button>
          </form>
        </section>
        <div>
          <p>{result}</p>
        </div>
      </article>
    </Layout>
  );

formを使ったページの実装自体は Data Fetching: Building Forms | Next.js のコードをそのまま流用しています。

関数 handleTokenizer では TinySegmenter.segment分かち書きを実行し、結果を setResult で表示します。

TinySegmenter に関しては、公式ページのコードは ESmodule に対応していないようだったので code4fukui/TinySegmenterlib/TinySegmenter.js として配置し、 import しています。

import { TinySegmenter } from '../../lib/TinySegmenter';

export default function TokenizeWithTinySegmenter() {
  const [result, setResult] = React.useState('ここに結果が出力されます。');
  const ref = useRef(null);

  const handleTokenizer = async (event: any) => {
    event.preventDefault();

    const data = ref.current.value;
    if (data) {
      try {
        const segs = TinySegmenter.segment(data);
        setResult(segs.join(' / '));
      } catch (error) {
        alert('Fail to tokenize text');
      }
    }
  };

実際の画面を触ってみるとわかりますが、本当に実行されているのか?と不安になるほど 高速に動きます。

TinySegmenter はフロントだけで完結でき、簡単に実装できるのがいいですね。

kuromoji.js

つづいて kuromoji.js も実行できるようにしました。 ちょうど TinySegmenter と kuromoji.js を比較する機会があったので、2つの結果を並べて表示し、可視化します。

実際のページ: https://shihono-nextjs-test.vercel.app/misc/compare-tokenizer

kuromoji.js は別の機能として切り出して実装することにしました。

具体的には pages/api/ ページに kuromoji.js の分かち書き結果を返すエンドポイント kuromojiSurface.ts を実装し、 pages 側でそのエンドポイントにリクエストする仕組みにしています。

api には非同期に対応した wrapper である kuromojin を使いました。

kuromoji.js は mecab と同様に品詞や読みも取得できますが、 今回は TinySegmenter との比較ができればよいため、分かち書き結果の表層 (surface_form) だけ返すようにしています。

// pages/api/kuromojiSurface.ts
import { tokenize } from 'kuromojin';
import { NextApiRequest, NextApiResponse } from 'next';

export type ResponseData = {
  result: string[];
};

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse<ResponseData>
) {
  const text = req.body.text;
  tokenize(text)
    .then((tokens) => {
      const surfaceList = tokens.map((data, _) => data.surface_form);
      res.status(200).json({ result: surfaceList });
    })
    .catch((e) => {
      console.error(e);
      res.status(500).end();
    });
}

画面側では TinySegmenter のページと同様、 form の onSubmit分かち書きを実行する関数 CompareTokenizers をセットします。 apiのレスポンスを使っているため、エラーハンドリングも足しておきました。

export default function CompareTokenizers() {
  const [kResult, setKResult] = React.useState('ここにkuromojiの結果が出力されます。');
  const [tResult, setTResult] = React.useState('ここにtiny segmenterの結果が出力されます。');
  const ref = useRef(null);

  const handleTokenizer = async (event: any) => {
    event.preventDefault();

    const data = ref.current.value;
    if (data) {
      const kuromojiResult = await fetch('/api/kuromojiSurface', {
        method: 'POST',
        body: JSON.stringify({ text: data }),
        headers: { 'Content-Type': 'application/json' },
      });
      if (kuromojiResult.ok) {
        const kuromojiSegs: ResponseData = await kuromojiResult.json();
        setKResult(`${kuromojiSegs['result'].join('/')}`);
      }
      const segs = TinySegmenter.segment(data);
      setTResult(segs.join('/'));
    }
  };

kuromoji.js で辞書をロードするため、レスポンスが少し遅いです。

以下のように結果が表示されるようになりました。

出力イメージ

差分が直感的にわかるよう、表示部分は今後修正する予定です。

細々とした説明・懸念点

  • 上記 kuromojiSurface.ts のコードでは省略してますが、 externalResolver: true をセットして warning を消しています
  • ローカルで動作確認をしたくて pages/api に kuromoji.js を切り出しましたが、libs/ に実装しても問題ない気がします
    • ディレクトリごとの切り分け方針がまだよくわかってません
  • kuromojin は (textlint での利用を考えてか) 結果をキャッシュします
    • 最初は自力で kuromoji.js を非同期化しようと思ったのですが、力尽きました
    • 結果として自力で実装するよりメリットが多いのでよしとします