「あ」→「雨」を自力で実装する|IMEに頼らない日本語サジェストの作り方

「あ」→「雨」を自力で実装する|IMEに頼らない日本語サジェストの作り方 Program
この記事は約14分で読めます。

Googleの検索窓に「あ」と打つと、「雨」「赤」「青」といった漢字の候補がすっと表示される。あれを自分のサイトでもやりたいと思ったことはありませんか?

私はブログの検索機能を改善しようとしたとき、最初に「IMEの変換候補を取得すればいいのでは」と考えました。でもそのアプローチは不可能でした。ブラウザからIMEの内部にはアクセスできない——これはバグではなくセキュリティ上の意図的な設計です。

では実際にどうするかというと、「漢字と読みの辞書」を自前で持ち、ひらがなで前方一致検索をかけます。IMEの力を借りるのではなく、IMEを迂回して自力で候補を出すわけです。

この記事では、この「読み検索」方式の日本語サジェストを、HTMLとJavaScriptだけでゼロから実装します。IMEの合成イベント対応、debounce、プレフィックス検索のロジックまで一通りカバーするので、この記事だけで動くものが作れます。

完成イメージはこちらです。入力欄に「あ」と打つと、読みが「あ」で始まる漢字候補がドロップダウンで表示されます。

完成したサジェスト機能のデモ画面(「あ」で候補が表示されている状態) 完成したサジェスト機能のデモ画面(「あ」で候補が表示されている状態)

応用編(外部API連携、サーバーサイド実装、パフォーマンス最適化)は後編で扱います。

なぜIMEの変換候補は取得できないのか

最初にこの疑問をクリアにしておきます。「IMEが持ってる候補をJavaScriptで読めばいいじゃん」——これは多くの人が最初に思いつくアプローチですが、技術的に不可能です。

IME(Input Method Editor)はOSレベルで動作するソフトウェアで、ブラウザのJavaScriptからは内部状態にアクセスできません。その理由はセキュリティとプライバシーです。

もしWebサイトがIMEの変換候補を取得できたら何が起きるか。ユーザーが入力しようとしている文字列を確定前に盗み見できてしまいます。しかもIMEには学習機能があるので、過去に変換した単語——家族の名前、住所、勤務先なども候補に含まれています。これらがWebサイト側に漏れるのは明らかにまずい。

ブラウザが提供してくれるのは、IMEが「合成中かどうか」と「合成が終わったかどうか」の2つの情報だけです。変換候補の中身には触れさせてもらえません。

正しいアプローチ:「読みによる前方一致検索」

IMEに頼らず、自前で「漢字と読み」のペアを持った辞書データを用意し、ひらがなで前方一致検索をかける。これが正解です。

考え方をIMEの「変換」から「検索」に切り替えるのがポイントです。

日本語は単語の先頭から順に入力するので、前方一致検索と相性がいい。入力が進むにつれて候補が絞り込まれていく——Googleの検索サジェストと同じ挙動です。

実装に必要なのは3つ。辞書データ、検索ロジック、表示UI。順番に作っていきます。

IMEの合成イベントを理解する——ここが日本語サジェストの核心

英語のサジェストならinputイベントを監視するだけで済みますが、日本語はそうはいきません。日本語入力にはIMEによる「合成(composition)」というステップがあり、これを正しく扱わないとサジェストが暴発します。

何が起きるのか:inputイベントの暴発問題

「あめ」と入力する場合を考えてみてください。キーボードで「a」「m」「e」と打つと、inputイベントは3回発火します。

もしinputイベントが発火するたびに検索を実行したら、「あ」の時点で候補が出て、「あm」で候補が消えて、「あめ」でまた出て……と表示がガタガタになります。

解決策:compositionイベントで合成中を判定する

ブラウザは、IMEの合成状態を3つのイベントで通知してくれます。

compositionstart——IMEによる合成が始まったとき。「今からIMEで入力するよ」という合図。

compositionupdate——合成中のテキストが更新されるたび。ローマ字→ひらがな変換のたびに発火。

compositionend——合成が終了(確定)したとき。「入力が確定したよ」という合図。

サジェスト検索を実行するベストなタイミングはcompositionendです。確定したときだけ検索を走らせれば、暴発問題は解決します。

実装:フラグ変数で合成中を追跡する

