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