はてだBlog(仮称)

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

Elasticsearch のAggregations(livedoorレストランデータセットを取り込み試してみる)

livedoor グルメの DataSet をElasticsearchに取り込んで、Aggregations(aggs)を試してみた、の例です。

Aggregationsは、SQLでいうところのgroup byに近い演算です。グループ化や集計対象が、SQL(RDB)の場合はカラムの値なのに対し、転置インデックスの格納値を元にしているところがやや味付けが異なっていると言えると思います。

ここでは、Aggregations の定番例かなと筆者が思うもののクエリのシンタックス例をいくつか並べてみています。

また、データを取り込んでみて実際に試してみたら思った以上に面白いな〜と感じた、GEO系のAggregationsをいくつかピックアップしました。

Aggregationsの公式リファレンス

www.elastic.co

データセットはこちら

blog.livedoor.jp

※今回取り込んだデータの変換方法の例は別途記事にする予定です*1 逆にいうとこの記事ではデータセットをよろしく登録した「ldgourmet」というインデックスが存在する前提で記載しています。タイトルに盛り込んで置いておきながらごめんなさい。

データセットの項目、あるいはデータ結合などして、今回のaggsクエリで用いたフィールドは次のとおりです。

  1. pref.raw 都道府県名相当 ( 13__東京都 )
  2. pref_id.raw 都道府県コード: 文字列型 ( '1', '2',... '13' ) '1'は北海道、'2'は青森県、'13'は東京都....
  3. area_name.raw そのお店のエリア(●●線沿線、池袋-高田馬場巣鴨、... )
  4. cates.raw そのお店のカテゴリ(ラーメン、フレンチ、居酒屋、....)
  5. kuchikomi お店の口コミを全て1フィールドに突っ込んだもの
  6. kuchikomi.mei kuchikomiのテキストのうち、「一般名詞」として分類されたワードのみを抜き出したもの
  7. location お店の緯度経度(座標) [139.65379194444,35.482973888888] ※ geo_point型です。
  8. access_count そのお店のページのPV数?(人気度と考えればよいでしょうか)

実際に確認したElasticsearchのバージョンは6.8ですが、7系の公式リファレンスを見ながらシンタックス確認したので、7系でも動作するクエリになっていると思います。

001 基本: Terms Aggregation(ただし例はマルチの例)

対象のデータに関する、都道府県ごとのお店件数とカテゴリごとのお店件数。

www.elastic.co

なお、このaggsに限りませんが、トップの「size:0」で、Aggregationsの戻り値のみ戻るようにできます。(ここでは、もともとfilter_pathで戻り値のフィールドを絞っていますので、さほど見た目は変わりませんが)

POST /ldgourmet/_search?filter_path=agg*
{
  "size":0,
  "aggs": {
    "a1": {
      "terms": {
        "field": "pref.raw",
        "size": 47
      }
    },
    "a2": {
      "terms": {
        "field": "cates.raw",
        "size": 50
      }
    }
  }
}

002 基本(ただし2階層)

都道府県ごとのカテゴリ一覧(カテゴリごとの件数)

いわゆるファセット。

a1という名称で呼ぶことにしたバケット(各都道府県ごとのgroup byに相当)にa2バケットのaggsをぶら下げることで、「a1ごとのa2」というクエリを表現することに注目ください。

POST /ldgourmet/_search?filter_path=agg*
{
  "aggs": {
    "a1": {
      "terms": {
        "field": "pref.raw",
        "size": 48
      },
      "aggs": {
        "a2": {
          "terms": {
            "field": "cates.raw",
            "size": 50
          }
        }
      }
    }
  }
}

003 significant_terms

significant_termsによる、都道府県ごとの特徴ある(とElasticsearchがカウントベースの方法で見なした)カテゴリの一覧。

www.elastic.co

例えば、通常のtermsのaggsの場合、福岡県では純粋な件数でいうと、和食、居酒屋、西洋料理、中華料理 ... という他の都道府県でも上位にくるお店の件数が多いのですが、significant_termsの場合は、「豚骨ラーメン」「博多ラーメン」「モツ鍋」といった地場の名物料理のお店が優先してピックアップされます。

