2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Vega-Liteを使った可視化の実装とデバッグ on Kibana

Last updated at Posted at 2025-07-19

はじめに

KibanaではLensを利用して多彩なデータの可視化を行うことができます。例えば時系列のデータを棒グラフ(Bar chart)や折れ線グラフ(Line chart)で表示したり、ヒートマップを使ってエンティティ(ホストなど)単位でのメトリクスの状態をわかりやすく表示したりすることができます。

しかしLensでは事前に決められた形のグラフしか表示することができません。それ以外のグラフを表示したい場合は、VegaVega-Liteを使う必要があります。VegaやVega-Liteは、より柔軟なデータの可視化を可能にするライブラリで、Kibanaでも利用することができます。

しかし正直なところ、VegaやVega-Liteは使い方が難しい。表現形式がJSONであるため、そもそもGUIでの操作ができません。しかもいわゆるプログラミング言語であるわけでもないので、あくまでもVega固有の表現方法を覚える必要があります。しかもこのVegaとVega-Liteは、確かに記述方法は似ているかもしれませんが、互換性はありません。Vegaで書いたものをVega-Liteで表示することはできませんし、その逆も同様です。似ているがゆえに、うっかり間違った方のオンラインマニュアルを見ていて動かない、ということもよくあります。

したがってLensや他のKibanaの機能で実現できるものは、可能な限りそちらを使うことを第一候補とし、どうしても必要な場合にのみVegaやVega-Liteを検討するようにしましょう。

この記事では、とりあえずVega-Liteを使ってElasticsearchに保存されているデータを可視化する方法について、最初の一歩を解説します。

VegaとVega-Lite

すでに記載した通り、VegaとVega-LiteはKibanaで利用できるデータの可視化ライブラリです。どちらも似たシンタックスを持っていますが、実際には互換性はなく、全くの別物です。Vegaはより高度な可視化を可能にする一方で、Vega-Liteはより簡単に使えるように設計されています。それぞれ、公式サイトでデモを見ることができるので、どのような可視化が実現できるかはそちらを参照して確認してください。以下はVega-LiteのExampleからの例です。

image.png

ここからは私の理解で、もしかしたら間違っていることがあるかもしれませんが、Vega-Liteは本質的に単一のデータセットのみを扱うことに特化することによって、構文の複雑さを低減したライブラリです。それに対してVegaは複数のデータ系列を扱うことができ、同一の可視化に例えば折れ線グラフと棒グラフを同時に表示したり、といったことが可能です。

実装する可視化要件が固定されており、単一のデータセットを扱う場合はVega-Liteを使うことができます。逆に、複数のデータセットを扱う必要がある場合や、今後どのような拡張が必要になるかわからない場合は、Vegaを選択しておかないと、ゆくゆく実装できない課題にぶつかるかもしれませんので、どちらを利用するかは慎重に選定してください。

散布図 / 単純な検索ベースの可視化

Vega-Liteを使って、Elasticsearchに保存されているデータを可視化するための最初の一歩として、散布図(Scatter plot)を作成してみましょう。以下は、KibanaでVega-Liteを使って散布図を作成するための基本的な手順です。

  1. Kibanaのダッシュボードを開く: Kibanaのダッシュボードにアクセスします。
  2. 新しいVega-Liteビジュアライゼーションを作成: ダッシュボードの「Add panel」ボタンをクリックし、「Custom Visualization」を選択します。
  3. Vega-Liteコードを入力: 以下のようなVega-Liteコードを入力します。この例では、Kibanaのecommerceサンプルデータを使用し、注文日時を横軸、売上高を縦軸とした散布図を作成します。
{
  "$schema": "https://vega.github.io/schema/vega-lite/v5.json",
  "title": "Ecommerce売上データ散布図",
  "data": {
    "url": {
      "index": "kibana_sample_data_ecommerce",
      "%context%": true,
      "%timefield%": "order_date",
      "body": {
        "size": 9999
      }
    },
    "format": {
      "property": "hits.hits"
    }
  },
  "transform": [
    {
      "calculate": "toDate(datum._source.order_date)",
      "as": "order_date"
    },
    {
      "calculate": "datum._source.taxful_total_price",
      "as": "sales_amount"
    },
    {
      "calculate": "datum._source.customer_gender",
      "as": "gender"
    },
    {
      "calculate": "datum._source.customer_full_name",
      "as": "customer_name"
    },
    {
      "calculate": "datum._source.order_id",
      "as": "order_id"
    }
  ],
  "mark": {
    "type": "point",
    "tooltip": true
  },
  "encoding": {
    "x": {
      "title": "注文日時",
      "field": "order_date",
      "type": "temporal"
    },
    "y": {
      "title": "売上高 (円)",
      "field": "sales_amount",
      "type": "quantitative"
    },
    "color": {
      "title": "顧客性別",
      "field": "gender",
      "type": "nominal"
    },
    "tooltip": [
      {"field": "order_id", "type": "nominal", "title": "注文ID"},
      {"field": "customer_name", "type": "nominal", "title": "顧客名"},
      {"field": "order_date", "type": "temporal", "title": "注文日時"},
      {"field": "sales_amount", "type": "quantitative", "title": "売上高"},
      {"field": "gender", "type": "nominal", "title": "性別"}
    ]
  }
}

