Cutting Edge

先行入力

Dino Esposito

Dino EspositoWeb 初期のころから、ページ自体やサイト内のコンテンツをすぐに検索できるように、多くのページに検索ボックスが用意されてきました。大規模サイトには、優れた検索機能が不可欠です。検索機能が優れていれば、サイトのマップやアーキテクチャに関わらず、目的のコンテンツを迅速かつ簡単に見つけることができます。

たとえば、ショッピングサイトでは、クエリ文字列を使用して製品、特典、ニュース、お知らせなどを検索することを考えます。プロ スポーツのチームなどに向けて構築されるサイトでは、検索機能を使用してニュース、試合結果、アスリート名、経歴などを検索できる必要があります。検索機能の実行対象となるデータ構造は、常に明らかなわけではありません。データ構造は明らかにアプリケーション固有のものです。

毎回最初から作り直すのではなく、Lucene.Net のようなアドホックなフルテキスト検索エンジンを使用して、検索機能を支援することを考えます。Lucene.Net のようなエンジンは、文字列を基盤とするドキュメント群にインデックスを付け、指定されたクエリをこのインデックスに照らして解析します。このようにすることで、クエリ文字列の複雑な組み合わせを指定できるようになります。多くのページやサイトにとって Lucene.Net は十分な役割を果たしているかもしれませんが、それでも、項目の果てしないリストをドロップダウンに配置するよりもスマートな種類の検索が必要です。

今回は、Twitter typeahead.js を中心にビルドするオート コンプリートを目的とした小規模フレームワークについて説明します。このフレームワークにより、Web ページでのオート コンプリートの使用が実に簡単になります。最大の魅力は、同じページ内でクエリを行い、異質でも関連する情報の取得が可能になるように、複数のデータセットを組み合わせられるようになることです。

Typeahead.js のセットアップ

今回は、実際のシナリオで typeahead を使用する場合の基礎について説明します。使用するバージョンの typeahead は NuGet で「typeahead」と入力すれば見つかります (図 1 参照)。Google で「typeahead」を検索すると、古い資料や古い JavaScript ファイル、オリジナルのプロジェクト コードとは異なる派生バージョンまで検索される可能性があります。ドキュメントの中には誤解を招くようなものもあります。

Twitter Typeahead.js 用 NuGet パッケージ
図 1 Twitter Typeahead.js 用 NuGet パッケージ

バンドル ファイルには、ローカル ブラウザーでヒントを管理するための Bloodhound エンジンなど、ライブラリを構成するすべてのパッケージが含まれています。typeahead.js を使用するように Web ページまたは Razor ビューをセットアップするのに必要なのは、使い慣れた jQuery に似た構文を使用して、選択した入力フィールドでプラグインをアクティブにするだけです。以下に例を示します。

<form action="@Url.Action("Query", "Home")" method="post">

  <input type="hidden" id="queryCode" name="queryCode" />

    <input type="text" name="queryString" id="queryString">

    <button id="queryButton" type="submit">Get</button>

</form>

<form action="@Url.Action("Query", "Home")" method="post">
  <input type="hidden" id="queryCode" name="queryCode" />
    <input type="text" name="queryString" id="queryString">
    <button id="queryButton" type="submit">Get</button>
</form>

何らかの利便性を目的に Web ページでオート コンプリートを使用するには、選択したヒントの一意 ID を収集するために、非表示の連携フィールドを用意する必要があることに注意してください。オート コンプリート入力フィールドの使用を考えるシナリオは数多くあります。今回考えるのは、果てしないドロップダウン リストの代わりにオート コンプリートを使用するシナリオです。そうすれば、Lucene.Net のようなフルテキスト エンジンの支援を受けることなく、軽量な Bing スタイルの検索を自身のサイトで利用できるようになります。

ビューに用意するスクリプト コード

typeahead.js を使用するには、jQuery 1.9.1 以上と typeahead スクリプトを参照します。ビューに必要最小限のコードを以下に示します。

$('#queryString').typeahead(
  null,
  {
    displayKey: 'value',
    source: hints.ttAdapter()
  }
});

これにより、既定の設定をすべて取得し、value プロパティを使用して返されたデータを ドロップダウン リストに設定するようエンジンに指示します。この場合、value という名前のプロパティが、フィルタリングするすべてのデータに存在すると想定します。理論上、あらゆる JavaScript データ配列にオートコンプリートをセットアップできます。ただし、実用としては、データをリモート ソースからダウンロードする際にオートコンプリートが特に意味を持ちます。

リモート ソースからのダウンロードには、同一生成元のブラウザー ポリシー、プリフェッチ、キャッシュなど、多くの問題があります。Twitter typeahead.js には、Bloodhound というヒント表示エンジンが付属しています。このエンジンにより、利用者が意識することなく、ヒントを表示する処理の大半が実行されます。NuGet から JavaScript バンドル ファイルを取得する場合、ダウンロードやセットアップを考える必要なく、Bloodhound への呼び出しを開始するだけです。上記のコードの hints 変数は、以下のコードの結果で、Bloodhound の標準の初期化方法です。

