LoginSignup
24
0

More than 1 year has passed since last update.

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

Posted at

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

やりたいこと

  1. 1店舗に対して、複数の駅情報が紐づいているデータ構造を実現したい(駅情報はN件)
  2. 条件に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で親子関係のあるデータを作る場合、以下のいずれかを採用するかと思います。

  1. join
  2. flattened
  3. 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

データがわかりやすくなりそうで期待大でしたが、例えば子データが3件あり、そのうち1件だけが検索条件に合致した場合も子データが全て抽出されてしまうため見送りました: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の仕様などを調べる際、日本語のドキュメントがなくとても苦労することが多いかと思います。
何かの助けになれば幸いです。

24
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
24
0