以前、 Next.js で作成したページを versel にデプロイしました。
せっかくなので、 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/TinySegmenter を lib/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 を非同期化しようと思ったのですが、力尽きました
- 結果として自力で実装するよりメリットが多いのでよしとします