JSON などのデータ構造をフラットにする意味

今日は、取り留めもない記事を。

# データのフラット化

ここ最近、「データ構造をフラットにしたら、～が良かった」という情報を立て続けに目にしました。

そもそも、データのフラット化とは、↓ これを、

```json
{
  "created_at": "Thu Apr 06 15:24:15 +0000 2017",
  "id_str": "850006245121695744",
  "text": "1\/ Today we\u2019re sharing our vision for the future of the Twitter API platform!\nhttps:\/\/t.co\/XweGngmxlP",
  "user": {
    "id": 2244994945,
    "name": "Twitter Dev",
    "screen_name": "TwitterDev",
    "location": "Internet",
    "url": "https:\/\/dev.twitter.com\/",
    "description": "Your official source for Twitter Platform news, updates & events. Need technical help? Visit https:\/\/twittercommunity.com\/ \u2328\ufe0f #TapIntoTwitter"
  },
  "place": {   
  },
  "entities": {
    "hashtags": [      
    ],
    "urls": [
      {
        "url": "https:\/\/t.co\/XweGngmxlP",
        "unwound": {
          "url": "https:\/\/cards.twitter.com\/cards\/18ce53wgo4h\/3xo1c",
          "title": "Building the Future of the Twitter API Platform"
        }
      }
    ],
    "user_mentions": [     
    ]
  }
}
```

↓ こう表現すること。

```ini
created_at="Thu Apr 06 15:24:15 +0000 2017"
id_str="850006245121695744"
text=1\/ Today we\u2019re sharing our vision for the future of the Twitter API platform!\nhttps:\/\/t.co\/XweGngmxlP"
user.id=2244994945
user.name="Twitter Dev"
user.screen_name="TwitterDev"
user.location="Internet"
user.url="https:\/\/dev.twitter.com\/"
user.description="Your official source for Twitter Platform news, updates & events. Need technical help? Visit https:\/\/twittercommunity.com\/ \u2328\ufe0f #TapIntoTwitter"
entities.urls.[0].url="https:\/\/t.co\/XweGngmxlP"
entities.urls.[0].unwound.url="https:\/\/cards.twitter.com\/cards\/18ce53wgo4h\/3xo1c",
entities.urls.[0].unwound.title="Building the Future of the Twitter API Platform"
```

フラット化のやり方によって細かい所は違うが、**ネストしたデータ構造を一行で表現できている** という点では一致する。

# 例

## gron

これは、JSON をフラット化して、`grep` しやすくする事を目的とした CLI ツール。
https://github.com/tomnomnom/gron

```bash
# Flatten 
$ gron "https://api.github.com/repos/tomnomnom/gron/commits?per_page=1" | fgrep "commit.author"
# json[0].commit.author = {};
# json[0].commit.author.date = "2016-07-02T10:51:21Z";
# json[0].commit.author.email = "mail@tomnomnom.com";
# json[0].commit.author.name = "Tom Hudson";

# Undo
gron "https://api.github.com/repos/tomnomnom/gron/commits?per_page=1" | fgrep "commit.author" | gron --ungron
# [
#   {
#     "commit": {
#       "author": {
#         "date": "2016-07-02T10:51:21Z",
#         "email": "mail@tomnomnom.com",
#         "name": "Tom Hudson"
#       }
#     }
#   }
# ]
```

- `grep` しやすい
- 構造をひと目で把握できる
- JavaScript のコードで出力される
    - `gron testdata/two.json > tmp.js && nodejs tmp.js` で Node は正しく Object として復元できる

## Prometheus

動的アーキテクチャ向けメトリクス収集管理ツール。
https://prometheus.io/

