はてだBlog(仮称)

私的なブログど真ん中のつもりでしたが、気づけばWebサイト系のアプリケーション開発周りで感じたこと寄りの自分メモなどをつれづれ述べています。2020年6月現在、Elasticsearch、pandas、CMSなどに関する話題が多めです。...ですが、だんだんとより私的なプログラムのスニペット置き場になりつつあります。ブログで述べている内容は所属組織で販売している製品などに関するものではなく、また所属する組織の見解を代表するものではありません。

ElasticsearchのAggregations(nested,reverse_nested,parent,children)の例

はじめに

ElasticsearchのAggregationsのnested,reverse_nested,parent,childrenについては次の過去記事ではごまかして説明をスキップしたので、あらためてまとめてみました。

itdepends.hateblo.jp

まとめてみた... と言っても、公式の例を筆者なりに噛み砕いた例におきかえただけですので悪しからず。

www.elastic.co

www.elastic.co

www.elastic.co

www.elastic.co

なぜこの4つか

...というほど大げさな話ではないものの、Bucket Aggregationsに所属するこの4つのAggsですが、実際のところ、同じBucket Aggregationsの最も定番かなと思われるtermsクエリと実際はほぼ一緒と言えると思います。

じゃあ、ほぼ一緒じゃない部分はどこかというと、termsはいくつかあるElasticsearchの基本的なデータタイプをターゲットにしているのに対し、この4つはnested、join(parent,children)というRDBでは苦手(というと語弊があるかもしれませんがひとまず)な検索エンジンおもしろじゃんという変わった型を扱うところが違います。

まあ、つまるところ、これらの型についてもterms Aggが使えるように拡張されたクエリととらえて良いのではないでしょうか。

Nested field type | Elasticsearch Guide [master] | Elastic

Join field type | Elasticsearch Guide [master] | Elastic

なお、この記事はElasticsearch ver 6.8 で確認して記述しました。ただし、ここで記述した範囲のことの本質は ver 7系でも変わっていないと思います。(違ったらごめんなさい)

ためしてみる

(1)nested, reverse_nested

ある製品種別に対して、取り扱い業者(販売業者ということにしました)がA、B、...のようにいて、業者には担当者がいるとしたデータを考えます。 また、販売業者ごとに同じ製品種別でも販売価格が違うこととします。

nestedデータの仕込み

PUT /products?include_type_name=false
{
  "mappings": {
    "properties": {
      "resellers": {
        "type": "nested",
        "properties": {
          "reseller": { "type": "keyword" },
          "staff":{"type":"keyword"},
          "price": { "type": "double" }
        }
      }
    }
  }
}
PUT /products/_doc/0
{
  "name": "LED TV",
  "n":"エルイーディーテレビ",
  "resellers": [
    {
      "reseller": "companyA",
      "staff": "Hachimura-San",
      "price": 350
    },
    {
      "reseller": "companyB",
      "staff": "Ohtani-San",
      "price": 500
    }
  ]
}

PUT /products/_doc/1
{
  "name": "dryer", 
  "n":"ドライヤー",
  "resellers": [
    {
      "reseller": "companyA",
      "staff": "Ohsaka-San",
      "price": 50
    },
    {
      "reseller": "companyB",
      "staff": "Ohtani-San",
      "price": 65
    },
        {
      "reseller": "companyC",
      "staff": "Nishigori-San",
      "price": 65
    }
  ]
}

製品情報に対して、これを取り扱う販売業者の情報を直接保持するというデータをある種自然な形で保持できるというところがポイントですね。 (JSON的には、オブジェクトの配列で保持されているところに注目)

(A)nested Agg

うまくいかない例

nested Aggの文法例を示す前に、あえてうまくいかない例を示します。

nested データタイプとかどうとかはひとまず考えず、Elasticsearchには、resellers.resellerみたいなドットで区切ってフィールドを指定できるパターンがあります。

常にうろ覚えの筆者のようなヒトだと次のようなクエリでいけそうな気がするのです。

GET /products/_search
{
  "size":0,
  "aggs": {
    "製品種別ごとの": {
      "terms": {
        "field": "name.keyword"
      },
      "aggs": {
        "について、販売業者の一覧": {
          "terms": {
            "field": "resellers.reseller"
          }
        }
      }
    }
  }
}

