やりたいこと
["a","b","c"]
や[1,2,3]
といった配列を扱うフィールドをインデックスに用意したい
話を進める前に公式ドキュメントを読む
配列に関してはこちらの公式ドキュメントに記載がある
まずそちらの内容を確認する
integerやkeywordで定義されたフィールドには複数の値を含めることができる
Elasticsearchにおいてはarray
という特定の型は用意されていないが、integerやkeywordで定義されたフィールドに複数の要素を投入することができる
例えば["a","b","c"]
という配列はもちろん[{"id": 1, "name": "Mary"},{"id": 2 "name": "Juan"}]
といったオブジェクトの配列を入れることもできる
ただし以下のような注意点がある
- 配列内のデータ型は一意に定まっていないといけない(例: 数値と文字列を混合した[1,"sample"]みたいな配列の投入は不可)
- オブジェクト配列の場合想定通りの挙動にならないことが多いため、基本的にはnestedの使用が推奨される
- 空の配列はmissing fieldとして扱われる
なぜintegerやkeywordで定義された型に複数の値を入れることができるのか
同じドキュメントの下部で説明されている1
簡潔に述べると以下の通り
- Luceneが文章中の単語を転置インデックスに登録する都合上、単純なtext fieldであっても複数の値が格納されることをデフォルトでサポートしていた
- 後から他のデータ型を追加する際もその構成が引き継がれた
ここからやること(これが本題)
投入できることはわかったので、ここからは実際の検索がどうなるかを小さいサンプルを用いて確認していく
①数値型(integer)、②文字列型(keyword)のそれぞれでみていく
オブジェクト型の検証は時間の都合上今回の対象から外している
integer
integerで定義された型に配列を入れる場合とnestedの子に定義されたフィールドに値を入れる場合の2通りで挙動の違いを調べる
ここで確認したいことは以下の通り
- 配列(およびnested内の配列/以下同じ)にある値以上の要素を含むドキュメントの検索
- 配列内の要素がN個以上であるドキュメントの検索
- 配列が空orNULLであるドキュメントの検索
以下のような二通りのインデックスを作成して調査をする
# 下準備
PUT /integer-index
{
"settings": {
"index.number_of_shards": 1,
"index.number_of_replicas": 1,
"index.write.wait_for_active_shards": 1
},
"mappings": {
"contents": {
"properties": {
"id": {
"type": "integer"
},
"array_field": {
"type": "integer"
}
}
}
}
}
PUT /integer-index-nested
{
"settings": {
"index.number_of_shards": 1,
"index.number_of_replicas": 1,
"index.write.wait_for_active_shards": 1
},
"mappings": {
"contents": {
"properties": {
"id": {
"type": "integer"
},
"nested_field": {
"type": "nested",
"properties": {
"array_field": {
"type": "integer"
}
}
}
}
}
}
}
投入するデータは以下の通り
integerの分
POST /integer-index/_doc
{"id": 1, "array_field": [100,200,300]}
POST /integer-index/_doc
{"id": 2, "array_field": [200,300,400]}
POST /integer-index/_doc
{"id": 3, "array_field": [300,400,500]}
POST /integer-index/_doc
{"id": 4, "array_field": [400,500,600]}
POST /integer-index/_doc
{"id": 5, "array_field": [500,600,700]}
POST /integer-index/_doc
{"id": 6, "array_field": [600,700,800]}
POST /integer-index/_doc
{"id": 7, "array_field": [200,200,400]}
POST /integer-index/_doc
{"id": 8, "array_field": [500,500,600]}
POST /integer-index/_doc
{"id": 9, "array_field": [200,300,400,500]}
POST /integer-index/_doc
{"id": 10, "array_field": [300,300,400,500]}
POST /integer-index/_doc
{"id": 11, "array_field": []}
POST /integer-index/_doc
{"id": 12, "array_field": null}
POST /integer-index/_doc
{"id": 13}
nestedの分
POST /integer-index-nested/_doc
{"id": 1, "nested_field": [{"array_field": 100},{"array_field": 200},{"array_field": 300}]}
POST /integer-index-nested/_doc
{"id": 2, "nested_field": [{"array_field": 200},{"array_field": 300},{"array_field": 400}]}
POST /integer-index-nested/_doc
{"id": 3, "nested_field": [{"array_field": 300},{"array_field": 400},{"array_field": 500}]}
POST /integer-index-nested/_doc
{"id": 4, "nested_field": [{"array_field": 400},{"array_field": 500},{"array_field": 600}]}
POST /integer-index-nested/_doc
{"id": 5, "nested_field": [{"array_field": 500},{"array_field": 600},{"array_field": 700}]}
POST /integer-index-nested/_doc
{"id": 6, "nested_field": [{"array_field": 600},{"array_field": 700},{"array_field": 800}]}
POST /integer-index-nested/_doc
{"id": 7, "nested_field": [{"array_field": 200},{"array_field": 200},{"array_field": 400}]}
POST /integer-index-nested/_doc
{"id": 8, "nested_field": [{"array_field": 500},{"array_field": 500},{"array_field": 600}]}
POST /integer-index-nested/_doc
{"id": 9, "nested_field": [{"array_field": 200},{"array_field": 300},{"array_field": 400},{"array_field": 500}]}
POST /integer-index-nested/_doc
{"id": 10, "nested_field": [{"array_field": 300},{"array_field": 300},{"array_field": 400},{"array_field": 500}]}
POST /integer-index-nested/_doc
{"id": 11, "nested_field": []}
POST /integer-index-nested/_doc
{"id": 12, "nested_field": [{"array_field": null}]}
POST /integer-index-nested/_doc
{"id": 13}
ある値以上の要素をもつドキュメントを検索
例えば「配列には購入した商品の価格が入る」「配列にはテストの点数が入る」みたいな使い方をしている場合に「XXXX円以上の商品を購入した履歴を抽出」「XX点未満の得点を持っている受験者を抽出」みたいなことをする場合に使う
「配列内のMin/MaxがXX以上」みたいな検索もこの方法でできそう
integer型の場合
GET /integer-index/_search
{
"query": {
"bool": {
"must": [
{
"range": {
"array_field": {
"gte": 600
}
}
}
]
}
}
}
検索結果
{
"took" : 1,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : 4,
"max_score" : 1.0,
"hits" : [
{
"_index" : "integer-index",
"_type" : "contents",
"_id" : "CMGrsX8B2mat8b4UZq1t",
"_score" : 1.0,
"_source" : {
"id" : 4,
"array_field" : [
400,
500,
600
]
}
},
{
"_index" : "integer-index",
"_type" : "contents",
"_id" : "CcGrsX8B2mat8b4UZq2W",
"_score" : 1.0,
"_source" : {
"id" : 5,
"array_field" : [
500,
600,
700
]
}
},
{
"_index" : "integer-index",
"_type" : "contents",
"_id" : "CsGrsX8B2mat8b4UZq2z",
"_score" : 1.0,
"_source" : {
"id" : 6,
"array_field" : [
600,
700,
800
]
}
},
{
"_index" : "integer-index",
"_type" : "contents",
"_id" : "DMGrsX8B2mat8b4UZq3Z",
"_score" : 1.0,
"_source" : {
"id" : 8,
"array_field" : [
500,
500,
600
]
}
}
]
}
}
nestedの場合
GET /integer-index-nested/_search
{
"query": {
"nested": {
"path": "nested_field",
"query": {
"bool": {
"must": [
{
"range": {
"nested_field.array_field": {
"gte": 600
}
}
}
]
}
}
}
}
}
検索結果
{
"took" : 1,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : 4,
"max_score" : 1.0,
"hits" : [
{
"_index" : "integer-index-nested",
"_type" : "contents",
"_id" : "FcGrsX8B2mat8b4U-q2j",
"_score" : 1.0,
"_source" : {
"id" : 4,
"nested_field" : [
{
"array_field" : 400
},
{
"array_field" : 500
},
{
"array_field" : 600
}
]
}
},
{
"_index" : "integer-index-nested",
"_type" : "contents",
"_id" : "FsGrsX8B2mat8b4U-q3F",
"_score" : 1.0,
"_source" : {
"id" : 5,
"nested_field" : [
{
"array_field" : 500
},
{
"array_field" : 600
},
{
"array_field" : 700
}
]
}
},
{
"_index" : "integer-index-nested",
"_type" : "contents",
"_id" : "F8GrsX8B2mat8b4U-q3d",
"_score" : 1.0,
"_source" : {
"id" : 6,
"nested_field" : [
{
"array_field" : 600
},
{
"array_field" : 700
},
{
"array_field" : 800
}
]
}
},
{
"_index" : "integer-index-nested",
"_type" : "contents",
"_id" : "GcGrsX8B2mat8b4U-60C",
"_score" : 1.0,
"_source" : {
"id" : 8,
"nested_field" : [
{
"array_field" : 500
},
{
"array_field" : 500
},
{
"array_field" : 600
}
]
}
}
]
}
}
配列内の要素がN個以上であるドキュメントを検索
この検索はstringやobjectの配列であっても同じことができそう
ECにおいて各ユーザが一度に購入した商品のIDを配列に持っていた場合に「N点以上購入したユーザ(購買行動)」を抽出するような場合に使えそう(Elasticsearchでこのような検索をするかはわからないが)
integerの場合
GET /integer-index/_search
{
"query": {
"bool": {
"must": [
{
"bool": {
"filter": {
"script": {
"script": "doc['array_field'].length > 3"
}
}
}
}
]
}
}
}
検索結果
{
"took" : 29,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : 2,
"max_score" : 0.0,
"hits" : [
{
"_index" : "integer-index",
"_type" : "contents",
"_id" : "DcGrsX8B2mat8b4UZq3t",
"_score" : 0.0,
"_source" : {
"id" : 9,
"array_field" : [
200,
300,
400,
500
]
}
},
{
"_index" : "integer-index",
"_type" : "contents",
"_id" : "DsGrsX8B2mat8b4UZq3-",
"_score" : 0.0,
"_source" : {
"id" : 10,
"array_field" : [
300,
300,
400,
500
]
}
}
]
}
}
nestedの場合→うまい方法が見つからない
配列が空orNULLであるドキュメントの検索
Elasticsearchにおいてnullと空の配列は同じ扱いになっている(参考)
配列内の要素がゼロの場合とNULLのケース両方で検索する
integerの場合①(配列内の要素が空の場合)
GET /integer-index/_search
{
"query": {
"bool": {
"must": [
{
"bool": {
"filter": {
"script": {
"script": "doc['array_field'].length < 1"
}
}
}
},
{
"bool"
}
]
}
}
}
integerの場合①(フィールドがNULLの場合)
GET /integer-index/_search
{
"query": {
"bool": {
"must_not": [
{
"bool": {
"filter": {
"exists": {
"field": "array_field"
}
}
}
}
]
}
}
}
検索結果
{
"took" : 1,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : 3,
"max_score" : 1.0,
"hits" : [
{
"_index" : "integer-index",
"_type" : "contents",
"_id" : "D8GrsX8B2mat8b4UZ60O",
"_score" : 1.0,
"_source" : {
"id" : 11,
"array_field" : [ ]
}
},
{
"_index" : "integer-index",
"_type" : "contents",
"_id" : "EMGrsX8B2mat8b4UZ60f",
"_score" : 1.0,
"_source" : {
"id" : 12,
"array_field" : null
}
},
{
"_index" : "integer-index",
"_type" : "contents",
"_id" : "EcGrsX8B2mat8b4UZ60v",
"_score" : 1.0,
"_source" : {
"id" : 13
}
}
]
}
}
nestedの場合
lengthを使った検索は先程の項目の通り見つかっていない
nullに対する検索は以下の通り(こちらの回答を用いた)
GET /integer-index-nested/_search
{
"query": {
"bool": {
"must_not": [
{
"nested": {
"path": "nested_field",
"query": {
"bool": {
"filter": {
"exists": {
"field": "nested_field"
}
}
}
}
}
}
]
}
}
}
{
"took" : 2,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : 3,
"max_score" : 1.0,
"hits" : [
{
"_index" : "integer-index-nested",
"_type" : "contents",
"_id" : "KcGvsX8B2mat8b4Ugq26",
"_score" : 1.0,
"_source" : {
"id" : 11,
"nested_field" : [ ]
}
},
{
"_index" : "integer-index-nested",
"_type" : "contents",
"_id" : "KsGvsX8B2mat8b4Ugq3K",
"_score" : 1.0,
"_source" : {
"id" : 12,
"nested_field" : [
{
"array_field" : null
}
]
}
},
{
"_index" : "integer-index-nested",
"_type" : "contents",
"_id" : "K8GvsX8B2mat8b4Ugq3a",
"_score" : 1.0,
"_source" : {
"id" : 13
}
}
]
}
}
先程のクエリはnested
やmust_not
の順番をいれかえると思わぬ結果になる
id=13はnested_field.array_fieldどころかnested_field自体がないので引っかからなそうではあるが(それでも引っかかりそうではあるが)、id=11は引っかかってもおかしくない気がする
GET /integer-index-nested/_search
{
"query": {
"nested": {
"path": "nested_field",
"query": {
"bool": {
"must_not": [
{
"exists": {
"field": "nested_field.array_field"
}
}
]
}
}
}
}
}
検索結果
{
"took" : 1,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : 1,
"max_score" : 1.0,
"hits" : [
{
"_index" : "integer-index-nested",
"_type" : "contents",
"_id" : "KsGvsX8B2mat8b4Ugq3K",
"_score" : 1.0,
"_source" : {
"id" : 12,
"nested_field" : [
{
"array_field" : null
}
]
}
}
]
}
}
文字列
integerの場合と同様の手順で2通りの場合の検証をする
手間と目的の都合上今回はkeywordのみの検証でnormalize等は特に行っていない
ここで確認したいことは以下の通り
- 配列にある値の要素を含むドキュメントの検索
- 配列内の要素が[A,B,C]であるドキュメントの検索(完全一致の検索)
以下のような二通りのインデックスを作成して調査をする
# 下準備
PUT /keyword-index
{
"settings": {
"index.number_of_shards": 1,
"index.number_of_replicas": 1,
"index.write.wait_for_active_shards": 1
},
"mappings": {
"contents": {
"properties": {
"id": {
"type": "integer"
},
"array_field": {
"type": "keyword"
}
}
}
}
}
PUT /keyword-index-nested
{
"settings": {
"index.number_of_shards": 1,
"index.number_of_replicas": 1,
"index.write.wait_for_active_shards": 1
},
"mappings": {
"contents": {
"properties": {
"id": {
"type": "integer"
},
"nested_field": {
"type": "nested",
"properties": {
"array_field": {
"type": "keyword"
}
}
}
}
}
}
}
投入するデータは以下の通り
keywordの分
POST /keyword-index/_doc
{"id": 1, "array_field": ["w6o","qbj","Aof"]}
POST /keyword-index/_doc
{"id": 2, "array_field": ["qbj","Aof","SYH"]}
POST /keyword-index/_doc
{"id": 3, "array_field": ["Aof","SYH","srN"]}
POST /keyword-index/_doc
{"id": 4, "array_field": ["SYH","srN","ebA"]}
POST /keyword-index/_doc
{"id": 5, "array_field": ["srN","ebA","hrp"]}
POST /keyword-index/_doc
{"id": 6, "array_field": ["ebA","hrp","PKD"]}
POST /keyword-index/_doc
{"id": 7, "array_field": ["qbj","qbj","SYH"]}
POST /keyword-index/_doc
{"id": 8, "array_field": ["srN","srN","ebA"]}
POST /keyword-index/_doc
{"id": 9, "array_field": ["qbj","Aof","SYH","srN"]}
POST /keyword-index/_doc
{"id": 10, "array_field": ["Aof","Aof","SYH","srN"]}
POST /keyword-index/_doc
{"id": 11, "array_field": []}
POST /keyword-index/_doc
{"id": 12, "array_field": null}
POST /keyword-index/_doc
{"id": 13}
nestedの分
POST /keyword-index-nested/_doc
{"id": 1, "nested_field": [{"array_field": "w6o"},{"array_field": "qbj"},{"array_field": "Aof"}]}
POST /keyword-index-nested/_doc
{"id": 2, "nested_field": [{"array_field": "qbj"},{"array_field": "Aof"},{"array_field": "SYH"}]}
POST /keyword-index-nested/_doc
{"id": 3, "nested_field": [{"array_field": "Aof"},{"array_field": "SYH"},{"array_field": "srN"}]}
POST /keyword-index-nested/_doc
{"id": 4, "nested_field": [{"array_field": "SYH"},{"array_field": "srN"},{"array_field": "ebA"}]}
POST /keyword-index-nested/_doc
{"id": 5, "nested_field": [{"array_field": "srN"},{"array_field": "ebA"},{"array_field": "hrp"}]}
POST /keyword-index-nested/_doc
{"id": 6, "nested_field": [{"array_field": "ebA"},{"array_field": "hrp"},{"array_field": "PKD"}]}
POST /keyword-index-nested/_doc
{"id": 7, "nested_field": [{"array_field": "qbj"},{"array_field": "qbj"},{"array_field": "SYH"}]}
POST /keyword-index-nested/_doc
{"id": 8, "nested_field": [{"array_field": "srN"},{"array_field": "srN"},{"array_field": "ebA"}]}
POST /keyword-index-nested/_doc
{"id": 9, "nested_field": [{"array_field": "qbj"},{"array_field": "Aof"},{"array_field": "SYH"},{"array_field": "srN"}]}
POST /keyword-index-nested/_doc
{"id": 10, "nested_field": [{"array_field": "Aof"},{"array_field": "Aof"},{"array_field": "SYH"},{"array_field": "srN"}]}
POST /keyword-index-nested/_doc
{"id": 11, "nested_field": []}
POST /keyword-index-nested/_doc
{"id": 12, "nested_field": [{"array_field": null}]}
POST /keyword-index-nested/_doc
{"id": 13}
配列(nested内の配列/以下同じ)にある値の要素を含むドキュメントの検索
keywordの場合
文章検索だからあたりまえの結果になる
GET /keyword-index/_search
{
"query": {
"bool": {
"must": [
{
"term": {
"array_field": {
"value": "Aof"
}
}
}
]
}
}
}
検索結果
{
"took" : 0,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : 5,
"max_score" : 0.6931472,
"hits" : [
{
"_index" : "keyword-index",
"_type" : "contents",
"_id" : "LMHVsX8B2mat8b4UIK3n",
"_score" : 0.6931472,
"_source" : {
"id" : 1,
"array_field" : [
"w6o",
"qbj",
"Aof"
]
}
},
{
"_index" : "keyword-index",
"_type" : "contents",
"_id" : "LcHVsX8B2mat8b4UIK37",
"_score" : 0.6931472,
"_source" : {
"id" : 2,
"array_field" : [
"qbj",
"Aof",
"SYH"
]
}
},
{
"_index" : "keyword-index",
"_type" : "contents",
"_id" : "LsHVsX8B2mat8b4UIa0M",
"_score" : 0.6931472,
"_source" : {
"id" : 3,
"array_field" : [
"Aof",
"SYH",
"srN"
]
}
},
{
"_index" : "keyword-index",
"_type" : "contents",
"_id" : "NMHVsX8B2mat8b4UIa2M",
"_score" : 0.6931472,
"_source" : {
"id" : 9,
"array_field" : [
"qbj",
"Aof",
"SYH",
"srN"
]
}
},
{
"_index" : "keyword-index",
"_type" : "contents",
"_id" : "NcHVsX8B2mat8b4UIa2c",
"_score" : 0.6931472,
"_source" : {
"id" : 10,
"array_field" : [
"Aof",
"Aof",
"SYH",
"srN"
]
}
}
]
}
}
nestedの場合
GET /keyword-index-nested/_search
{
"query": {
"nested": {
"path": "nested_field",
"query": {
"bool": {
"must": [
{
"term": {
"nested_field.array_field": {
"value": "Aof"
}
}
}
]
}
}
}
}
}
配列内の要素が[A,B,C]であるドキュメントの検索(完全一致の検索)
先程はAof
という単語が含まれるドキュメントの検索をしたが、今回はAof
,SYH
,srN
という3つの単語がその要素となっているドキュメントを検索する2
keywordの場合
もっといいやり方ある気がする(がこれ以上時間がとれないので必要になったらまた調べる)
またこのやり方だとnestedには使えないし、完全一致とは言いながらも重複した要素がある場合は期待通りにいっていない(今回はid=3のみを抽出したかった)
GET /keyword-index/_search
{
"query": {
"bool": {
"must": [
{
"term": {
"array_field": {
"value": "Aof"
}
}
},
{
"term": {
"array_field": {
"value": "SYH"
}
}
},
{
"term": {
"array_field": {
"value": "srN"
}
}
},
{
"script": {
"script": "doc['array_field'].length == 3"
}
}
]
}
}
}
検索結果
{
"took" : 1,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : 2,
"max_score" : 2.7453334,
"hits" : [
{
"_index" : "keyword-index",
"_type" : "contents",
"_id" : "Y8H2sX8B2mat8b4U0a1g",
"_score" : 2.7453334,
"_source" : {
"id" : 3,
"array_field" : [
"Aof",
"SYH",
"srN"
]
}
},
{
"_index" : "keyword-index",
"_type" : "contents",
"_id" : "asH2sX8B2mat8b4U0a3h",
"_score" : 2.7453334,
"_source" : {
"id" : 10,
"array_field" : [
"Aof",
"Aof",
"SYH",
"srN"
]
}
}
]
}
}
時間があったらやりたいこと
- 文字列型の検索関係もう少し調べたい
- オブジェクト型の場合の検証をしたい(特にオブジェクト内の要素にアクセスする場合)