図1●インクリメンタル検索アプリケーション<br>クエリー文字を含む文章が,前後の文脈付きで一覧表示されます。今回から2回にわたり,このAjaxアプリケーションの作成方法を紹介します。
図1●インクリメンタル検索アプリケーション<br>クエリー文字を含む文章が,前後の文脈付きで一覧表示されます。今回から2回にわたり,このAjaxアプリケーションの作成方法を紹介します。
[画像のクリックで拡大表示]
図2●作成したクライアントをテスト&lt;br&gt;クライアントから受信したデータをそのまま返送するサーバーを使ってテストした様子。入力データがそのまま表示されるのが分かります。
図2●作成したクライアントをテスト<br>クライアントから受信したデータをそのまま返送するサーバーを使ってテストした様子。入力データがそのまま表示されるのが分かります。
[画像のクリックで拡大表示]

この連載記事の目次へ

 前回はAjaxの概念や利点を説明し,筆者が作成したAjaxアプリケーションを紹介しました。紹介したものの一つがインクリメンタル検索用アプリケーションです。検索ボタンをクリックして初めて検索される通常のWeb検索とは異なり,インクリメンタル検索はキーワードを1文字入力するたびに即座に検索を実行します。検索結果はKWICという形式で表示します(図1[拡大表示])。KWICはKeyWord In Contextの略で,前後の文脈付きで検索結果を表示する形式です。

 今回から2回にわたり,このインクリメンタル検索を実現するAjaxアプリケーションを作成します。単なる動作説明用の「おもちゃ的な」サンプルではなく,実用性のあるツールとして仕上げる予定です。検索速度はあまり追求しませんので大規模な検索には向きませんが,個人のブログ程度であれば十分な速度で検索できるはずです。

クライアント側を実装

 Ajaxアプリケーションのクライアント側は,JavaScriptとHTMLを使って実装します。JavaScriptで特に重要なのが,XmlHttpRequestオブジェクトに関する実装です。同オブジェクトはサーバーとHTTPで通信し,リクエスト発行やサーバーからの結果を受信するAjaxの中心的なオブジェクトです。

 XmlHttpRequestオブジェクトには注意すべきことがあります。それは,Webブラウザごとに作成方法が異なることです。例えば,MozillaやFirefoxではXMLHttpRequestクラスを使いますが,Internet Explorer(IE)ではActiveXObjectクラスを使います。しかも,IEのバージョンやOSによって指定するIDが異なり,IE 6.0以降では「Msxml2.XMLHTTP」,それ以前では「Microsoft.XMLHTTP」を指定します。

 このような環境による差違は,専用のラッパー関数を作成して吸収すると良いでしょう。今回,ラッパー関数「createXmlHttpRequest」を次の通り作成しました。これは,ほぼすべてのAjaxアプリケーションに適用できる汎用的な関数です。

function createXmlHttpRequest() {
  var xmlhttp = false;
  if( window.XMLHttpRequest) {
    xmlhttp = new XMLHttpRequest();
  } else if(window.ActiveXObject) {
    try {
      xmlhttp = new ActiveXObject("Msxml2.XMLHTTP");
    } catch(e) {
      xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
    }
  }
  return xmlhttp;
}

 続いて考慮すべきなのが,XmlHttpRequestオブジェクトがどのようなタイミングでサーバーと通信するかということです。インクリメンタル検索を実装する場合,一般に次の2つのタイミングが考えられます。

  1. キーが押し下げられた際にサーバーと通信する
  2. 一定間隔で入力を監視し,入力があった場合にサーバーと通信する

 キーの押し下げは,onkeydownイベントもしくはonkeypressイベントにより判別します。前者の場合,これらのイベント発生時にサーバーと通信するコードを記述します。後者の場合,入力されたクエリー文字列を監視し,変化があった場合にサーバーと通信する関数を定義し,これを定期的に実行します。

 今回は後者の方法(onkeypressイベント)を採用します。後者の方法には,前者に比べて次のような利点があります。

  • 無駄なリクエストが減るためサーバーの負荷が小さい
  • 処理のタイミングをプログラム側が制御するため動作が安定する
  • IMEの変換が未確定な状態でも入力を監視できる

 最後に挙げたものは,Webブラウザの実装に依存した「利点」です。筆者が調べた範囲では,IEの場合だとonkeyupイベントを使うと未確定な文字列を拾えましたが,MozillaやFireFoxではうまくいきませんでした。一方,定期的に監視すると,多くのWebブラウザで未確定な文字列を拾えるようです。

 クエリー文字列の状態監視は,次のようなpeekQuery関数を定義し,これを定期的に実行して実現します。

var oldquery = "";
var xmlhttp = 0;

