はじめに
オークファンの新人エンジニアです。現在は、商品の販売履歴をElasticsearchに保存するタスクを、KotlinとSpring Bootを用いて開発しています。
まだ経験が浅く、様々な箇所で躓いて時間を消費してしまうことが多々あります。今回はアドベントカレンダー参加の機会をいただけたので、そんな私の時間を溶かしてきた内容のうちのいくつかを紹介しようと思います。
こんなことで躓く人がいるんだなっていう温かい気持ちで読み流していただければと思います。もし似たような内容で躓いている方がいましたら、この記事が解決の一助になればこの上ない喜びです。
Kotlin関連
ファイルパスのsort結果を環境によらず同じにしたかった
背景
ローカルから取ってきたCSVファイルのリストを、ファイルのパスで一意にソートする処理を追加。(どのような順番でもいいので、一意なソート結果になってほしい。)
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に直せば環境に左右されません。
fileList.sortedBy{ it.toPath().toUri() }
Jarのプロセスで内部のリソースを呼びたかった
背景
プロジェクトのresources/schema
内のelasticsearch-schema.json
を、JacksonライブラリのObjectMapperで読み込んでマッピングしたい。
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
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させるようにしました。
// 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の自分のチャンネルに残していたメモから拾ってきた限りで挙げてみました。躓いたところをその都度ドキュメントに書き起こしておけばよかったなぁ(定期的に復習できるし、将来新人教育の一環に使えるかもしれない)という後悔もあります。
これからもいろいろな失敗をしていくと思いますが、その失敗を糧にエンジニアとして成長していきたいです。涙の数だけ強くなれると思っています。