瀏覽代碼

Do not produce infinity values in synthetic source for range fields (#108699)

Oleksandr Kolomiiets 1 年之前
父節點
當前提交
a454ac1987

+ 37 - 1
docs/reference/mapping/types/range.asciidoc

@@ -352,7 +352,7 @@ Will become:
 // TEST[s/^/{"_source":/ s/\n$/}/]
 
 [[range-synthetic-source-inclusive]]
-Range field vales are always represented as inclusive on both sides with bounds adjusted accordingly. For example:
+Range field vales are always represented as inclusive on both sides with bounds adjusted accordingly. Default values for range bounds are represented as `null`. This is true even if range bound was explicitly provided. For example:
 [source,console,id=synthetic-source-range-normalization-example]
 ----
 PUT idx
@@ -388,6 +388,42 @@ Will become:
 ----
 // TEST[s/^/{"_source":/ s/\n$/}/]
 
+[[range-synthetic-source-default-bounds]]
+Default values for range bounds are represented as `null` in synthetic source. This is true even if range bound was explicitly provided with default value. For example:
+[source,console,id=synthetic-source-range-bounds-example]
+----
+PUT idx
+{
+  "mappings": {
+    "_source": { "mode": "synthetic" },
+    "properties": {
+      "my_range": { "type": "integer_range" }
+    }
+  }
+}
+
+PUT idx/_doc/1
+{
+  "my_range": {
+    "lte": 2147483647
+  }
+}
+----
+// TEST[s/$/\nGET idx\/_doc\/1?filter_path=_source\n/]
+
+Will become:
+
+[source,console-result]
+----
+{
+  "my_range": {
+    "gte": null,
+    "lte": null
+  }
+}
+----
+// TEST[s/^/{"_source":/ s/\n$/}/]
+
 `date` ranges are formatted using provided `format` or by default using `yyyy-MM-dd'T'HH:mm:ss.SSSZ` format. For example:
 [source,console,id=synthetic-source-range-date-example]
 ----

+ 12 - 12
rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/range/20_synthetic_source.yml

@@ -117,7 +117,7 @@ setup:
         id: "6"
   - match:
       _source:
-        integer_range: { "gte": -2147483648, "lte": 10 }
+        integer_range: { "gte": null, "lte": 10 }
 
   - do:
       get:
@@ -125,7 +125,7 @@ setup:
         id: "7"
   - match:
       _source:
-        integer_range: { "gte": 1, "lte": 2147483647 }
+        integer_range: { "gte": 1, "lte": null }
 
 ---
 "Long range":
@@ -220,7 +220,7 @@ setup:
         id: "6"
   - match:
       _source:
-        long_range: { "gte": -9223372036854775808, "lte": 10 }
+        long_range: { "gte": null, "lte": 10 }
 
   - do:
       get:
@@ -228,7 +228,7 @@ setup:
         id: "7"
   - match:
       _source:
-        long_range: { "gte": 1, "lte": 9223372036854775807 }
+        long_range: { "gte": 1, "lte": null }
 
 ---
 "Float range":
@@ -309,7 +309,7 @@ setup:
         id: "5"
   - match:
       _source:
-        float_range: { "gte": "-Infinity", "lte": 10.0 }
+        float_range: { "gte": null, "lte": 10.0 }
 
   - do:
       get:
@@ -317,7 +317,7 @@ setup:
         id: "6"
   - match:
       _source:
-        float_range: { "gte": 1.0, "lte": "Infinity" }
+        float_range: { "gte": 1.0, "lte": null }
 
 ---
 "Double range":
@@ -398,7 +398,7 @@ setup:
         id: "5"
   - match:
       _source:
-        double_range: { "gte": "-Infinity", "lte": 10.0 }
+        double_range: { "gte": null, "lte": 10.0 }
 
   - do:
       get:
@@ -406,7 +406,7 @@ setup:
         id: "6"
   - match:
       _source:
-        double_range: { "gte": 1.0, "lte": "Infinity" }
+        double_range: { "gte": 1.0, "lte": null }
 
 ---
 "IP range":
@@ -515,7 +515,7 @@ setup:
         id: "7"
   - match:
       _source:
-        ip_range: { "gte": "::", "lte": "10.10.10.10" }
+        ip_range: { "gte": null, "lte": "10.10.10.10" }
 
   - do:
       get:
@@ -523,7 +523,7 @@ setup:
         id: "8"
   - match:
       _source:
-        ip_range: { "gte": "2001:db8::", "lte": "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff" }
+        ip_range: { "gte": "2001:db8::", "lte": null }
 
 ---
 "Date range":
@@ -646,7 +646,7 @@ setup:
         id: "8"
   - match:
       _source:
-        date_range: { "gte": "-292275055-05-16T16:47:04.192Z", "lte": "2017-09-05T00:00:00.000Z" }
+        date_range: { "gte": null, "lte": "2017-09-05T00:00:00.000Z" }
 
   - do:
       get:
@@ -654,4 +654,4 @@ setup:
         id: "9"
   - match:
       _source:
-        date_range: { "gte": "2017-09-05T00:00:00.000Z", "lte": "+292278994-08-17T07:12:55.807Z" }
+        date_range: { "gte": "2017-09-05T00:00:00.000Z", "lte": null }

+ 12 - 0
server/src/main/java/org/elasticsearch/index/mapper/BinaryRangeUtil.java

@@ -102,6 +102,10 @@ enum BinaryRangeUtil {
         return decodeRanges(encodedRanges, RangeType.DATE, BinaryRangeUtil::decodeLong);
     }
 
+    static List<RangeFieldMapper.Range> decodeIntegerRanges(BytesRef encodedRanges) throws IOException {
+        return decodeRanges(encodedRanges, RangeType.INTEGER, BinaryRangeUtil::decodeInt);
+    }
+
     static List<RangeFieldMapper.Range> decodeRanges(
         BytesRef encodedRanges,
         RangeType rangeType,
@@ -184,6 +188,14 @@ enum BinaryRangeUtil {
         return encode(number, sign);
     }
 
+    static int decodeInt(byte[] bytes, int offset, int length) {
+        // We encode integers same as longs but we know
+        // that during parsing we got actual integers.
+        // So every decoded long should be inside the range of integers.
+        long longValue = decodeLong(bytes, offset, length);
+        return Math.toIntExact(longValue);
+    }
+
     static long decodeLong(byte[] bytes, int offset, int length) {
         boolean isNegative = (bytes[offset] & 128) == 0;
         // Start by masking off the last three bits of the first byte - that's the start of our number

+ 31 - 10
server/src/main/java/org/elasticsearch/index/mapper/RangeFieldMapper.java

@@ -558,21 +558,42 @@ public class RangeFieldMapper extends FieldMapper {
         public XContentBuilder toXContent(XContentBuilder builder, DateFormatter dateFormatter) throws IOException {
             builder.startObject();
 
-            if (includeFrom) {
-                builder.field("gte");
+            // Default range bounds for double and float ranges
+            // are infinities which are not valid inputs for range field.
+            // As such it is not possible to specify them manually,
+            // and they must come from defaults kicking in
+            // when the bound is null or not present.
+            // Therefore, range should be represented in that way in source too
+            // to enable reindexing.
+            //
+            // We apply this logic to all range types for consistency.
+            if (from.equals(type.minValue())) {
+                assert includeFrom : "Range bounds were not properly adjusted during parsing";
+                // Null value which will be parsed as a default
+                builder.nullField("gte");
             } else {
-                builder.field("gt");
+                if (includeFrom) {
+                    builder.field("gte");
+                } else {
+                    builder.field("gt");
+                }
+                var valueWithAdjustment = includeFrom ? from : type.nextDown(from);
+                builder.value(type.formatValue(valueWithAdjustment, dateFormatter));
             }
-            Object f = includeFrom || from.equals(type.minValue()) ? from : type.nextDown(from);
-            builder.value(type.formatValue(f, dateFormatter));
 
-            if (includeTo) {
-                builder.field("lte");
+            if (to.equals(type.maxValue())) {
+                assert includeTo : "Range bounds were not properly adjusted during parsing";
+                // Null value which will be parsed as a default
+                builder.nullField("lte");
             } else {
-                builder.field("lt");
+                if (includeTo) {
+                    builder.field("lte");
+                } else {
+                    builder.field("lt");
+                }
+                var valueWithAdjustment = includeTo ? to : type.nextUp(to);
+                builder.value(type.formatValue(valueWithAdjustment, dateFormatter));
             }
-            Object t = includeTo || to.equals(type.maxValue()) ? to : type.nextUp(to);
-            builder.value(type.formatValue(t, dateFormatter));
 
             builder.endObject();
 

+ 3 - 2
server/src/main/java/org/elasticsearch/index/mapper/RangeType.java

@@ -570,12 +570,13 @@ public enum RangeType {
 
         @Override
         public List<RangeFieldMapper.Range> decodeRanges(BytesRef bytes) throws IOException {
-            return LONG.decodeRanges(bytes);
+            return BinaryRangeUtil.decodeIntegerRanges(bytes);
         }
 
         @Override
         public Double doubleValue(Object endpointValue) {
-            return LONG.doubleValue(endpointValue);
+            assert endpointValue instanceof Integer;
+            return ((Integer) endpointValue).doubleValue();
         }
 
         @Override

+ 9 - 5
server/src/test/java/org/elasticsearch/index/mapper/DateRangeFieldMapperTests.java

@@ -16,8 +16,8 @@ import org.junit.AssumptionViolatedException;
 
 import java.io.IOException;
 import java.time.Instant;
+import java.util.HashMap;
 import java.util.Map;
-import java.util.stream.Collectors;
 
 import static org.hamcrest.Matchers.containsString;
 
@@ -89,11 +89,15 @@ public class DateRangeFieldMapperTests extends RangeFieldMapperTests {
             Object toExpectedSyntheticSource() {
                 Map<String, Object> expectedInMillis = (Map<String, Object>) super.toExpectedSyntheticSource();
 
-                return expectedInMillis.entrySet()
-                    .stream()
-                    .collect(
-                        Collectors.toMap(Map.Entry::getKey, e -> expectedDateFormatter.format(Instant.ofEpochMilli((long) e.getValue())))
+                Map<String, Object> expectedFormatted = new HashMap<>();
+                for (var entry : expectedInMillis.entrySet()) {
+                    expectedFormatted.put(
+                        entry.getKey(),
+                        entry.getValue() != null ? expectedDateFormatter.format(Instant.ofEpochMilli((long) entry.getValue())) : null
                     );
+                }
+
+                return expectedFormatted;
             }
         };
     }

+ 7 - 57
server/src/test/java/org/elasticsearch/index/mapper/DoubleRangeFieldMapperTests.java

@@ -8,69 +8,12 @@
 
 package org.elasticsearch.index.mapper;
 
-import org.elasticsearch.core.CheckedConsumer;
-import org.elasticsearch.xcontent.ToXContent;
 import org.elasticsearch.xcontent.XContentBuilder;
 import org.junit.AssumptionViolatedException;
 
 import java.io.IOException;
-import java.util.LinkedHashMap;
-import java.util.List;
-
-import static org.hamcrest.Matchers.equalTo;
 
 public class DoubleRangeFieldMapperTests extends RangeFieldMapperTests {
-
-    public void testSyntheticSourceDefaultValues() throws IOException {
-        // Default range ends for double are negative and positive infinity
-        // and they can not pass `roundTripSyntheticSource` test.
-
-        CheckedConsumer<XContentBuilder, IOException> mapping = b -> {
-            b.startObject("field");
-            minimalMapping(b);
-            b.endObject();
-        };
-
-        var inputValues = List.of(
-            (builder, params) -> builder.startObject().field("gte", (Double) null).field("lte", 10).endObject(),
-            (builder, params) -> builder.startObject().field("lte", 20).endObject(),
-            (builder, params) -> builder.startObject().field("gte", 10).field("lte", (Double) null).endObject(),
-            (builder, params) -> builder.startObject().field("gte", 20).endObject(),
-            (ToXContent) (builder, params) -> builder.startObject().endObject()
-        );
-
-        var expected = List.of(new LinkedHashMap<>() {
-            {
-                put("gte", "-Infinity");
-                put("lte", 10.0);
-            }
-        }, new LinkedHashMap<>() {
-            {
-                put("gte", "-Infinity");
-                put("lte", 20.0);
-            }
-        }, new LinkedHashMap<>() {
-            {
-                put("gte", "-Infinity");
-                put("lte", "Infinity");
-            }
-        }, new LinkedHashMap<>() {
-            {
-                put("gte", 10.0);
-                put("lte", "Infinity");
-            }
-        }, new LinkedHashMap<>() {
-            {
-                put("gte", 20.0);
-                put("lte", "Infinity");
-            }
-        });
-
-        var source = getSourceFor(mapping, inputValues);
-        var actual = source.source().get("field");
-        assertThat(actual, equalTo(expected));
-    }
-
     @Override
     protected XContentBuilder rangeSource(XContentBuilder in) throws IOException {
         return rangeSource(in, "0.5", "2.7");
@@ -103,6 +46,13 @@ public class DoubleRangeFieldMapperTests extends RangeFieldMapperTests {
         var includeTo = randomBoolean();
         Double to = randomDoubleBetween(from, Double.MAX_VALUE, false);
 
+        if (rarely()) {
+            from = null;
+        }
+        if (rarely()) {
+            to = null;
+        }
+
         return new TestRange<>(rangeType(), from, to, includeFrom, includeTo);
     }
 

+ 7 - 57
server/src/test/java/org/elasticsearch/index/mapper/FloatRangeFieldMapperTests.java

@@ -8,69 +8,12 @@
 
 package org.elasticsearch.index.mapper;
 
-import org.elasticsearch.core.CheckedConsumer;
-import org.elasticsearch.xcontent.ToXContent;
 import org.elasticsearch.xcontent.XContentBuilder;
 import org.junit.AssumptionViolatedException;
 
 import java.io.IOException;
-import java.util.LinkedHashMap;
-import java.util.List;
-
-import static org.hamcrest.Matchers.equalTo;
 
 public class FloatRangeFieldMapperTests extends RangeFieldMapperTests {
-
-    public void testSyntheticSourceDefaultValues() throws IOException {
-        // Default range ends for float are negative and positive infinity
-        // and they can not pass `roundTripSyntheticSource` test.
-
-        CheckedConsumer<XContentBuilder, IOException> mapping = b -> {
-            b.startObject("field");
-            minimalMapping(b);
-            b.endObject();
-        };
-
-        var inputValues = List.of(
-            (builder, params) -> builder.startObject().field("gte", (Float) null).field("lte", 10).endObject(),
-            (builder, params) -> builder.startObject().field("lte", 20).endObject(),
-            (builder, params) -> builder.startObject().field("gte", 10).field("lte", (Float) null).endObject(),
-            (builder, params) -> builder.startObject().field("gte", 20).endObject(),
-            (ToXContent) (builder, params) -> builder.startObject().endObject()
-        );
-
-        var expected = List.of(new LinkedHashMap<>() {
-            {
-                put("gte", "-Infinity");
-                put("lte", 10.0);
-            }
-        }, new LinkedHashMap<>() {
-            {
-                put("gte", "-Infinity");
-                put("lte", 20.0);
-            }
-        }, new LinkedHashMap<>() {
-            {
-                put("gte", "-Infinity");
-                put("lte", "Infinity");
-            }
-        }, new LinkedHashMap<>() {
-            {
-                put("gte", 10.0);
-                put("lte", "Infinity");
-            }
-        }, new LinkedHashMap<>() {
-            {
-                put("gte", 20.0);
-                put("lte", "Infinity");
-            }
-        });
-
-        var source = getSourceFor(mapping, inputValues);
-        var actual = source.source().get("field");
-        assertThat(actual, equalTo(expected));
-    }
-
     @Override
     protected XContentBuilder rangeSource(XContentBuilder in) throws IOException {
         return rangeSource(in, "0.5", "2.7");
@@ -103,6 +46,13 @@ public class FloatRangeFieldMapperTests extends RangeFieldMapperTests {
         var includeTo = randomBoolean();
         Float to = (float) randomDoubleBetween(from + Math.ulp(from), Float.MAX_VALUE, true);
 
+        if (rarely()) {
+            from = null;
+        }
+        if (rarely()) {
+            to = null;
+        }
+
         return new TestRange<>(rangeType(), from, to, includeFrom, includeTo);
     }
 

+ 60 - 13
server/src/test/java/org/elasticsearch/index/mapper/IpRangeFieldMapperTests.java

@@ -113,15 +113,16 @@ public class IpRangeFieldMapperTests extends RangeFieldMapperTests {
         // So this assert needs to be not sensitive to order and in "reference"
         // implementation of tests from MapperTestCase it is.
         var actual = source.source().get("field");
-        if (inputValues.size() == 1) {
+        var expected = new HashSet<>(expectedValues);
+        if (expected.size() == 1) {
             assertEquals(expectedValues.get(0), actual);
         } else {
             assertThat(actual, instanceOf(List.class));
-            assertTrue(((List<Object>) actual).containsAll(new HashSet<>(expectedValues)));
+            assertTrue(((List<Object>) actual).containsAll(expected));
         }
     }
 
-    private Tuple<Object, Object> generateValue() {
+    private Tuple<Object, Map<String, Object>> generateValue() {
         String cidr = randomCidrBlock();
         InetAddresses.IpRange range = InetAddresses.parseIpRangeFromCidr(cidr);
 
@@ -134,27 +135,73 @@ public class IpRangeFieldMapperTests extends RangeFieldMapperTests {
         if (randomBoolean()) {
             // CIDRs are always inclusive ranges.
             input = cidr;
-            output.put("gte", InetAddresses.toAddrString(range.lowerBound()));
-            output.put("lte", InetAddresses.toAddrString(range.upperBound()));
+
+            var from = InetAddresses.toAddrString(range.lowerBound());
+            inclusiveFrom(output, from);
+
+            var to = InetAddresses.toAddrString(range.upperBound());
+            inclusiveTo(output, to);
         } else {
             var fromKey = includeFrom ? "gte" : "gt";
             var toKey = includeTo ? "lte" : "lt";
             var from = rarely() ? null : InetAddresses.toAddrString(range.lowerBound());
             var to = rarely() ? null : InetAddresses.toAddrString(range.upperBound());
-            input = (ToXContent) (builder, params) -> builder.startObject().field(fromKey, from).field(toKey, to).endObject();
-
-            var rawFrom = from != null ? range.lowerBound() : (InetAddress) rangeType().minValue();
-            var adjustedFrom = includeFrom ? rawFrom : (InetAddress) rangeType().nextUp(rawFrom);
-            output.put("gte", InetAddresses.toAddrString(adjustedFrom));
+            input = (ToXContent) (builder, params) -> {
+                builder.startObject();
+                if (includeFrom && from == null && randomBoolean()) {
+                    // skip field entirely since it is equivalent to a default value
+                } else {
+                    builder.field(fromKey, from);
+                }
+
+                if (includeTo && to == null && randomBoolean()) {
+                    // skip field entirely since it is equivalent to a default value
+                } else {
+                    builder.field(toKey, to);
+                }
+
+                return builder.endObject();
+            };
+
+            if (includeFrom) {
+                inclusiveFrom(output, from);
+            } else {
+                var fromWithDefaults = from != null ? range.lowerBound() : (InetAddress) rangeType().minValue();
+                var adjustedFrom = (InetAddress) rangeType().nextUp(fromWithDefaults);
+                output.put("gte", InetAddresses.toAddrString(adjustedFrom));
+            }
 
-            var rawTo = to != null ? range.upperBound() : (InetAddress) rangeType().maxValue();
-            var adjustedTo = includeTo ? rawTo : (InetAddress) rangeType().nextDown(rawTo);
-            output.put("lte", InetAddresses.toAddrString(adjustedTo));
+            if (includeTo) {
+                inclusiveTo(output, to);
+            } else {
+                var toWithDefaults = to != null ? range.upperBound() : (InetAddress) rangeType().maxValue();
+                var adjustedTo = (InetAddress) rangeType().nextDown(toWithDefaults);
+                output.put("lte", InetAddresses.toAddrString(adjustedTo));
+            }
         }
 
         return Tuple.tuple(input, output);
     }
 
+    private void inclusiveFrom(Map<String, Object> output, String from) {
+        // This is helpful since different representations can map to "::"
+        var normalizedMin = InetAddresses.toAddrString((InetAddress) rangeType().minValue());
+        if (from != null && from.equals(normalizedMin) == false) {
+            output.put("gte", from);
+        } else {
+            output.put("gte", null);
+        }
+    }
+
+    private void inclusiveTo(Map<String, Object> output, String to) {
+        var normalizedMax = InetAddresses.toAddrString((InetAddress) rangeType().maxValue());
+        if (to != null && to.equals(normalizedMax) == false) {
+            output.put("lte", to);
+        } else {
+            output.put("lte", null);
+        }
+    }
+
     public void testInvalidSyntheticSource() {
         Exception e = expectThrows(IllegalArgumentException.class, () -> createDocumentMapper(syntheticSourceMapping(b -> {
             b.startObject("field");

+ 28 - 5
server/src/test/java/org/elasticsearch/index/mapper/RangeFieldMapperTests.java

@@ -331,7 +331,22 @@ public abstract class RangeFieldMapperTests extends MapperTestCase {
             var fromKey = includeFrom ? "gte" : "gt";
             var toKey = includeTo ? "lte" : "lt";
 
-            return (ToXContent) (builder, params) -> builder.startObject().field(fromKey, from).field(toKey, to).endObject();
+            return (ToXContent) (builder, params) -> {
+                builder.startObject();
+                if (includeFrom && from == null && randomBoolean()) {
+                    // skip field entirely since it is equivalent to a default value
+                } else {
+                    builder.field(fromKey, from);
+                }
+
+                if (includeTo && to == null && randomBoolean()) {
+                    // skip field entirely since it is equivalent to a default value
+                } else {
+                    builder.field(toKey, to);
+                }
+
+                return builder.endObject();
+            };
         }
 
         Object toExpectedSyntheticSource() {
@@ -339,17 +354,25 @@ public abstract class RangeFieldMapperTests extends MapperTestCase {
             // Also, "to" field always comes first.
             Map<String, Object> output = new LinkedHashMap<>();
 
-            var fromWithDefaults = from != null ? from : rangeType().minValue();
             if (includeFrom) {
-                output.put("gte", fromWithDefaults);
+                if (from == null || from == rangeType().minValue()) {
+                    output.put("gte", null);
+                } else {
+                    output.put("gte", from);
+                }
             } else {
+                var fromWithDefaults = from != null ? from : rangeType().minValue();
                 output.put("gte", type.nextUp(fromWithDefaults));
             }
 
-            var toWithDefaults = to != null ? to : rangeType().maxValue();
             if (includeTo) {
-                output.put("lte", toWithDefaults);
+                if (to == null || to == rangeType().maxValue()) {
+                    output.put("lte", null);
+                } else {
+                    output.put("lte", to);
+                }
             } else {
+                var toWithDefaults = to != null ? to : rangeType().maxValue();
                 output.put("lte", type.nextDown(toWithDefaults));
             }
 

+ 90 - 0
server/src/test/java/org/elasticsearch/search/aggregations/bucket/histogram/RangeHistogramAggregatorTests.java

@@ -81,6 +81,51 @@ public class RangeHistogramAggregatorTests extends AggregatorTestCase {
         }
     }
 
+    public void testFloats() throws Exception {
+        RangeType rangeType = RangeType.FLOAT;
+        try (Directory dir = newDirectory(); RandomIndexWriter w = new RandomIndexWriter(random(), dir)) {
+            for (RangeFieldMapper.Range range : new RangeFieldMapper.Range[] {
+                new RangeFieldMapper.Range(rangeType, 1.0f, 5.0f, true, true), // bucket 0 5
+                new RangeFieldMapper.Range(rangeType, -3.1f, 4.2f, true, true), // bucket -5, 0
+                new RangeFieldMapper.Range(rangeType, 4.2f, 13.3f, true, true), // bucket 0, 5, 10
+                new RangeFieldMapper.Range(rangeType, 22.5f, 29.3f, true, true), // bucket 20, 25
+            }) {
+                Document doc = new Document();
+                BytesRef encodedRange = rangeType.encodeRanges(Collections.singleton(range));
+                doc.add(new BinaryDocValuesField("field", encodedRange));
+                w.addDocument(doc);
+            }
+
+            HistogramAggregationBuilder aggBuilder = new HistogramAggregationBuilder("my_agg").field("field").interval(5);
+
+            try (IndexReader reader = w.getReader()) {
+                InternalHistogram histogram = searchAndReduce(reader, new AggTestConfig(aggBuilder, rangeField("field", rangeType)));
+                assertEquals(7, histogram.getBuckets().size());
+
+                assertEquals(-5d, histogram.getBuckets().get(0).getKey());
+                assertEquals(1, histogram.getBuckets().get(0).getDocCount());
+
+                assertEquals(0d, histogram.getBuckets().get(1).getKey());
+                assertEquals(3, histogram.getBuckets().get(1).getDocCount());
+
+                assertEquals(5d, histogram.getBuckets().get(2).getKey());
+                assertEquals(2, histogram.getBuckets().get(2).getDocCount());
+
+                assertEquals(10d, histogram.getBuckets().get(3).getKey());
+                assertEquals(1, histogram.getBuckets().get(3).getDocCount());
+
+                assertEquals(15d, histogram.getBuckets().get(4).getKey());
+                assertEquals(0, histogram.getBuckets().get(4).getDocCount());
+
+                assertEquals(20d, histogram.getBuckets().get(5).getKey());
+                assertEquals(1, histogram.getBuckets().get(5).getDocCount());
+
+                assertEquals(25d, histogram.getBuckets().get(6).getKey());
+                assertEquals(1, histogram.getBuckets().get(6).getDocCount());
+            }
+        }
+    }
+
     public void testLongs() throws Exception {
         RangeType rangeType = RangeType.LONG;
         try (Directory dir = newDirectory(); RandomIndexWriter w = new RandomIndexWriter(random(), dir)) {
@@ -126,6 +171,51 @@ public class RangeHistogramAggregatorTests extends AggregatorTestCase {
         }
     }
 
+    public void testInts() throws Exception {
+        RangeType rangeType = RangeType.INTEGER;
+        try (Directory dir = newDirectory(); RandomIndexWriter w = new RandomIndexWriter(random(), dir)) {
+            for (RangeFieldMapper.Range range : new RangeFieldMapper.Range[] {
+                new RangeFieldMapper.Range(rangeType, 1, 5, true, true), // bucket 0 5
+                new RangeFieldMapper.Range(rangeType, -3, 4, true, true), // bucket -5, 0
+                new RangeFieldMapper.Range(rangeType, 4, 13, true, true), // bucket 0, 5, 10
+                new RangeFieldMapper.Range(rangeType, 22, 29, true, true), // bucket 20, 25
+            }) {
+                Document doc = new Document();
+                BytesRef encodedRange = rangeType.encodeRanges(Collections.singleton(range));
+                doc.add(new BinaryDocValuesField("field", encodedRange));
+                w.addDocument(doc);
+            }
+
+            HistogramAggregationBuilder aggBuilder = new HistogramAggregationBuilder("my_agg").field("field").interval(5);
+
+            try (IndexReader reader = w.getReader()) {
+                InternalHistogram histogram = searchAndReduce(reader, new AggTestConfig(aggBuilder, rangeField("field", rangeType)));
+                assertEquals(7, histogram.getBuckets().size());
+
+                assertEquals(-5d, histogram.getBuckets().get(0).getKey());
+                assertEquals(1, histogram.getBuckets().get(0).getDocCount());
+
+                assertEquals(0d, histogram.getBuckets().get(1).getKey());
+                assertEquals(3, histogram.getBuckets().get(1).getDocCount());
+
+                assertEquals(5d, histogram.getBuckets().get(2).getKey());
+                assertEquals(2, histogram.getBuckets().get(2).getDocCount());
+
+                assertEquals(10d, histogram.getBuckets().get(3).getKey());
+                assertEquals(1, histogram.getBuckets().get(3).getDocCount());
+
+                assertEquals(15d, histogram.getBuckets().get(4).getKey());
+                assertEquals(0, histogram.getBuckets().get(4).getDocCount());
+
+                assertEquals(20d, histogram.getBuckets().get(5).getKey());
+                assertEquals(1, histogram.getBuckets().get(5).getDocCount());
+
+                assertEquals(25d, histogram.getBuckets().get(6).getKey());
+                assertEquals(1, histogram.getBuckets().get(6).getDocCount());
+            }
+        }
+    }
+
     public void testMultipleRanges() throws Exception {
         RangeType rangeType = RangeType.LONG;
         try (Directory dir = newDirectory(); RandomIndexWriter w = new RandomIndexWriter(random(), dir)) {