が、結果としては次のようになりそれらしき値は取得できません。

{
  "aggregations" : {
    "製品種別ごとの" : {
      "doc_count_error_upper_bound" : 0,
      "sum_other_doc_count" : 0,
      "buckets" : [
        {
          "key" : "LED TV",
          "doc_count" : 1,
          "について、販売業者の一覧" : {
            "doc_count_error_upper_bound" : 0,
            "sum_other_doc_count" : 0,
            "buckets" : [ ]
          }
        },
        {
          "key" : "dryer",
          "doc_count" : 1,
          "について、販売業者の一覧" : {
            "doc_count_error_upper_bound" : 0,
            "sum_other_doc_count" : 0,
            "buckets" : [ ]
          }
        }
      ]
    }
  }
}

nested を使った動作する例1

nested型のフィールドにはおまじないが必要です。

GET /products/_search
{
  "size":0,
  "aggs": {
    "製品種別ごとの": {
      "terms": {
        "field": "name.keyword"
      },
      "aggs": {
        "について、販売業者の一覧": {
          "nested": {
            "path": "resellers"
          },
          "aggs": {
            "名前": {
              "terms": {
                "field": "resellers.reseller"
              }
            }
          }
        }
      }
    }
  }
}

クエリの結果↓

{
  "aggregations" : {
    "製品種別ごとの" : {
      "doc_count_error_upper_bound" : 0,
      "sum_other_doc_count" : 0,
      "buckets" : [
        {
          "key" : "LED TV",
          "doc_count" : 1,
          "について、販売業者の一覧" : {
            "doc_count" : 2,
            "名前" : {
              "doc_count_error_upper_bound" : 0,
              "sum_other_doc_count" : 0,
              "buckets" : [
                {
                  "key" : "companyA",
                  "doc_count" : 1
                },
                {
                  "key" : "companyB",
                  "doc_count" : 1
                }
              ]
            }
          }
        },
        {
          "key" : "dryer",
          "doc_count" : 1,
          "について、販売業者の一覧" : {
            "doc_count" : 3,
            "名前" : {
              "doc_count_error_upper_bound" : 0,
              "sum_other_doc_count" : 0,
              "buckets" : [
                {
                  "key" : "companyA",
                  "doc_count" : 1
                },
                {
                  "key" : "companyB",
                  "doc_count" : 1
                },
                {
                  "key" : "companyC",
                  "doc_count" : 1
                }
              ]
            }
          }
        }
      ]
    }
  }
}

nested を使った動作する例2

nestedを間に挟めば、Metrics aggregationも使えます。 最安価格をチョイスします。

GET /products/_search
{
  "size":0,
  "aggs": {
    "製品種別ごとの": {
      "terms": {
        "field": "name.keyword"
      },
      "aggs": {
        "について、販売業者の一覧": {
          "nested": {
            "path": "resellers"
          },
          "aggs": {
            "最安価格": {
              "min": {
                "field": "resellers.price"
              }
            }
          }
        }
      }
    }
  }
}

クエリの結果↓

{
  "aggregations" : {
    "製品種別ごとの" : {
      "doc_count_error_upper_bound" : 0,
      "sum_other_doc_count" : 0,
      "buckets" : [
        {
          "key" : "LED TV",
          "doc_count" : 1,
          "について、販売業者の一覧" : {
            "doc_count" : 2,
            "最安価格" : {
              "value" : 350.0
            }
          }
        },
        {
          "key" : "dryer",
          "doc_count" : 1,
          "について、販売業者の一覧" : {
            "doc_count" : 3,
            "最安価格" : {
              "value" : 50.0
            }
          }
        }
      ]
    }
  }
}

(B)reverse nested Agg

つづいてreverse_nested です。

こちらは、nestedを下敷きに、nested内にある、ある値を持つドキュメントをトップレベルの値のバリエーションでグループ化できるというやつです。

(販売業者の)各担当者がどの製品を担当しているかといったことを先述のドキュメントから抽出するのが次のreverse_nestedを使ったクエリです。

