Elasticsearchで親子関係のあるデータ作りたくて四苦八苦した話

RDBからElasticsearchに移行していい感じに検索機能を実現したいと思ったものの、親子関係のあるデータをどう実現すればいいか分からず四苦八苦した際の記録です。
Elasticsearchのバージョンは8.5系です。

# やりたいこと
1. １店舗に対して、複数の駅情報が紐づいているデータ構造を実現したい（駅情報はN件）
1. 条件にHITした子レコードだけ出力したい。


### データの例
店舗
| shop_id | shop_name |
| --- | --- |
| 1 | 和食の店 渋谷店|



最寄駅情報
| shop_id | no | ensen_cd | ensen_name | eki_cd | eki_name | 
| --- | --- | --- | --- | --- | --- |
| 1 | 1| 100| 山手線 | 1001 | 渋谷 |
| 1 | 2| 100| 山手線 | 1002 | 原宿 |
| 1 | 3| 200| 銀座線 | 2002 | 表参道 |



## 検討したデータ型
Elasticsearchで親子関係のあるデータを作る場合、以下のいずれかを採用するかと思います。

3. join
2. flattened
1. nested



### 1. join
joinはやりたいことはできそうではあったのですが、データを生成する際に
**どれが親でどれが子供か**を指定する必要があり、データ生成処理が複雑になりそうなので見送りました。

```
# Mapping
PUT test
{
  "mappings": {
    "properties": {
      "shop_no": {
        "type": "keyword"
      },
      "shop_name": {
        "type": "keyword"
      },
      "search_eki": { 
        "type": "join",
        "relations": {
          "shop": "eki" <- shopが親、ekiが子という定義
        }
      }
    }
  }
}
```
```
# 店舗データ(親)
PUT join_test/_doc/1
{
  "shop_no": 1,
  "shop_name": "和食の店 渋谷店",
  "search_eki":"shop"
}

# 駅データ（子）
PUT join_test/_doc/2?routing=1
{
  "shop_no": 1,
  "no": 1,
  "ensen_code": "100",
  "ensen_name": "山手線",
  "eki_cd": "1001",
  "eki_name": "渋谷",
  "search_eki": {
    "name": "eki",
    "parent": "1"
  }
}
```

### 2.flattened
データがわかりやすくなりそうで期待大でしたが、**例えば子データが３件あり、そのうち１件だけが検索条件に合致した場合も子データが全て抽出されてしまう**ため見送りました:cry:
調べた限り `painless_script` を使えばいい感じにできそう・・・という記事も見かけましたが、クエリの可読性が落ちそうなので不採用となりました。。。

```
PUT flatted_test
{
  "mappings": {
    "properties": {
      "shop_no": {
        "type": "keyword"
      },
      "shop_name": {
        "type": "keyword"
      },
      "search_eki":{
        "type": "flattened"
      }
    }
  }
}
```
```
PUT flatted_test/_doc/1
{
  "shop_no": 1,
  "shop_name": "和食の店 渋谷店",
  "search_eki": [
    {
      "no": 1,
      "ensen_code": "100",
      "ensen_name": "山手線",
      "eki_cd": "1001",
      "eki_name": "渋谷"
    },
    {
      "no": 2,
      "ensen_code": "100",
      "ensen_name": "山手線",
      "eki_cd": "1002",
      "eki_name": "原宿"
    },
    {
      "no": 3,
      "ensen_code": "200",
      "ensen_name": "銀座線",
      "eki_cd": "2002",
      "eki_name": "表参道"
    }
  ]
}
```
```
GET flatted_test/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "term": {
            "search_eki.eki_cd": "1001"
          }
        }
      ]
    }
  }
}
```