結果はこんな感じ。散布図はLensでは提供されていないですが、Vega-Liteを使うことで表示できていますね。

image.png

解説

では、何が起こっているのか見ていきましょう。

Vega-Liteの宣言

KibanaではVegaとVega-Liteで、UI上の違いは全くありません。JSONの記載の中に"$schema"という要素として、どのバージョンの何を使うかを以下のように定義します。

"$schema": "https://vega.github.io/schema/vega-lite/v5.json"

Data

Vega-Liteでは、一つの系列のデータを可視化することができます。ここで言っている「系列」とは、要するに配列状のデータのことです。今回の例では、Elasticsearchからの検索結果そのものです。

Elasticsearchで検索を実行すると以下のような結果が得られます。

検索API

GET kibana_sample_data_ecommerce/_search

結果(かなり省略しています)

{
  "hits": {
    "total": {
      "value": 4675
    },
    "hits": [
      {
        "_source": {
          "customer_full_name": "Eddie Underwood",
          "customer_gender": "MALE",
          "customer_id": 38,
          "order_date": "2025-03-31T09:28:48+00:00",
          "order_id": 584677,
          "taxful_total_price": 36.98
        }
      },
      {
        "_source": {
          "customer_full_name": "Mary Bailey",
          "customer_gender": "FEMALE",
          "customer_id": 20,
          "order_date": "2025-03-30T21:59:02+00:00",
          "order_id": 584021,
          "taxful_total_price": 53.98,
        }
      },
      ...
    }
  }
}

見てわかる通り、結果のJSONのhits.hits以下に結果のドキュメントが配列として格納されているので、これを可視化対象として扱えばよさそうです。そこで、Vega-Liteの方では以下のような記載をしています。

  "data": {
    "url": {
      "index": "kibana_sample_data_ecommerce",
      "%context%": true,
      "%timefield%": "order_date",
      "body": {
        "size": 9999
      }
    },
    "format": {
      "property": "hits.hits"
    }
  },

まず、data.url.index要素で検索対象となるインデックスを指定しています。

次に、Dashboard上のフィルターやタイムピッカーの絞り込みを引き継ぐためにdata.url.%context%にtrueを指定します。そして時刻フィールドを明示するため、data.url.%timefield%にこのインデックスで時刻情報として扱うべきフィールド(order_data)を指定しています。

そして、実際のデータ(配列)が保存されているフィールドをformat.propertyで指定します。こうすることで、このフィールドのデータを可視化に利用することができるようになります。

Transform

この例では必ずしも必要ありませんが、要件によってはElasticsearchから取得した情報を加工してから表示する必要がある場合があります。そのようなデータの加工はTransformで実現します。どのような変形が可能なのかは公式ドキュメントを参照してください。

この例では、実質的にデータに単に別名を与えているだけです。

  "transform": [
    {
      "calculate": "toDate(datum._source.order_date)",
      "as": "order_date"
    },
    {
      "calculate": "datum._source.taxful_total_price",
      "as": "sales_amount"
    },
    {
      "calculate": "datum._source.customer_gender",
      "as": "gender"
    },
    {
      "calculate": "datum._source.customer_full_name",
      "as": "customer_name"
    },
    {
      "calculate": "datum._source.order_id",
      "as": "order_id"
    }
  ],

唯一意味のあるTransformをしているのは、時刻情報が文字列で与えられてしまうので、それをUnixtimeに変形するためにtoDateを与えているところだけですね。

ただし、ここで重要な要素はdatumです。datumにはformat.propertyで指定したデータの、それぞれの要素が代入されると考えてください。この例では、Elasticsearchのkibana_sample_data_ecommerceインデックスに保存されているそれぞれのドキュメントが渡され、それをtransformによって変形して、asで指定したプロパティーとして結果を保存します。

Mark

markではそれぞれのデータをどのように描画するかを指定します。ここでは点をプロット指定のでpointを指定しています。また、ツールチップを表示できるようにしています。ツールチップの内容は次のencodeで指定します。

  "mark": {
    "type": "point",
    "tooltip": true
  },