function peekQuery () {

  if (! xmlhttp) xmlhttp = createXmlHttpRequest();

  if (! xmlhttp || xmlhttp.readyState == 1 || 
      xmlhttp.readyState == 2 || xmlhttp.readyState == 3){
    return; 
  }

  var result  = document.getElementById("result");
  var textbox = document.getElementById('query');
  var query   = encodeURI(textbox.value);

  if (query == "") {
    result.style.display = "none";
  } else if (oldquery != query) {

   xmlhttp.open("GET", "search.cgi?" + query, true);
    xmlhttp.onreadystatechange = function() {
      if (xmlhttp.readyState == 4 && xmlhttp.status == 200) {
	result.style.display = "block";
	result.innerHTML = xmlhttp.responseText;
      }
    }
    xmlhttp.send(null)
  }
    oldquery = query;
}

onload = function () { setInterval("peekQuery()", 800); }

 同関数の処理の流れは次の通りです。

  1. XmlHttpRequestオブジェクトが存在しなければ作成
  2. XmlHttpRequestオブジェクトが以前のデータを処理中ならば,新規処理を中断
  3. サーバーからの応答があった際に実行する関数を定義
  4. open,sendメソッドを使ってサーバーに接続
  5. setInterval関数で定期的に自分自身を呼び出す

 以下で,それぞれの処理の内容を詳しく見ていきましょう。

●XmlHttpRequest オブジェクトの作成
 最初にXmlHttpRequestオブジェクトを作成します。Webブラウザごとの違いを吸収するため,先ほど作成したラッパー関数(createXmlHttpRequest)を呼び出します。 XmlHttpRequestオブジェクトが作成済みの場合は,それを再利用します。 該当するコードは下記の部分になります。

if (! xmlhttp) xmlhttp = createXmlHttpRequest();

●XmlHttpRequestオブジェクトの状態確認
 XmlHttpRequestオブジェクトのreadyStateプロパティを参照すれば,オブジェクトの状態が確認できます。readStateプロパティは以下の値になります。
  • 0:UNINITIALIZED (未初期化)
  • 1:LOADING (サーバーとの通信開始中)
  • 2:LOADED (サーバーからの応答待ち)
  • 3:INTERACTIVE (サーバーからの応答を処理中)
  • 4:COMPLETED (処理完了)
 このうちreadyStateプロパティが1~3の場合は,XmlHttpRequestオブジェクトが処理中であるため,新規処理は実施しないことにします。もちろん,XmlHttpRequestオブジェクトが作成されていない場合にも処理を継続できません。これらの場合は,return文を実行して関数を終了させます。

  if (! xmlhttp || xmlhttp.readyState == 1 || 
      xmlhttp.readyState == 2 || xmlhttp.readyState == 3){
    return; 
  }

●サーバーからの応答を処理する関数の定義
 サーバーとの通信処理が正常に終了すると,応答データを処理しなければなりません。これには,応答処理用の関数をまず定義しておき,それをイベント・ハンドラとして登録します。具体的には,関数を定義し,その関数をXmlHttpRequestオブジェクトのonreadystatechangeプロパティに設定します。例えば,requestHandler()という関数を定義したとすると,次のようなコードで登録できます。

function requestHandler () { ... }
...
xmlhttp.onreadstatechange = requestHandler;

 ただ,このイベント・ハンドラは他の部分からは呼び出されないので,わざわざ名前を付けて定義する必要はありません。こういう場合,JavaScriptの無名関数の機能を使えばコード量を減らせます。具体的には,次のようにイベント・ハンドラの登録部分で関数を定義します。

xmlhttp.onreadystatechange = function() {
  if (xmlhttp.readyState == 4 && xmlhttp.status == 200) {
    result.style.display = "block";
    result.innerHTML = xmlhttp.responseText;
  }
}

 イベント・ハンドラとして登録した関数は,まずreadyStateプロパティが「処理完了」であることと,HTTPのステータス・コードが「200」(リクエスト成功)であることを確認します。これらが共に満たされていれば,サーバーとの通信処理が正しく実施されたと判断できます。
 次に検索結果を表示するため,HTML文書内の結果表示部分(「result」とID指定しているdiv要素)のスタイルをJavaScriptで変更します。具体的には,標準では非表示(display:none)としている設定を,ブロック・レベル要素として表示(display:block)させます。これには,result.style.displayプロパティに「block」を指定します。さらに,result.innerHTMLプロパティに,サーバーからの応答データ(xmlhttp.responseText)を設定します。これにより,結果表示部分に応答データが表示されます。
 サーバーと交換するデータが複雑だったり,データを純粋なデータとして扱い,表示処理とデータの内容を分離したいときには,XmlHttpRequestオブジェクトのresponseXMLプロパティを使用します。このプロパティを使う際は,DOMオブジェクトを操作して必要な情報を切り出すことになります。

