CLOVER🍀

That was when it all began.

Apache Solr 5.xでファセットを試す

Apache Solrで、ファセットを試してみました。やろうやろうと思っていたのですが、なかなか取り組めていなかったので。

ファセットって何?という話もありますが、単語やクエリ、範囲に対する件数を返す機能です。

tree-tips: solrjでfacet query検索 | Apache Solr

moco(beta)'s backup: Solr Faceting パラメータいろいろ (1)

Amazonとかで、カテゴリ単位とかで件数が出ているやつですね。

スキーマ定義

使用するschema.xmlには、今回使用するフィールドを以下のように定義。
※「text_ja」は、Kuromojiを使用したフィールドです

    <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>

まあ、書籍の定義ですね。

ファセットに利用しようとしているフィールドは、以下になります。

    <field name="price" type="int" indexed="true" stored="true" multiValued="false"/>

    <field name="publish_year" type="string" indexed="true" stored="false" multiValued="false"/>

    <field name="author_facet" type="string" indexed="true" stored="false" multiValued="true"/>

    <field name="tag_facet" type="string" indexed="true" stored="false" multiValued="true"/>

ファセットに使うフィールドは、analyzeする必要がありません(…のはず)。また、通常のクエリ時にも使えるように、ファセットで使うフィールドはcopyFieldを使用して収集していたりします。

利用するデータ

このスキーマに、以下のようなデータを投入します。
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"]
  }
]

データを登録します。今回のSolrのコアの名前は、「mycore」とします。

$ curl -H 'Content-Type: application/json' 'http://localhost:8983/solr/mycore/update?commit=true' --data-binary @data.json

ファセットを使ってみる

それでは、最初に挙げたサイトや以下のドキュメントを参考にしつつ、ファセットを試してみます。

Faceting | Apache Solr Reference Guide 6.6

まずは、最も簡単なパターンから。

$ curl 'http://localhost:8983/solr/mycore/select?wt=json&indent=true&facet=true&facet.sort=index&facet.field=price&facet.field=publish_year&facet.field=tag_facet' -d '{ "query": "*:*" }'

「facet=true」でファセットを有効に、「facet.field」を指定することで取得するフィールドを決めることができます。「facet.sort」は、ファセットがどのような順番でソートされて返却されるかの指定ですね。

結果は、通常の検索結果も得られるのですが