var hints = new Bloodhound({
  datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'),
  queryTokenizer: Bloodhound.tokenizers.whitespace,
  remote: "/hint/s?query=%QUERY"
});
hints.initialize();

remote 属性に注意してください。これは、ドロップダウン リストに表示するヒントを返す役割を担うサーバーのエンドポイントにすぎません。%QUERY 構文にも注意します。これは、ヒントを求めてサーバーに送信される入力フィールドの文字列を示します。つまり、%QUERY は、入力フィールドに含まれるテキストのプレースホルダーです。既定の typeahead.js では、1 文字入力するとすぐにヒントが表示されます。文字をバッファーに待機させてからオートコンプリートを開始する場合は、プラグインの最初の引数として設定オブジェクトを追加します。

$('#queryString').typeahead(
  {
    minLength: 2
  },
  {
    displayKey: 'value',
    source: hints.ttAdapter()
  }
});

リモート呼び出しを開始できるだけの情報がバッファーがたまると、Bloodhound によって処理が開始されます。Bloodhound は、JSON データをダウンロードして、表示用に変換します。これで、機能する基本的なオートコンプリート エンジンが用意され、サーバーのロジックに基づいてヒントがポップアップ表示されるようになります。ただし、実際のページで効果的にオートコンプリートを使用できるようにするには、まだ多くの作業が必要です。

Typeahead.js と Bootstrap の使用

複雑なプラグインでは、外観を整えるためにある程度の CSS が必要になります。Typeahead.js も例外ではありません。Typeahead.js プラグインは独自の既定 UI を装備していますが、特に Twitter Bootstrap と共に使用する場合は修正を加えたくなります。また、色やパディングなどの表示属性のカスタマイズも考えるかもしれません。図 2 は、typeahead.js コンポーネントの外観をカスタマイズするために使用できる CSS クラスの一覧を示しています。

図 2 Typeahead.js コンポーネントをカスタマイズするために編集する CSS クラス

CSS クラス 説明
twitter-typeahead ユーザーがヒントを入力する入力フィールドのスタイル設定。
tt-hint 入力したヒントと最初のヒントとの差を表すテキストのスタイル設定。このクラスは、hint プロパティを true に設定する場合のみ使用 (既定値は false)。
tt-dropdown-menu ヒントを一覧するドロップダウン ポップアップのスタイル設定。
tt-cursor ドロップダウン ボックスで強調表示するヒントのスタイル設定。
tt-highlight クエリ文字列に一致するテキスト部分のスタイル設定。

図 3 に、カスタマイズした CSS クラスを利用する考え方を示します。また、機能の観点から、プラグイン全体の動作をカスタマイズすることもできます。

アプリに異なる効果を実現できるカスタマイズ対象の CSS クラス
図 3 アプリに異なる効果を実現できるカスタマイズ対象の CSS クラス

クライアント側ロジックの追加

オートコンプリート フィールドは、長いリストを備えたドロップダウンよりも高速です。選択肢となる項目数が数百個でも、従来のドロップダウン リストは低速です。そのため、オートコンプリート入力フィールドを使用して特定の値 (製品名、顧客など) を選択することを計画している場合は、プレーンな typeahead.js プラグインでは不十分です。次のように、プラグインの selected イベントにバインドする追加のスクリプト コードが必要です。

$('#queryString').on('typeahead:selected', 
  function (e, datum) {
  $("#queryCode").val(datum.id);
});

オートコンプリート入力の主なメリットは、ユーザーが何か意味のある名前を入力することで、システムが関連する一意コードまたは一意 ID を取得できることです。この機能は明示的にコーディングする必要があります。selected イベント ハンドラーでは、datum オブジェクトから ID 情報を取得し、非表示フィールドに安全に格納します。オートコンプリート入力フィールドが属するフォームが投稿されるときに、selected フィールドの ID も投稿されます。datum オブジェクト (ドロップダウンの選択済みデータ項目) の形式は、サーバー側から受け取るデータの形式によって異なります。

入力フィールドにはテキストをどのように表示すべきでしょう。このシナリオでは、入力フィールドに意味のあるテキストを表示する必要はおそらくありません。以降の操作に関係する入力は、非表示フィールドに格納します。何を表示するかは開発者次第です。プラグイン設定の displayKey プロパティを指定するかどうかだけに注目します。このプロパティの値が自動的に入力フィールドに表示されるためです。いずれにせよ、プログラムから任意の値を設定できます。

$("#queryString").val(datum.label);

場合によっては、オートコンプリートのテキスト ボックスを HTML フォーム唯一の要素にすることがあります。つまり、データが選択された時点ですぐにそのデータを処理することを考える場合です。typeahead.js の selected イベント ハンドラーに以下のコード行を追加することで、フォームの送信ボタンのクリックをシミュレーションします。

$("#queryButton").click();

