Elasticsearchにオセロの記譜（GGF）を取り込みElastic-OthelloDBにする


## はじめに
どうも！生産技術部で製品の検査工程を担当しているエンジニアです。最近はElastic-stack(Elasticsearch)にハマっていますｗ

Advent Calender 2020が始まって、５日目になりますね！

今年もテーマがオセロということで、書くことないよね？って思いながら、２０１９年のAdvent Calenderを眺めていたら、いいアイデアを見つけちゃいました。

@tanaka-a 様の[オセロの棋譜データベースWTHORの読み込み方](https://qiita.com/tanaka-a/items/e21d32d2931a24cfdc97)、[オセロの棋譜にも使えるファイル形式SGFとは](https://qiita.com/tanaka-a/items/b7fb505b881857b04983)
@hajipong 様の[連盟公式データをAPIで連携させる計画の話](https://qiita.com/hajipong/items/12ea7fb1370aedf1876e)

オセロの棋譜に使われているファイル形式関連ですね。上記の記事を拝読して、Elasticsearch？棋譜？取り込めるんじゃない？ってなりましたので、棋譜を取り込んでみます。取り込んだデータをどうしたいの？に対する回答は持ち合わせてないです。どなたか使ってあげてくださいｗ

リポジトリはこちら -> [elastic-othellodb](https://gitlab.com/iyama_yuki/elastic-othellodb)
以前に投稿した関連記事 -> [脱Excel Elastic Stack（docker-compose）でcsvログを分析・可視化 - Elastic Stackとは](https://qiita.com/TomoyukiSugiyama/items/619ab258ba230dfcf0a0)

## やったこと

[GGF](https://skatgame.net/mburo/ggsa/ggf)形式で保存されたオセロの棋譜データをパースして、Elasticsearchに保存し、Kibanaで可視化しました。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/451049/33ed9e17-fbe2-e239-e9eb-f8ca8db8f576.png)


KibanaのDashbordに黒番を持ったプレイヤーのレーティングの最大値をランキング順に並べてみました。（なんとなくDashbordに表示したかっただけなので、データに意味はないです。ｗ）


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/451049/5eb8d327-71be-1733-0435-0a192628a86a.png)

## オセロの保存形式って何があるの？
[Edaxのソースコード](https://github.com/abulmo/edax-reversi/blob/master/src/game.c)を軽く見てみました。ちょっと待って、いろいろありすぎて分からない。ターゲットはどの形式にしたら良いのか。

```c
/**
 * @brief convert an allinf.oko game to a Game.
 */
void oko_to_game(const OkoGame *oko, Game *game)

/**
 * @brief convert a Wthor game to a Game.
 */
void wthor_to_game(const WthorGame *thor, Game *game)

/**
 * @brief Parse a ggf game.
 */

static int game_parse_ggf(FILE *f, char *tag, char *value)

/**
 * @brief Parse a Smart Game Format (sgf) game.
 */
static int game_parse_sgf(FILE *f, char *tag, char *value)

/**
 * @brief Read a game from a pgn file.
 */
void game_import_pgn(Game *game, FILE *f)

/**
 * @brief Write a game to an eps file.
 */
void game_export_eps(const Game *game, FILE *f)
```

## ターゲットの保存形式は何にしよう
@tanaka-a 様のWTHOR形式についての記事によると、そもそもバイナリデータであるそうなので、即却下しました。大変そうだし、Elasticsearchの公式ブログにもこう書いてあるので。

>Filebeatは、ファイル形式のソースからデータを読み取り、事前処理、送信を行います。多くのケースでFilebeatはログファイルの読み取りに使用されていますが、実はあらゆるノンバイナリファイル形式をサポートしています。

続いて、もうひとつの記事に記載されているGGFってなんだろうと思い、調べてみると、`Generic Game Format` の略語であり、オセロ以外のゲームの棋譜も記述できるようです。GGF形式のドキュメントを見る限り、囲碁、チェス、チェッカー、アマゾン？でも使えそうです。データの例は、こんな感じで扱いやすそうです。（ただし、実際のデータは改行されず1行で書かれます。）[サンプルデータ](https://www.skatgame.net/mburo/ggs/game-archive/Othello/?C=N;O=A)も豊富にありましたので、ターゲットは**GGF**に決定です。

```
(;GM[Amazons]PC[GGS/ams]DT[945277754]
PB[amsbot5]PW[Raphael]
RB[1897.01]RW[1874.15]
TI[30:00/00:05/02:00]
TY[10]RE[-3.00]
BO[10
---*--*---
----------
----------
*--------*
----------
----------
O--------O
----------
----------
---O--O---
*]
B[D1-D7-G7/11.50/5.04]W[J7-G4-B4//21.42]
B[G1-E3-E10/8.30/5.01]W[G10-I8-I5//14.01]
B[A4-A6-G6/9.00/5.01]W[A7-D4-A7//8.03]
B[A6-C8-H8/10.60/5.01]W[G4-I4-J5//7.42]
B[D7-F9-D9/12.20/5.01]W[D10-F8-E8//10.12]
B[J4-H2-H4/10.80/5.02]W[I4-G2-G3//6.24]
B[C8-C6-D5/10.00/5.01]W[F8-F5-F8//15.82]
B[F9-H9-E6/9.60/5.02]W[D4-C5-B5//12.27]
B[C6-C7-B7/13.60/5.01]W[C5-C6-D6//8.54]
B[E3-E2-C4/8.10/5.02]W[F5-D3-D2//4.29]
B[H2-I2-H1/8.30/5.01]W[G2-H3-G2//6.33]
B[E2-E3-I7/6.80/5.02]W[H3-F5-H3//10.15]
B[I2-J3-I3/8.00/5.03]W[F5-H5-I4//4.56]
B[E3-E5-G5/6.30/5.02]W[D3-E4-D4//10.25]
B[E5-F4-G4/10.60/5.01]W[E4-E3-F3//2.99]
B[F4-E4-C2/2.30/5.57]W[E3-D3-E3//3.94]
B[J3-I2-I1/2.10/5.01]W[D3-A3-D3//19.68]
B[C7-D7-C7/2.00/5.01]W[H5-H7-F9//35.67]
B[H9-I9-H10/0.50/5.01]W[H7-G8-H9//13.11]
B[I9-J8-I9/2.00/5.01]W[G8-F7-F5//34.53]
B[D7-D8-D7/-1.50/5.09]W[I8-J7-I8//40.46]
B[D8-F10-D8/1.00/5.01]W[F7-G8-G9//7.21]
B[E4-E5-E4/-3.00/5.01]W[G8-F7-F6//73.36]
B[I2-H2-G1/-3.00/5.01]
;)
```

## 使用するデータ
データは[サンプルデータ](https://www.skatgame.net/mburo/ggs/game-archive/Othello/?C=N;O=A)の最新のファイルを使います。解凍すると拡張子がついていないため、ggfを付けました。データ量が多いため、2018年以前のデータは削除し、`Othello.latest.294420.ggf`としました。


## 構成とデータの流れ

Elastic-stackの構成とデータの流れは以下になります。
1. ggfデータをFilebeatで取り込み、Logstashに１行づつ転送
2. Logstashで１行分のデータを加工
3. Elasticsearchにデータを取り込む
4. Kibanaで可視化

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/451049/ef89aa76-6908-abb2-cd49-5ac6993a2c03.png)

## 実際のログからパースの方法を考える
実際のログを見てみると、

* 頭に数字がついており、1行に記載されている試合数を示しているようです。（；から；）までが１試合分であり、２と書いてあれば２試合分が1行となっています。（泣いてる顔文字にしか見えない）
* [　]で囲まれた部分が取り出したい情報となっています。取り出したい情報には、`GGS/os`のように単純な英数字だけでなく記号のスラッシュが含まれています。
* タイムスタンプに使う時間は、`DT[2018.03.03_03:45:03.MST]`のように年.月.日_時:分:秒.タイムゾーンの形式になっています。

```
1 (;GM[Othello]PC[GGS/os]DT[2018.03.03_03:45:03.MST]PB[rho]PW[Saio7000]RB[2351.94]RW[2345.59]TI[05:00//02:00]TY[8]RE[+0.000]BO[8 -------- -------- -------- ---O*--- ---*O--- -------- -------- -------- *]B[C4//0.01]W[e3/0.64/0.01]B[F6//0.02]W[e6/0.65/0.01]B[F5//0.03]W[c5/0.66/0.01]B[F4//0.01]W[g6/0.69/0.01]B[F7//0.02]W[g5/0.71/0.01]B[D6//0.03]W[d3/0.71/0.01]B[F3//0.03]W[b5/0.68/0.01]B[C3//0.02]W[g3/0.72/0.01]B[B3//0.03]W[b4/0.74/0.01]B[C6//0.02]W[e2/0.76/0.01]B[C2//0.60]W[d2/0.81/0.01]B[H6//0.01]W[f2/0.89/0.01]B[H5//0.01]W[b6/0.90/0.01]B[A6//0.02]W[c7/0.92/0.01]B[D8//0.03]W[c8/0.92/0.01]B[B8//0.02]W[a5/0.96/0.01]B[A4//0.02]W[e7//0.01]B[G4//0.01]W[h4//5.26]B[E8//0.01]W[h7//0.01]B[D7//0.01]W[b7//0.01]B[H2//0.01]W[c1//0.01]B[F1//0.01]W[d1//0.01]B[E1//0.01]W[g1//0.01]B[H3//0.01]W[h1//0.01]B[G2//0.01]W[a2//0.01]B[A8//0.01]W[a7//0.01]B[A3//0.01]W[b2//0.01]B[B1//0.01]W[a1//0.01]B[G7//0.01]W[f8//0.01]B[G8//0.01]W[h8//0.01];)
2 (;GM[Othello]PC[GGS/os]DT[2018.03.03_04:04:54.MST]PB[rho]PW[Saio7000]RB[2649.71]RW[2731.86]TI[05:00//02:00]TY[s8r18]RE[-18.000]BO[8 -------- --O--*-- -OOO*--- OOO****- -*O**--- ---*---- -------- -------- *]B[E2/-16.47/43.89]W[a5/16.00/48.24]B[C1/-20.00/18.63]W[d2/16.00/29.86]B[D1/-20.00/32.02]W[f3/16.00/25.66]B[B6/-20.00/55.01]W[c7/20.00/127.31]B[A6/-20.00/27.79]W[a7/20.00/17.32]B[A2/-20.00/22.22]W[h5/18.00/13.40]B[B2/-18.00/17.66]W[g5/18.00/6.32]B[F5/-18.00/1.16]W[c6/18.00/4.35]B[H3/-18.00/0.91]W[e7/18.00/4.26]B[D7/-18.00/0.76]W[a3/18.00/6.26]B[A8/-18.00/1.16]W[a1/18.00/0.01]B[B8/-18.00/1.01]W[b1/18.00/0.01]B[F7/-18.00/0.64]W[e6/18.00/0.01]B[B7/-18.00/0.02]W[h4/18.00/0.01]B[F6/-18.00/0.02]W[g3/18.00/0.01]B[H6/-18.00/0.02]W[g6/18.00/0.01]B[H2/-18.00/0.02]W[g1/18.00/0.01]B[G7/-18.00/0.02]W[f8/18.00/0.01]B[D8/-18.00/0.02]W[h8/18.00/0.01]B[G2/-18.00/0.02]W[h7/18.00/0.01]B[G8/-18.00/0.02]W[e1/18.00/0.01]B[F1/-18.00/0.02]W[h1/18.00/0.01]B[E8/-18.00/0.02]W[c8/18.00/0.01];)(;GM[Othello]PC[GGS/os]DT[2018.03.03_04:04:54.MST]PB[Saio7000]PW[rho]RB[2731.86]RW[2649.71]TI[05:00//02:00]TY[s8r18]RE[-22.000]BO[8 -------- --O--*-- -OOO*--- OOO****- -*O**--- ---*---- -------- -------- *]B[e2/-16.00/41.56]W[A5/20.00/71.41]B[c1/-16.00/30.59]W[D2/20.00/26.85]B[d1/-16.00/26.77]W[F3/20.00/53.86]B[a6/-16.00/23.96]W[A7/20.00/45.09]B[a2/-18.00/20.41]W[B6/20.00/37.31]B[c6/-18.00/17.39]W[H5/22.00/27.02]B[b2/-20.00/86.28]W[G5/22.00/13.80]B[g3/-20.00/9.24]W[F1/22.00/4.79]B[e1/-22.00/19.24]W[A3/22.00/1.27]B[a8/-22.00/4.40]W[A1/22.00/1.38]B[h4/-22.00/0.01]W[B1/22.00/2.04]B[f6/-22.00/0.01]W[H3/22.00/1.66]B[g2/-22.00/0.01]W[G1/22.00/1.13]B[c7/-22.00/0.01]W[E6/22.00/0.02]B[f7/-22.00/0.01]W[H1/22.00/0.02]B[h2/-22.00/0.01]W[F5/22.00/0.03]B[g6/-22.00/0.01]W[H6/22.00/0.02]B[h7/-22.00/0.01]W[H8/22.00/0.02]B[pass]W[F8/22.00/0.02]B[g7/-22.00/0.01]W[G8/22.00/0.02]B[pass]W[E7/22.00/0.02]B[d7/-22.00/0.01]W[D8/22.00/0.02]B[c8/-22.00/0.01]W[E8/22.00/0.02]B[b7/-22.00/0.01]W[B8/22.00/0.01];)
```

## 1行に複数試合が含まれるものを分割する
Filebeatで受け取った時に、分割することをまず最初に考えますが、Filebeatは複数行を1行にまとめることはできますが分割することはできません。そこで、Logstashに転送した後に、分割します。分割方法は、`；）`で区切り、配列にした後、splitを使って分割します。

```yml
  mutate {
    split => { "message" => ";)" }
  }
  split {
    field => "message"
  }
```

## [　]で囲まれた部分の情報を取り出す
今回はgrokフィルタを使って取り出してみます。[grok-pattern](https://grokdebug.herokuapp.com/patterns#)の一覧には、[ ]の中身を上手く取り出すことができる正規表現がありません。そこでカスタムパターンを作ります。　]では無い連続したデータを対象の文字列としました。

```
CUSTOM_WORD [^\]]+
```

作成したカスタムパターンは以下のように使用します。

```yml
  grok {
    patterns_dir => ["/opt/logstash/extra_patterns"]
    match => { "message" => "GM[\[]%{CUSTOM_WORD:game}[\]]PC[\[]%{CUSTOM_WORD:place}[\]]" }
  }
```

## ログの時間をパースして、timestampに利用する
ログに含まれる時間も、標準のパターンには無いため、カスタムパターンを用意します。

```
CUSTOM_DATE %{YEAR}[\.]%{MONTHNUM}[\.]%{MONTHDAY}
CUSTOM_TIMESTAMP %{CUSTOM_DATE}[\_]%{TIME}
```

作成したカスタムパターンは以下のように使用します。

```yml
  grok {
    patterns_dir => ["/opt/logstash/extra_patterns"]
    match => { "message" => "DT[\[]%{CUSTOM_TIMESTAMP:custom_timestamp}[\.]%{WORD:custom_timezone}[\]] }
  }
```

取り出したcustom_timestampフィールドは文字列なのでdateフィルタでdate型に変換し、timestampに上書きします。timezoneは上手く設定する方法がわかりませんでした、、、

```yml
  date {
    match => ["custom_timestamp", "yyyy.MM.dd_HH:mm:ss"]
    target => "@timestamp"
     #TODO: fix timezone
    timezone => "Asia/Tokyo"
  }
```

## 最後に
blacks_ratingとwhites_ratingを文字列からfloat型に変換して完成です。

```yml
  mutate {
    convert => {
      "blacks_rating" => "float"
      "whites_rating" => "float"
    }
  }
```

一応、ElasticsearchのID重複エラーの対策で、以下を実施しています。
[Elasticsearchでのイベントベースのデータ重複を効果的に防止](https://www.elastic.co/jp/blog/efficient-duplicate-prevention-for-event-based-data-in-elasticsearch)

Advent Calender 2020はまだまだ続きますが、いろんな方の記事を楽しみに待っています。