Encoding

Dataで指定した項目をMarkで指定した形で描画することは定義できたので、それを実際にどのように描画するのかを定義するのがEncodingです。

  "encoding": {
    "x": {
      "title": "注文日時",
      "field": "order_date",
      "type": "temporal"
    },
    "y": {
      "title": "売上高 (円)",
      "field": "sales_amount",
      "type": "quantitative"
    },
    "color": {
      "title": "顧客性別",
      "field": "gender",
      "type": "nominal"
    },
    "tooltip": [
      {"field": "order_id", "type": "nominal", "title": "注文ID"},
      {"field": "customer_name", "type": "nominal", "title": "顧客名"},
      {"field": "order_date", "type": "temporal", "title": "注文日時"},
      {"field": "sales_amount", "type": "quantitative", "title": "売上高"},
      {"field": "gender", "type": "nominal", "title": "性別"}
    ]
  }

ここはこの定義を見てもらえればそのま素直に理解はできるかと思います。それぞれのx軸y軸にどのデータを利用するのかを指定し、色分けの条件を指定しています(性別で別の色にしている)。

またそれぞれのポイントにマウスオーバーしたときにツールチップを表示するよう定義し、そのツールチップに表示する情報を定義しています。

Bubble chart / Aggregationベースの可視化

実際のユースケースではインデックスに保存されているドキュメントの情報をそのまま使うというより、Aggregationで集計した結果を可視化する方が多いかと思いますので、その例を一つ紹介しておきます。

以下は先ほどと同様KibanaのE-Commerceのサンプルデータを使い、各曜日ごとにそれぞれのContinent(アジア、ヨーロッパなどの単位)の売り上げをバブルチャートととして表示する例です。