実際の戻り値はここでは割愛しますが、livedoorのデータセットすごい、significant_terms面白いという結果が見て取れました。統計的にというところもさることながら文字面の定性レベルで分かるほど((ただし筆者の個人の感想ですが。))各地域のイメージにそったデータが得られてびっくりしました。

POST /ldgourmet/_search?filter_path=agg*
{
  "aggs": {
    "a1": {
      "terms": {
        "field": "pref.raw",
        "size": 47
      },
      "aggs": {
        "a2": {
          "significant_terms": {
            "field": "cates.raw",
            "size": 50,
            "min_doc_count": 5
          }
        }
      }
    }
  }
}

シンタックスとしては、手の込んだことをしなければ、Terms Aggregationの「terms」部分を「significant_terms」に変更するだけですね。

004 significant_terms 別例1

significant_termsに限らずですが、もともと、 検索クエリで絞り込んだ検索結果に対して、aggs可能です。

ここでは、「東京都」の「エリア」ごとの特徴的な「カテゴリ」の一覧を取得してみます。

POST /ldgourmet/_search?filter_path=agg*
{
  "query": {"term": {
    "pref.raw": {
      "value": "13__東京都"
    }
  }}, 
  "aggs": {
    "a1": {
      "terms": {
        "field": "area_name.raw",
        "size": 47
      },
      "aggs": {
        "a2": {
          "significant_terms": {
            "field": "cates.raw",
            "size": 50,
            "min_doc_count": 5, 
            "chi_square": {}
          }
        }
      }
    }
  }
}

こちらも、都道府県ごと以上に特色が出ている様子。

銀座エリア → 「バー」がフィーチャー。

新宿エリア → 「アジア・エスニック」がフィーチャー(単純なカウントは「居酒屋」の件数の方が多いにも関わらずというところがポイントでしょうか)。

秋葉原-水道橋・神田 エリア → 「カレー」がフィーチャー (カレーは実際の数も多いでしょうが...)

005 significant_terms 別例2

カテゴリごとの、口コミによく現れる一般名詞でそのカテゴリで特徴的なものを取得。

POST /ldgourmet/_search?filter_path=agg*
{
  "aggs": {
    "a1": {
      "terms": {
        "field": "cates.raw",
        "size": 20
      },
      "aggs": {
        "a2": {
          "significant_terms": {
            "field": "kuchikomi.mei",
            "size": 50,
            "min_doc_count": 5, 
            "chi_square": {}
          }
        }
      }
    }
  }
}

和食 → 天ぷら、刺身

居酒屋 → 焼酎、酒

西洋料理 → パスタ、ハンバーグ

カフェ・喫茶 → コーヒー、ケーキ

のような、当たり前といえばあたり前ですが、そのカテゴリらしいワードがピックアップされましたね。(一般名詞に絞った情報を「kuchikomi.mei」に格納しているところも隠れたポイントかも)

006 range

www.elastic.co

数字フィールドに対して、range、rangesで階級ごとの件数をカウントできます。

PV数が「100まで」「100〜1000」「1000〜5000」「5000〜」という階級で、お店の件数を数えてみます。

POST /ldgourmet/_search?filter_path=agg*
{
  "aggs": {
    "a1": {
      "range": {
        "field": "access_count",
        "ranges":[
          {"to":100},
          {"from":100, "to":1000},
          {"from":1000, "to":5000},
          {"from":5000}
          ]
      }
    }
  }
}

007 range(painless scriptによるアレンジ)

Painless scriptにより、基準となるフィールドに対して関数などで変換した値を基準に階級づけできます。

ただし、ここでは前項と同じ結果になるような例を示すことにしました。

POST /ldgourmet/_search?filter_path=agg*
{
  "aggs": {
    "a1": {
      "range": {
        "script":{
          "lang":"painless",
          "source": "doc['access_count'].value"
        },
        "ranges":[
          {"to":100},
          {"from":100, "to":1000},
          {"from":1000, "to":5000},
          {"from":5000}
          ]
      }
    }
  }
}

008 range (painless scriptによるアレンジ2)

Painless scriptを使った例、その2。

