Help us understand the problem. What is going on with this article?

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

はじめに

オークファンの新人エンジニアです。現在は、商品の販売履歴を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に直せば環境に左右されません。

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

Jarのプロセスで内部のリソースを呼びたかった

背景

プロジェクトのresources/schema内のelasticsearch-schema.jsonを、JacksonライブラリのObjectMapperで読み込んでマッピングしたい。

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
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
// 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
}

原因と解決策

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

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

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

最後に

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

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

tmot
aucfan
あらゆる商品に関する正確でフェアな情報を提供する企業として、世界における唯一無二の存在となるというビジョンの元、基盤となる技術を作り続けるマザーズベンチャー
https://aucfan.co.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away