{
  "responseHeader":{
    "status":0,
    "QTime":155,
    "params":{
      "facet.field":["price",
        "publish_year",
        "tag_facet"],
      "indent":"true",
      "json":"{ \"query\": \"*:*\" }",
      "wt":"json",
      "facet":"true",
      "facet.sort":"index"}},
  "response":{"numFound":6,"start":0,"docs":[
      {
        "isbn":"978-4774161631",
        "title":"[改訂新版] Apache Solr入門 〜オープンソース全文検索エンジン",
        "price":3600,
        "publish_date":"20131129",
        "author":["大谷 純",
          "阿部 慎一朗",
          "大須賀 稔",
          "北野 太郎",
          "鈴木 教嗣",
          "平賀 一昭",
          "株式会社リクルートテクノロジーズ"],
        "tag":["Java",
          "Lucene",
          "Solr",
          "全文検索"],
        "_version_":1513979250196086784},

〜省略〜

ファセットの結果も含まれるようになります。

  "facet_counts":{
    "facet_queries":{},
    "facet_fields":{
      "price":[
        "2500",1,
        "2800",1,
        "3200",1,
        "3600",1,
        "5200",2],
      "publish_year":[
        "2006",1,
        "2011",1,
        "2013",1,
        "2014",2,
        "2015",1],
      "tag_facet":[
        "DDD",2,
        "Elasticsearch",1,
        "Java",4,
        "Java EE",1,
        "Lucene",2,
        "Solr",1,
        "Spring",1,
        "Spring Boot",1,
        "全文検索",3]},
    "facet_dates":{},
    "facet_ranges":{},
    "facet_intervals":{},
    "facet_heatmaps":{}}}

それっぽいのが出力されていますね。

他にバリエーションを見てみましょう。

クエリで結果を絞り込みます。

$ curl 'http://localhost:8983/solr/mycore/select?wt=json&indent=true&facet=true&facet.sort=index&facet.field=price&facet.field=publish_year&facet.field=tag_facet' -d '{ "query": "tag:全文検索" }'

検索結果が絞り込まれますが、ファセットも絞り込まれます。

  "facet_counts":{
    "facet_queries":{},
    "facet_fields":{
      "price":[
        "2500",0,
        "2800",1,
        "3200",1,
        "3600",1,
        "5200",0],
      "publish_year":[
        "2006",1,
        "2011",0,
        "2013",1,
        "2014",1,
        "2015",0],
      "tag_facet":[
        "DDD",0,
        "Elasticsearch",1,
        "Java",3,
        "Java EE",1,
        "Lucene",2,
        "Solr",1,
        "Spring",0,
        "Spring Boot",0,
        "全文検索",3]},
    "facet_dates":{},
    "facet_ranges":{},
    "facet_intervals":{},
    "facet_heatmaps":{}}}

あ、Java EEの本に全文検索のタグが付いてました…。

もうちょっと例を。

$ curl 'http://localhost:8983/solr/mycore/select?wt=json&indent=true&facet=true&facet.sort=index&facet.field=price&facet.field=publish_year&facet.field=tag_facet' -d '{ "query": "publish_year:2014 AND tag_facet:Java" }'

結果。

  "facet_counts":{
    "facet_queries":{},
    "facet_fields":{
      "price":[
        "2500",1,
        "2800",1,
        "3200",0,
        "3600",0,
        "5200",0],
      "publish_year":[
        "2006",0,
        "2011",0,
        "2013",0,
        "2014",2,
        "2015",0],
      "tag_facet":[
        "DDD",0,
        "Elasticsearch",1,
        "Java",2,
        "Java EE",0,
        "Lucene",1,
        "Solr",0,
        "Spring",1,
        "Spring Boot",1,
        "全文検索",1]},
    "facet_dates":{},
    "facet_ranges":{},
    "facet_intervals":{},
    "facet_heatmaps":{}}}

最後にファセット向けにクエリを投げ、その結果でファセットを作る機能を紹介。

$ curl -g 'http://localhost:8983/solr/mycore/select?wt=json&indent=true&facet.sort=index&facet=true&facet.field=publish_year&facet.field=tag_facet&facet.query=price:[2000%20TO%203000]&facet.query=price:[3001%20TO%204000]&facet.query=price:[4001%20TO%205000]&facet.query=price:[5001%20TO%206000]' -d '{ "query": "*:*" }'

「facet.query」で、クエリを指定することができます。今回は、価格で範囲を付けるようなクエリを投げています。

結果。

  "facet_counts":{
    "facet_queries":{
      "price:[2000 TO 3000]":2,
      "price:[3001 TO 4000]":2,
      "price:[4001 TO 5000]":0,
      "price:[5001 TO 6000]":2},
    "facet_fields":{
      "publish_year":[
        "2006",1,
        "2011",1,
        "2013",1,
        "2014",2,
        "2015",1],
      "tag_facet":[
        "DDD",2,
        "Elasticsearch",1,
        "Java",4,
        "Java EE",1,
        "Lucene",2,
        "Solr",1,
        "Spring",1,
        "Spring Boot",1,
        "全文検索",3]},
    "facet_dates":{},
    "facet_ranges":{},
    "facet_intervals":{},
    "facet_heatmaps":{}}}

こういう感じで、クエリの結果に対するファセットが得られましたね。

    "facet_queries":{
      "price:[2000 TO 3000]":2,
      "price:[3001 TO 4000]":2,
      "price:[4001 TO 5000]":0,
      "price:[5001 TO 6000]":2},

この他、レンジファセットというものもあるらしいのですが、今回は省略。

新ファセット API

Solr 5.3から、新しいファセットAPIが使えるようになったみたいです。

https://cwiki.apache.org/confluence/display/solr/Faceted+Search

Solr JSON Facet API

JSONを使って、もうちょっと簡単にファセットリクエストを投げられるようになったのだとか。

では、試してみます。

まずは簡単な例。

