新人が Kotlin や Elasticsearch でハマったこと

# はじめに
オークファンの新人エンジニアです。現在は、商品の販売履歴をElasticsearchに保存するタスクを、KotlinとSpring Bootを用いて開発しています。

まだ経験が浅く、様々な箇所で躓いて時間を消費してしまうことが多々あります。今回はアドベントカレンダー参加の機会をいただけたので、そんな私の時間を溶かしてきた内容のうちのいくつかを紹介しようと思います。
こんなことで躓く人がいるんだなっていう温かい気持ちで読み流していただければと思います。もし似たような内容で躓いている方がいましたら、この記事が解決の一助になればこの上ない喜びです。

# Kotlin関連
## ファイルパスのsort結果を環境によらず同じにしたかった
### 背景
ローカルから取ってきたCSVファイルのリストを、ファイルのパスで一意にソートする処理を追加。(どのような順番でもいいので、一意なソート結果になってほしい。)

``` Kotlin
fileList.sortedBy{ it.toPath() }
```

この処理の挙動を確認するため、以下のようなディレクトリ構成でテスト。

```
resources/
┠ 20190101.csv
└ 2019/
     └ 01/ 
        └ 01/
           └ 20190101.csv
```

### 結果
Windows環境とLinux環境でソートの結果が違う。
Windows :
 ["resources/20190101.csv", "resources/2019/01/01/20190101.csv"]
Linux:
 ["resources/2019/01/01/20190101.csv","resources/20190101.csv"]