GET /products/_search
{
  "size": 0,
  "aggs": {
    "(販売業者)": {
      "nested": {
        "path": "resellers"
      },
      "aggs": {
        "担当者が": {
          "terms": {
            "field": "resellers.staff"
          },
          "aggs": {
            "どの製品を担当しているか": {
              "reverse_nested": {},
              "aggs": {
                "___": {
                  "terms": {
                    "field": "name.keyword"
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}

↑クエリ構造としては、aggsの4階層なのですが、3階層目に「reverse_nested:{}」のおまじないを入れることで、4階層目にあるドキュメントのトップに定義されているフィールドであるnameを使ってドキュメント全体をバケットにグループ分けできる...という理解でいいと思います。 ↓ 検索結果です。

{
  "aggregations" : {
    "(販売業者)" : {
      "doc_count" : 5,
      "担当者が" : {
        "doc_count_error_upper_bound" : 0,
        "sum_other_doc_count" : 0,
        "buckets" : [
          {
            "key" : "Ohtani-San",
            "doc_count" : 2,
            "どの製品を担当しているか" : {
              "doc_count" : 2,
              "___" : {
                "doc_count_error_upper_bound" : 0,
                "sum_other_doc_count" : 0,
                "buckets" : [
                  {
                    "key" : "LED TV",
                    "doc_count" : 1
                  },
                  {
                    "key" : "dryer",
                    "doc_count" : 1
                  }
                ]
              }
            }
          },
          {
            "key" : "Hachimura-San",
            "doc_count" : 1,
            "どの製品を担当しているか" : {
              "doc_count" : 1,
              "___" : {
                "doc_count_error_upper_bound" : 0,
                "sum_other_doc_count" : 0,
                "buckets" : [
                  {
                    "key" : "LED TV",
                    "doc_count" : 1
                  }
                ]
              }
            }
          },
          {
            "key" : "Nishigori-San",
            "doc_count" : 1,
            "どの製品を担当しているか" : {
              "doc_count" : 1,
              "___" : {
                "doc_count_error_upper_bound" : 0,
                "sum_other_doc_count" : 0,
                "buckets" : [
                  {
                    "key" : "dryer",
                    "doc_count" : 1
                  }
                ]
              }
            }
          },
          {
            "key" : "Ohsaka-San",
            "doc_count" : 1,
            "どの製品を担当しているか" : {
              "doc_count" : 1,
              "___" : {
                "doc_count_error_upper_bound" : 0,
                "sum_other_doc_count" : 0,
                "buckets" : [
                  {
                    "key" : "dryer",
                    "doc_count" : 1
                  }
                ]
              }
            }
          }
        ]
      }
    }
  }
}

(2)join系(parent,children)

join系(parent,children)の例です。

join系データの仕込み

まず、joinタイプを使ったデータを仕込みます。

公式の「stack overflow」風の例を少し置き換えた、以下のようなデータを仕込みます。 (親となる質問ドキュメント(Q)と子となる回答ドキュメント("ANSWER"でラベリング&親のドキュメントIDを指定))から構成されます。)

PUT cp_example?include_type_name=false
{
  "mappings": {
    "properties": {
      "join": {
        "type": "join",
        "relations": {
          "Q": "ANSWER"
        }
      }
    }
  }
}
PUT cp_example/_doc/1
{
  "join": {
    "name": "Q"
  },
  "body": "Windows2003とWindows2008の話です...",
  "title": "ファイルの転送",
  "TAGS": [
    "WIN2003",
    "WIN2008",
    "FTP"
  ]
}

PUT cp_example/_doc/2?routing=1
{
  "join": {
    "name": "ANSWER",
    "parent": "1"
  },
  "OWNER": "太郎",
  "body": "WinでFTPはやめた方が...",
  "creation_date": "2009-05-04T13:45:37.030"
}

PUT cp_example/_doc/3?routing=1
{
  "join": {
    "name": "ANSWER",
    "parent": "1"
  },
  "OWNER": "花子",
  "body": "Linuxを使え...",
  "creation_date": "2009-05-05T13:45:37.030"
}

PUT cp_example/_doc/4?routing=1
{
  "join": {
    "name": "ANSWER",
    "parent": "1"
  },
  "OWNER": "花子",
  "body": "macはLinuxではない。BSDは...",
  "creation_date": "2009-05-05T13:46:01.021"
}

(C)childrenと(D)parent

以下、childrenとparentをまとめて例示します。

各タグに関して、回答者の一覧とその回答投稿数を"children"を使って取得します。 また、回答者ごとの回答をおこなった質問のタグ(分野)の一覧を"parent"を使って取得します。

(やりたいことが対照的になっていることと、それに合わせて実際のクエリのカタチもchildren、parentを境に反転したイメージになっていることにご注目ください。)

POST cp_example/_search?size=0
{
  "aggs": {
    "TAGS": {
      "terms": {
        "field": "TAGS.keyword",
        "size": 10
      },
      "aggs": {
        "というタグの質問への": {
          "children": {
            "type" : "ANSWER" 
          },
          "aggs": {
            "回答者、その回答投稿数": {
              "terms": {
                "field": "OWNER.keyword",
                "size": 10
              }
            }
          }
        }
      }
    }
  }
}
POST cp_example/_search?size=0
{
  "aggs": {
    "OWNERS": {
      "terms": {
        "field": "OWNER.keyword",
        "size": 10
      },
      "aggs": {
        "という人の": {
          "parent": {
            "type" : "ANSWER" 
          },
          "aggs": {
            "答えている質問のタグ(分野)の一覧": {
              "terms": {
                "field": "TAGS.keyword",
                "size": 10
              }
            }
          }
        }
      }
    }
  }
}

↓クエリの結果(children)

{
  "aggregations" : {
    "TAGS" : {
      "doc_count_error_upper_bound" : 0,
      "sum_other_doc_count" : 0,
      "buckets" : [
        {
          "key" : "FTP",
          "doc_count" : 1,
          "というタグの質問への" : {
            "doc_count" : 3,
            "回答者、その回答投稿数" : {
              "doc_count_error_upper_bound" : 0,
              "sum_other_doc_count" : 0,
              "buckets" : [
                {
                  "key" : "花子",
                  "doc_count" : 2
                },
                {
                  "key" : "太郎",
                  "doc_count" : 1
                }
              ]
            }
          }
        },
        {
          "key" : "WIN2003",
          "doc_count" : 1,
          "というタグの質問への" : {
            "doc_count" : 3,
            "回答者、その回答投稿数" : {
              "doc_count_error_upper_bound" : 0,
              "sum_other_doc_count" : 0,
              "buckets" : [
                {
                  "key" : "花子",
                  "doc_count" : 2
                },
                {
                  "key" : "太郎",
                  "doc_count" : 1
                }
              ]
            }
          }
        },
        {
          "key" : "WIN2008",
          "doc_count" : 1,
          "というタグの質問への" : {
            "doc_count" : 3,
            "回答者、その回答投稿数" : {
              "doc_count_error_upper_bound" : 0,
              "sum_other_doc_count" : 0,
              "buckets" : [
                {
                  "key" : "花子",
                  "doc_count" : 2
                },
                {
                  "key" : "太郎",
                  "doc_count" : 1
                }
              ]
            }
          }
        }
      ]
    }
  }
}

↓クエリの結果(parent)

{
  "aggregations" : {
    "OWNERS" : {
      "doc_count_error_upper_bound" : 0,
      "sum_other_doc_count" : 0,
      "buckets" : [
        {
          "key" : "花子",
          "doc_count" : 2,
          "という人の" : {
            "doc_count" : 1,
            "答えている質問のタグ(分野)の一覧" : {
              "doc_count_error_upper_bound" : 0,
              "sum_other_doc_count" : 0,
              "buckets" : [
                {
                  "key" : "FTP",
                  "doc_count" : 1
                },
                {
                  "key" : "WIN2003",
                  "doc_count" : 1
                },
                {
                  "key" : "WIN2008",
                  "doc_count" : 1
                }
              ]
            }
          }
        },
        {
          "key" : "太郎",
          "doc_count" : 1,
          "という人の" : {
            "doc_count" : 1,
            "答えている質問のタグ(分野)の一覧" : {
              "doc_count_error_upper_bound" : 0,
              "sum_other_doc_count" : 0,
              "buckets" : [
                {
                  "key" : "FTP",
                  "doc_count" : 1
                },
                {
                  "key" : "WIN2003",
                  "doc_count" : 1
                },
                {
                  "key" : "WIN2008",
                  "doc_count" : 1
                }
              ]
            }
          }
        }
      ]
    }
  }
}