$ curl 'http://localhost:8983/solr/mycore/select?wt=json&indent=true' -d 'q=*:*&json.facet={ prices: { terms: price }}'

json.facet」で、パラメーターをJSONで指定します。…厳密なJSONフォーマットでなくてもいいのかな?ダブルクォートがなくても動くのですが…。

ここで、

json.facet={ prices: { terms: price }}

ですが、「prices」がレスポンスのフィールド名で「terms」がファセットの結果として単語が戻ること、priceで価格についてのファセットを取得します、と。

結果。

  "facets":{
    "count":6,
    "prices":{
      "buckets":[{
          "val":5200,
          "count":2},
        {
          "val":2500,
          "count":1},
        {
          "val":2800,
          "count":1},
        {
          "val":3200,
          "count":1},
        {
          "val":3600,
          "count":1}]}}}

ちょっと表示方法が変わりましたね。

既存のファセットAPIの例では使いませんでしたが、レンジファセットも簡単に使えるようです。

$ curl 'http://localhost:8983/solr/mycore/select?wt=json&indent=true' -d 'q=*:*&json.facet={ prices: { range: { field: price, start: 2000, end: 6000, gap: 1000 } } }'

ここでは、「range」して開始(start)、終了(end)、増加幅(gap)を指定してファセット用のクエリを組み立てています。

結果。

  "facets":{
    "count":6,
    "prices":{
      "buckets":[{
          "val":2000,
          "count":2},
        {
          "val":3000,
          "count":2},
        {
          "val":4000,
          "count":0},
        {
          "val":5000,
          "count":2}]}}}

複数の指定も可能。

$ curl 'http://localhost:8983/solr/mycore/select?wt=json&indent=true' -d 'q=*:*&json.facet={ prices: { range: { field: price, start: 2000, end: 6000, gap: 1000 } }, publish_years: { terms: publish_year }, tags: { terms: tag_facet } }'

結果。

  "facets":{
    "count":6,
    "prices":{
      "buckets":[{
          "val":2000,
          "count":2},
        {
          "val":3000,
          "count":2},
        {
          "val":4000,
          "count":0},
        {
          "val":5000,
          "count":2}]},
    "publish_years":{
      "buckets":[{
          "val":"2014",
          "count":2},
        {
          "val":"2006",
          "count":1},
        {
          "val":"2011",
          "count":1},
        {
          "val":"2013",
          "count":1},
        {
          "val":"2015",
          "count":1}]},
    "tags":{
      "buckets":[{
          "val":"Java",
          "count":4},
        {
          "val":"全文検索",
          "count":3},
        {
          "val":"DDD",
          "count":2},
        {
          "val":"Lucene",
          "count":2},
        {
          "val":"Elasticsearch",
          "count":1},
        {
          "val":"Java EE",
          "count":1},
        {
          "val":"Solr",
          "count":1},
        {
          "val":"Spring",
          "count":1},
        {
          "val":"Spring Boot",
          "count":1}]}}}

ソートを行う場合は、ファセットリクエストのtypeを明示し(ここでは「terms」)、sortフィールドで何でソートするのか、順番はどうするのかということを指定します。

$ curl 'http://localhost:8983/solr/mycore/select?wt=json&indent=true' -d 'q=*:*&json.facet={ prices: { type: terms, field: price, sort: { index: asc } } }'

結果。

  "facets":{
    "count":6,
    "prices":{
      "buckets":[{
          "val":2500,
          "count":1},
        {
          "val":2800,
          "count":1},
        {
          "val":3200,
          "count":1},
        {
          "val":3600,
          "count":1},
        {
          "val":5200,
          "count":2}]}}}

あと、レンジファセットも合わせて使うと、このような結果に。

$ curl 'http://localhost:8983/solr/mycore/select?wt=json&indent=true' -d 'q=*:*&json.facet={ prices: { type: range, field: price, start: 2000, end: 6000, gap: 1000, sort: {index: asc } }, publish_years: { type: terms, field: publish_year, sort: { index: asc} }, tags: { type: terms, field: tag_facet, sort: { index: asc } } }'

