私が高校生だった頃、英語で「グーゴゥ」と発音する単語は、非常に大きな数を意味する googol という名詞しかありませんでした。現在「グーゴゥ」と発音すると、それはオンラインでブラウズや検索をするのと同義の google という動詞であったり、Google という名前の企業を指していたりします。ほとんどどんな質問に対しても、その答えを得る方法として「Google 先生」を思い浮かべて「ググってみなよ!」と答えるのは一般的なことです。要するに、アプリケーション・ユーザーは、アプリケーションに保管されているデータ (ファイル、ログ、記事、画像など) を検索できることを期待しているわけです。これにより、ソフトウェア開発者にとっては、睡眠時間を削りすぎることも、お金をかけすぎることもなく、迅速かつ簡単に検索機能を有効にすることが課題となります。
ユーザーのクエリーは次第に複雑で個人的なものになってきていますが、クエリーに対して適切な応答を提供するために必要なデータのほとんどは、元来構造化されていません。かつては SQL の LIKE 節で十分用が足りていたとしても、最近のクエリーには高度なアルゴリズムが必要になることがあります。幸い、プラガブル検索技術の必要性に応えるプラットフォームは、Lucene、Sphinx、Solr、Amazon CloudSearch、Xapian をはじめとし、オープンソースのものでも商用のものでも多数存在します。そのうち、連載「Java 開発 2.0」の今回の記事ではオープンソース検索プラットフォームの分野に比較的最近加わった ElasticSearch を紹介します。
記事ではまず、ElasticSearch をインストールして構成する方法を簡単に説明した後、検索インフラストラクチャーを定義する方法、検索可能なコンテンツを追加する方法、そして追加したコンテンツを検索する方法を順に説明します。記事に記載するサンプル・コードは、既存のアプリケーション (USA Today の Music Reviews フィードおよび API) をベースにしていますが、皆さんが構築するアプリケーションでも問題なく機能するはずです。この記事では、ElasticSearch と併せて、いくつかのオープンソース・ツールを使用します。そのうちの 1 つは、プラットフォームによらずに使えるコマンドライン・ツールとして、HTTP の URL を操作するためのツールである cURL です。もう 1 つは、ElasticSearch 用に作成された Java ライブラリーである Jest です。記事では Jest を使用して、サンプル・データの取得、保管、操作を行います。
ElasticSearch は数あるオープンソース検索プラットフォームのうちの 1 つで、そのサービスとして、データベースと Web フロント・エンドを備えたアプリケーションにコンポーネント (検索可能リポジトリー) を追加します。ElasticSearch によって、検索アルゴリズムと関連インフラストラクチャーをアプリケーションに追加すれば、開発者はアプリケーション・データを ElasticSearch データストアにアップロードし、RESTful な URL を介してそのデータを操作するだけで済みます。データの操作は直接行うことも、cURL や Jest などのライブラリーを使用して間接的に行うこともできます。
ElasticSearch はダウンロード可能なアプリケーションです。一部のクラウド・ベースのプラットフォームでは、ElasticSearch をサービスとして提供し始めています。この記事では ElasticSearch を組み込みツールとして使用します。
ElasticSearch のアーキテクチャーは、これまでの検索プラットフォームとは明らかに異なります。その違いは、水平スケーリングを明確に念頭に置いて作成されている点です。他の大半の検索プラットフォームとは異なり、ElasticSearch は分散型となるように設計されています。この特徴は、クラウドやビッグ・データ技術の台頭に見事に適合します。ElasticSearch は安定したオープンソース検索エンジンである Lucene をベースに構築されていて、スキーマレスな JSON ドキュメント・データストアと同じように機能します。その唯一の目的は、テキスト・ベースの検索を可能にすることです。
ElasticSearch をアプリケーションにインストールして統合するのは簡単です。ユーザーは RESTful な API を使用して、任意の言語で ElasticSearch とやりとりすることができます。ElasticSearch には、活発に成長を続けるオープンソース・コミュニティーによって作成された多数の言語アダプターも付随しています。
ElasticSearch は Lucene をベースに作成されているため、ElasticSearch に含まれるのは、詰まるところすべて Java コードです。ElasticSearch を使い始める上で必要な作業は、ElasticSearch の最新リリースをダウンロードして解凍し、ターゲット・プラットフォームの起動スクリプトを呼び出すことで、ElasticSearch を起動することだけです。ElasticSearch にはさまざまな構成が用意されていますが、この記事ではデフォルトで提供されている構成だけを使用します。このサンプル・アプリーションでは、ノードが互いをオート・ディスカバリーしてクラスターを作成する機能 (ちなみにこれは画期的な機能です) を有効にせずに、ドキュメントのデータベースとして機能する単一のノードを基本とします。
前述したように、ユーザーは、アプリケーションが保管し、操作するほぼあらゆる種類のデータを検索できることを期待します。したがって、この実際の例で最初に必要となるのは、何らかのデータです。つまらないサンプルにならないように、この記事では USA Today サイトの API から無料で入手できるデータを使用することにします。これから、USA Today の Music Reviews からミュージック・レビューのフィードを取得して、それを ElasticSearch にアップロードします。このプロセスは、一般に「インデクシング (索引付け)」として知られています。
現在、USA Today のミュージック・レビューは、特定のジャンルまたはアーティスト別のカテゴリーに分類されていますが、連想検索を行いたい場合、つまり、自分の好みのアーティストと似たようなアーティストについての好意的なレビューを検索したい場合には、このカテゴリー分けへの対処が課題となります。ここでは一例として Buddy Guy と似たようなサウンドのブルース・アーティストを検索する場合を取り上げます。
この記事の手順に従って USA Today からデータを取得するには、サイトに登録して無料の開発者キーを受け取る必要があります。登録が完了すると、RESTful な URL を使用して API にアクセスできるようになります。リスト 1 に、ミュージック・レビューを 1 件取得する場合の呼び出し例を記載します (皆さんのコードでは、ご自分の開発者キーを使用してください)。
リスト 1. USA Today のミュージック・レビュー・サービスに対する API 呼び出し
curl-XGET 'http://api.usatoday.com/open/reviews/music/recent?count=1&api_key=your_key' |
リスト 2 に、上記の呼び出しに対する JSON レスポンスの例を記載します。
リスト 2. サービスからのレスポンス
{"APIParameters":
{"Count":"1","MinimumRating":"","MaximumRating":"","Artist":"",
"ArtistSearch":true,"Album":"",
"AlbumSearch":true,"Year":""},
"Found":1,"Albums":null,"Artists":null,
"MusicReviews":[
{"AlbumName":"Away From the World",
"ArtistName":"Dave Matthews Band",
"ReleaseDate":"",
"Rating":"3",
"DownloadSongs":"Mercy, Snow Outside, Drunken Soldier",
"ConsiderSongs":"",
"Reviewer":"Brian Mansfield",
"ReviewDate":"9/11/2012 10:11:00 AM",
"Brief":"...",
"WebUrl":"..."
}
]
}
|
この例では自分の好みに合った音楽を検索するので、レビューからは、少なくとも Brief (ミュージック・レビューの核心)、Rating、WebUrl の 3 つの要素を取り込む必要があります。これにより、個人によるレビュー、数値による評価、そしてその音楽を自分で試聴してみるための URL を確認することができます。
ElasticSearch のインデックスをセットアップする
ElasticSearch では、RESTful な Web インターフェースを使用して操作を行います。これからコマンドライン・ツール cURL を使用して、このインターフェースにアクセスします。ドキュメントを ElasticSearch に挿入するには、その前にインデックス (索引) を作成しなければなりません。ElasticSearch のインデックスはデータベース・テーブルのようなもので、このインデックスの中に検索可能なドキュメント (この例ではミュージック・レビュー) を格納します。リスト 3 を見ると、cURL を使用すると簡単に ElasticSearch のインデックスを作成できることがわかります (デフォルトで ElasticSearch は指定されたすべてのドキュメントを取り込んで、そのそれぞれに対してインデックスを作成します)。
リスト 3. cURL を使用して ElasticSearch のインデックスを作成する
curl -XPUT 'http://localhost:9200/music_reviews/' |
インデックスを作成した後は、ドキュメントの個々の属性に対するマッピングを指定することができます。これらの個々の属性は、自動的に推測されます。例えば、ドキュメントに name:‘test' のような値が含まれている場合、ElasticSearch は name 属性が String であると推測します。あるいは、ドキュメントに属性として score:1 が含まれているとしたら、ElasticSearch は当然のことながら、score が数値であると推測します。
時には、ElasticSearch の推測が誤っている場合もあります。String としてフォーマット設定された日付がその一例です。このような場合には、ElasticSearch に特定の値をマッピングする方法を指示することができます。リスト 4 は、ElasticSearch に対し、ミュージック・レビューの reviewDate を String ではなく Date として扱うように指示する例です。
リスト 4. music_reviews インデックスでのマッピング
curl -XPUT 'http://localhost:9200/music_reviews/_mapping' -d
'{"review": { "properties": {
"reviewDate":
{"type":"date", "format":"MM/dd/YY HH:mm:ss aaa", "store":"yes"} } } }'
|
リスト 4 から明らかなように、cURL では簡単に ElasticSearch の RESTful な API を扱うことができます。
ここまでの手順で、ElasticSearch のインデックスを定義し、個々の属性をマッピングしました。次は、ミュージック・レビューを挿入します。この作業には、Jest と呼ばれる Java API を使用します。Jest は Java オブジェクトのシリアライズを極めて巧みに行います。Jest を使用することで、通常の Java オブジェクトを取得し、そのインデックスを ElasticSearch に作成することが可能になります。検索結果は、ElasticSearch の検索 API を使用して再び Java オブジェクトに変換することができます。また、自動的に POJO をシリアライズする機能は、ElasticSearch が必要とする JSON ドキュメント構造を扱う必要がないという点で重宝する可能性があります。
これからまず、ミュージック・レビューを表す単純な Java オブジェクトを作成し、続いて Jest を使ってこのオブジェクトのインデックスを作成します。最終的には、USA Today の API からミュージック・レビューの JSON 表現を受け取ることになるため、JSON ドキュメントを Java オブジェクトに変換するファクトリー・メソッドのコードを作成します。POJO のステップを完全に省略するのは難しいことではありませんが (これを省略して、USA Today からの JSON に対して直接インデックスを作成することもできます)、この後、検索結果を自動的に POJO に変換する方法を説明するため、ここでは POJO のステップを省略しないことにします。
リスト 5. ミュージック・レビューの結果を表す単純な POJO
import io.searchbox.annotations.JestId;
import net.sf.json.JSONObject;
public class MusicReview {
private String albumName;
private String artistName;
private String rating;
private String brief;
private String reviewDate;
private String url;
@JestId
private Long id;
public static MusicReview fromJSON(JSONObject json) {
return new MusicReview(
json.getString("Id"),
json.getString("AlbumName"),
json.getString("ArtistName"),
json.getString("Rating"),
json.getString("Brief"),
json.getString("ReviewDate"),
json.getString("WebUrl"));
}
public MusicReview(String id, String albumName, String artistName, String rating,
String brief,
String reviewDate, String url) {
this.id = Long.valueOf(id);
this.albumName = albumName;
this.artistName = artistName;
this.rating = rating;
this.brief = brief;
this.reviewDate = reviewDate;
this.url = url;
}
//...setters and getters omitted
}
|
ElasticSearch では、インデックスが作成されたドキュメントごとに id が割り当てられることに注意してください。これは、主キーのようなもので、所定のドキュメントを取得するには、常にそのドキュメントの id を使用することができます。したがって Jest API で、@JestId アノテーションを使用して、ElasticSearch のドキュメント id をオブジェクトに関連付けます (リスト 5 を参照)。この例で使用している ID は、USA Today API から提供されたものです。
続いて、Jest を使用して USA Today の API を呼び出すことでレビューのコレクションを取得し、これらの JSON ドキュメントを MusicReview オブジェクトに変換した後、ローカルで実行中のElasticSearch アプリケーション内に各オブジェクトのインデックスを作成します。
リスト 6 に記載する Jest の API 呼び出しに示されているように、ElasticSearch はクラスターで機能するように設計されています。この例の場合、接続先のサーバー・ノードは 1 つしかありませんが、接続でサーバー・アドレスのリストを取得できるという点は注目に値します。
リスト 6. Jest で ElasticSearch インスタンスへの接続を作成する
ClientConfig clientConfig = new ClientConfig();
Set<String> servers = new LinkedHashSet<String>();
servers.add("http://localhost:9200");
clientConfig.getServerProperties().put(ClientConstants.SERVER_LIST, servers);
|
ClientConfig オブジェクトを完全に初期化した後は、リスト 7 に示すような JestClient のインスタンスを作成することができます。
リスト 7. クライアント・オブジェクトを作成する
JestClientFactory factory = new JestClientFactory(); factory.setClientConfig(clientConfig); JestClient client = factory.getObject(); |
接続にローカルで実行中の ElasticSearch インスタンスの場所を指定すれば、USA Today サービスから (例えば、300 件の) ミュージック・レビューを取得してインデックスを作成することができます。
リスト 8. ローカル ElasticSearch インスタンスにミュージック・レビューを取り込んでインデックスを作成する
URL url =
new URL("http://api.usatoday.com/open/reviews/music/recent?count=300&api_key=_key_");
String jsonTxt = IOUtils.toString(url.openConnection().getInputStream());
JSONObject json = (JSONObject) JSONSerializer.toJSON(jsonTxt);
JSONArray reviews = (JSONArray) json.getJSONArray("MusicReviews");
for (Object jsonReview : reviews) {
MusicReview review = MusicReview.fromJSON((JSONObject) jsonReview);
client.execute(new Index.Builder(review).index("music_reviews")
.type("review").build());
}
|
リスト 8 の最後の for ループの行に注目してください。このコードは、MusicReview POJO を取り、そのインデックスを ElasticSearch 内に作成します。つまり、タイプを review として指定した music_reviews インデックス内に MusicReview POJO を配置します。これで、ElasticSearch がこのドキュメントを取り、ある種の本格的な魔法をかけることによって、このドキュメントに伴う各種の側面を検索できるようになります。
ElasticSearch の威力は、非構造化データの検索を可能にするところにあります。非構造化データの一例は、ミュージック・レビューの brief の部分です。ある特定の音楽を言い表すテキスト・パラグラフである brief には、多くのデータが含まれます。ただし、検索に必要となるのは、類似性を示せるキーワードです。つまり、検索エンジンがユーザーの求める結果だけを返せるようにするキーワードの関連付けが必要になります。この例では、私の好みの音楽に基づいて、聴いてみたいと思いそうな音楽を検索しているので、好みの音楽を言い表すために使用されているキーワードと同じキーワードが記述に含まれる音楽を検索します。
例えば、インデクシングされたコレクションの brief 属性で、「jazz」という単語を検索するとします (この検索では大/小文字が区別されることに注意してください)。Jest を使って検索を行うには、いくつかの準備作業が必要となります。まず、QueryBuilder タイプで用語クエリーを作成する必要があります。続いて、その作成したクエリーを、インデックスとタイプを指す Search に追加します。Jest は ElasticSearch からの JSON レスポンスを取り、それを MusicReview のコレクションに変換することにも注意してください。
リスト 9. Jest で検索を実行する
QueryBuilder queryBuilder = QueryBuilders.termQuery("brief", "jazz");
Search search = new Search(queryBuilder);
search.addIndex("music_reviews");
search.addType("review");
JestResult result = client.execute(search);
List<MusicReview> reviewList = result.getSourceAsObjectList(MusicReview.class);
for(MusicReview review: reviewList){
System.out.println("search result is " + review);
}
|
Java 開発者にとって、リスト 10 の検索操作はお馴染みのことでしょう。Jest を使用して POJO を操作するのは簡単なプロセスです。その一方、ElasticSearch は完全にRESTful に制御されるため、cURL を使ったとしても、同じ検索を簡単に実行することができます。以下はその一例です。
リスト 10. cURL で検索を実行する
curl -XGET 'http://localhost:9200/music_reviews/_search?pretty=true' -d
' {"explain": true, "query" : { "term" : { "brief" : "jazz" } }}'
|
JSON は読みにくくなりがちなので、検索リクエストには常に pretty=true オプションを渡すようにして構いません。リスト 10 では ElasticSearch に対し、検索の実行方法に適用した実行計画 (explain plan) を返すようにも指定しています。これを指定するために、JSON ドキュメントを渡すときに、"explain":true 句をドキュメントに追加しました。
リスト 9 とリスト 10 の検索は、10 件の結果を返しました (結果の件数は、インデックスを作成したドキュメントの数によって異なります)。したがってこの単純な検索では、私が興味を持ちそうなレビューとして、300 件のレビューをわずか 10 件の候補に絞り込んだことになります。ただし、評価の点数は 3.0 から 4.0 の範囲であることに注意してください。これよりも複雑なクエリーにより、試聴してみたい音楽のなかでもさらに評価の高いものに候補を絞り込むことができます。
リスト 11 でインポートしているのは、複雑なクエリーの作成を多少楽にしてくれる便利な静的メソッドです。突きつめるところ、私がここで行っているのは、brief に jazz という単語が含まれていて、rating の値が 3.5 から 4.0 のドキュメントを検出するクエリーにするという作業です。このようにすることで、前の検索結果を絞り込んで、私のジャズの好みにぴったり合う音楽を見つけられる可能性が高くなります。
リスト 11. Jest で範囲とフィルターを指定して検索する
import static org.elasticsearch.index.query.FilterBuilders.rangeFilter;
import static org.elasticsearch.index.query.QueryBuilders.filteredQuery;
import static org.elasticsearch.index.query.QueryBuilders.termQuery;
//later in the code
QueryBuilder queryBuilder = filteredQuery(termQuery("brief", "jazz"),
rangeFilter("rating").from(3.5).to(4.0));
Search search = new Search(queryBuilder);
search.addIndex("music_reviews");
search.addType("review");
JestResult result = client.execute(search);
List<MusicReview> reviewList = result.getSourceAsObjectList(MusicReview.class);
for(MusicReview review: reviewList){
System.out.println("search result is " + review);
}
|
上記とまったく同じ検索は、cURL を使用しても実行できることを忘れないでください。
リスト 12. cURL で範囲とフィルターを指定して検索する
curl -XGET 'http://192.168.1.11:9200/music_reviews/_search?pretty=true' -d
'{"query": { "filtered" : { "filter" : { "range" : { "rating" :
{"from": 3.5, "to":4.0} } },
"query" : { "term" : { "brief" : "jazz" } } } }}'
|
この最新版の検索は結果をさらに絞り込み、有望ないくつかのアルバムを試聴候補として残しますが、これよりもさらに限定された結果が得られるようにしたい場合はどうすればよいのでしょう?前に、私がブルース・ギタリストの Buddy Guy のファンであるという話をしました。検索にワイルドカードを追加すると、どのような結果になるのか見てみましょう (リスト 13 を参照)。
リスト 13. ワイルドカードを使用して検索する
import static org.elasticsearch.index.query.QueryBuilders.wildcardQuery;
//later in the code
QueryBuilder queryBuilder = filteredQuery(wildcardQuery("brief", "buddy*"),
rangeFilter("rating").from(3.5).to(4.0));
//see listing 12 for the template search and response
|
リスト 13 で検索しているのは、rating の値が 3.5 から 4.0 の範囲で、brief に buddy という単語が含まれるレビューです。この検索では、Buddy Guy を参照する 1 つか 2 つのレビューが結果として返されます。この場合、これらの結果を試聴すると気に入ることは、ほぼ間違いありません。その一方、buddy という単語が含まれ、無作為に抽出されたドキュメントを受け取る可能性もあります。これが、一般的なワイルドカード検索の欠点です。
けれどもこの例では、ワイルドカードが功を奏しています。検索によって取得した 2 つのドキュメントのレビューは、私のお気に入りのギタリストの影響を受けた、ブルース・スタイルの音楽であることを示しています。一日分の仕事としては、まずまずの結果です。
この記事では、ElasticSearch の構成が複雑にならないように、クラスターを構成することも、デフォルトのインデクシング・ストラテジーに手を加えることもしませんでした。ElasticSearch では、記事で説明した機能より遥かに高度な機能を使用することができます。例えば、インデックス・マッピングを定義する際に、特定のフィールドに対するインデクシングの方法を構成することが可能です。さまざまなトークナイザー・ストラテジーにより、必要に応じて極めて強力で複雑な検索を作成することができます。USA Today の brief 要素を例にとると、Snowball アナライザーまたはキーワード・アナライザーを指定することができます。Snowball のトークン・アルゴリズムでは、英単語を基本形に変換することによって、検索結果の範囲を広げます (例えば、jazzy という単語を jazz にします)。アプリケーションの検索機能を微調整するには、各種のアナライザーを扱うのが最適な方法です。さらに、ElasticSearch のような検索プラットフォームを使用する場合、これらのオプションはすでに揃っているので、自分で作成する必要はありません。
検索機能は、もはやオプションの機能ではありません。データを利用、生成、または保管するほとんどのアプリケーションに、当然用意されていると見込まれる機能です。しかし、最近の複雑な検索の基礎となっている高度なアルゴリズムの数々を考えると、誰もが検索技術のスペシャリストになりたいと思い立つことはないでしょう。既存のオープンソース検索プラットフォームを知っていれば、大幅に時間とお金を節約して、その分、ソフトウェアの主要な機能の微調整に時間をかけることができます。
この記事では、簡単に使い始めることができて、極めて拡張性の高い分散検索プラットフォームである ElasticSearch を紹介しました。ElasticSearch の精巧さと使いやすさは圧巻であり、データ要件にスケーラビリティーが含まれている場合には (近頃では、スケーラビリティーが要件とならない場合が果たしてあるでしょうか?)、水平スケーラビリティーをサポートする ElasticSearch が世界中で選ばれています。
学ぶために
- 連載「Java 開発 2.0」: この dW の連載では Java 開発の様相を塗り替える技術を詳しく探っています。これまでに取り上げた話題には、Redis (2011年12月)、Amazon RDS (2011年7月)、Hadoop MapReduce (2011年1月) が含まれます。
- 連載「Java 開発 2.0」: この連載の索引で、今回の話題に関連する記事を探してください。
- 「ElasticSearch on EC2」(James Cook 著、ElasticSearch.org、2011年8月): ElasticSearch の概要に続き、ElasticSearch を Amazon EC2 内で使用するためのチュートリアルを紹介しています。
- ElasticSearch Guide (ElasticSearch.org): セットアップ、API、クエリー DSL、マッピング、モジュールをはじめ、ElasticSearch について包括的に説明しています。
- 「ElasticSearch, Sphinx, Lucene, Solr, Xapian. Which fits for which usage?」(Stackoverflow、2010年2月): ElasticSearch の作成者が、Lucene をベースに作成された ElasticSearch の価値について説明しています。
- 「How does Amazon CloudSearch compare to ElasticSearch, Solr, or Sphinx?"」(Stackoverflow、2012年6月): Stackoverflow コミュニティーの開発者たちが、比較的最近の検索プラットフォームの利点について検討し、比較しています。
- Knowledge path:「NoSQL を使用してビッグ・データを分析する」(developerWorks、2011年5月): NoSQL、ビッグ・データ、そしてデータ・マイニングについて学ぶための dW リソースが揃っています。
- Java Technology bookstore で、この記事で取り上げた技術やその他の技術に関する本を探してください。
- developerWorks Java technology ゾーン: Java プログラミングのあらゆる側面を網羅した記事が豊富に用意されています。
製品や技術を入手するために
- この記事のソース・コードをダウンロードしてください。
- ElasticSearch のダウンロード: 必ず最新の安定版リリースをダウンロードしてください。
- cURL のダウンロード: URL 構文でデータを転送するために使用できる、プラットフォームにとらわれないコマンドライン・ツールです。
- Jest のダウンロード: ElasticSearch Java 用の Java REST クライアントです。
- USA Today API: 開発者キーを登録して、USA Today のミュージック・レビューをはじめとするオンライン・サービスにアクセスしてください。
議論するために
- developerWorks コミュニティーに参加してください。ここでは他の developerWorks ユーザーとのつながりを持てる他、開発者によるブログ、フォーラム、グループ、Wiki を調べることができます。

Andrew Glover は、ビヘイビア駆動開発、継続的インテグレーション、アジャイル・ソフトウェア開発に情熱を持つ開発者であるとともに、著者、講演者、起業家でもあります。また、easyb BDD (Behavior-Driven Development) フレームワークの創始者、そして『継続的インテグレーション入門 開発プロセスを自動化する47の作法』、『Groovy in Action』、『Java Testing Patterns』の 3 冊の本の共著者でもあります。詳細は彼のブログにアクセスするか、Twitter で彼をフォローしてください。