pref_idは文字列型なので、それをrangesで扱えるように、数字に変換しているという例です。 (内容そのものは、可視化・分析の実用としてはさほど意味はない、painless scriptの例のための例です。)

また、「keyed: true」と「key」というおまじないで、階級に個別の名称を与えて、階級の意図がわかりやすくなるようにしています。

POST /ldgourmet/_search?filter_path=agg*
{
  "aggs": {
    "a1": {
      "range": {
        "keyed": true, 
        "script": {
          "lang": "painless",
          "source": "Integer.parseInt(doc['pref_id'].value)"
        },
        "ranges": [
          {
            "key": "不明", 
            "to": 1
          },
          {
            "key": "北の方",
            "from": 1,
            "to": 10
          },
          {
            "key": "真ん中の北寄り",
            "from": 10,
            "to": 20
          },
          {
            "key": "真ん中の南寄り",
            "from": 20,
            "to": 30
          },
          {
            "key": "南の方",
            "from": 30,
            "to": 48
          }
        ]
      }
    }
  }
}

009 GEO系まとめて

SQLのgroup byに無い視点として、GEO系のグルーピング相当のことが可能です。

各お店の「location(緯度経度)」の集計演算の結果として、都道府県ごとの次のような値を取得できます。

  1. 境界のBOXの左上・右下の座標 (geo_bounds)
  2. 重心・中心点相当(geo_centroid) → 例えば、geo_boundsとgeo_centroidを使うと、検索結果のお店の一覧を確実に含むような「地図」の中心点と縮尺を決められますね。
  3. GeoHashのグリッド(geohash_grid)による、グリッドごとの件数 (precisionでグリッドの大きさ(相当)を設定) → 自分で可視化してみるところまではやっていないが、コロプレス図などに利用できる(と思う)。
  4. geo_distanceによる、ある地点(originで指定)からの距離の範囲ごとの件数(例のoriginは「大阪府」のある地点 → 大阪府内は、10kmや20kmあたりに件数が集中するが、東京都の場合は、300km以上のカテゴリに全件が当てはまるようなカウント結果となる)

上2つは、得られるのが集約された値そのものなので「Metrics Aggregation」に分類されているようです。

Geo-bounds aggregation | Elasticsearch Guide [7.13] | Elastic

Geo-centroid aggregation | Elasticsearch Guide [7.13] | Elastic

下2つは、「BucketAggregations」です。

Geohash grid aggregation | Elasticsearch Guide [7.13] | Elastic

Geo-distance aggregation | Elasticsearch Guide [7.13] | Elastic

POST /ldgourmet/_search?filter_path=agg*
{
  "size": 0,
  "aggs": {
    "a1": {
      "terms": {
        "field": "pref.raw",
        "size": 47
      },
      "aggs": {
        "a2-gb": {
          "geo_bounds": {
            "field": "location"
          }
        },
        "a2-gc":{
          "geo_centroid": {
            "field": "location"
          }
        },
        "a2-gh":{
          "geohash_grid":{
            "field":"location",
            "precision":3
          }
        },
        "a2-gd":{
          "geo_distance": {
            "field": "location",
            "origin": {
              "lat": 34.6769, 
              "lon": 135.5142
            },
            "unit": "km", 
            "ranges": [
              { 
                "to":10
              },
              { 
                "from":10,
                "to":20
              },
              {
                "from": 20,
                "to":100
              },
              {
                "from": 100,
                "to": 300
              },
              {
                "from":300
              }
            ]
          }
        }
      }
    }
  }
}

なお、Elasticsearch ver 6系では未対応だが、GeoTile Grid Aggregation(シンタックスの見た目は、geotile_grid プロパティを使うこと以外geohash_gridと類似)というものがあります。

Geotile grid aggregation | Elasticsearch Guide [7.13] | Elastic

自分で試してはいないのですが、、「地図タイル」ごとに集計ということが述べられていることから、大地を文字通り正方形の区画に見立てた領域ごとの件数になるであろうことから、コロプレス図などとしてはgeohash_gridよりもこちらの方がしっくりくると思われます。

*1: ということで、追加記事を書きました(2020年6月某日)

itdepends.hateblo.jp