eki_cd=1001の子データだけ出力したかったのですが、全て出力されてしまう・・・:sob:
```
{
  "took": 2,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 1,
      "relation": "eq"
    },
    "max_score": 0.46223074,
    "hits": [
      {
        "_index": "flatted_test",
        "_id": "1",
        "_score": 0.46223074,
        "_source": {
          "shop_no": 1,
          "shop_name": "和食の店 渋谷店",
          "search_eki": [
            {
              "no": 1,
              "ensen_code": "100",
              "ensen_name": "山手線",
              "eki_cd": "1001",
              "eki_name": "渋谷"
            },
            {
              "no": 2,
              "ensen_code": "100",
              "ensen_name": "山手線",
              "eki_cd": "1002",
              "eki_name": "原宿"
            },
            {
              "no": 3,
              "ensen_code": "200",
              "ensen_name": "銀座線",
              "eki_cd": "2002",
              "eki_name": "表参道"
            }
          ]
        }
      }
    ]
  }
}
```

### 3. nested
紆余曲折を経て、最終的にnetsedを採用しました。

```
PUT nest_test
{
  "mappings": {
    "properties": {
      "shop_no": {
        "type": "keyword"
      },
      "shop_name": {
        "type": "keyword"
      },
      "search_eki": {
        "type": "nested",
        "properties": {
          "eki_cd": {
            "type": "keyword"
          },
          "ensn_cd": {
            "type": "keyword"
          },
          "block_cd": {
            "type": "keyword"
          },
          "ensn_name": {
            "type": "keyword"
          },
          "eki_name": {
            "type": "keyword"
          }
        }
      }
    }
  }
}
```
```
PUT nest_test/_doc/1
{
  "shop_no": 1,
  "shop_name": "和食の店 渋谷店",
  "search_eki": [
    {
      "no": 1,
      "ensen_code": "100",
      "ensen_name": "山手線",
      "eki_cd": "1001",
      "eki_name": "渋谷"
    },
    {
      "no": 2,
      "ensen_code": "100",
      "ensen_name": "山手線",
      "eki_cd": "1002",
      "eki_name": "原宿"
    },
    {
      "no": 3,
      "ensen_code": "200",
      "ensen_name": "銀座線",
      "eki_cd": "2002",
      "eki_name": "表参道"
    }
  ]
}
```
クエリにinner_hitsを指定すると条件にHITした子レコードだけが取得できます。
```
GET nest_test/_search
{
  "query": {
    "nested": {
      "path": "search_eki",
      "query": {
        "bool": {
          "must": [
            {
              "terms": {
                "search_eki.eki_cd": [
                  "1001"
                ]
              }
            }
          ]
        }
      },
      "inner_hits": {
        "_source": "true",
        "docvalue_fields": [
          "search_eki.eki_cd",
          "search_eki.eki_name"
        ]
      }
    }
  }
}
```
eki_cd=1001のデータが出力されます。
```
{
  "took": 2,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 1,
      "relation": "eq"
    },
    "max_score": 1,
    "hits": [
      {
        "_index": "nest_test",
        "_id": "1",
        "_score": 1,
        "_source": {
          "shop_no": 1,
          "shop_name": "和食の店 渋谷店",
          "search_eki": [
            {
              "no": 1,
              "ensen_code": "100",
              "ensen_name": "山手線",
              "eki_cd": "1001",
              "eki_name": "渋谷"
            },
            {
              "no": 2,
              "ensen_code": "100",
              "ensen_name": "山手線",
              "eki_cd": "1002",
              "eki_name": "原宿"
            },
            {
              "no": 3,
              "ensen_code": "200",
              "ensen_name": "銀座線",
              "eki_cd": "2002",
              "eki_name": "表参道"
            }
          ]
        },
        "inner_hits": {
          "search_eki": {
            "hits": {
              "total": {
                "value": 1,
                "relation": "eq"
              },
              "max_score": 1,
              "hits": [
                {
                  "_index": "nest_test",
                  "_id": "1",
                  "_nested": {
                    "field": "search_eki",
                    "offset": 0
                  },
                  "_score": 1,
                  "_source": {},
                  "fields": {
                    "search_eki.eki_name": [
                      "渋谷"
                    ],
                    "search_eki.eki_cd": [
                      "1001"
                    ]
                  }
                }
              ]
            }
          }
        }
      }
    ]
  }
}
```

いい感じです・・・！！！:joy:

## おわりに
Elasticsearchの仕様などを調べる際、日本語のドキュメントがなくとても苦労することが多いかと思います。
何かの助けになれば幸いです。