Elasticsearchで簡単なパーソナライズ

#概要

elasticsearchのterms filterとfunction_scoreを使って、パーソナライズのようなことをしてみます。

blogを検索をすることを考えます。
blogには下記のような属性があるとします。

| 属性 | 説明 | 
|:-----------|:------------|
| source       |        作者 | 
| date     |      投稿日付 |
| text     |        本文 | 
| tag         |     ブログのカテゴリのタグ |  

blogを検索するユーザには下記のような属性があるとします。

| 属性 | 説明 | 
|:-----------|:------------|
| follows      |  お気に入りユーザ的なやつ  | 
| tags     |  お気に入りのカテゴリとか |

たとえば、blogを全文検索したときのソート結果について、フォローしているユーザの日記や、
好きなカテゴリの日記を上位に表示したりということをやります。

#準備
blogのインデックスを作ります。

```bash
curl -XPUT 'http://localhost:9200/blog' -d '
{
    "settings": {
        "number_of_shards": 1,
        "number_of_replicas": 0
    },
    "mappings": {
        "item": {
            "_all": {
                "enabled": false
            },
            "_source": {
                "enabled": true
            },
            "_type": {
                "index": "no"
            },
            "_id": {
                "index": "not_analyzed",
                "store": "yes"
            },
            "_routing": {},
            "properties": {
                "text": {
                    "type": "string",
                    "store": "yes"
                },
                "date": {
                    "type": "date",
                    "format": "yyyy-MM-dd HH:mm:ss"
                },
                "tag": {
                    "type": "string"
                },
                "source": {
                    "type": "integer"
                }
            }
        }
    }
}'
```

userのインデックスを作ります。

```bash
curl -XPUT localhost:9200/users -d '
{
    "settings": {
        "number_of_shards": 1,
        "number_of_replicas": 0
    },
    "mappings": {
        "source": {
            "_all": {
                "enabled": false
            },
            "_source": {
                "enabled": true
            },
            "_type": {
                "index": "no"
            },
            "_id": {
                "index": "not_analyzed",
                "store": "yes"
            },
            "_routing": {},
            "properties": {
                "follows": {
                    "type": "integer"
                },
                "tags": {
                    "type": "string"
                }
            }
        }
    }
}'
```

blogのドキュメントを作ります。

```bash
curl -XPUT "http://localhost:9200/blog/item/1" -d '{
"text": "aaa bbb ccc",
"date": "2015-06-22 15:00:00",
"tag": "game",
"source": "111"
}'

curl -XPUT "http://localhost:9200/blog/item/2" -d '{
"text": "bbb ccc ddd eee",
"date": "2015-06-22 15:00:00",
"tag": "game",
"source": "222"
}'

curl -XPUT "http://localhost:9200/blog/item/3" -d '{
"text": "ccc ddd eee",
"date": "2015-06-22 15:00:00",
"tag": "music",
"source": "333"
}'

curl -XPUT "http://localhost:9200/blog/item/4" -d '{
"text": "aaa bbb ccc",
"date": "2015-06-22 15:00:00",
"tag": "game",
"source": "444"
}'
```

userを作ります。
ユーザ333と444を気に入っており、音楽カテゴリのブログを好むユーザです。

```bash
curl -XPUT "http://localhost:9200/users/user/1" -d '{
"follows": [333,444],
"tags": "music"
}'
```

#検索
##普通の検索
```bash
curl -XGET "http://localhost:9200/blog/_search" -d '{
  "query" : {
    "simple_query_string" : {
      "query": "ccc",
      "fields": ["text"]
     }
  }
}'
```
結果