### 原因と解決策
WindowsとLinuxで、ディレクトリ構成の区切り文字が異なることが原因でした。ASCIIコードでは`/`→`[0-9]`→`\`という順番なので、`resources/2019`の次の文字の順番が変わります。Windowsは区切り文字`\`が`0`よりも後で、Linuxは区切り文字`/`が`0`よりも前です。

パスをURIに直せば環境に左右されません。

```kotlin:Kotlin
fileList.sortedBy{ it.toPath().toUri() }
```

## Jarのプロセスで内部のリソースを呼びたかった
### 背景
プロジェクトの`resources/schema`内の`elasticsearch-schema.json`を、JacksonライブラリのObjectMapperで読み込んでマッピングしたい。

``` kotlin:Kotlin
val schemaFile = ClassPathResource("schema/elasticsearch-schema.json").file
val schema = objectMapper.readTree(schemaFile)[type].toString()
...
```

単体テストも通ったため、パッケージのjarをテスト用サーバーに配置して実行してみた。

### 結果
```
java.io.FileNotFoundException: class path resource
```

### 原因と解決策
`resource.getFile()`は実行環境のファイルシステムに依存するため、別の環境でjarの実行をしてもリソースを利用できないそうです。
FileではなくInputStreamにすることで、リソースの配置場所によらず参照できます。 [^1]

``` kotlin:Kotlin
val schema = ClassPathResource("schema/elasticsearch-schema.json").inputStream.use {
    val schema = objectMapper.readTree(schemaFile)[type].toString()
    ...
}
```

## MavenのJUnitテストに新しいテストクラスを追加したかった
### 背景
`SaveService`クラスをテストするための`SaveServiceTest`というテストクラスと、
DI対象のクラスの一部メソッドをmock化してテストする`SaveServiceTestWithMock`
の2種類のクラスを作った。
一通りできたのでMavenプロジェクトのJUnitテストを走らせてみよう。

### 結果
`SaveServiceTestWithMock`のテストが実行されていない！！

### 原因と解決策
Maven Surefireプラグインがテストクラスとして認識してくれるのは`Test*` `*Test ` `*Tests` `*TestCase`という名前のものだけ[^2]！！

今回はクラス名を変更して対応しましたが、`pom.xml`に設定を追加すればマッチするパターンを変更できるようです。

こんな基本的なミスをしてしまった(上に結構な時間悩むことになった)なんて自分でも驚きです。

ちなみに、私が携わっているプロジェクトは歴史が比較的長いためMavenを使用していますが、会社の最近のプロジェクトはGradleを使用しています。

# Elasticsearch関連
## レコード保存後すぐに検索を実行したかった
### 背景
Elasticsearchに商品の売上データを保存する際に、それと同一のJANの商品で同じ日付のレコードがすでにElasticsearchに保存されているかどうかを検索し、すでにある場合は上書きして`updated`のフラグがつけるという処理を行う。

JANと日付が重複するデータを含むファイルを保存する単体テストによって上記の挙動を確認できたため、pushしてプルリク。

### 結果
CIの単体テスト失敗。さらに他の人のローカル環境でも単体テスト実行してもらったが同じく失敗。JANと日付が重複するデータが上書きされていなかった。

### 原因と解決策
保存のリクエストがElasticsearchに反映されるまでにタイムラグがあり、前のデータの保存処理がElasticsearchに反映される前に次のデータについての重複レコードの検索が走ってしまっていたようです。そのため重複しているはずのレコードが検索にヒットせず、別のレコードとして保存されてしまっていました。

自分の環境で成功していたのは、~~私のPCのスペックの問題で、~~ たまたま次のデータの重複レコード検索処理までにElasticsearch側の反映が間に合っていたからだと考えられます。(デフォルトではrefreshの間隔は1秒)

プログラムの都合上、refreshのインターバルよりも早く検索を行う必要があるので、保存のリクエストを送った後に強制的にrefreshさせるようにしました。

``` kotlin:Kotlin
// client: RestHighLevelClient
client.bulk(request)
// 強制的にrefresh
client.lowLevelClient.performRequest("POST", "/_refresh")
```

## テキストフィールドをAggregationしたかった
### 背景
Elasticsearchに保存したデータをAggregationによって集計してみます。とりあえず同一の商品名(`name`プロパティ)をもつもの同士でグループ分けを試してみようかと思い、Kibanaのコンソールをたたきました。

```
GET _search
{
    "name_aggregation" : {
        "terms" : {
            "field" : "name"
        }
    }
}
```

### 結果
```
{
  "error": {
    "root_cause": [
      {
        "type": "illegal_argument_exception",
        "reason": "Fielddata is disabled on text fields by default. Set fielddata=true on [itemInfo.name] in order to load fielddata in memory by uninverting the inverted index. Note that this can however use significant memory. Alternatively use a keyword field instead."
      }
    ],
  .
  .
  .
  "status": 400
}
```

### 原因と解決策
私のプロジェクトでは、商品名`name`を`text`型でElasticsearchに保存するようにしていましたが、`keyword`型でないとAggregationに使えないようです。`text`型はAnalyzerで単語に分割されて保存され、元の語句が保持されないためとのことです。

`text`型の形態素解析による検索と`keyword`型の完全一致検索を両立させたい場合、マルチフィールド型として保存する必要があります。
ちなみに、マッピング定義を指定せずに動的マッピングをすると、文字列は自動的に`text`と`keyword`のマルチフィールドになります。

この問題は、reasonがエラー応答に記載されている上、本[^3]にも説明があったので、すぐに解決しました。


# 最後に
もっといろいろ躓いたところがあるような気がしますが、Slackの自分のチャンネルに残していたメモから拾ってきた限りで挙げてみました。躓いたところをその都度ドキュメントに書き起こしておけばよかったなぁ(定期的に復習できるし、将来新人教育の一環に使えるかもしれない)という後悔もあります。

これからもいろいろな失敗をしていくと思いますが、その失敗を糧にエンジニアとして成長していきたいです。涙の数だけ強くなれると思っています。

[^1]: [Classpath resource not found when running as jar - [Stack Overflow]](https://stackoverflow.com/questions/25869428/classpath-resource-not-found-when-running-as-jar)
[^2]: [JUnit 5 ユーザーガイド](https://oohira.github.io/junit5-doc-jp/user-guide/#running-tests-build-maven)
[^3]: [Elasticsearch実践ガイド (惣道 哲也　著) - [インプレス]](https://book.impress.co.jp/books/1117101041)