JavaでSolrとお話をする時に利用するライブラリ、Solrj。
Solrと同じプロジェクト内で開発されており、利用しているとSolr本体のコードともちょっとだけ親しくなれる、勉強用としても有用な気がする一品。
今日はそんなSolrjさんを使って、ドキュメントの登録、検索、更新、削除などの基本的な操作から、ファセットやMoreLikeThis検索などを実行してみた。
以下のURLからSolrを落としてきて、中に入っているsolr-core-x.x.x.jarとsolr-solrj-x.x.x.jarをクラスパスに追加。
http://lucene.apache.org/solr/
Mavenの利用も可能。レポジトリは以下を参照。
本記事のサンプルコードは、idとtextとdateという3つのフィールドを持つSchemaを利用しています。
schema.xmlは以下のような感じ。
<?xml version="1.0" encoding="UTF-8" ?>
<schema name="coreName" version="1.4">
<types>
<fieldType name="string" class="solr.StrField" sortMissingLast="true" omitNorms="true"/>
<fieldType name="date" class="solr.DateField" sortMissingLast="true" omitNorms="true" />
<fieldtype name="text_cjk" class="solr.TextField">
<analyzer>
<tokenizer class="solr.CJKTokenizerFactory"/>
</analyzer>
</fieldtype>
</types>
<fields>
<field name="id" type="string" indexed="true" stored="true" required="true" />
<field name="text" type="text_cjk" indexed="true" stored="true" required="true" />
<field name="date" type="date" indexed="true" stored="true" />
</fields>
<uniqueKey>id</uniqueKey>
<defaultSearchField>text</defaultSearchField>
<solrQueryParser defaultOperator="OR"/>
</schema>
SolrjではSolrServerというクラスを使って、そこからquery(検索とか実行できる)とかadd(ドキュメントを追加する)などのメソッドを実行する。
SolrServerにはいくつか種類があって、HTTP通信でSolrとやりとりをするCommonHttpSolrServerや、ロードバランシング機能付きのLBHttpSolrServer、HTTP通信をせずにローカルのSolrを直接操作するEmbeddedSolrServerなどがいる。
CommonHttpSolrServerは以下のようにSolrのURLを指定して初期化する。
CommonsHttpSolrServer server = new CommonsHttpSolrServer("http://localhost:8983/solr");
CommonHttpSolrServerはHTTP通信らしく、setConnectionTimeoutとかsetMaxRetriesとかを指定できる。
// 10秒でタイムアウトに設定
server.setConnectionTimeout( 10000 );
// 3回までリトライ
server.setMaxRetries( 3 );
サンプルコード全文
通信にはHTTPClientの3.1を使用しているらしい。
EmbeddedSolrServerはHTTP通信を挟まないでSolrと直接っぽくやりとりできる機能。当然、CommonHttpSolrServerより処理速度は速くなる。
使い方によっては、SQLiteやH2を組み込みモードで使うようなシーンで代替にできたりすることもあるかもしれない。ないかもしれない。個人的にはそんな感じで使うこともあります。
初期化する際は、CoreContainerにsolr.xmlとSOLR_HOMEのパスを指定してやってnewする。Solrのサーバが立ち上がってなくても動く。
// solr.xmlのパス
String solrXmlPath = "apache-solr-3.5.0/example/solr/solr.xml";
// SolrのHomeのパス
String solrHome = "apache-solr-3.5.0/example/solr";
// CoreContainerを初期化
CoreContainer container = new CoreContainer();
container.load(solrHome, new File(solrXmlPath));
// solr.xmlに書いてあるコアの名前を指定してサーバを立ち上げる
EmbeddedSolrServer server = new EmbeddedSolrServer(container, "coreName");
// 終わる時はCoreContainerをshutdown
container.shutdown();
サンプルコード全文
shutdownをしないとJVMが終了せずに待ち状態になってしまう。ので、shutdownはfinallyで行った方が良さそう。
試しにSolrのサーバを立ち上げた状態でEmbeddedSolrServerでデータを登録してみたところ、サーバ側からはそのデータは見えなかった(サーバ側をCommitしたら反映された)。
文書の追加はSolrInputDocumentを利用する。
// 登録用のSolrInputDocumentを作る
SolrInputDocument doc = new SolrInputDocument();
doc.addField("id", "1");
doc.addField("text", "サンプル用の文書です");
doc.addField("date", new Date()); // 日付はDateで入れられる
// 登録処理
server.add(doc);
// 登録した文書はcommitしないと反映されない
server.commit();
複数の文書をまとめて登録する場合は、CollectionにSolrInputDocumentを詰めて渡す。
List<SolrInputDocument> list = new ArrayList<SolrInputDocument>();
SolrInputDocument doc2 = new SolrInputDocument();
doc2.addField("id", "2");
doc2.addField("text", "サンプル文書その2");
SolrInputDocument doc3 = new SolrInputDocument();
doc3.addField("id", "3");
doc3.addField("text", "サンプルぶんしょその3");
doc3.addField("date", "2012-02-01T00:00:00Z"); // 文字列で日付を入れる
list.add(doc2);
list.add(doc3);
// 登録処理
server.add(list);
// commitする前ならrollback可
server.rollback();
検索はSolrQueryに検索用の文字列を渡す。
検索用文字列は「フィールド名:検索ワード」のような形式で渡せば、指定したフィールドを検索できる。
// 検索
SolrQuery query = new SolrQuery("text:文書");
query.setFields("id", "text"); // 返すフィールドの指定
QueryResponse response = server.query(query);
// 検索結果の出力
SolrDocumentList list = response.getResults();
System.out.println(list.getNumFound() + "件ヒットしました");
for (SolrDocument doc : list)
System.out.println(doc.get("id") + "," + doc.get("text"));
Solrのフィールドは一意のキーを持つことになっている。
既に登録されているキーで、再度ドキュメントの登録を行うと、そのキーのドキュメントが更新される。
// 同一のIDの文書をaddすると、そのIDの文書が更新される
SolrInputDocument doc = new SolrInputDocument();
doc.addField("id", "1");
doc.addField("text", "サンプル用の文書です(更新)");
// 更新処理
server.add(doc);
削除はIDで削除する方法と、Queryで条件指定して削除する方法が用意されている。
// IDで削除
server.deleteById("1");
// Queryで削除(textにぶんしょが含まれるレコードを削除)
server.deleteByQuery("text:ぶんしょ");
フィールドやクエリの集計をすることができるファセット。
SimpleFacetParameters - Solr Wiki
http://wiki.apache.org/solr/SimpleFacetParameters
試しに、dateが1月のドキュメントと2月のドキュメントの数を集計する、こんな感じのクエリを実行してみる。
http://localhost:8983/solr/select/?q=*:*&facet=true&facet.query=date:[2012-01-01T00:00:00Z%20TO%202012-01-31T23:59:59Z]&facet.query=date:[2012-02-01T00:00:00Z%20TO%202012-02-28T23:59:59Z]
// クエリを設定
SolrQuery query = new SolrQuery("*:*");
query.setFacet(true);
query.addFacetQuery("date:[2012-01-01T00:00:00Z TO 2012-01-31T23:59:59Z]");
query.addFacetQuery("date:[2012-02-01T00:00:00Z TO 2012-02-28T23:59:59Z]");
// 検索実行
QueryResponse response = server.query(query);
// Facetの結果を取得
System.out.println(response.getResponse());
for (Map.Entry<String, Integer> item : response.getFacetQuery().entrySet())
System.out.println(item);
上の例ではfacet.queryを利用している。
facet.fieldを利用した場合は下記のような書き方で取れるらしい。
// クエリを設定
SolrQuery query = new SolrQuery("*:*");
query.setFacet(true);
query.addFacetField("text");
// 検索実行
QueryResponse response = server.query(query);
// Facetの結果を取得
for (FacetField field : response.getFacetFields()) {
System.out.println("総数 : " + field.getValueCount());
for (Count count : field.getValues())
System.out.println(count.getName() + ", " + count.getCount());
}
類似文書を検索する際に利用する、MoreLikeThis。Solrでは検索結果に引っかかった文書それぞれに対して、類似文書を提示するような機能が用意されている。
例えば以下のようなURLを書くと「id:1」の条件に当てはまる文書と、それに対する類似文書が返る。
http://localhost:8983/solr/select?q=id:1&mlt=true&mlt.fl=text&mlt.mindf=1&mlt.mintf=1
MoreLikeThisで指定するパラメータについては以下を参照。
MoreLikeThis - Solr Wiki
http://wiki.apache.org/solr/MoreLikeThis
Solrjで実行する場合は、Responseの中から直接MoreLikeThisの要素を引き出してきて中身を表示するらしい。
// ID=1の文書の類似文書を探すクエリを設定
SolrQuery query = new SolrQuery("id:1");
query.set("mlt", true);
query.set("mlt.fl", "text");
query.set("mlt.mindf", 1);
query.set("mlt.mintf", 1);
query.set("fl", "id,score,text");
// 検索実行
QueryResponse response = server.query(query);
// MoreLikeThisの結果を取得
NamedList<Object> moreLikeThis = (NamedList<Object>) response.getResponse().get("moreLikeThis");
// 1つ目のドキュメントに対する類似文書の結果を取得(検索条件によっては複数返る)
List<SolrDocument> docs = (List<SolrDocument>)moreLikeThis.getVal(0);
// 結果を表示
for (SolrDocument doc : docs)
System.out.println(doc.get("score") + ", " + doc.get("text"));
今回のサンプルのような短い文書で利用する場合は、mlt.mintfを1にしないとたいてい結果が取れない。ここで指定した数字以上の回数出現する文字でないと類似検索条件に利用されないため。
正直、NamedListを使わないといけないのは少し書きづらい。Facetみたいに専用のメソッドができてくれたらいいなぁ。
SolrServerにはrequestというメソッドがいる。requestはSolrRequestを引数に渡して、NamedListで結果を受け取る。
試しにSolrRequestの子クラスであるSolrPingを使ってみる。SolrPingは対象のSolrが生きてるか確認する時などに使う最小限のリクエスト。
レスポンス結果はローカルにサーバを立てている場合は以下で確認できる。
http://localhost:8983/solr/admin/ping
上記にリクエストすると以下のようなXMLが返ってくる。
<response>
<lst name="responseHeader">
<int name="status">0</int>
<int name="QTime">9</int>
<lst name="params">
<str name="echoParams">all</str>
<str name="rows">10</str>
<str name="echoParams">all</str>
<str name="q">solrpingquery</str>
<str name="qt">search</str>
</lst>
</lst>
<str name="status">OK</str>
</response>
この内容をSolrPingで扱う場合はこんな感じで書けば取れるようだ。
// リクエスト実行
NamedList<Object> namedList = server.request(new SolrPing());
// ステータス確認
System.out.println(namedList.get("status"));
// => OK
// reponseHeader要素の中を取得
NamedList<Object> responseHeader = (NamedList<Object>) namedList.get("responseHeader");
// 実行にかかった時間(ミリ秒)
System.out.println(responseHeader.get("QTime"));
// => 6
// params要素の中を取得
NamedList<Object> params = (NamedList<Object>) responseHeader.get("params");
// paramsの中をすべて表示
for (Object o : params) {
System.out.println(o);
}
Java(というか静的型付言語)でこの手のコードを書くとどうもコード量がかさむ。
LukeRequestはインデックスの内容をある解析した結果を返してくれる。ドキュメント数とかセグメント数とかが取れたり、登録されているドキュメントで頻出する値を取れたりする。
URL的には/admin/lukeを指す。
http://localhost:8983/solr/admin/luke
// リクエスト実行
NamedList<Object> namedList = server.request(new LukeRequest());
NamedList<Object> index = (NamedList<Object>) namedList.get("index");
// ドキュメント数
System.out.println(index.get("numDocs"));
//=> 5
// セグメント数
System.out.println(index.get("segmentCount"));
//=> 3
// 最終更新日時
System.out.println(index.get("lastModified"));
//=> Sat Feb 11 17:11:21 JST 2012
// テキストフィールドの頻出語を取る
NamedList<Object> fields = (NamedList<Object>) namedList.get("fields");
NamedList<Object> text = (NamedList<Object>) fields.get("text");
NamedList<Object> topTerms = (NamedList<Object>) text.get("topTerms");
for (Object term : topTerms)
System.out.println(term);
//=> プル=3
//=> ンプ=3
//=> サン=3 以下略
登録するドキュメントがどんな感じで登録されるかを確認できる、DocumentAnalysisRequest。
ブラウザからも使えるのでSolrjから使う意味はあまりないけど、なんとなく使ってみた。ブラウザから使う場合は下記URL。
http://localhost:8983/solr/admin/analysis.jsp
// 解析するDocument
SolrInputDocument doc = new SolrInputDocument();
doc.addField("id", "7");
doc.addField("text", "解析用のドキュメントの本文です");
// リクエスト実行
DocumentAnalysisRequest request = new DocumentAnalysisRequest();
request.addDocument(doc);
NamedList<Object> namedList = server.request(request);
// テキストがどう登録されるか確認する
NamedList<Object> analysis = (NamedList<Object>) namedList.get("analysis");
NamedList text = (NamedList) ((NamedList) analysis.getVal(0)).get("text");
List list = (List) ((NamedList) ((NamedList) text.getVal(0)).getVal(0)).getVal(0);
for (Object item : list)
System.out.println(item);
SolrjにはないLuceneの機能を使いたくなった時のために以前試してみたコード。
SolrCoreからgetして終わったらdecrefしてあげるらしい。こういう処理を裏でやっていてくれるお陰で、更新と検索を同時にやってもうまくいくようだ。
// 初期化処理
String solrXmlPath = "apache-solr-3.5.0/example/solr/solr.xml";
String solrHome = "apache-solr-3.5.0/example/solr";
CoreContainer container = new CoreContainer();
container.load(solrHome, new File(solrXmlPath));
// SolrCoreからSolrIndexSearcherを取得
SolrCore core = container.getCore("coreName");
RefCounted<SolrIndexSearcher> ref = core.getSearcher();
SolrIndexSearcher searcher = ref.get();
// SolrIndexSearcherはLuceneのIndexSearcherを継承してる
Query query = new TermQuery(new Term("text", "文書"));
TopDocs topDocs = searcher.search(query, 10);
for (ScoreDoc scoreDoc : topDocs.scoreDocs) {
System.out.println(searcher.doc(scoreDoc.doc));
}
// 終了処理
ref.decref();
core.close();
container.shutdown();
ちょっとCOMを使っていた時のことを思い出した。