Elasticsearch でユニークな配列データが欲しいと思ったことはありませんか? 僕はめちゃくちゃあります。どうやって実現するか色々悩んで右往左往したので記事にして残します。もし、もっと良い方法を知っているよという方がいれば @zaru まで教えてください。ありがとうございます。

// OK
labels: [1, 2, 3]

// NG
labels: [1, 2, 3, 3]

最初の案、Object で管理する

最初に考えたのは Object Type を使って key-value を同一の値にすればいいのでは?というものでした。

labels: {
  "1" : 1,
  "2" : 2,
  "3" : 3,
}

これであれば同一 Key で重複せずに管理ができます。

Dynamic を有効にすることで任意の値を Key にすることができます。

{
   "properties" : {
    "labels" : { "type" : "object", "dynamic": true }
   }
}

Upsert 処理はこのような感じになります。

{
  "script": {
    "lang": "painless",
    "soruce" "for (label in params.labels.entrySet() ) {
                ctx._source.labels[label.getKey()] = label.getValue();
              }",
    "params": {
    	"labels": { "1": 1, "2", 2 }
    }
}

これでユニーク性をデータ構造で担保するという目的は達成できたのですが、値の種類が増えるほどフィールド数が増えていくという致命的な弱点がありました。例えば 1,000 個のユニークなラベルを保存すると 1,000 個フィールド定義が増えるということになります。

Elasticsearch ではデフォルトでインデックスに作成できるフィールド数は 1,000 個です。それを超えると Limit of total fields [1000] in index [test_index] has been exceeded というエラーが出ます。

この制限自体を拡張することは可能ですが、あまり推奨されたものではないので避けたほうがいいでしょう。

PUT index_name/_settings
{
  "index.mapping.total_fields.limit": 3000
}

次の案、素直に普通にやる

Object Type ではフィールド数の制限に引っかかるので、素直に普通のやり方にしました。Elasticsearch では Array Type というものはなく、普通のフィールドが配列として値を保持することができます。

{
   "properties" : {
    "labels" : { "type" : "long" }
   }
}

この定義で以下のようにデータを保持できます。しかし当然ながらユニーク性は担保されていないので重複する状態が発生し得ます。毎回完璧なデータで Upsert できればいいのですが、部分 Upsert をしたくなるケースでは、差分があるのか事前に調べなければなりません。

それが面倒なので、今回はスクリプトでユニークな状態にして再代入するという形にしました。具体的には以下のスクリプトで Upsert 処理をしています。

{
  "script": {
    "lang": "painless",
    "soruce" "ctx._source.labels.addAll(params.labels);
              /* 重複 ID を除外するために改めてユニークなものにして、代入している */
              ctx._source.labels = ctx._source.labels.stream().distinct().sorted().collect(Collectors.toList());",
    "params": {
    	"labels": [1, 2]
    }
}

こうすることで、ユニーク性を担保しつつフィールド数が増えないで実現することができました。課題としてはデータ構造でユニーク性を担保しているわけではないので、アプリケーション側で対応が漏れると重複が発生してしまうという点です。何かいい方法があれば…いいなぁ。