ユーザーが入力フィールドへの入力を開始するとドロップダウンが表示され、ユーザーが選択を行わずに入力を中止したとします。その後、ユーザーが入力を再開する際、どうすべきでしょう。基本的には、ユーザーが選択を行うまで入力を再開できるようにします。ユーザーが選択を行ったら、何らかのコードが格納されるため、編集を再開する際は、それまでの選択内容をキャンセルする必要があります。これを行うには、ローカル変数が必要です。

var typeaheadItemSelected = false;
$('#queryString').on('typeahead:selected', function (e, datum) {
  $("#queryCode").val(datum.id);
  typeaheadItemSelected = true;
});

格納されているデータをリセットするには、入力フィールドの focus イベントのハンドラーも必要です。

$('#queryString').on('input', function () {
  if (typeaheadItemSelected) {
    typeaheadItemSelected = false;
    $('#queryString').val(''); 
    $("#queryCode").val('');
  }
});

この追加のクライアント側ロジックの最終目標は、オートコンプリート入力フィールドがドロップダウン リストと同じように機能することです。

オートコンプリートのサーバー側

クライアント側のコードはどのようなものであれ、厳密にはサーバーから返されるデータによって異なります。極端に言えば、サーバーのエンドポイントは、JSON データを返す URL にすぎません。Product オブジェクトのコレクションを返すエンドポイントにしたならば、たとえば、typeahead.js の displayKey プロパティを使用して、ヒントのドロップダウン リストで何らかのデータ バインドを実行することになります。最もシンプルな形式の場合、JSON を返すコントローラー メソッドは以下のようになります。

public JsonResult P(string query)
{
  var productHints = _service.GetMatchingProducts(query);
  return Json(productHints, JsonRequestBehavior.AllowGet);
}

オートコンプリート入力フィールドが同種のデータ項目のヒントを表示するとすれば、これは理想的なアプローチです。クライアント側では、実際、typeahead.js に組み込まれたテンプレート メカニズムを利用し、ヒントのカスタム ビューを簡単に用意できます。

$('#queryString').typeahead(
null,
{
  templates: {
    suggestion: Handlebars.compile('<b>({{Id}}</b>: {{notes}}')
  },
  source: hints.ttAdapter()
});

templates プロパティは displayKey を置き換え、ドロップダウンのコンテンツ用のカスタム レイアウトを設定します。図 3 のドロップダウンは、前述のコード スニペットの結果です。テンプレートを調整する場合は、Handlebars などのアドホック テンプレート エンジンの使用を検討できます (handlebarsjs.com、英語)。typeahead.js とは別に、Handlebars をプロジェクトにリンクする必要があります。Handlebars の使用はオプションです。JavaScript を手作業で用意するか、書式設定済みの HTML と共にサーバー側のオブジェクトを返すことで、HTML テンプレートをいつでも書式設定できます。

オートコンプリート入力で、製品と特典など、種類の異なるヒントを返すことを考える場合、事態は複雑になります。このような場合は、ユーザーが選択できるだけの情報を含む、何らかの中間データ型の配列を返す必要があります。今回のオートコンプリートでは、以下に示す基本的な AutoCompleteItem クラスを用意しています。

public class AutoCompleteItem
{
  public String label { get; set; }
  public String id { get; set; }
  public String value { get; set; }
}

id プロパティが一意 ID を含みます。投稿時に、この一意 ID が受信側のコントローラーにとって重要になります。一意 ID は通常、2 つの要素で構成されます。1 つは実際の ID です。もう 1 つは、クエリで返される可能性があるデータセット (製品または特典) の 1 つを一意に識別する ID です。このプロパティの値は、ドロップダウン リストに表示する文字列コンテンツです。他のプロパティは、必要な内容を表す一種の搬送プロパティです。value プロパティには、カスタム ヒントのレイアウト用にサーバー側で調整される HTML の文字列を含めます。サーバー側のコードでは、必要なクエリをすべて実行し、AutoCompleteItem オブジェクトのコレクションにデータを通知します。

まとめ

最新の Web サイトでは、ユーザビリティの重要性が日々高まっています。Web サイトで利用者が目にする部分はもちろんですが、実際の情報を挿入する Web サイトのバック エンドの重要性が増しています。Web サイトを管理する立場から言うと、かつて Web サイトでドロップダウン リストに 700 個以上の項目を表示したことがあります。機能はしたものの、低速でした。ドロップダウン リストをオートコンプリートに置き換えたところ、驚くほど高速になりました。これを行うための GitHub プロジェクトと他のユーティリティを用意しました。bit.ly/1zubJea (英語) を参照してください。


Dino Esposito は、『Microsoft .NET: Architecting Applications for the Enterprise』(Microsoft Press、2014 年) および『Programming ASP.NET MVC 5』(Microsoft Press、2014 年) の共著者です。JetBrains の Microsoft .NET Framework および Android プラットフォームのテクニカル エバンジェリストでもあります。世界各国で開催される業界のイベントで頻繁に講演しており、software2cents.wordpress.com (英語) や Twitter (twitter.com/despos、英語) でソフトウェアに関するビジョンを紹介しています。

この記事のレビューに協力してくれた技術スタッフの Jon Arne Saeteras に心より感謝いたします。