前にApache Solr 5.xでファセットを使ってみるエントリを書いたのですが、これをSolrjでやりたいと思いまして。
Apache Solr 5.xでファセットを試す - CLOVER
JSON Facet APIというのは、こちらで紹介されているAPIです。
以前のファセットのAPIに比べて、相当速くなったのだとか。
で、前に書いたエントリのスキーマ定義、データなどは流用しつつ、コードはSolrjを使ってやってみましょう。
スキーマ定義とデータ
Solrのスキーマ定義は、このように。
<field name="isbn" type="string" indexed="true" stored="true" required="true" multiValued="false" /> <field name="title" type="text_ja" indexed="true" stored="true" multiValued="false"/> <field name="price" type="int" indexed="true" stored="true" multiValued="false"/> <field name="publish_date" type="string" indexed="true" stored="true" multiValued="false"/> <field name="publish_year" type="string" indexed="true" stored="false" multiValued="false"/> <field name="author" type="text_ja" indexed="true" stored="true" multiValued="true"/> <field name="author_facet" type="string" indexed="true" stored="false" multiValued="true"/> <field name="tag" type="text_ja" indexed="true" stored="true" multiValued="true"/> <field name="tag_facet" type="string" indexed="true" stored="false" multiValued="true"/> <copyField source="publish_date" dest="publish_year" maxChars="4"/> <copyField source="author" dest="author_facet"/> <copyField source="tag" dest="tag_facet"/> <uniqueKey>isbn</uniqueKey>
登録するデータは、こんな感じ。
data.json
[ { "isbn": "978-4774161631", "title": "[改訂新版] Apache Solr入門 〜オープンソース全文検索エンジン", "price": 3600, "publish_date": "20131129", "author": ["大谷 純", "阿部 慎一朗", "大須賀 稔", "北野 太郎", "鈴木 教嗣", "平賀 一昭", "株式会社リクルートテクノロジーズ"], "tag": ["Java", "Lucene", "Solr", "全文検索"] }, { "isbn": "978-4048662024", "title": "高速スケーラブル検索エンジン ElasticSearch Server", "price": 2800, "publish_date": "20140321", "author": ["Rafal Kuc", "Marek Rogozinski", "大岩 達也", "大谷 純", "兼山 元太", "水戸 祐介", "守谷 純之介", "株式会社リクルートテクノロジーズ"], "tag": ["Java", "Lucene", "Elasticsearch", "全文検索"] }, { "isbn": "978-4774127804", "title": "Apache Lucene 入門 〜Java・オープンソース・全文検索システムの構築", "price": 3200, "publish_date": "20060517", "author": [" 関口 宏司"], "tag": ["Java", "Lucene", "全文検索"] }, { "isbn": "978-4774127804", "title": "Beginning Java EE 6 GlassFish 3で始めるエンタープライズJava", "price": 3200, "publish_date": "20060517", "author": ["Antonio Goncalves", "日本オラクル株式会社", "株式会社プロシステムエルオーシー"], "tag": ["Java", "Java EE"] }, { "isbn": "978-4777518654", "title": "はじめてのSpring Boot―「Spring Framework」で簡単Javaアプリ開発", "price": 2500, "publish_date": "20141101", "author": ["槇 俊明"], "tag": ["Java", "Spring", "Spring Boot"] }, { "isbn": "978-4798121963", "title": "エリック・エヴァンスのドメイン駆動設計", "price": 5200, "publish_date": "20110409", "author": ["エリック・エヴァンス", "今関 剛", "和智 右桂", "牧野 祐子"], "tag": ["DDD"] }, { "isbn": "978-4798131610", "title": "実践ドメイン駆動設計", "price": 5200, "publish_date": "20150317", "author": ["ヴァーン・ヴァーノン", "高木 正弘"], "tag": ["DDD"] } ]
データ登録。
$ curl -H 'Content-Type: application/json' 'http://localhost:8983/solr/mycore/update?commit=true' --data-binary @data.json {"responseHeader":{"status":0,"QTime":498}}
確認。
$ curl 'http://localhost:18983/solr/mycore/select?wt=json&indent=true' -d 'q=*:*' { "responseHeader":{ "status":0, "QTime":0, "params":{ "q":"*:*", "indent":"true", "wt":"json"}}, "response":{"numFound":6,"start":0,"docs":[ { "isbn":"978-4774161631", "title":"[改訂新版] Apache Solr入門 〜オープンソース全文検索エンジン", 〜省略〜
OKですね。
準備
プログラムを書くための準備を。
Maven依存関係。
<dependency> <groupId>org.apache.solr</groupId> <artifactId>solr-solrj</artifactId> <version>5.3.1</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>jcl-over-slf4j</artifactId> <version>1.7.12</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-simple</artifactId> <version>1.7.12</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.6.3</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency> <dependency> <groupId>org.assertj</groupId> <artifactId>assertj-core</artifactId> <version>3.2.0</version> <scope>test</scope> </dependency>
SLF4Jは、Solrj関係で入れています。あと、JacksonはJSON Facet APIのために入れました。こちらは、また後で。
JUnitとAssertJが入っているのは、テストコード用ですね。
テストコード
テストコードの雛形は、こちら。
src/test/java/org/littlewings/solrj/NewFacetApiTest.java
package org.littlewings.solrj; import java.io.IOException; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.solr.client.solrj.SolrClient; import org.apache.solr.client.solrj.SolrQuery; import org.apache.solr.client.solrj.SolrRequest; import org.apache.solr.client.solrj.SolrServerException; import org.apache.solr.client.solrj.impl.HttpSolrClient; import org.apache.solr.client.solrj.request.QueryRequest; import org.apache.solr.client.solrj.response.FacetField; import org.apache.solr.client.solrj.response.QueryResponse; import org.apache.solr.common.util.NamedList; import org.junit.Test; import static org.assertj.core.api.Assertions.assertThat; public class NewFacetApiTest { // ここに、テストを書く! }
以前のファセットAPIを使ってみる
いきなりJSON Facet APIを使うのではなく、とりあえず既存のファセットAPIを使ってみましょう。
$ curl 'http://localhost:8983/solr/mycore/select?wt=json&indent=true&facet=true&facet.sort=index&facet.field=tag_facet' -d '{ "query": "*:*" }'
ファセット部分のレスポンスは、こちらです。
"facet_counts":{ "facet_queries":{}, "facet_fields":{ "tag_facet":[ "DDD",2, "Elasticsearch",1, "Java",4, "Java EE",1, "Lucene",2, "Solr",1, "Spring",1, "Spring Boot",1, "全文検索",2]}, "facet_dates":{}, "facet_ranges":{}, "facet_intervals":{}, "facet_heatmaps":{}}}
これを、Solrjを使って書くとこのようになります。
@Test public void testNormalFacetApi() throws IOException, SolrServerException { try (SolrClient solrClient = new HttpSolrClient("http://localhost:8983/solr/mycore")) { SolrQuery solrQuery = new SolrQuery(); // query solrQuery.setQuery("*:*"); // facet solrQuery.setFacet(true); solrQuery.addFacetField("tag_facet"); solrQuery.setFacetSort("index"); QueryResponse queryResponse = solrClient.query(solrQuery); assertThat(queryResponse.getStatus()) .isEqualTo(0); assertThat(queryResponse.getFacetField("tag_facet").getValueCount()) .isEqualTo(9); List<FacetField.Count> facetValues = queryResponse.getFacetField("tag_facet").getValues(); assertThat(facetValues.get(0).getFacetField().getName()).isEqualTo("tag_facet"); assertThat(facetValues.get(0).getName()).isEqualTo("DDD"); assertThat(facetValues.get(0).getCount()).isEqualTo(2); assertThat(facetValues.get(1).getFacetField().getName()).isEqualTo("tag_facet"); assertThat(facetValues.get(1).getName()).isEqualTo("Elasticsearch"); assertThat(facetValues.get(1).getCount()).isEqualTo(1); assertThat(facetValues.get(2).getFacetField().getName()).isEqualTo("tag_facet"); assertThat(facetValues.get(2).getName()).isEqualTo("Java"); assertThat(facetValues.get(2).getCount()).isEqualTo(4); assertThat(facetValues.get(3).getFacetField().getName()).isEqualTo("tag_facet"); assertThat(facetValues.get(3).getName()).isEqualTo("Java EE"); assertThat(facetValues.get(3).getCount()).isEqualTo(1); assertThat(facetValues.get(4).getFacetField().getName()).isEqualTo("tag_facet"); assertThat(facetValues.get(4).getName()).isEqualTo("Lucene"); assertThat(facetValues.get(4).getCount()).isEqualTo(2); assertThat(facetValues.get(5).getFacetField().getName()).isEqualTo("tag_facet"); assertThat(facetValues.get(5).getName()).isEqualTo("Solr"); assertThat(facetValues.get(5).getCount()).isEqualTo(1); assertThat(facetValues.get(5).getFacetField().getName()).isEqualTo("tag_facet"); assertThat(facetValues.get(6).getName()).isEqualTo("Spring"); assertThat(facetValues.get(6).getCount()).isEqualTo(1); assertThat(facetValues.get(7).getFacetField().getName()).isEqualTo("tag_facet"); assertThat(facetValues.get(7).getName()).isEqualTo("Spring Boot"); assertThat(facetValues.get(7).getCount()).isEqualTo(1); assertThat(facetValues.get(8).getFacetField().getName()).isEqualTo("tag_facet"); assertThat(facetValues.get(8).getName()).isEqualTo("全文検索"); assertThat(facetValues.get(8).getCount()).isEqualTo(2); } }
最後に結果を確認しているのでいろいろ長いですが、リクエスト作ってを投げているのはこの部分ですね。
try (SolrClient solrClient = new HttpSolrClient("http://localhost:8983/solr/mycore")) { SolrQuery solrQuery = new SolrQuery(); // query solrQuery.setQuery("*:*"); // facet solrQuery.setFacet(true); solrQuery.addFacetField("tag_facet"); solrQuery.setFacetSort("index"); QueryResponse queryResponse = solrClient.query(solrQuery);
あとは、レスポンスからファセットの内容を読んでいきます。
assertThat(queryResponse.getFacetField("tag_facet").getValueCount()) .isEqualTo(9); List<FacetField.Count> facetValues = queryResponse.getFacetField("tag_facet").getValues(); assertThat(facetValues.get(0).getFacetField().getName()).isEqualTo("tag_facet"); assertThat(facetValues.get(0).getName()).isEqualTo("DDD"); assertThat(facetValues.get(0).getCount()).isEqualTo(2);
こちらはOKそうですね。
JSON Facet APIを使う
$ curl 'http://localhost:18983/solr/mycore/select?wt=json&indent=true' -d 'q=*:*&json.facet={ "tags": { "type": "terms", "field": "tag_facet" } }'
ファセットの部分のレスポンスは、このようになります。
"facets":{ "count":6, "tags":{ "buckets":[{ "val":"Java", "count":4}, { "val":"DDD", "count":2}, { "val":"Lucene", "count":2}, { "val":"全文検索", "count":2}, { "val":"Elasticsearch", "count":1}, { "val":"Java EE", "count":1}, { "val":"Solr", "count":1}, { "val":"Spring", "count":1}, { "val":"Spring Boot", "count":1}]}}}
ところで、ファセットの「count」は既存のAPIと異なり、ヒットしたドキュメント数っぽいですね。既存のAPIだと、ファセットのエントリ数っぽいのですが。
で、これをSolrjでやりたいと思い、APIを見ます。
しかしですね、それっぽいAPIないんですよ。
最新のソースコードを見ると、JSON Facet APIに関するクラスが追加されているようですが、少なくともSolr 5.3.1にはなさそうです。
仕方がありません、とりあえず強引にでもリクエストを通す方法を考えてみます。
要するに「json.facet」をPOSTできればよいので、そのように考えます。
ファセット用のクラスを用意。
src/test/java/org/littlewings/solrj/Facet.java
package org.littlewings.solrj; public class Facet { private String type; private String field; private String sort; Facet(String type, String field, String sort) { this.type = type; this.field = field; this.sort = sort; } public static Facet create(String type, String field) { return create(type, field, null); } public static Facet create(String type, String field, String sort) { return new Facet(type,field, sort); } public String getType() { return type; } public String getField() { return field; } public String getSort() { return sort; } }
「json.facet」の中身に対応します。とりあえず、今回はRangeやQueryを使ったファセットについては考えないことにします。
で、テストコードに簡易的なオブジェクト⇒JSON変換クラスを用意。
private String toJson(Object target) throws JsonProcessingException { ObjectMapper objectMapper = new ObjectMapper() .configure(DeserializationFeature.FAIL_ON_IGNORED_PROPERTIES, false) .setSerializationInclusion(JsonInclude.Include.NON_NULL) .setSerializationInclusion(JsonInclude.Include.NON_EMPTY); return objectMapper.writeValueAsString(target); }
これらを使用して、できあがったテストコードがこちら。
@Test public void testNewFacetApi() throws IOException, SolrServerException { try (SolrClient solrClient = new HttpSolrClient("http://localhost:8983/solr/mycore")) { SolrQuery solrQuery = new SolrQuery(); // query solrQuery.setQuery("*:*"); // json facet Map<String, Object> jsonFacetMap = new LinkedHashMap<>(); jsonFacetMap.put("tags", Facet.create("terms", "tag_facet", "index asc")); solrQuery.add("json.facet", toJson(jsonFacetMap)); NamedList<Object> queryResponse = solrClient.request(new QueryRequest(solrQuery, SolrRequest.METHOD.POST)); NamedList<Object> facetResponse = (NamedList<Object>) queryResponse.get("facets"); assertThat(facetResponse.get("count")) .isOfAnyClassIn(Integer.class); assertThat((Integer) facetResponse.get("count")) .isEqualTo(6); List<NamedList<Object>> buckets = (List<NamedList<Object>>) ((NamedList<Object>) facetResponse.get("tags")).get("buckets"); assertThat(buckets.get(0).get("val")).isEqualTo("DDD"); assertThat((Integer) buckets.get(0).get("count")).isEqualTo(2); assertThat(buckets.get(1).get("val")).isEqualTo("Elasticsearch"); assertThat((Integer) buckets.get(1).get("count")).isEqualTo(1); assertThat(buckets.get(2).get("val")).isEqualTo("Java"); assertThat((Integer) buckets.get(2).get("count")).isEqualTo(4); assertThat(buckets.get(3).get("val")).isEqualTo("Java EE"); assertThat((Integer) buckets.get(3).get("count")).isEqualTo(1); assertThat(buckets.get(4).get("val")).isEqualTo("Lucene"); assertThat((Integer) buckets.get(4).get("count")).isEqualTo(2); assertThat(buckets.get(5).get("val")).isEqualTo("Solr"); assertThat((Integer) buckets.get(5).get("count")).isEqualTo(1); assertThat(buckets.get(6).get("val")).isEqualTo("Spring"); assertThat((Integer) buckets.get(6).get("count")).isEqualTo(1); assertThat(buckets.get(7).get("val")).isEqualTo("Spring Boot"); assertThat((Integer) buckets.get(7).get("count")).isEqualTo(1); assertThat(buckets.get(8).get("val")).isEqualTo("全文検索"); assertThat((Integer) buckets.get(8).get("count")).isEqualTo(2); } }
だいぶえらいことになっていますが、現時点だとこんな感じなんでしょうか…。
「json.facet」に対するMapを作成し、ファセットに対応するエントリ(ここでは、Facetクラスで作っていますが)を登録します。
try (SolrClient solrClient = new HttpSolrClient("http://localhost:8983/solr/mycore")) { SolrQuery solrQuery = new SolrQuery(); // query solrQuery.setQuery("*:*"); // json facet Map<String, Object> jsonFacetMap = new LinkedHashMap<>(); jsonFacetMap.put("tags", Facet.create("terms", "tag_facet", "index asc"));
そして、JSON変換。
solrQuery.add("json.facet", toJson(jsonFacetMap));
リクエストのメソッドをPOSTにして、クエリを投げます。
NamedList<Object> queryResponse =
solrClient.request(new QueryRequest(solrQuery, SolrRequest.METHOD.POST));
あとは「facet」を取得して、その中から「count」や指定したファセットの単位(ここでは「tags」)、そして「buckets」を取得していきます。
NamedList<Object> facetResponse = (NamedList<Object>) queryResponse.get("facets"); assertThat(facetResponse.get("count")) .isOfAnyClassIn(Integer.class); assertThat((Integer) facetResponse.get("count")) .isEqualTo(6); List<NamedList<Object>> buckets = (List<NamedList<Object>>) ((NamedList<Object>) facetResponse.get("tags")).get("buckets");
で、結果確認、と。
assertThat(buckets.get(0).get("val")).isEqualTo("DDD"); assertThat((Integer) buckets.get(0).get("count")).isEqualTo(2); assertThat(buckets.get(1).get("val")).isEqualTo("Elasticsearch"); assertThat((Integer) buckets.get(1).get("count")).isEqualTo(1);
とりあえず、動いた感じですね…。
ちょっと強引なので、これでいいのかなーという気もしますが、現時点でのSolrjでJSON Facet APIを使う場合はこんな感じになるんでしょうか…。