結果。

  "facets":{
    "count":6,
    "prices":{
      "buckets":[{
          "val":2000,
          "count":2},
        {
          "val":3000,
          "count":2},
        {
          "val":4000,
          "count":0},
        {
          "val":5000,
          "count":2}]},
    "publish_years":{
      "buckets":[{
          "val":"2006",
          "count":1},
        {
          "val":"2011",
          "count":1},
        {
          "val":"2013",
          "count":1},
        {
          "val":"2014",
          "count":2},
        {
          "val":"2015",
          "count":1}]},
    "tags":{
      "buckets":[{
          "val":"DDD",
          "count":2},
        {
          "val":"Elasticsearch",
          "count":1},
        {
          "val":"Java",
          "count":4},
        {
          "val":"Java EE",
          "count":1},
        {
          "val":"Lucene",
          "count":2},
        {
          "val":"Solr",
          "count":1},
        {
          "val":"Spring",
          "count":1},
        {
          "val":"Spring Boot",
          "count":1},
        {
          "val":"全文検索",
          "count":3}]}}}

最後のテーマ。ファセット用のクエリを投げてみます。

$ curl 'http://localhost:8983/solr/mycore/select?wt=json&indent=true' -d 'q=*:*&json.facet={ price_2000_to_3000: { query : "price: [2000 TO 3000]" } }'

「query」フィールドで指定します。

結果。

  "facets":{
    "count":6,
    "price_2000_to_3000":{
      "count":2}}}

全レンジを指定してみます。

$ curl 'http://localhost:8983/solr/mycore/select?wt=json&indent=true' -d 'q=*:*&json.facet={ price_2000_to_3000: { query : "price: [2000 TO 3000]" }, price_3001_to_4000: { query : "price: [3001 TO 4000]" }, price_4001_to_5000: { query : "price: [4001 TO 5000]" }, price_5001_to_6000: { query : "price: [5001 TO 6000]" } }'

結果。

  "facets":{
    "count":6,
    "price_2000_to_3000":{
      "count":2},
    "price_3001_to_4000":{
      "count":2},
    "price_4001_to_5000":{
      "count":0},
    "price_5001_to_6000":{
      "count":2}}}

最後は、既存のファセットAPIを使ったものと、同様の意味のものを。

curl 'http://localhost:8983/solr/mycore/select?wt=json&indent=true' -d 'q=*:*&json.facet={ price_2000_to_3000: { query : "price: [2000 TO 3000]" }, price_3001_to_4000: { query : "price: [3001 TO 4000]" }, price_4001_to_5000: { query : "price: [4001 TO 5000]" }, price_5001_to_6000: { query : "price: [5001 TO 6000]" }, publish_years: { terms: publish_year }, tags: { type: terms, field: tag_facet, sort: { index: asc } } }'

結果。

  "facets":{
    "count":6,
    "price_2000_to_3000":{
      "count":2},
    "price_3001_to_4000":{
      "count":2},
    "price_4001_to_5000":{
      "count":0},
    "price_5001_to_6000":{
      "count":2},
    "publish_years":{
      "buckets":[{
          "val":"2014",
          "count":2},
        {
          "val":"2006",
          "count":1},
        {
          "val":"2011",
          "count":1},
        {
          "val":"2013",
          "count":1},
        {
          "val":"2015",
          "count":1}]},
    "tags":{
      "buckets":[{
          "val":"DDD",
          "count":2},
        {
          "val":"Elasticsearch",
          "count":1},
        {
          "val":"Java",
          "count":4},
        {
          "val":"Java EE",
          "count":1},
        {
          "val":"Lucene",
          "count":2},
        {
          "val":"Solr",
          "count":1},
        {
          "val":"Spring",
          "count":1},
        {
          "val":"Spring Boot",
          "count":1},
        {
          "val":"全文検索",
          "count":3}]}}}

この他、以下のドキュメントを見ていると結果に関数を適用して導出することもできそうですが、ここでは除外します。

https://cwiki.apache.org/confluence/display/solr/Faceted+Search

まとめ

Solrのファセット機能を試してみました。Solr 5.3から、ファセット用のAPIが追加されているようだったので、今回はそのあたりも含めて試してみました。

なんとなく、ファセットは使えるようになったかな…?