まめーじぇんと@Tech

技術ネタに関して (Android, GAE, Angular). Twitter: @mame01122

GAEのSearch APIでデータを検索してみた

対応しようと思ったきっかけ

今作っているサービスで、GAEでDB内のデータのまるごと検索をしたい!と思ったのがきっかけです。Datastoreのデータは、Keyを使ってのgetはかなり早いですが、文字列を使っての検索(しかも、特定カラムではなく全部)を検索するのは、結構時間がかかります(そもそも、KVSのデータベースに対してこのクエリ方法はアンチパターンだったりしそうです)また、Datastoreの中を検索するためには(=インデックスさせるためには)、それぞれのアイテムの文字数を500文字以内におさめないといけないという制限もあります。それを超えると、インデックスされないTextという形式で保存する必要があります。明確にそれぞれが500文字以内だと分かっているサービスではいいのかもしれませんが、僕のサービスだとこの500文字が全く保証されないtというのもあり、Datastoreの中を直接検索する機能では対応することができませんでした。
そこで何かいい方法はないかなぁ・・・と見つけたのがSearch APIでした。

Search APIとは?

まあ、読んで字のごとく、DB内を検索できるAPIです。
https://cloud.google.com/appengine/docs/java/search/
ざっくりというと、Documentというオブジェクトに、検索対象となる値を格納し、最後にIndexにputすることで検索可能となります。

以下、使い方です。

その1: Documentを作る

String title = "Text title";
String description "My description";

Document doc = Document
	.newBuilder()
	.setId(key)
	.addField(
		Field.newBuilder()
			.setName("title")
			.setText(title))
	.addField(
		Field.newBuilder()
			.setName("description")
			.setText(description))).build();

上記のように、DocumentのBuilderを使って各フィールドを設定していきます。
処理は見たままなので、あまり説明する必要はないかな、と思います。

その2: Indexのインスタンスを確保する。

Index INDEX = SearchServiceFactory.getSearchService()
			.getIndex(IndexSpec.newBuilder().setName("search_name"));

その3: Indexに書き込む

try {
	INDEX.put(doc);
} catch (PutException e) {
	if (StatusCode.TRANSIENT_ERROR.equals(e.getOperationResult().getCode())) {
		// Retry
	}

書き込みに失敗したらPutExceptionを受けてもう一度やり直すらしいのですが、僕はやっていません・・・(もちろんやった方がいい)

上記Textの他、HTMLやAtomも書き込むことができるようです。(詳細は上の方に書いた公式ドキュメントを見てくださいませ)

検索してみる

上記で出てきたIndexのsearchメソッドを呼び出すことにより検索することができます。こんな感じ。

try {
	String parameter = "search word";

	Query q = Query.newBuilder().build(parameter);
	Results<ScoredDocument> results = INDEX.search(q);
	for (ScoredDocument document : results) {

		String docId = document.getId();
		
		String category = document.getOnlyField("title").getText();
		String subCategory = document.getOnlyField("description").getText();

	}
} catch (SearchException e) {
} catch (IllegalArgumentException e) {
} catch (SearchQueryException e) {
}

"search word"を渡すと検索結果ゼロですが、上記の例のように"title"に"Text title"が、"description"に"My description"が入っている状態で、検索文字を"text"とか"my"とかを渡してあげると、それぞれ1件ずつヒットするハズ。

削除する

不要になったものを消します。
try {
	INDEX.delete(documentId);
} catch (DeleteException e) {
} catch (IllegalArgumentException e) {
}

削除の注意点

いろいろなドキュメントを見てみましたが、現在、GAEのコンソール上から不要になったものを消すための機能がないようです(2015年5月現在。新旧両方のUIを含む)何と不便な・・・と思わなくもないですが、消せないものは仕方ない。テストデータなんかを突っ込んでしまって一つひとつ消すのが面倒だと思うあなたは、全消しの下記メソッドを使ってください(本当に全部消えるので、注意してください。自己責任でお願いします)

public void deleteAllDocuments() {
	try {
		Results<ScoredDocument> results = INDEX.search("");
		for (ScoredDocument document : results) {

			String docId = document.getId();
			deleteWisdom(docId);
		}
	} catch (SearchException e) {
	} catch (IllegalArgumentException e) {
	} catch (SearchQueryException e) {
	}

}

料金

これも公式ドキュメントを見ると書いてありますが、現在(2015年5月)、無料枠で使えるのは下記のようです。
・ストレージ合計: 0.25GB
・1日あたりのクエリ数: 1000回
・1日あたりのDocumentのIndexへの登録: 0.01GB

少し大きめのサービスになると、上記のいずれも結構厳しいかもしれません・・・、

おまけ

このIndexへの書き込みは僕のサービスの場合だと、書き込みを待たなくてもいいので、書き込みの作業自体をTaskQueueに丸投げしています。パフォーマンスアップ活動の一環です。