inputイベントのisComposingプロパティで判定できますが、古いSafariやiOSでは正しく動かないケースがあります。自前のフラグ変数を併用するのが確実です。

e.isComposing || isComposingで二重チェックしているのは、ブラウザ間の差異を吸収するためです。Chrome、Firefox、Safari、Edgeのどれでも動きます。

DevToolsのConsoleでcompositionイベントの発火を確認している画面

debounce——連続入力による無駄な検索を防ぐ

IMEの合成中は検索をスキップできるようになりました。次の問題は「連続確定」です。

「天気予報」と入力するとき、「てんき」確定→「よほう」確定と2回の確定が発生します。確定のたびに検索を実行すると、「てんき」の検索結果が一瞬表示されてすぐ消える。ユーザーにとっては無駄なチラつきです。

debounceは、「最後の入力から一定時間(150ms程度)経つまで検索を待つ」仕組みです。入力が続いている間はタイマーをリセットし続け、入力が落ち着いたら1回だけ検索を実行します。

debounceなしの場合のタイムライン

debounce 150msありの場合のタイムライン

debounceありの場合、入力があるたびにタイマーがリセットされ、最後の入力から150ms後に1回だけ検索が走っています。

debounce関数の実装

setTimeoutで遅延実行を仕掛けつつ、次の呼び出しが来たらclearTimeoutで前のタイマーをキャンセルする。シンプルですが効果は絶大です。

遅延時間は100〜300msが一般的です。150msはローカル検索(辞書データがブラウザ内にある場合)にちょうどいいバランスで、体感的にはほぼ即時に見えます。

プレフィックス検索のロジック

いよいよ検索ロジックの実装です。JavaScriptのfilterstartsWithを組み合わせます。

基本の検索関数

読みだけでなくテキスト自体も検索対象にしているのは、「アメリカ」を「あめりか」でも「アメリカ」でもヒットさせるためです。

ソートは完全一致を最優先にし、それ以外は読みが短いもの(より具体的な単語)を上に表示します。slice(0, 10)で候補を最大10件に制限——多すぎると選びづらくなるからです。

セキュリティ:HTMLエスケープを忘れない

候補をHTMLに挿入する際、エスケープ処理を忘れるとXSS(クロスサイトスクリプティング)の脆弱性になります。辞書データが自前であっても、将来的にユーザー入力やAPI経由のデータを候補に含める可能性があるなら、最初からエスケープしておくべきです。

textContentに代入→innerHTMLで取り出すと、<&が自動的にエスケープされます。地味ですが、本番運用では必須の処理です。

ここまでの全パーツを統合する

IME合成判定、debounce、プレフィックス検索、HTMLエスケープ——ここまでのパーツを一つにまとめます。

動作の流れを確認

IMEで「あめ」と入力→確定した場合:

IME不使用で「test」と入力した場合:

日本語でも英語でも正しく動くことが確認できました。

次のステップ:UIの構築と完全版コード

ここまでで、日本語サジェストの「頭脳」部分(IME対応 + 検索ロジック)は完成しています。

後編では、この検索ロジックに「体」を与えます。HTMLとCSSで候補表示UIを構築し、キーボード操作(↑↓キー、Enter、Escape)、マウスクリック、フォーカス制御まで実装して、コピペで動く完全なサンプルコードを仕上げます。さらに、外部API連携やサーバーサイド実装など、本番運用向けの応用テクニックもカバーします。

macOSの日本語入力には、最初の1文字だけ英字になる独特の問題もあります。IMEとイベント処理の関係を掘り下げた記事はこちらです。

まとめ

「あ→雨」はIMEの変換ではなく、読みによる前方一致検索で実現する。IMEの変換候補にはブラウザからアクセスできないので、辞書データを自前で持ち、startsWithで検索をかけるのが正しいアプローチです。

日本語特有の課題はIMEとの共存です。compositionstart/compositionendイベントで合成中を判定し、合成中は検索をスキップする。加えてdebounceで連続確定による無駄な検索を防ぐ。この2つを組み合わせれば、日本語でもストレスのないサジェスト体験が実現できます。

Program
この記事を書いた人
rapls

Web開発歴6年以上のフリーランスエンジニア。WordPressプラグイン開発とAIツール活用が専門。「現場で本当に役立つ情報」をモットーに、開発で遭遇したトラブルと解決策を発信しています。

raplsをフォローする
raplsをフォローする

コメント

タイトルとURLをコピーしました