Elasticsearchで誕生日をお祝いしたい

# Elasticsearchで誕生日をお祝いしたい

[@kaiba](https://twitter.com/kaiba)と申します。

年々誕生日が憂鬱になってきましたが、「今月誕生日を迎える人」を探すようなケースで苦労したので記事にしてみます。

## TL;DR

- 「今月誕生日を迎える人」を探す方法はいくつかあるが月、日をindexするのが良いかも。
- [公式のフォーラム](https://discuss.elastic.co/t/birthday-querying/70684)では
  - 次の誕生日をindexに追加しておく方法(定期的にindex更新が必要になる)
  - script queryを書いて頑張る(とてもメンテしたくない感じのコードだ…)

## やってみる :muscle:

ユーザの誕生日を保持しているサービスは多いと思います。
年月日を入力し、DBにはDate型で保持するケースが多いのではないでしょうか？
とりあえずElasticsearchにそのまま入れてみます。

indexを作ります。名前はbirthdayにしました。

```sh
curl -X PUT -H "Content-Type: application/json" http://b.lvh.me:9200/birthday
```

documentを入れます。以下のドキュメントを用意しました。

```json
{
    "name": "kaiba",
    "birthday": "1992-12-05"
}
```

名前はdocで ID 1 で入れました。

```json
curl -X PUT -H "Content-Type: application/json" http://b.lvh.me:9200/birthday/doc/1?pretty -d @user.json
```

データ型を確認しておきまして。dateになっていますね。

```json
curl -X GET -H "Content-Type: application/json" http://b.lvh.me:9200/birthday/_mapping?pretty
{
  "birthday" : {
    "mappings" : {
      "doc" : {
        "properties" : {
          "birthday" : {
            "type" : "date"
          },
          "name" : {
            "type" : "text",
            "fields" : {
              "keyword" : {
                "type" : "keyword",
                "ignore_above" : 256
              }
            }
          }
        }
      }
    }
  }
}
```

## Queryを考えてみる

`birthday.month >= 12月` みたいのを書けばいいでしょう！ と思っていたんですが、Elasicsearchだとすんなりいきません。
[公式のフォーラム](https://discuss.elastic.co/t/birthday-querying/70684)にバッチリの質問がありましたので紹介します。

### 案1 次の誕生日を保持しておく

`1992-12-05` 生まれの場合、2019年現在、次の誕生日は `2019-12-05` ですね。それをindexしておき以下のように探します。

```json
{
    "query": {
        "range": {
           "next_birthday": {
              "gte": "now",
              "lte": "now+10d/d"
           }
        }
    }
}
```

クエリがわかりやすい。Elasticsearchらしいアプローチで素晴らしいですね！
しかし、 `next_birthday` を定期的に更新してやる必要がでてきます…。
いつ検索するのかにもよりますが、大抵の場合、毎日になりそうです。コストも気になります。

### 案2 script queryで頑張る

```json
{
   "query": {
      "bool": {
         "must": {
            "script": {
               "script": {
                  "inline": "def today = LocalDate.parse(params.today); 中略",
                  "lang": "painless",
                  "params": {
                     "today": "2017-01-09",
                     "daysFuture": 10
                  }
               }
            }
         }
      }
   }
}
```

`birthday.month >= 12月` みたいのを書く例です。
painlessのscript queryを書くことになりメンテしづらいし、パフォーマンスも悪そう。
Elasticsearchらしくないアプローチに感じます。

## 僕の考えた最強の誕生日検索

本題です。こうするのはどうでしょうか？
誕生日の月、日を別のデータとしてindexします。

```json
{
    "name": "kaiba",
    "birthday_month": 12,
    "birthday_date": 5
}
```

年をまたぐ場合はもうひと手間必要ですが難しくはないでしょう。

```json
{
    "query": {
        "bool": {
            "must": [
                {
                    "range": {
                        "birthday_month": {
                            "gte": 12
                        }
                    }
                },
                {
                    "range": {
                        "birthday_date": {
                            "gte": 1,
                            "lte": 10
                        }
                    }
                }
            ]
        }
    }
}
```

検索してみます。

```json
curl -X POST -H "Content-Type: application/json" http://b.lvh.me:9200/birthday/_search\?pretty -d @search.json
{
  "took" : 4,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : 1,
    "max_score" : 2.0,
    "hits" : [
      {
        "_index" : "birthday",
        "_type" : "doc",
        "_id" : "1",
        "_score" : 2.0,
        "_source" : {
          "name" : "kaiba",
          "birthday" : "1992-12-05",
          "birthday_month" : 12,
          "birthday_date" : 5
        }
      }
    ]
  }
}
```

LGTM!

あとは以下のケースを判断して、Esのクエリを組み立てると良いと思います。手書きですみませんね。

![IMG_20191119_012902.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/21799/a5ca8ef8-9e81-5ed2-0e8a-c86a49663e4a.jpeg)

年と月をまたぐ場合も考慮する場合はもうひと頑張りですね。

# まとめ

公式のフォーラムに比べて以下がよいかな、と思います。

- indexを定期的に更新する必要がない
- Queryがわかりやすい
- Elasticsearchらしいアプローチ

いざこうして文章に落としてみると「何をそんな当然のことを」と思うかもしれません。
僕もそうなのですがRDBに慣れているとなかなかこういった発想ができず難しく考えてしまうことがありますね。
僕のように困った人の手助けになるように、フォーラムに追記したいのですが、できないんですよね。残念。