●open,sendメソッドを使いサーバーに接続
 サーバーとの通信は,XmlHttpRequestオブジェクトのopenメソッドとsendメソッドで実施します。名前の通り,openメソッドは接続をオープンし,sendメソッドはデータを送信します。
 openメソッドには3つの引数を指定します。なお,セキュリティ上の理由から,他ドメインのサーバーにリクエストを送ることはできません。
  1. 第1引数には,GET,POSTといったHTTPリクエスト・メソッドを指定
  2. 第2引数には,リクエストを送るサーバーのURLを指定
  3. 第3引数には,非同期通信の有無を指定。 「true」が非同期,「false」が同期通信を示す
 sendメソッドの引数には,サーバーにPOSTメソッドで送るデータを指定できます。今回はGETメソッドを使うため,「null」を指定してPOSTメソッドではデータを送信しないことにします。これらのコードを実装したのが次の部分です。

xmlhttp.open('GET', 'search.cgi?' + query, true);
...
xmlhttp.send(null);

(2005年12月1日追記)IEでの動作に不具合があったため,コードを一部変更しました。具体的にはopenメソッドを呼んだあとに,イベント・ハンドラの定義をするように修正しました。

●setInterval関数で定期的に自分自身を呼び出す
 最後に,一定時間ごとにpeekQuery関数が呼び出されるようにイベント・ハンドラを設定します。これには,JavaScriptコードの読み込みを示すイベント「onload」に対し,一定時間ごとにpeekQuery関数を実行するための処理を設定します。一定時間ごとに処理を実行するには,setInterval関数が利用できますので,これにpeekQuery関数を指定して記述します。実際のコードは次の通りです。なお,ここでもイベント・ハンドラを無名関数として定義しています。

onload = function () { setInterval("peekQuery()", 800); }

 setIntervalは,指定された関数を指定された周期で呼び出す関数です。第1引数に呼び出す関数を,第2引数に周期をミリ秒単位で記述します。この例では,peekQuery関数が0.8秒ごとに起動されます。
 ここまで紹介したラッパー関数(createXmlHttpRequest),peekQuery関数,そしてそれ以外のグローバル変数をまとめ,1つのJavaScriptプログラムにしたものが,リスト1[表示]です。これを「search.js」という名前で保存します。

HTML文書の用意

 続いて,ユーザーとの窓口になるHTML文書を用意します。このHTML文書には,検索用のフォームと結果表示エリアを配置します。結果表示エリアは,先に説明したように「display:none」というスタイルを設定することで,デフォルトでは表示されないようにします。 具体的には次のようにHTMLを記述します。

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<html>
<head>
<title>Incremental search with Ajax</title>
</head>
<body>
<script type="text/javascript" charset="UTF-8" src="search.js"></script>
<form>
<input id="query" type="text" size="50">
</form>
<div id="result" style="display:none;">
</body>
</html>

動作確認用のサーバー実装

 クライアントの動作確認用に,クエリーをそのまま返送するサーバー(CGIプログラム)を作成します。いろんな実装方法がありますが,ここではシェル・スクリプトを使って,次のように記述しました。これを「search.cgi」という名前で保存し,実行できる状態にします。なお,ここで挙げたサンプルは,JavaScriptファイル,HTMLファイル,CGIプログラムの3つが同一ディレクトリにあると仮定しています。異なる場所に配置する場合は,HTML文書内のscript要素のsrc属性や,JavaScriptファイル内のopenメソッドのURL指定などを変更してください。

#!/bin/sh
echo "Content-type: text/plain charset=\"UTF-8\""
echo
echo "$1"

 動作確認用サーバーを使った際の様子を図2[拡大表示]に挙げました。テキスト・ボックスに入力したクエリー文字列が,検索ボタンなどをクリックすることなく,そのまま結果表示エリアに表示されるのがわかります。ごく簡単なものですが,これも立派なAjaxアプリケーションです。

次回は

 ここまでインクリメンタル検索のクライアント側の実装を紹介しました。これからは,いよいよサーバー側の実装に進みます。 Ajaxアプリケーションの解説では,JavaScriptやDHTML,CSSといったクライアント側の技術に焦点が置かれることが多く,サーバー側の具体的な実装が表に出てくる機会はなかなかありません。しかしながら,サーバー側の実装はそんなに単純ではありません。特に,リクエスト数の増加に備えたり,高いユーザビリティを実現するためには,ある程度高速な処理が要求されます。

 次回は,インクリメンタル検索に適したデータ構造の選択や,C言語を使ったサーバーの実装などについて紹介します。

この連載記事の目次へ

工藤 拓(くどう たく)

1976年生まれ。奈良先端科学技術大学院大学 情報科学研究科 松本研究室にて自然言語処理学,機械学習を研究。在学中に、次世代形態素解析エンジン「MeCab」や、係り受け解析器「CaboCha」など多数のソフトウエアを開発する。平成14年には自然言語処理学会年次大会優秀発表賞を受賞。