```bash
{
    "took": 2,
    "timed_out": false,
    "_shards": {
        "total": 1,
        "successful": 1,
        "failed": 0
    },
    "hits": {
        "total": 4,
        "max_score": 0.3884282,
        "hits": [
            {
                "_index": "blog",
                "_type": "item",
                "_id": "1",
                "_score": 0.3884282,
                "_source": {
                    "text": "aaa bbb ccc",
                    "date": "2015-06-22 15:00:00",
                    "tag": "game",
                    "source": "111"
                }
            },
            {
                "_index": "blog",
                "_type": "item",
                "_id": "2",
                "_score": 0.3884282,
                "_source": {
                    "text": "bbb ccc ddd eee",
                    "date": "2015-06-22 15:00:00",
                    "tag": "game",
                    "source": "222"
                }
            },
～～～～～～～～～～～～～～～～～～～～～～～～～～～～～～
```
##ユーザ1がフォローしているユーザと気に入っているタグを重視してソート

ユーザ1がフォローしているユーザ(333と444)と、
気に入っているタグ(music)を指定して検索します。
function_scoreで、filterがマッチしたドキュメントのスコアを2倍しています。

```bash
curl -XGET "http://localhost:9200/blog/_search" -d '{
    "query": {
        "function_score": {
            "query": {
                "simple_query_string": {
                    "query": "ccc",
                    "fields": [
                        "text"
                    ]
                }
            },
            "functions": [
                {
                    "filter": {
                        "terms": {
                            "source": [
                                333,
                                444
                            ]
                        }
                    },
                    "weight": 2
                },
                {
                    "filter": {
                        "terms": {
                            "tag": [
                                "music"
                            ]
                        }
                    },
                    "weight": 2
                }
            ],
            "score_mode": "multiply"
        }
    }
}'
```

結果

```bash
{
    "took": 3,
    "timed_out": false,
    "_shards": {
        "total": 1,
        "successful": 1,
        "failed": 0
    },
    "hits": {
        "total": 4,
        "max_score": 1.5537128,
        "hits": [
            {
                "_index": "blog",
                "_type": "item",
                "_id": "3",
                "_score": 1.5537128,
                "_source": {
                    "text": "ccc ddd eee",
                    "date": "2015-06-22 15:00:00",
                    "tag": "music",
                    "source": "333"
                }
            },
            {
                "_index": "blog",
                "_type": "item",
                "_id": "4",
                "_score": 0.7768564,
                "_source": {
                    "text": "aaa bbb ccc",
                    "date": "2015-06-22 15:00:00",
                    "tag": "game",
                    "source": "444"
                }
            },
            {
                "_index": "blog",
                "_type": "item",
                "_id": "1",
                "_score": 0.3884282,
                "_source": {
                    "text": "aaa bbb ccc",
                    "date": "2015-06-22 15:00:00",
                    "tag": "game",
                    "source": "111"
                }
            },
            {
                "_index": "blog",
                "_type": "item",
                "_id": "2",
                "_score": 0.3884282,
                "_source": {
                    "text": "bbb ccc ddd eee",
                    "date": "2015-06-22 15:00:00",
                    "tag": "game",
                    "source": "222"
                }
            }
        ]
    }
}
```
##lookup
なお、上記のクエリは、terms filterのlookupという機能を使用して下記のように書くことが可能です。
usersインデックスに作ったユーザ1のfollows,tags属性をフィルタにセットして検索しています。
https://www.elastic.co/guide/en/elasticsearch/reference/1.4/query-dsl-terms-filter.html#_terms_lookup_twitter_example
これを使ったほうが効率が良さそうです。
（たくさんフォローしてたりしてもクエリが長くならないし、lookupキャッシュとかも作ってくれるらしい。）

```bash
curl -XGET "http://localhost:9200/blog/_search" -d '{
"query": {
  "function_score": {
    "query": { 
       "simple_query_string" : {
         "query": "ccc",
         "fields": ["text"]
       }
     },
     "functions": [ 
     {
       "filter": { 
         "terms": {
           "source" : {
             "index" : "users",
             "type" : "user",
             "id" : "1",
             "path" : "follows"
           }
         }
       }, 
       "weight": 2
     },
     {
       "filter": { 
         "terms": {
           "tag" : {
             "index" : "users",
             "type" : "user",
             "id" : "1",
             "path" : "tags"
           }
         }
       },
       "weight": 2
     }
     ],
     "score_mode": "multiply"
   }
 }
}'
```