{
  "$schema": "https://vega.github.io/schema/vega-lite/v5.json",
  "data": {
    "url": {
      "index": "kibana_sample_data_ecommerce",
      "%context%": true,
      "%timefield%": "order_date",
      "body": {
        "size": 0,
        "aggs": {
          "day_of_week": {
            "terms": {
              "field": "day_of_week"
            },
            "aggs": {
              "continents": {
                "terms": {
                  "field": "geoip.continent_name",
                  "order": {
                    "_key": "asc"
                  }
                },
                "aggs": {
                  "sales": {
                    "sum": {
                      "field": "taxful_total_price"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "format": {
      "property": "aggregations.day_of_week.buckets"
    }
  },
  "transform": [
    {
      "calculate": "datum.key",
      "as": "day_of_week"
    },
    {
      "flatten": ["continents.buckets"],
      "as": ["continent_data"]
    },
    {
      "calculate": "datum.continent_data.key",
      "as": "continent"
    },
    {
      "calculate": "datum.continent_data.sales.value",
      "as": "sales"
    }
  ],
  "mark": {
    "type": "circle",
    "opacity": 0.8,
    "strokeWidth": 0
  },
  "encoding": {
    "x": {
      "field": "day_of_week",
      "type": "ordinal",
      "title": "曜日",
      "sort": ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
    },
    "y": {
      "field": "continent",
      "type": "nominal",
      "title": "大陸"
    },
    "size": {
      "field": "sales",
      "type": "quantitative",
      "title": "売上高",
      "scale": {"range": [50, 10000]}
    },
    "color": {
      "field": "continent",
      "type": "nominal",
      "title": "大陸"
    }
  }
}

表示結果が以下。

image.png

解説

こちらもそれぞれの要素ごとに内容を確認します。

Data

今回はAggregationベースのクエリーを発行するので、そのAggregationの定義をします。

  "data": {
    "url": {
      "index": "kibana_sample_data_ecommerce",
      "%context%": true,
      "%timefield%": "order_date",
      "body": {
        "size": 0,
        "aggs": {
          "day_of_week": {
            "terms": {
              "field": "day_of_week"
            },
            "aggs": {
              "continents": {
                "terms": {
                  "field": "geoip.continent_name",
                  "order": {
                    "_key": "asc"
                  }
                },
                "aggs": {
                  "sales": {
                    "sum": {
                      "field": "taxful_total_price"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "format": {
      "property": "aggregations.day_of_week.buckets"
    }
  }

Aggregationの結果は以下のようになっています。

{
  "aggregations": {
    "day_of_week": {
      "doc_count_error_upper_bound": 0,
      "sum_other_doc_count": 0,
      "buckets": [
        {
          "key": "Thursday",
          "doc_count": 775,
          "continent": {
            "doc_count_error_upper_bound": 0,
            "sum_other_doc_count": 0,
            "buckets": [
              {
                "key": "Africa",
                "doc_count": 156,
                "sales": {
                  "value": 12077.5
                }
              },
              {
                "key": "Asia",
                "doc_count": 201,
                "sales": {
                  "value": 14067.375
                }
              }
              ...
            ]
          }
        },
        {
          "key": "Friday",
          "doc_count": 770,
          "continent": {
            "doc_count_error_upper_bound": 0,
            "sum_other_doc_count": 0,
            "buckets": [
              {
                "key": "Africa",
                "doc_count": 156,
                "sales": {
                  "value": 11611.375
                }
              },
              {
                "key": "Asia",
                "doc_count": 206,
                "sales": {
                  "value": 16552.65625
                }
              }
              ...
            ]
          }
        },
        ...
      ]
    }
  }
}

まず曜日(day_of_week)で、次にContinent(geoip.continent_name)でグループ化し、売り上げの合計をtaxful_total_priceに対するsumとして集計しています。

ここで表示対象のデータの配列はaggregations.day_of_week.bucketsになっていることがわかるので、これをformat.propertyに指定します。

Transform

基本的にTransformからはもはやただのJSON操作が変わるだけで、実装内容は先ほどの通常の検索APIの結果を可視化したのと変わりません。ただ今回の例では、例えば「月曜日」というグループの中に各Continentのデータが複数含まれる構造になっています。

Vega-LiteでMarkを描画する際は、それぞれのMarkが一つのデータとなっている必要があるため、この「月曜日」の中に複数ある「Continent」のデータをflattenを使って一段上の要素として列挙する必要があります。

flattenが行う処理は以下のようなものです。(公式ドキュメントからの引用)

flattenのTransform定義

{"flatten": ["foo", "bar"]}

入力

[
  {"key": "alpha", "foo": [1, 2], "bar": ["A", "B"]},
  {"key": "beta", "foo": [3, 4, 5], "bar": ["C", "D"]}
]

出力

[
  {"key": "alpha", "foo": 1, "bar": "A"},
  {"key": "alpha", "foo": 2, "bar": "B"},
  {"key": "beta", "foo": 3, "bar": "C"},
  {"key": "beta", "foo": 4, "bar": "D"},
  {"key": "beta", "foo": 5, "bar": null}
]

今回の例では以下のようになります。

    {
      "flatten": ["continents.buckets"],
      "as": ["continent_data"]
    },

ここまで来ればあとは適切なデータを利用してEncodingを指定するだけです。

デバッグ

Vega-Lite(およびVega)で一番大変だと思われるのはデバッグです。プログラミングではないので途中経過というものがなく、ステップ実行などもできないので内部で何が起こっているのかをトラッキングするのが難しい。

そこで、具体的にKibana上で利用できるデバッグ手法を二つ紹介します。またこれらの手法はVegaでも同様に利用できます。

Inspectを使う

Vegaを編集しているとき、画面上にInspectというリンクがあるので、これをクリックします。

image.png

表示されたダイアログの右上のプルダウンから、Vega debugをクリックすると、そのときにVegaが取り扱っているデータを閲覧することができます。

image.png

さらに、Data setsのプルダウンからVega-Liteの場合は「data_0」を選択すると、現在のTransformで変形されたデータを表形式で閲覧することができます。

image.png

ここで表示される1行が、一つのMarkとして描画されるわけです。

コンソールでVEGA_DEGUBを使う

ブラウザの開発ツールを利用するデバッグ方法です。まずChromeのF12などで開発ツールを開き、Consoleを表示します。そこでVEGA_DEBUG.view.data("data_0")と入力します。(ここで"data_0"はデータにつけた名前です。Vega-Liteでは"data_0"、Vegaの場合は自分でdataにつけた名前になります。)

すると以下のように変形結果のデータを詳しく閲覧することができます。

image.png

データの階層構造などを詳しく知りたい場合にはこちらの方が適しているかもしれません。

おわりに

この記事ではVega-Liteを使い、Kibana上でカスタムの可視化を行う方法について紹介しました。VegaおよびVega-Liteは非常に強力な反面、Lensのような直感的なUI操作で作成できるようなものではありません。一度作成するだけならともかく、メンテナンスのことも考えると、Lensなど他の方法で作成できる場合にはそちらを利用することができないかをまず検討するべきです。しかしそれが難しい場合、Vega/Vega-Liteを利用すれば、相当程度の要件についてはなんとかなるはずです。この記事などを参考に頑張ってみてください。

参考

以下、参考URLを貼っておきます。

2
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
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?