diff --git a/CHANGELOG.md b/CHANGELOG.md index 9aabbbf75f00c..512ba48941c87 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -91,6 +91,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), - Fix remote shards balance ([#15335](https://github.com/opensearch-project/OpenSearch/pull/15335)) - Always use `constant_score` query for `match_only_text` field ([#16964](https://github.com/opensearch-project/OpenSearch/pull/16964)) - Fix Shallow copy snapshot failures on closed index ([#16868](https://github.com/opensearch-project/OpenSearch/pull/16868)) +- Fix multi-value sort for unsigned long ([#16732](https://github.com/opensearch-project/OpenSearch/pull/16732)) ### Security diff --git a/rest-api-spec/src/main/resources/rest-api-spec/test/search/260_sort_double.yml b/rest-api-spec/src/main/resources/rest-api-spec/test/search/260_sort_double.yml new file mode 100644 index 0000000000000..eccafaf96dd23 --- /dev/null +++ b/rest-api-spec/src/main/resources/rest-api-spec/test/search/260_sort_double.yml @@ -0,0 +1,136 @@ +setup: + - do: + indices.create: + index: double_sort + body: + settings: + number_of_shards: 3 + number_of_replicas: 0 + mappings: + properties: + field: + type: double + +--- +"test sorting against double only fields": + + - do: + bulk: + refresh: true + body: + - '{ "index" : { "_index" : "double_sort", "_id" : "1" } }' + - '{"field" : [ 900719925474099.1, 1.1 ] }' + - '{ "index" : { "_index" : "double_sort", "_id" : "2" } }' + - '{"field" : [ 900719925474099.2, 900719925474099.3 ] }' + - '{ "index" : { "_index" : "double_sort", "_id" : "3" } }' + - '{"field" : [ 450359962737049.4, 3.5, 4.6 ] }' + - '{ "index" : { "_index" : "double_sort", "_id" : "4" } }' + - '{"field" : [ 450359962737049.7, 5.8, -1.9, -2.0 ] }' + + - do: + search: + index: double_sort + body: + size: 5 + sort: [{ field: { mode: max, order: desc } } ] + - match: {hits.total.value: 4 } + - length: {hits.hits: 4 } + - match: { hits.hits.0._index: double_sort } + - match: { hits.hits.0._source.field: [ 900719925474099.2, 900719925474099.2 ] } + - match: { hits.hits.0.sort.0: 900719925474099.2 } + - match: { hits.hits.1._source.field: [ 900719925474099.1, 1.1 ] } + - match: { hits.hits.1.sort.0: 900719925474099.1 } + - match: { hits.hits.2._source.field: [ 450359962737049.7, 5.8, -1.9, -2.0 ] } + - match: { hits.hits.2.sort.0: 450359962737049.7 } + - match: { hits.hits.3._source.field: [ 450359962737049.4, 3.5, 4.6 ] } + - match: { hits.hits.3.sort.0: 450359962737049.4 } + + - do: + search: + index: double_sort + body: + size: 5 + sort: [ { field: { mode: max, order: asc } } ] + - match: { hits.total.value: 4 } + - length: { hits.hits: 4 } + - match: { hits.hits.0._index: double_sort } + - match: { hits.hits.0._source.field: [ 450359962737049.4, 3.5, 4.6 ] } + - match: { hits.hits.0.sort.0: 450359962737049.4 } + - match: { hits.hits.1._source.field: [ 450359962737049.7, 5.8, -1.9, -2.0 ] } + - match: { hits.hits.1.sort.0: 450359962737049.7 } + - match: { hits.hits.2._source.field: [ 900719925474099.1, 1.1 ] } + - match: { hits.hits.2.sort.0: 900719925474099.1 } + - match: { hits.hits.3._source.field: [ 900719925474099.2, 900719925474099.2 ] } + - match: { hits.hits.3.sort.0: 900719925474099.2 } + + - do: + search: + index: double_sort + body: + size: 5 + sort: [ { field: { mode: min, order: asc } } ] + - match: { hits.total.value: 4 } + - length: { hits.hits: 4 } + - match: { hits.hits.0._index: double_sort } + - match: { hits.hits.0._source.field: [ 450359962737049.7, 5.8, -1.9, -2.0 ] } + - match: { hits.hits.0.sort: [ -2.0 ] } + - match: { hits.hits.1._source.field: [ 900719925474099.1, 1.1 ] } + - match: { hits.hits.1.sort.0: 1.1 } + - match: { hits.hits.2._source.field: [ 450359962737049.4, 3.5, 4.6 ] } + - match: { hits.hits.2.sort.0: 3.5 } + - match: { hits.hits.3._source.field: [ 900719925474099.2, 900719925474099.2 ] } + - match: { hits.hits.3.sort.0: 900719925474099.2 } + + - do: + search: + index: double_sort + body: + size: 5 + sort: [ { field: { mode: median, order: desc } } ] + - match: { hits.total.value: 4 } + - length: { hits.hits: 4 } + - match: { hits.hits.0._index: double_sort } + - match: { hits.hits.0._source.field: [ 900719925474099.2, 900719925474099.2 ] } + - match: { hits.hits.0.sort.0: 900719925474099.2 } + - match: { hits.hits.1._source.field: [ 900719925474099.1, 1.1 ] } + - match: { hits.hits.1.sort.0: 450359962737050.1 } + - match: { hits.hits.2._source.field: [ 450359962737049.4, 3.5, 4.6 ] } + - match: { hits.hits.2.sort.0: 4.6 } + - match: { hits.hits.3._source.field: [ 450359962737049.7, 5.8, -1.9, -2.0 ] } + - match: { hits.hits.3.sort.0: 1.95 } + + - do: + search: + index: double_sort + body: + size: 5 + sort: [ { field: { mode: avg, order: asc } } ] + - match: { hits.total.value: 4 } + - length: { hits.hits: 4 } + - match: { hits.hits.0._index: double_sort } + - match: { hits.hits.0._source.field: [ 450359962737049.7, 5.8, -1.9, -2.0 ] } + - match: { hits.hits.0.sort.0: 112589990684262.89 } + - match: { hits.hits.1._source.field: [ 450359962737049.4, 3.5, 4.6 ] } + - match: { hits.hits.1.sort.0: 150119987579019.16 } + - match: { hits.hits.2._source.field: [ 900719925474099.1, 1.1 ] } + - match: { hits.hits.2.sort.0: 450359962737050.1 } + - match: { hits.hits.3._source.field: [ 900719925474099.2, 900719925474099.2 ] } + - match: { hits.hits.3.sort.0: 900719925474099.2 } + + - do: + search: + index: double_sort + body: + size: 5 + sort: [ { field: { mode: sum, order: desc } } ] + - match: { hits.total.value: 4 } + - length: { hits.hits: 4 } + - match: { hits.hits.0._index: double_sort } + - match: { hits.hits.0._source.field: [ 900719925474099.2, 900719925474099.2 ] } + - match: { hits.hits.0.sort.0: 1801439850948198.5 } + - match: { hits.hits.1._source.field: [ 900719925474099.1, 1.1 ] } + - match: { hits.hits.1.sort.0: 900719925474100.2 } + - match: { hits.hits.2._source.field: [ 450359962737049.4, 3.5, 4.6 ] } + - match: { hits.hits.2.sort.0: 450359962737057.5 } + - match: { hits.hits.3._source.field: [ 450359962737049.7, 5.8, -1.9, -2.0 ] } + - match: { hits.hits.3.sort.0: 450359962737051.56 } diff --git a/rest-api-spec/src/main/resources/rest-api-spec/test/search/260_sort_long.yml b/rest-api-spec/src/main/resources/rest-api-spec/test/search/260_sort_long.yml new file mode 100644 index 0000000000000..f354dff6cbf02 --- /dev/null +++ b/rest-api-spec/src/main/resources/rest-api-spec/test/search/260_sort_long.yml @@ -0,0 +1,137 @@ +setup: + - do: + indices.create: + index: long_sort + body: + settings: + number_of_shards: 3 + number_of_replicas: 0 + mappings: + properties: + field: + type: long + +--- +"test sorting against long only fields": + + - do: + bulk: + refresh: true + body: + - '{ "index" : { "_index" : "long_sort", "_id" : "1" } }' + - '{"field" : [ 9223372036854775807, 1 ] }' + - '{ "index" : { "_index" : "long_sort", "_id" : "2" } }' + - '{"field" : [ 922337203685477777, 2 ] }' + - '{ "index" : { "_index" : "long_sort", "_id" : "3" } }' + - '{"field" : [ 2147483647, 3, 4 ] }' + - '{ "index" : { "_index" : "long_sort", "_id" : "4" } }' + - '{"field" : [ 2147483648, 5, -1, -2 ] }' + + - do: + search: + index: long_sort + body: + size: 5 + sort: [{ field: { mode: max, order: desc } } ] + - match: {hits.total.value: 4 } + - length: {hits.hits: 4 } + - match: { hits.hits.0._index: long_sort } + - match: { hits.hits.0._source.field: [ 9223372036854775807, 1 ] } + - match: { hits.hits.0.sort.0: 9223372036854775807 } + - match: { hits.hits.1._source.field: [ 922337203685477777, 2 ] } + - match: { hits.hits.1.sort.0: 922337203685477777 } + - match: { hits.hits.2._source.field: [ 2147483648, 5, -1, -2 ] } + - match: { hits.hits.2.sort.0: 2147483648 } + - match: { hits.hits.3._source.field: [ 2147483647, 3, 4 ] } + - match: { hits.hits.3.sort.0: 2147483647 } + + - do: + search: + index: long_sort + body: + size: 5 + sort: [ { field: { mode: max, order: asc } } ] + - match: { hits.total.value: 4 } + - length: { hits.hits: 4 } + - match: { hits.hits.0._index: long_sort } + - match: { hits.hits.0._source.field: [ 2147483647, 3, 4 ] } + - match: { hits.hits.0.sort.0: 2147483647 } + - match: { hits.hits.1._source.field: [ 2147483648, 5, -1, -2 ] } + - match: { hits.hits.1.sort.0: 2147483648 } + - match: { hits.hits.2._source.field: [ 922337203685477777, 2 ] } + - match: { hits.hits.2.sort.0: 922337203685477777 } + - match: { hits.hits.3._source.field: [ 9223372036854775807, 1 ] } + - match: { hits.hits.3.sort.0: 9223372036854775807 } + + + - do: + search: + index: long_sort + body: + size: 5 + sort: [{ field: { mode: min, order: desc } } ] + - match: { hits.total.value: 4 } + - length: { hits.hits: 4 } + - match: { hits.hits.0._index: long_sort } + - match: { hits.hits.0._source.field: [ 2147483647, 3, 4 ] } + - match: { hits.hits.0.sort.0: 3 } + - match: { hits.hits.1._source.field: [ 922337203685477777, 2 ] } + - match: { hits.hits.1.sort.0: 2 } + - match: { hits.hits.2._source.field: [ 9223372036854775807, 1 ] } + - match: { hits.hits.2.sort.0: 1 } + - match: { hits.hits.3._source.field: [ 2147483648, 5, -1, -2 ] } + - match: { hits.hits.3.sort: [ -2 ] } + + - do: + search: + index: long_sort + body: + size: 5 + sort: [ { field: { mode: median, order: asc } } ] + - match: { hits.total.value: 4 } + - length: { hits.hits: 4 } + - match: { hits.hits.0._index: long_sort } + - match: { hits.hits.0._source.field: [ 2147483648, 5, -1, -2 ] } + - match: { hits.hits.0.sort.0: 2 } + - match: { hits.hits.1._source.field: [ 2147483647, 3, 4 ] } + - match: { hits.hits.1.sort.0: 4 } + - match: { hits.hits.2._source.field: [ 922337203685477777, 2 ] } + - match: { hits.hits.2.sort.0: 461168601842738880 } + - match: { hits.hits.3._source.field: [ 9223372036854775807, 1 ] } + - match: { hits.hits.3.sort.0: 4611686018427387904 } + + - do: + search: + index: long_sort + body: + size: 5 + sort: [ { field: { mode: avg, order: desc } } ] + - match: { hits.total.value: 4 } + - length: { hits.hits: 4 } + - match: { hits.hits.0._index: long_sort } + - match: { hits.hits.0._source.field: [ 922337203685477777, 2 ] } + - match: { hits.hits.0.sort.0: 461168601842738880 } + - match: { hits.hits.1._source.field: [ 2147483647, 3, 4 ] } + - match: { hits.hits.1.sort.0: 715827885 } + - match: { hits.hits.2._source.field: [ 2147483648, 5, -1, -2 ] } + - match: { hits.hits.2.sort.0: 536870913 } + - match: { hits.hits.3._source.field: [ 9223372036854775807, 1 ] } + - match: { hits.hits.3.sort: [ -4611686018427387904 ] } + + - do: + search: + index: long_sort + body: + size: 5 + sort: [ { field: { mode: sum, order: asc } } ] + - match: { hits.total.value: 4 } + - length: { hits.hits: 4 } + - match: { hits.hits.0._index: long_sort } + - match: { hits.hits.0._source.field: [ 9223372036854775807, 1 ] } + - match: { hits.hits.0.sort: [ -9223372036854775808 ] } + - match: { hits.hits.1._source.field: [ 2147483648, 5, -1, -2 ] } + - match: { hits.hits.1.sort.0: 2147483650 } + - match: { hits.hits.2._source.field: [ 2147483647, 3, 4 ] } + - match: { hits.hits.2.sort.0: 2147483654 } + - match: { hits.hits.3._source.field: [ 922337203685477777, 2 ] } + - match: { hits.hits.3.sort.0: 922337203685477779 } diff --git a/rest-api-spec/src/main/resources/rest-api-spec/test/search/260_sort_unsigned_long.yml b/rest-api-spec/src/main/resources/rest-api-spec/test/search/260_sort_unsigned_long.yml new file mode 100644 index 0000000000000..056b2f58b2229 --- /dev/null +++ b/rest-api-spec/src/main/resources/rest-api-spec/test/search/260_sort_unsigned_long.yml @@ -0,0 +1,167 @@ +setup: + - do: + indices.create: + index: unsigned_long_sort + body: + settings: + number_of_shards: 3 + number_of_replicas: 0 + mappings: + properties: + field: + type: unsigned_long + +--- +"test sorting against unsigned_long only fields": + - skip: + version: " - 2.19.99" + reason: "this change is added in 3.0.0" + + - do: + bulk: + refresh: true + body: + - '{ "index" : { "_index" : "unsigned_long_sort", "_id" : "1" } }' + - '{"field" : [ 13835058055282163712, 1 ] }' + - '{ "index" : { "_index" : "unsigned_long_sort", "_id" : "2" } }' + - '{"field" : [ 13835058055282163713, 13835058055282163714 ] }' + - '{ "index" : { "_index" : "unsigned_long_sort", "_id" : "3" } }' + - '{"field" : [ 13835058055282163715, 13835058055282163716, 2 ] }' + - '{ "index" : { "_index" : "unsigned_long_sort", "_id" : "4" } }' + - '{"field" : [ 13835058055282163717, 13835058055282163718, 13835058055282163719 ] }' + - '{ "index" : { "_index" : "unsigned_long_sort", "_id" : "5" } }' + - '{"field" : [ 13835058055282163720, 13835058055282163721, 3, 4 ] }' + - '{ "index" : { "_index" : "unsigned_long_sort", "_id" : "6" } }' + - '{"field" : [ 13835058055282163722, 5, 6, 7 ] }' + + - do: + search: + index: unsigned_long_sort + body: + size: 10 + sort: [{ field: { mode: max, order: desc } } ] + - match: {hits.total.value: 6 } + - length: {hits.hits: 6 } + - match: { hits.hits.0._index: unsigned_long_sort } + - match: { hits.hits.0._source.field: [ 13835058055282163722, 5, 6, 7 ] } + - match: { hits.hits.0.sort.0: 13835058055282163722 } + - match: { hits.hits.1._source.field: [ 13835058055282163720, 13835058055282163721, 3, 4 ] } + - match: { hits.hits.1.sort.0: 13835058055282163721 } + - match: { hits.hits.2._source.field: [ 13835058055282163717, 13835058055282163718, 13835058055282163719 ] } + - match: { hits.hits.2.sort.0: 13835058055282163719 } + - match: { hits.hits.3._source.field: [ 13835058055282163715, 13835058055282163716, 2 ] } + - match: { hits.hits.3.sort.0: 13835058055282163716 } + - match: { hits.hits.4._source.field: [ 13835058055282163713, 13835058055282163714 ] } + - match: { hits.hits.4.sort.0: 13835058055282163714 } + - match: { hits.hits.5._source.field: [ 13835058055282163712, 1 ] } + - match: { hits.hits.5.sort.0: 13835058055282163712 } + + - do: + search: + index: unsigned_long_sort + body: + size: 10 + sort: [{ field: { mode: max, order: asc } } ] + - match: {hits.total.value: 6 } + - length: {hits.hits: 6 } + - match: { hits.hits.0._index: unsigned_long_sort } + - match: { hits.hits.0._source.field: [ 13835058055282163712, 1 ] } + - match: { hits.hits.0.sort.0: 13835058055282163712 } + - match: { hits.hits.1._source.field: [ 13835058055282163713, 13835058055282163714 ] } + - match: { hits.hits.1.sort.0: 13835058055282163714 } + - match: { hits.hits.2._source.field: [ 13835058055282163715, 13835058055282163716, 2 ] } + - match: { hits.hits.2.sort.0: 13835058055282163716 } + - match: { hits.hits.3._source.field: [ 13835058055282163717, 13835058055282163718, 13835058055282163719 ] } + - match: { hits.hits.3.sort.0: 13835058055282163719 } + - match: { hits.hits.4._source.field: [ 13835058055282163720, 13835058055282163721, 3, 4 ] } + - match: { hits.hits.4.sort.0: 13835058055282163721 } + - match: { hits.hits.5._source.field: [ 13835058055282163722, 5, 6, 7 ] } + - match: { hits.hits.5.sort.0: 13835058055282163722 } + + - do: + search: + index: unsigned_long_sort + body: + size: 10 + sort: [ { field: { mode: median, order: asc } } ] + - match: { hits.total.value: 6 } + - length: { hits.hits: 6 } + - match: { hits.hits.0._index: unsigned_long_sort } + - match: { hits.hits.0._source.field: [ 13835058055282163722, 5, 6, 7 ] } + - match: { hits.hits.0.sort.0: 7 } + - match: { hits.hits.1._source.field: [ 13835058055282163713, 13835058055282163714 ] } + - match: { hits.hits.1.sort.0: 4611686018427387906 } + - match: { hits.hits.2._source.field: [ 13835058055282163712, 1 ] } + - match: { hits.hits.2.sort.0: 6917529027641081857 } + - match: { hits.hits.3._source.field: [ 13835058055282163720, 13835058055282163721, 3, 4 ] } + - match: { hits.hits.3.sort.0: 6917529027641081862 } + - match: { hits.hits.4._source.field: [ 13835058055282163715, 13835058055282163716, 2 ] } + - match: { hits.hits.4.sort.0: 13835058055282163715 } + - match: { hits.hits.5._source.field: [ 13835058055282163717, 13835058055282163718, 13835058055282163719 ] } + - match: { hits.hits.5.sort.0: 13835058055282163718 } + + - do: + search: + index: unsigned_long_sort + body: + size: 10 + sort: [ { field: { mode: sum, order: desc } } ] + - match: { hits.total.value: 6 } + - length: { hits.hits: 6 } + - match: { hits.hits.0._index: unsigned_long_sort } + - match: { hits.hits.0._source.field: [ 13835058055282163722, 5, 6, 7 ] } + - match: { hits.hits.0.sort.0: 13835058055282163740 } + - match: { hits.hits.1._source.field: [ 13835058055282163712, 1 ] } + - match: { hits.hits.1.sort.0: 13835058055282163713 } + - match: { hits.hits.2._source.field: [ 13835058055282163720, 13835058055282163721, 3, 4 ] } + - match: { hits.hits.2.sort.0: 9223372036854775832 } + - match: { hits.hits.3._source.field: [ 13835058055282163715, 13835058055282163716, 2 ] } + - match: { hits.hits.3.sort.0: 9223372036854775817 } + - match: { hits.hits.4._source.field: [ 13835058055282163713, 13835058055282163714 ] } + - match: { hits.hits.4.sort.0: 9223372036854775811 } + - match: { hits.hits.5._source.field: [ 13835058055282163717, 13835058055282163718, 13835058055282163719 ] } + - match: { hits.hits.5.sort.0: 4611686018427387922 } + + - do: + search: + index: unsigned_long_sort + body: + size: 10 + sort: [ { field: { mode: avg, order: desc } } ] + - match: { hits.total.value: 6 } + - length: { hits.hits: 6 } + - match: { hits.hits.0._index: unsigned_long_sort } + - match: { hits.hits.0._source.field: [ 13835058055282163712, 1 ] } + - match: { hits.hits.0.sort.0: 6917529027641081857 } + - match: { hits.hits.1._source.field: [ 13835058055282163713, 13835058055282163714 ] } + - match: { hits.hits.1.sort.0: 4611686018427387906 } + - match: { hits.hits.2._source.field: [ 13835058055282163722, 5, 6, 7 ] } + - match: { hits.hits.2.sort.0: 3458764513820540935 } + - match: { hits.hits.3._source.field: [ 13835058055282163715, 13835058055282163716, 2 ] } + - match: { hits.hits.3.sort.0: 3074457345618258606 } + - match: { hits.hits.4._source.field: [ 13835058055282163720, 13835058055282163721, 3, 4 ] } + - match: { hits.hits.4.sort.0: 2305843009213693958 } + - match: { hits.hits.5._source.field: [ 13835058055282163717, 13835058055282163718, 13835058055282163719 ] } + - match: { hits.hits.5.sort.0: 1537228672809129307 } + + - do: + search: + index: unsigned_long_sort + body: + size: 10 + sort: [ { field: { mode: min, order: asc } } ] + - match: { hits.total.value: 6 } + - length: { hits.hits: 6 } + - match: { hits.hits.0._index: unsigned_long_sort } + - match: { hits.hits.0._source.field: [ 13835058055282163712, 1 ] } + - match: { hits.hits.0.sort.0: 1 } + - match: { hits.hits.1._source.field: [ 13835058055282163715, 13835058055282163716, 2 ] } + - match: { hits.hits.1.sort.0: 2 } + - match: { hits.hits.2._source.field: [ 13835058055282163720, 13835058055282163721, 3, 4 ] } + - match: { hits.hits.2.sort.0: 3 } + - match: { hits.hits.3._source.field: [ 13835058055282163722, 5, 6, 7 ] } + - match: { hits.hits.3.sort.0: 5 } + - match: { hits.hits.4._source.field: [ 13835058055282163713, 13835058055282163714 ] } + - match: { hits.hits.4.sort.0: 13835058055282163713 } + - match: { hits.hits.5._source.field: [ 13835058055282163717, 13835058055282163718, 13835058055282163719 ] } + - match: { hits.hits.5.sort.0: 13835058055282163717 } diff --git a/server/src/main/java/org/opensearch/index/fielddata/LongToSortedNumericUnsignedLongValues.java b/server/src/main/java/org/opensearch/index/fielddata/LongToSortedNumericUnsignedLongValues.java new file mode 100644 index 0000000000000..eb8d8f1667218 --- /dev/null +++ b/server/src/main/java/org/opensearch/index/fielddata/LongToSortedNumericUnsignedLongValues.java @@ -0,0 +1,55 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.index.fielddata; + +import org.apache.lucene.index.SortedNumericDocValues; + +import java.io.IOException; + +/** + * Wraps long-based {@link SortedNumericDocValues} as unsigned long ones + * (primarily used by {@link org.opensearch.search.MultiValueMode} + * + * @opensearch.internal + */ +public final class LongToSortedNumericUnsignedLongValues extends SortedNumericUnsignedLongValues { + private final SortedNumericDocValues values; + + public LongToSortedNumericUnsignedLongValues(SortedNumericDocValues values) { + this.values = values; + } + + @Override + public boolean advanceExact(int target) throws IOException { + return values.advanceExact(target); + } + + @Override + public long nextValue() throws IOException { + return values.nextValue(); + } + + @Override + public int docValueCount() { + return values.docValueCount(); + } + + public int advance(int target) throws IOException { + return values.advance(target); + } + + public int docID() { + return values.docID(); + } + + /** Return the wrapped values. */ + public SortedNumericDocValues getNumericUnsignedLongValues() { + return values; + } +} diff --git a/server/src/main/java/org/opensearch/index/fielddata/SortedNumericUnsignedLongValues.java b/server/src/main/java/org/opensearch/index/fielddata/SortedNumericUnsignedLongValues.java new file mode 100644 index 0000000000000..fa4c5152b9f90 --- /dev/null +++ b/server/src/main/java/org/opensearch/index/fielddata/SortedNumericUnsignedLongValues.java @@ -0,0 +1,62 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.index.fielddata; + +import org.apache.lucene.index.SortedNumericDocValues; +import org.opensearch.common.annotation.PublicApi; + +import java.io.IOException; + +/** + * Clone of {@link SortedNumericDocValues} for unsigned long values. + * + * @opensearch.api + */ +@PublicApi(since = "2.19.0") +public abstract class SortedNumericUnsignedLongValues { + + /** Sole constructor. (For invocation by subclass + * constructors, typically implicit.) */ + protected SortedNumericUnsignedLongValues() {} + + /** Advance the iterator to exactly {@code target} and return whether + * {@code target} has a value. + * {@code target} must be greater than or equal to the current + * doc ID and must be a valid doc ID, ie. ≥ 0 and + * < {@code maxDoc}.*/ + public abstract boolean advanceExact(int target) throws IOException; + + /** + * Iterates to the next value in the current document. Do not call this more than + * {@link #docValueCount} times for the document. + */ + public abstract long nextValue() throws IOException; + + /** + * Retrieves the number of values for the current document. This must always + * be greater than zero. + * It is illegal to call this method after {@link #advanceExact(int)} + * returned {@code false}. + */ + public abstract int docValueCount(); + + /** + * Advances to the first beyond the current whose document number is greater than or equal to + * target, and returns the document number itself. Exhausts the iterator and returns {@link + * org.apache.lucene.search.DocIdSetIterator#NO_MORE_DOCS} if target is greater than the highest document number in the set. + * + * This method is being used by {@link org.apache.lucene.search.comparators.NumericComparator.NumericLeafComparator} when point values optimization kicks + * in and is implemented by most numeric types. + */ + public int advance(int target) throws IOException { + throw new UnsupportedOperationException(); + } + + public abstract int docID(); +} diff --git a/server/src/main/java/org/opensearch/index/fielddata/fieldcomparator/UnsignedLongValuesComparatorSource.java b/server/src/main/java/org/opensearch/index/fielddata/fieldcomparator/UnsignedLongValuesComparatorSource.java index 9db5817450cd0..6fc85bd0b2689 100644 --- a/server/src/main/java/org/opensearch/index/fielddata/fieldcomparator/UnsignedLongValuesComparatorSource.java +++ b/server/src/main/java/org/opensearch/index/fielddata/fieldcomparator/UnsignedLongValuesComparatorSource.java @@ -10,7 +10,6 @@ import org.apache.lucene.index.LeafReaderContext; import org.apache.lucene.index.NumericDocValues; -import org.apache.lucene.index.SortedNumericDocValues; import org.apache.lucene.search.DocIdSetIterator; import org.apache.lucene.search.FieldComparator; import org.apache.lucene.search.LeafFieldComparator; @@ -24,6 +23,8 @@ import org.opensearch.index.fielddata.IndexFieldData; import org.opensearch.index.fielddata.IndexNumericFieldData; import org.opensearch.index.fielddata.LeafNumericFieldData; +import org.opensearch.index.fielddata.LongToSortedNumericUnsignedLongValues; +import org.opensearch.index.fielddata.SortedNumericUnsignedLongValues; import org.opensearch.index.search.comparators.UnsignedLongComparator; import org.opensearch.search.DocValueFormat; import org.opensearch.search.MultiValueMode; @@ -57,14 +58,13 @@ public SortField.Type reducedType() { return SortField.Type.LONG; } - private SortedNumericDocValues loadDocValues(LeafReaderContext context) { + private SortedNumericUnsignedLongValues loadDocValues(LeafReaderContext context) { final LeafNumericFieldData data = indexFieldData.load(context); - SortedNumericDocValues values = data.getLongValues(); - return values; + return new LongToSortedNumericUnsignedLongValues(data.getLongValues()); } private NumericDocValues getNumericDocValues(LeafReaderContext context, BigInteger missingValue) throws IOException { - final SortedNumericDocValues values = loadDocValues(context); + final SortedNumericUnsignedLongValues values = loadDocValues(context); if (nested == null) { return FieldData.replaceMissing(sortMode.select(values), missingValue); } diff --git a/server/src/main/java/org/opensearch/search/MultiValueMode.java b/server/src/main/java/org/opensearch/search/MultiValueMode.java index a99da674836f2..fa2e776eca67a 100644 --- a/server/src/main/java/org/opensearch/search/MultiValueMode.java +++ b/server/src/main/java/org/opensearch/search/MultiValueMode.java @@ -42,6 +42,7 @@ import org.apache.lucene.util.BitSet; import org.apache.lucene.util.BytesRef; import org.apache.lucene.util.BytesRefBuilder; +import org.opensearch.common.Numbers; import org.opensearch.common.annotation.PublicApi; import org.opensearch.core.common.io.stream.StreamInput; import org.opensearch.core.common.io.stream.StreamOutput; @@ -50,9 +51,11 @@ import org.opensearch.index.fielddata.AbstractNumericDocValues; import org.opensearch.index.fielddata.AbstractSortedDocValues; import org.opensearch.index.fielddata.FieldData; +import org.opensearch.index.fielddata.LongToSortedNumericUnsignedLongValues; import org.opensearch.index.fielddata.NumericDoubleValues; import org.opensearch.index.fielddata.SortedBinaryDocValues; import org.opensearch.index.fielddata.SortedNumericDoubleValues; +import org.opensearch.index.fielddata.SortedNumericUnsignedLongValues; import java.io.IOException; import java.util.Locale; @@ -143,6 +146,44 @@ protected double pick( return totalCount > 0 ? totalValue : missingValue; } + + @Override + protected long pick(SortedNumericUnsignedLongValues values) throws IOException { + final int count = values.docValueCount(); + long total = 0; + for (int index = 0; index < count; ++index) { + total += values.nextValue(); + } + return total; + } + + @Override + protected long pick( + SortedNumericUnsignedLongValues values, + long missingValue, + DocIdSetIterator docItr, + int startDoc, + int endDoc, + int maxChildren + ) throws IOException { + int totalCount = 0; + long totalValue = 0; + int count = 0; + for (int doc = startDoc; doc < endDoc; doc = docItr.nextDoc()) { + if (values.advanceExact(doc)) { + if (++count > maxChildren) { + break; + } + + final int docCount = values.docValueCount(); + for (int index = 0; index < docCount; ++index) { + totalValue += values.nextValue(); + } + totalCount += docCount; + } + } + return totalCount > 0 ? totalValue : missingValue; + } }, /** @@ -228,6 +269,46 @@ protected double pick( } return totalValue / totalCount; } + + @Override + protected long pick(SortedNumericUnsignedLongValues values) throws IOException { + final int count = values.docValueCount(); + long total = 0; + for (int index = 0; index < count; ++index) { + total += values.nextValue(); + } + return count > 1 ? divideUnsignedAndRoundUp(total, count) : total; + } + + @Override + protected long pick( + SortedNumericUnsignedLongValues values, + long missingValue, + DocIdSetIterator docItr, + int startDoc, + int endDoc, + int maxChildren + ) throws IOException { + int totalCount = 0; + long totalValue = 0; + int count = 0; + for (int doc = startDoc; doc < endDoc; doc = docItr.nextDoc()) { + if (values.advanceExact(doc)) { + if (++count > maxChildren) { + break; + } + final int docCount = values.docValueCount(); + for (int index = 0; index < docCount; ++index) { + totalValue += values.nextValue(); + } + totalCount += docCount; + } + } + if (totalCount < 1) { + return missingValue; + } + return totalCount > 1 ? divideUnsignedAndRoundUp(totalValue, totalCount) : totalValue; + } }, /** @@ -259,6 +340,45 @@ protected double pick(SortedNumericDoubleValues values) throws IOException { return values.nextValue(); } } + + @Override + protected long pick(SortedNumericUnsignedLongValues values) throws IOException { + int count = values.docValueCount(); + long firstValue = values.nextValue(); + if (count == 1) { + return firstValue; + } else if (count == 2) { + long total = firstValue + values.nextValue(); + return (total >>> 1) + (total & 1); + } else if (firstValue >= 0) { + for (int i = 1; i < (count - 1) / 2; ++i) { + values.nextValue(); + } + if (count % 2 == 0) { + long total = values.nextValue() + values.nextValue(); + return (total >>> 1) + (total & 1); + } else { + return values.nextValue(); + } + } + + final long[] docValues = new long[count]; + docValues[0] = firstValue; + int firstPositiveIndex = 0; + for (int i = 1; i < count; ++i) { + docValues[i] = values.nextValue(); + if (docValues[i] >= 0 && firstPositiveIndex == 0) { + firstPositiveIndex = i; + } + } + final int mid = ((count - 1) / 2 + firstPositiveIndex) % count; + if (count % 2 == 0) { + long total = docValues[mid] + docValues[(mid + 1) % count]; + return (total >>> 1) + (total & 1); + } else { + return docValues[mid]; + } + } }, /** @@ -382,6 +502,47 @@ protected int pick(SortedDocValues values, DocIdSetIterator docItr, int startDoc return hasValue ? ord : -1; } + + @Override + protected long pick(SortedNumericUnsignedLongValues values) throws IOException { + final int count = values.docValueCount(); + final long min = values.nextValue(); + if (count == 1 || min > 0) { + return min; + } + for (int i = 1; i < count; ++i) { + long val = values.nextValue(); + if (val >= 0) { + return val; + } + } + return min; + } + + @Override + protected long pick( + SortedNumericUnsignedLongValues values, + long missingValue, + DocIdSetIterator docItr, + int startDoc, + int endDoc, + int maxChildren + ) throws IOException { + boolean hasValue = false; + long minValue = Numbers.MAX_UNSIGNED_LONG_VALUE_AS_LONG; + int count = 0; + for (int doc = startDoc; doc < endDoc; doc = docItr.nextDoc()) { + if (values.advanceExact(doc)) { + if (++count > maxChildren) { + break; + } + final long docMin = pick(values); + minValue = Long.compareUnsigned(docMin, minValue) < 0 ? docMin : minValue; + hasValue = true; + } + } + return hasValue ? minValue : missingValue; + } }, /** @@ -525,6 +686,46 @@ protected int pick(SortedDocValues values, DocIdSetIterator docItr, int startDoc } return ord; } + + @Override + protected long pick(SortedNumericUnsignedLongValues values) throws IOException { + final int count = values.docValueCount(); + long max = values.nextValue(); + long val; + for (int i = 1; i < count; ++i) { + val = values.nextValue(); + if (max < 0 && val >= 0) { + return max; + } + max = val; + } + return max; + } + + @Override + protected long pick( + SortedNumericUnsignedLongValues values, + long missingValue, + DocIdSetIterator docItr, + int startDoc, + int endDoc, + int maxChildren + ) throws IOException { + boolean hasValue = false; + long maxValue = Numbers.MIN_UNSIGNED_LONG_VALUE_AS_LONG; + int count = 0; + for (int doc = startDoc; doc < endDoc; doc = docItr.nextDoc()) { + if (values.advanceExact(doc)) { + if (++count > maxChildren) { + break; + } + final long docMax = pick(values); + maxValue = Long.compareUnsigned(maxValue, docMax) < 0 ? docMax : maxValue; + hasValue = true; + } + } + return hasValue ? maxValue : missingValue; + } }; /** @@ -1032,6 +1233,126 @@ protected int pick(SortedDocValues values, DocIdSetIterator docItr, int startDoc throw new IllegalArgumentException("Unsupported sort mode: " + this); } + /** + * Return a {@link NumericDoubleValues} instance that can be used to sort documents + * with this mode and the provided values. When a document has no value, + * missingValue is returned. + *

+ * Allowed Modes: SUM, AVG, MEDIAN, MIN, MAX + */ + public NumericDocValues select(final SortedNumericUnsignedLongValues values) { + SortedNumericDocValues sortedNumericDocValues = null; + if (values instanceof LongToSortedNumericUnsignedLongValues) { + sortedNumericDocValues = ((LongToSortedNumericUnsignedLongValues) values).getNumericUnsignedLongValues(); + } + + final NumericDocValues singleton = DocValues.unwrapSingleton(sortedNumericDocValues); + if (singleton != null) { + return singleton; + } else { + return new AbstractNumericDocValues() { + + private long value; + + @Override + public boolean advanceExact(int target) throws IOException { + if (values.advanceExact(target)) { + value = pick(values); + return true; + } + return false; + } + + @Override + public int docID() { + return values.docID(); + } + + @Override + public long longValue() throws IOException { + return value; + } + }; + } + } + + protected long pick(SortedNumericUnsignedLongValues values) throws IOException { + throw new IllegalArgumentException("Unsupported sort mode: " + this); + } + + /** + * Return a {@link SortedDocValues} instance that can be used to sort root documents + * with this mode, the provided values and filters for root/inner documents. + *

+ * For every root document, the values of its inner documents will be aggregated. + *

+ * Allowed Modes: MIN, MAX + *

+ * NOTE: Calling the returned instance on docs that are not root docs is illegal + * The returned instance can only be evaluate the current and upcoming docs + */ + public NumericDocValues select( + final SortedNumericUnsignedLongValues values, + final long missingValue, + final BitSet parentDocs, + final DocIdSetIterator childDocs, + int maxDoc, + int maxChildren + ) throws IOException { + if (parentDocs == null || childDocs == null) { + return FieldData.replaceMissing(DocValues.emptyNumeric(), missingValue); + } + + return new AbstractNumericDocValues() { + + int lastSeenParentDoc = -1; + long lastEmittedValue = missingValue; + + @Override + public boolean advanceExact(int parentDoc) throws IOException { + assert parentDoc >= lastSeenParentDoc : "can only evaluate current and upcoming parent docs"; + if (parentDoc == lastSeenParentDoc) { + return true; + } else if (parentDoc == 0) { + lastEmittedValue = missingValue; + return true; + } + final int prevParentDoc = parentDocs.prevSetBit(parentDoc - 1); + final int firstChildDoc; + if (childDocs.docID() > prevParentDoc) { + firstChildDoc = childDocs.docID(); + } else { + firstChildDoc = childDocs.advance(prevParentDoc + 1); + } + + lastSeenParentDoc = parentDoc; + lastEmittedValue = pick(values, missingValue, childDocs, firstChildDoc, parentDoc, maxChildren); + return true; + } + + @Override + public int docID() { + return lastSeenParentDoc; + } + + @Override + public long longValue() { + return lastEmittedValue; + } + }; + } + + protected long pick( + SortedNumericUnsignedLongValues values, + long missingValue, + DocIdSetIterator docItr, + int startDoc, + int endDoc, + int maxChildren + ) throws IOException { + throw new IllegalArgumentException("Unsupported sort mode: " + this); + } + @Override public void writeTo(StreamOutput out) throws IOException { out.writeEnum(this); @@ -1040,4 +1361,16 @@ public void writeTo(StreamOutput out) throws IOException { public static MultiValueMode readMultiValueModeFrom(StreamInput in) throws IOException { return in.readEnum(MultiValueMode.class); } + + /** + * Copied from {@link Long#divideUnsigned(long, long)} and {@link Long#remainderUnsigned(long, long)} + */ + private static long divideUnsignedAndRoundUp(long dividend, long divisor) { + assert divisor > 0; + final long q = (dividend >>> 1) / divisor << 1; + final long r = dividend - q * divisor; + final long quotient = q + ((r | ~(r - divisor)) >>> (Long.SIZE - 1)); + final long rem = r - ((~(r - divisor) >> (Long.SIZE - 1)) & divisor); + return quotient + Math.round((double) rem / divisor); + } } diff --git a/server/src/test/java/org/opensearch/search/MultiValueModeTests.java b/server/src/test/java/org/opensearch/search/MultiValueModeTests.java index 948d2cffceabe..e011dd0bcf6c0 100644 --- a/server/src/test/java/org/opensearch/search/MultiValueModeTests.java +++ b/server/src/test/java/org/opensearch/search/MultiValueModeTests.java @@ -41,6 +41,7 @@ import org.apache.lucene.util.BitSetIterator; import org.apache.lucene.util.BytesRef; import org.apache.lucene.util.FixedBitSet; +import org.opensearch.common.Numbers; import org.opensearch.common.io.stream.BytesStreamOutput; import org.opensearch.core.common.io.stream.StreamInput; import org.opensearch.index.fielddata.AbstractBinaryDocValues; @@ -52,9 +53,13 @@ import org.opensearch.index.fielddata.NumericDoubleValues; import org.opensearch.index.fielddata.SortedBinaryDocValues; import org.opensearch.index.fielddata.SortedNumericDoubleValues; +import org.opensearch.index.fielddata.SortedNumericUnsignedLongValues; import org.opensearch.test.OpenSearchTestCase; import java.io.IOException; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.math.RoundingMode; import java.util.Arrays; import static org.hamcrest.Matchers.equalTo; @@ -776,6 +781,96 @@ public int docValueCount() { verifySortedSet(multiValues, numDocs, rootDocs, innerDocs, randomIntBetween(1, numDocs)); } + public void testSingleValuedUnsignedLongs() throws Exception { + final int numDocs = scaledRandomIntBetween(1, 100); + final long[] array = new long[numDocs]; + final FixedBitSet docsWithValue = randomBoolean() ? null : new FixedBitSet(numDocs); + for (int i = 0; i < array.length; ++i) { + if (randomBoolean()) { + array[i] = randomUnsignedLong().longValue(); + if (docsWithValue != null) { + docsWithValue.set(i); + } + } else if (docsWithValue != null && randomBoolean()) { + docsWithValue.set(i); + } + } + + final Supplier multiValues = () -> new SortedNumericUnsignedLongValues() { + int docId = -1; + + @Override + public boolean advanceExact(int target) throws IOException { + this.docId = target; + return docsWithValue == null || docsWithValue.get(docId); + } + + @Override + public int docID() { + return docId; + } + + @Override + public long nextValue() { + return array[docId]; + } + + @Override + public int docValueCount() { + return 1; + } + }; + verifySortedUnsignedLong(multiValues, numDocs); + final FixedBitSet rootDocs = randomRootDocs(numDocs); + final FixedBitSet innerDocs = randomInnerDocs(rootDocs); + verifySortedUnsignedLong(multiValues, numDocs, rootDocs, innerDocs, Integer.MAX_VALUE); + verifySortedUnsignedLong(multiValues, numDocs, rootDocs, innerDocs, randomIntBetween(1, numDocs)); + } + + public void testMultiValuedUnsignedLongs() throws Exception { + final int numDocs = scaledRandomIntBetween(1, 100); + final long[][] array = new long[numDocs][]; + for (int i = 0; i < numDocs; ++i) { + final long[] values = new long[randomInt(4)]; + for (int j = 0; j < values.length; ++j) { + values[j] = randomUnsignedLong().longValue(); + } + Arrays.sort(values); + array[i] = values; + } + final Supplier multiValues = () -> new SortedNumericUnsignedLongValues() { + int doc; + int i; + + @Override + public long nextValue() { + return array[doc][i++]; + } + + @Override + public boolean advanceExact(int doc) { + this.doc = doc; + i = 0; + return array[doc].length > 0; + } + + @Override + public int docValueCount() { + return array[doc].length; + } + + @Override + public int docID() { + return doc; + } + }; + verifySortedUnsignedLong(multiValues, numDocs); + final FixedBitSet rootDocs = randomRootDocs(numDocs); + final FixedBitSet innerDocs = randomInnerDocs(rootDocs); + verifySortedUnsignedLong(multiValues, numDocs, rootDocs, innerDocs, Integer.MAX_VALUE); + verifySortedUnsignedLong(multiValues, numDocs, rootDocs, innerDocs, randomIntBetween(1, numDocs)); + } + private void verifySortedSet(Supplier supplier, int maxDoc) throws IOException { for (MultiValueMode mode : new MultiValueMode[] { MultiValueMode.MIN, MultiValueMode.MAX }) { SortedSetDocValues values = supplier.get(); @@ -857,6 +952,141 @@ private void verifySortedSet( } } + private void verifySortedUnsignedLong(Supplier supplier, int maxDoc) throws IOException { + for (MultiValueMode mode : MultiValueMode.values()) { + SortedNumericUnsignedLongValues values = supplier.get(); + final NumericDocValues selected = mode.select(values); + for (int i = 0; i < maxDoc; ++i) { + Long actual = null; + if (selected.advanceExact(i)) { + actual = selected.longValue(); + verifyLongValueCanCalledMoreThanOnce(selected, actual); + } + + BigInteger expected = null; + if (values.advanceExact(i)) { + int numValues = values.docValueCount(); + if (mode == MultiValueMode.MAX) { + expected = Numbers.MIN_UNSIGNED_LONG_VALUE; + } else if (mode == MultiValueMode.MIN) { + expected = Numbers.MAX_UNSIGNED_LONG_VALUE; + } else { + expected = BigInteger.ZERO; + } + for (int j = 0; j < numValues; ++j) { + if (mode == MultiValueMode.SUM || mode == MultiValueMode.AVG) { + expected = expected.add(Numbers.toUnsignedBigInteger(values.nextValue())); + } else if (mode == MultiValueMode.MIN) { + expected = expected.min(Numbers.toUnsignedBigInteger(values.nextValue())); + } else if (mode == MultiValueMode.MAX) { + expected = expected.max(Numbers.toUnsignedBigInteger(values.nextValue())); + } + } + if (mode == MultiValueMode.AVG) { + expected = Numbers.toUnsignedBigInteger(expected.longValue()); + expected = numValues > 1 + ? new BigDecimal(expected).divide(new BigDecimal(numValues), RoundingMode.HALF_UP).toBigInteger() + : expected; + } else if (mode == MultiValueMode.MEDIAN) { + final Long[] docValues = new Long[numValues]; + for (int j = 0; j < numValues; ++j) { + docValues[j] = values.nextValue(); + } + Arrays.sort(docValues, Long::compareUnsigned); + int value = numValues / 2; + if (numValues % 2 == 0) { + expected = Numbers.toUnsignedBigInteger(docValues[value - 1]) + .add(Numbers.toUnsignedBigInteger(docValues[value])); + expected = Numbers.toUnsignedBigInteger(expected.longValue()); + expected = new BigDecimal(expected).divide(new BigDecimal(2), RoundingMode.HALF_UP).toBigInteger(); + } else { + expected = Numbers.toUnsignedBigInteger(docValues[value]); + } + } + } + + final Long expectedLong = expected == null ? null : expected.longValue(); + assertEquals(mode.toString() + " docId=" + i, expectedLong, actual); + } + } + } + + private void verifySortedUnsignedLong( + Supplier supplier, + int maxDoc, + FixedBitSet rootDocs, + FixedBitSet innerDocs, + int maxChildren + ) throws IOException { + for (long missingValue : new long[] { 0, randomUnsignedLong().longValue() }) { + for (MultiValueMode mode : new MultiValueMode[] { + MultiValueMode.MIN, + MultiValueMode.MAX, + MultiValueMode.SUM, + MultiValueMode.AVG }) { + SortedNumericUnsignedLongValues values = supplier.get(); + final NumericDocValues selected = mode.select( + values, + missingValue, + rootDocs, + new BitSetIterator(innerDocs, 0L), + maxDoc, + maxChildren + ); + int prevRoot = -1; + for (int root = rootDocs.nextSetBit(0); root != -1; root = root + 1 < maxDoc ? rootDocs.nextSetBit(root + 1) : -1) { + assertTrue(selected.advanceExact(root)); + final long actual = selected.longValue(); + verifyLongValueCanCalledMoreThanOnce(selected, actual); + + BigInteger expected = BigInteger.ZERO; + if (mode == MultiValueMode.MAX) { + expected = Numbers.MIN_UNSIGNED_LONG_VALUE; + } else if (mode == MultiValueMode.MIN) { + expected = Numbers.MAX_UNSIGNED_LONG_VALUE; + } + int numValues = 0; + int count = 0; + for (int child = innerDocs.nextSetBit(prevRoot + 1); child != -1 && child < root; child = innerDocs.nextSetBit( + child + 1 + )) { + if (values.advanceExact(child)) { + if (++count > maxChildren) { + break; + } + for (int j = 0; j < values.docValueCount(); ++j) { + if (mode == MultiValueMode.SUM || mode == MultiValueMode.AVG) { + expected = expected.add(Numbers.toUnsignedBigInteger(values.nextValue())); + } else if (mode == MultiValueMode.MIN) { + expected = expected.min(Numbers.toUnsignedBigInteger(values.nextValue())); + } else if (mode == MultiValueMode.MAX) { + expected = expected.max(Numbers.toUnsignedBigInteger(values.nextValue())); + } + ++numValues; + } + } + } + final long expectedLong; + if (numValues == 0) { + expectedLong = missingValue; + } else if (mode == MultiValueMode.AVG) { + expected = Numbers.toUnsignedBigInteger(expected.longValue()); + expected = numValues > 1 + ? new BigDecimal(expected).divide(new BigDecimal(numValues), RoundingMode.HALF_UP).toBigInteger() + : expected; + expectedLong = expected.longValue(); + } else { + expectedLong = expected.longValue(); + } + + assertEquals(mode.toString() + " docId=" + root, expectedLong, actual); + + prevRoot = root; + } + } + } + } + public void testValidOrdinals() { assertThat(MultiValueMode.SUM.ordinal(), equalTo(0)); assertThat(MultiValueMode.AVG.ordinal(), equalTo(1));