Prometheus は、メトリクスを公開するエンドポイントを巡回してメトリクスを収集するが、その [フォーマットがフラットなデータ](https://prometheus.io/docs/instrumenting/exposition_formats/) となっている。

例えば、[postgres_exporter](https://github.com/wrouesnel/postgres_exporter) は、以下のようなメトリクスを公開する。

```
pg_locks_count{datname="postgres",mode="accessexclusivelock"} 0   // `{}` 内は、Label という集計の為の属性値
pg_settings_autovacuum_analyze_scale_factor 0.1
pg_settings_enable_seqscan 1
pg_settings_log_rotation_age_seconds 86400
...
```

もし JSON なら、以下のようになるだろうか。

```json
{
  "pg" : {
    "locks" : {
      "count" : 0
    },
    "settings" : {
      "autovacuum" : {
        "analyze" : {
          "scale_factor" : 0.1
        }
      },
      "enable_seqscan" : 1,
      "log_rotation" : {
        "age_seconds" : 86400
      }
    }
  }
}
```

- 人間が読みやすい
- パースコストが低い
    - シンプルな構造
    - Validation が最小

## Viper

Go の設定関係をごそっと管理してくれるライブラリ。
https://github.com/spf13/viper

viper は、設定ファイル, Key/Value ストア, CLI フラグ, 環境変数等により与えられる設定情報を統一して扱う為に、[ネストしたデータ構造はフラット化したキーで設定値を特定できるようになっている](https://github.com/spf13/viper#accessing-nested-keys)。

```json:JSONの場合
{
    "host": {
        "address": "localhost",
        "port": 5799
    },
    "datastore": {
        "metric": {
            "host": "127.0.0.1",
            "port": 3099
        },
        "warehouse": {
            "host": "198.0.0.1",
            "port": 2112
        }
    }
}
```

```bash:環境変数の場合
export DATASTORE_METRIC_HOST="192.168.0.1"
# ただし、コード側に `viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))` が必要
```

```:フラグの場合
./main --datastore.metric.host="192.168.100.100"
```

```go:優先度の一番高いフラグにシャドーイングされる
GetString("datastore.metric.host")
// 192.168.100.100
```

- フォーマットが違うデータ構造同士の中間表現として利用
    - 階層的なデータ構造なら全て変換可能
    - グラフデータは表現不可

# 人は、どんなときにデータをフラット化するのか

## 人間に見やすい

`Docker inspect` なんかを見ていると、自分が一体何を見ているのかが分からなくなる時がある。
`jq` で絞るにも、そもそもの構造覚えていないとできない。
そこで、`Docker inspect` の結果を gron でパースすれば、以下のようになる。

```
...
json[0].NetworkSettings.Networks.bridge.Gateway = "192.168.0.1";
json[0].NetworkSettings.Networks.bridge.GlobalIPv6Address = "";
json[0].NetworkSettings.Networks.bridge.GlobalIPv6PrefixLen = 0;
json[0].NetworkSettings.Networks.bridge.IPAMConfig = null;
json[0].NetworkSettings.Networks.bridge.IPAddress = "192.168.0.2";
...
```

この様に、大きくて構造が複雑なデータを閲覧する場合、フラット化することで少なくとも構造はひと目で分かる様になるし、`grep` しながら望みのデータを探索的に見つけることもできる。

ただし、**記述のしやすさ** という点では、Yaml や Toml の様な構造の方が良いと思う。

## ログに適している

ログも `grep` する事が多い対象。
JSON 等をフォーマットして複数行で入れてしまうと後から探しづらいし、間に別のログが入ることもありうる。
一行で意味を持てるフラットなデータはよりロバストだ。

賢いロガーなら複数行のログに対応している場合もあるので、使えるのであればそれを使うのがベストだろう。

## 機械的にもそこそこ見やすい

機械的な見やすさとは、パーサがなくとも直感的なデータ操作で最低限扱えるという意味です。

例えば Shell の場合、 `grep -v "#" | grep user.name | cut -d= -f 2` で対象のデータが取得できる。

プログラム側でも、簡単に読み込むことができそう。
例えば JavaScript なら `new Map(text.split('\n').filter(a => !a.startsWith("#")).map(a => a.split('='))` で Map 化ができるし、
Python なら `{ b[0]:b[1] for b in [a.split('=') for a in text.split('\n') if not t.startswith('#')]}` で Dict 化できる。
もちろん、エスケープとか考えると辛い面も多いが、100 行以下のコードでパースできるというコピペビリティの高さは役に立つ場面もありそう。

**扱いやすさ** という点では、しっかりとしたパーサがあり、Validation もしっかりしていて、構造体等に Unmarshal してくれるようなデータ構造の方が便利なケースは多いだろう。

## ビッグデータに適している

特に時系列データを収集する場合、共時的な構造を維持する必要が無く、Timestamp + Key/Value にして保存する場合が多い。

# シリアライズ

読む側にはとても楽であると分かったが、出力する側にとっては、案外面倒な事が多い。
ライブラリや実装例を見つけてみた。

## JavaScript

https://github.com/hughsk/flat

## Python

https://github.com/amirziai/flatten

## Go

https://github.com/jeremywohl/flatten

## Java

https://github.com/wnameless/json-flattener

## Scala

https://stackoverflow.com/questions/24273433/play-scala-how-to-flatten-a-json-object

## 全体

各言語でそれぞれ実装がなされているので、必要性はあるんだと思うが、何か統一したフォーマットがある訳でもなく。
自他ともにニーズありそうだけど、どうなんだろう。

# まとめ

記述するという点では、構造化されている方が分かりやすく扱いやすいです。私は Java の Properties よりは Toml の方を選びます。
しかし、データを見る・公開するという点に置いては、適用先をしっかり見極めれば、わりと応用できる場所は多いのではないかなと思ってます。
いまは、適用できそうな分野を色々試行錯誤中です。

# あとがき

※ この記事は個人の見解であり、所属する組織を代表するものではありません。