You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@lucene.apache.org by dn...@apache.org on 2020/02/20 18:21:20 UTC

[lucene-solr] branch master updated: LUCENE-9228: Sort dvUpdates by terms before apply

This is an automated email from the ASF dual-hosted git repository.

dnhatn pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/lucene-solr.git


The following commit(s) were added to refs/heads/master by this push:
     new a0b8f5c  LUCENE-9228: Sort dvUpdates by terms before apply
a0b8f5c is described below

commit a0b8f5c7c28bd897bdf48ed7af20b385a236b3a9
Author: Nhat Nguyen <nh...@elastic.co>
AuthorDate: Thu Feb 20 11:47:35 2020 -0500

    LUCENE-9228: Sort dvUpdates by terms before apply
    
    With this change, we sort dvUpdates in the term order before applying if
    they all update a single field to the same value. This optimization can
    reduce the flush time by around 20% for the docValues update user cases.
---
 lucene/CHANGES.txt                                 |  4 +
 .../apache/lucene/index/FieldUpdatesBuffer.java    | 57 +++++++++++--
 .../apache/lucene/index/FrozenBufferedUpdates.java |  6 +-
 .../java/org/apache/lucene/util/BytesRefArray.java | 83 ++++++++++++++++---
 .../lucene/index/TestFieldUpdatesBuffer.java       | 93 +++++++++++++++++-----
 5 files changed, 204 insertions(+), 39 deletions(-)

diff --git a/lucene/CHANGES.txt b/lucene/CHANGES.txt
index 733e367..fa600e2 100644
--- a/lucene/CHANGES.txt
+++ b/lucene/CHANGES.txt
@@ -162,6 +162,10 @@ Improvements
 * LUCENE-9194: Simplify XYShapeXQuery API by adding a new abstract class called XYGeometry. Queries are
   executed with input objects that extend such interface. (Ignacio Vera)
 
+* LUCENE-9228: Sort dvUpdates in the term order before applying if they all update a
+  single field to the same value. This optimization can reduce the flush time by around
+  20% for the docValues update user cases. (Nhat Nguyen, Adrien Grand, Simon Willnauer)
+
 Optimizations
 ---------------------
 
diff --git a/lucene/core/src/java/org/apache/lucene/index/FieldUpdatesBuffer.java b/lucene/core/src/java/org/apache/lucene/index/FieldUpdatesBuffer.java
index c91f650..45bb729 100644
--- a/lucene/core/src/java/org/apache/lucene/index/FieldUpdatesBuffer.java
+++ b/lucene/core/src/java/org/apache/lucene/index/FieldUpdatesBuffer.java
@@ -19,6 +19,7 @@ package org.apache.lucene.index;
 
 import java.io.IOException;
 import java.util.Arrays;
+import java.util.Comparator;
 
 import org.apache.lucene.util.ArrayUtil;
 import org.apache.lucene.util.Bits;
@@ -51,6 +52,7 @@ final class FieldUpdatesBuffer {
   // on CPU for those. We also save on not needing to sort in order to apply the terms in order
   // since by definition we store them in order.
   private final BytesRefArray termValues;
+  private BytesRefArray.SortState termSortState;
   private final BytesRefArray byteValues; // this will be null if we are buffering numerics
   private int[] docsUpTo;
   private long[] numericValues; // this will be null if we are buffering binaries
@@ -59,6 +61,7 @@ final class FieldUpdatesBuffer {
   private long minNumeric = Long.MAX_VALUE;
   private String[] fields;
   private final boolean isNumeric;
+  private boolean finished = false;
 
   private FieldUpdatesBuffer(Counter bytesUsed, DocValuesUpdate initialValue, int docUpTo, boolean isNumeric) {
     this.bytesUsed = bytesUsed;
@@ -115,6 +118,7 @@ final class FieldUpdatesBuffer {
   }
 
   void add(String field, int docUpTo, int ord, boolean hasValue) {
+    assert finished == false : "buffer was finished already";
     if (fields[0].equals(field) == false || fields.length != 1 ) {
       if (fields.length <= ord) {
         String[] array = ArrayUtil.grow(fields, ord+1);
@@ -195,7 +199,26 @@ final class FieldUpdatesBuffer {
     return numUpdates++;
   }
 
+  void finish() {
+    if (finished) {
+      throw new IllegalStateException("buffer was finished already");
+    }
+    finished = true;
+    final boolean sortedTerms = hasSingleValue() && hasValues == null && fields.length == 1;
+    if (sortedTerms) {
+      // sort by ascending by term, then sort descending by docsUpTo so that we can skip updates with lower docUpTo.
+      termSortState = termValues.sort(Comparator.naturalOrder(),
+          (i1, i2) -> Integer.compare(
+              docsUpTo[getArrayIndex(docsUpTo.length, i2)],
+              docsUpTo[getArrayIndex(docsUpTo.length, i1)]));
+      bytesUsed.addAndGet(termSortState.ramBytesUsed());
+    }
+  }
+
   BufferedUpdateIterator iterator() {
+    if (finished == false) {
+      throw new IllegalStateException("buffer is not finished yet");
+    }
     return new BufferedUpdateIterator();
   }
 
@@ -264,26 +287,36 @@ final class FieldUpdatesBuffer {
    * An iterator that iterates over all updates in insertion order
    */
   class BufferedUpdateIterator {
-    private final BytesRefIterator termValuesIterator;
+    private final BytesRefArray.IndexedBytesRefIterator termValuesIterator;
+    private final BytesRefArray.IndexedBytesRefIterator lookAheadTermIterator;
     private final BytesRefIterator byteValuesIterator;
     private final BufferedUpdate bufferedUpdate = new BufferedUpdate();
     private final Bits updatesWithValue;
-    private int index = 0;
 
     BufferedUpdateIterator() {
-      this.termValuesIterator = termValues.iterator();
+      this.termValuesIterator = termValues.iterator(termSortState);
+      this.lookAheadTermIterator = termSortState != null ? termValues.iterator(termSortState) : null;
       this.byteValuesIterator = isNumeric ? null : byteValues.iterator();
       updatesWithValue = hasValues == null ? new Bits.MatchAllBits(numUpdates) : hasValues;
     }
 
     /**
+     * If all updates update a single field to the same value, then we can apply these
+     * updates in the term order instead of the request order as both will yield the same result.
+     * This optimization allows us to iterate the term dictionary faster and de-duplicate updates.
+     */
+    boolean isSortedTerms() {
+      return termSortState != null;
+    }
+
+    /**
      * Moves to the next BufferedUpdate or return null if all updates are consumed.
      * The returned instance is a shared instance and must be fully consumed before the next call to this method.
      */
     BufferedUpdate next() throws IOException {
-      BytesRef next = termValuesIterator.next();
+      BytesRef next = nextTerm();
       if (next != null) {
-        final int idx = index++;
+        final int idx = termValuesIterator.ord();
         bufferedUpdate.termValue = next;
         bufferedUpdate.hasValue = updatesWithValue.get(idx);
         bufferedUpdate.termField = fields[getArrayIndex(fields.length, idx)];
@@ -304,6 +337,20 @@ final class FieldUpdatesBuffer {
         return null;
       }
     }
+
+    BytesRef nextTerm() throws IOException {
+      if (lookAheadTermIterator != null) {
+        final BytesRef lastTerm = bufferedUpdate.termValue;
+        BytesRef lookAheadTerm;
+        while ((lookAheadTerm = lookAheadTermIterator.next()) != null && lookAheadTerm.equals(lastTerm)) {
+          BytesRef discardedTerm = termValuesIterator.next(); // discard as the docUpTo of the previous update is higher
+          assert discardedTerm.equals(lookAheadTerm) : "[" + discardedTerm + "] != [" + lookAheadTerm + "]";
+          assert docsUpTo[getArrayIndex(docsUpTo.length, termValuesIterator.ord())] <= bufferedUpdate.docUpTo :
+              docsUpTo[getArrayIndex(docsUpTo.length, termValuesIterator.ord())] + ">" + bufferedUpdate.docUpTo;
+        }
+      }
+      return termValuesIterator.next();
+    }
   }
 
   private static int getArrayIndex(int arrayLength, int index) {
diff --git a/lucene/core/src/java/org/apache/lucene/index/FrozenBufferedUpdates.java b/lucene/core/src/java/org/apache/lucene/index/FrozenBufferedUpdates.java
index e58114d..bc7e6c2 100644
--- a/lucene/core/src/java/org/apache/lucene/index/FrozenBufferedUpdates.java
+++ b/lucene/core/src/java/org/apache/lucene/index/FrozenBufferedUpdates.java
@@ -109,6 +109,7 @@ final class FrozenBufferedUpdates {
     // so that it maps to all fields it affects, sorted by their docUpto, and traverse
     // that Term only once, applying the update to all fields that still need to be
     // updated.
+    updates.fieldUpdates.values().forEach(FieldUpdatesBuffer::finish);
     this.fieldUpdates = Map.copyOf(updates.fieldUpdates);
     this.fieldUpdatesCount = updates.numFieldUpdates.get();
 
@@ -490,7 +491,7 @@ final class FrozenBufferedUpdates {
       boolean isNumeric = value.isNumeric();
       FieldUpdatesBuffer.BufferedUpdateIterator iterator = value.iterator();
       FieldUpdatesBuffer.BufferedUpdate bufferedUpdate;
-      TermDocsIterator termDocsIterator = new TermDocsIterator(segState.reader, false);
+      TermDocsIterator termDocsIterator = new TermDocsIterator(segState.reader, iterator.isSortedTerms());
       while ((bufferedUpdate = iterator.next()) != null) {
         // TODO: we traverse the terms in update order (not term order) so that we
         // apply the updates in the correct order, i.e. if two terms update the
@@ -520,7 +521,6 @@ final class FrozenBufferedUpdates {
             longValue = bufferedUpdate.numericValue;
             binaryValue = bufferedUpdate.binaryValue;
           }
-           termDocsIterator.getDocs();
           if (dvUpdates == null) {
             if (isNumeric) {
               if (value.hasSingleValue()) {
@@ -824,7 +824,7 @@ final class FrozenBufferedUpdates {
             return null; // requested term does not exist in this segment
           } else if (cmp == 0) {
             return getDocs();
-          } else if (cmp > 0) {
+          } else {
             TermsEnum.SeekStatus status = termsEnum.seekCeil(term);
             switch (status) {
               case FOUND:
diff --git a/lucene/core/src/java/org/apache/lucene/util/BytesRefArray.java b/lucene/core/src/java/org/apache/lucene/util/BytesRefArray.java
index cb0b3e3..f0ee909 100644
--- a/lucene/core/src/java/org/apache/lucene/util/BytesRefArray.java
+++ b/lucene/core/src/java/org/apache/lucene/util/BytesRefArray.java
@@ -19,6 +19,7 @@ package org.apache.lucene.util;
 import java.util.Arrays;
 import java.util.Comparator;
 import java.util.Objects;
+import java.util.function.IntBinaryOperator;
 
 /**
  * A simple append only random-access {@link BytesRef} array that stores full
@@ -117,8 +118,12 @@ public final class BytesRefArray implements SortableBytesRefArray {
     }
     pool.setBytesRef(spare, result, offset, length);
   }
-  
-  private int[] sort(final Comparator<BytesRef> comp) {
+
+
+  /**
+   * Returns a {@link SortState} representing the order of elements in this array. This is a non-destructive operation.
+   */
+  public SortState sort(final Comparator<BytesRef> comp, final IntBinaryOperator tieComparator) {
     final int[] orderedEntries = new int[size()];
     for (int i = 0; i < orderedEntries.length; i++) {
       orderedEntries[i] = i;
@@ -136,22 +141,28 @@ public final class BytesRefArray implements SortableBytesRefArray {
         final int idx1 = orderedEntries[i], idx2 = orderedEntries[j];
         setBytesRef(scratch1, scratchBytes1, idx1);
         setBytesRef(scratch2, scratchBytes2, idx2);
-        return comp.compare(scratchBytes1, scratchBytes2);
+        return compare(idx1, scratchBytes1, idx2, scratchBytes2);
       }
       
       @Override
       protected void setPivot(int i) {
-        final int index = orderedEntries[i];
-        setBytesRef(pivotBuilder, pivot, index);
+        pivotIndex = orderedEntries[i];
+        setBytesRef(pivotBuilder, pivot, pivotIndex);
       }
       
       @Override
       protected int comparePivot(int j) {
         final int index = orderedEntries[j];
         setBytesRef(scratch2, scratchBytes2, index);
-        return comp.compare(pivot, scratchBytes2);
+        return compare(pivotIndex, pivot, index, scratchBytes2);
+      }
+
+      private int compare(int i1, BytesRef b1, int i2, BytesRef b2) {
+        int res = comp.compare(b1, b2);
+        return res == 0 ? tieComparator.applyAsInt(i1, i2) : res;
       }
 
+      private int pivotIndex;
       private final BytesRef pivot = new BytesRef();
       private final BytesRef scratchBytes1 = new BytesRef();
       private final BytesRef scratchBytes2 = new BytesRef();
@@ -159,14 +170,14 @@ public final class BytesRefArray implements SortableBytesRefArray {
       private final BytesRefBuilder scratch1 = new BytesRefBuilder();
       private final BytesRefBuilder scratch2 = new BytesRefBuilder();
     }.sort(0, size());
-    return orderedEntries;
+    return new SortState(orderedEntries);
   }
   
   /**
    * sugar for {@link #iterator(Comparator)} with a <code>null</code> comparator
    */
   public BytesRefIterator iterator() {
-    return iterator(null);
+    return iterator((SortState) null);
   }
   
   /**
@@ -185,20 +196,66 @@ public final class BytesRefArray implements SortableBytesRefArray {
    */
   @Override
   public BytesRefIterator iterator(final Comparator<BytesRef> comp) {
+    return iterator(sort(comp, (i, j) -> 0));
+  }
+
+  /**
+   * Returns an {@link IndexedBytesRefIterator} with point in time semantics. The iterator provides access to all
+   * so far appended {@link BytesRef} instances. If a non-null sortState is specified then the iterator will iterate
+   * the byte values in the order of the sortState; otherwise, the order is the same as the values were appended.
+   */
+  public IndexedBytesRefIterator iterator(final SortState sortState) {
+    final int size = size();
+    final int[] indices = sortState == null ? null : sortState.indices;
+    assert indices == null || indices.length == size : indices.length + " != " + size;
     final BytesRefBuilder spare = new BytesRefBuilder();
     final BytesRef result = new BytesRef();
-    final int size = size();
-    final int[] indices = comp == null ? null : sort(comp);
-    return new BytesRefIterator() {
-      int pos = 0;
+
+    return new IndexedBytesRefIterator() {
+      int pos = -1;
+      int ord = 0;
       @Override
       public BytesRef next() {
+        ++pos;
         if (pos < size) {
-          setBytesRef(spare, result, indices == null ? pos++ : indices[pos++]);
+          ord = indices == null ? pos : indices[pos];
+          setBytesRef(spare, result, ord);
           return result;
         }
         return null;
       }
+
+      @Override
+      public int ord() {
+        return ord;
+      }
     };
   }
+
+  /**
+   * Used to iterate the elements of an array in a given order.
+   */
+  public final static class SortState implements Accountable {
+    private final int[] indices;
+
+    private SortState(int[] indices) {
+      this.indices = indices;
+    }
+
+    @Override
+    public long ramBytesUsed() {
+      return RamUsageEstimator.NUM_BYTES_ARRAY_HEADER + indices.length * Integer.BYTES;
+    }
+  }
+
+  /**
+   * An extension of {@link BytesRefIterator} that allows retrieving the index of the current element
+   */
+  public interface IndexedBytesRefIterator extends BytesRefIterator {
+    /**
+     * Returns the ordinal position of the element that was returned in the latest call of {@link #next()}.
+     * Do not call this method if {@link #next()} is not called yet or the last call returned a null value.
+     */
+    int ord();
+  }
 }
diff --git a/lucene/core/src/test/org/apache/lucene/index/TestFieldUpdatesBuffer.java b/lucene/core/src/test/org/apache/lucene/index/TestFieldUpdatesBuffer.java
index a63c107..2d74b73 100644
--- a/lucene/core/src/test/org/apache/lucene/index/TestFieldUpdatesBuffer.java
+++ b/lucene/core/src/test/org/apache/lucene/index/TestFieldUpdatesBuffer.java
@@ -20,7 +20,10 @@ package org.apache.lucene.index;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Comparator;
 import java.util.List;
+import java.util.SortedMap;
+import java.util.TreeMap;
 
 import com.carrotsearch.randomizedtesting.generators.RandomPicks;
 import org.apache.lucene.util.BytesRef;
@@ -46,6 +49,7 @@ public class TestFieldUpdatesBuffer extends LuceneTestCase {
     assertTrue(buffer.isNumeric());
     assertEquals(13, buffer.getMaxNumeric());
     assertEquals(6, buffer.getMinNumeric());
+    buffer.finish();
     FieldUpdatesBuffer.BufferedUpdateIterator iterator = buffer.iterator();
     FieldUpdatesBuffer.BufferedUpdate value = iterator.next();
     assertNotNull(value);
@@ -99,6 +103,7 @@ public class TestFieldUpdatesBuffer extends LuceneTestCase {
       buffer.addNoValue(new Term("id", "3"), Integer.MAX_VALUE);
     }
     buffer.addUpdate(new Term("id", "4"), intValue, Integer.MAX_VALUE);
+    buffer.finish();
     FieldUpdatesBuffer.BufferedUpdateIterator iterator = buffer.iterator();
     FieldUpdatesBuffer.BufferedUpdate value;
     int count = 0;
@@ -131,6 +136,7 @@ public class TestFieldUpdatesBuffer extends LuceneTestCase {
       buffer.addNoValue(new Term("id", "3"), Integer.MAX_VALUE);
     }
     buffer.addUpdate(new Term("id", "4"), new BytesRef(""), Integer.MAX_VALUE);
+    buffer.finish();
     FieldUpdatesBuffer.BufferedUpdateIterator iterator = buffer.iterator();
     FieldUpdatesBuffer.BufferedUpdate value;
     int count = 0;
@@ -149,12 +155,20 @@ public class TestFieldUpdatesBuffer extends LuceneTestCase {
     assertFalse(buffer.isNumeric());
   }
 
+  int randomDocUpTo() {
+    if (random().nextInt(5) == 0) {
+      return Integer.MAX_VALUE;
+    } else {
+      return random().nextInt(10000);
+    }
+  }
+
   DocValuesUpdate.BinaryDocValuesUpdate getRandomBinaryUpdate() {
     String termField = RandomPicks.randomFrom(random(), Arrays.asList("id", "_id", "some_other_field"));
     String docId = "" + random().nextInt(10);
-    DocValuesUpdate.BinaryDocValuesUpdate value =  new DocValuesUpdate.BinaryDocValuesUpdate(new Term(termField, docId), "binary",
+    DocValuesUpdate.BinaryDocValuesUpdate value = new DocValuesUpdate.BinaryDocValuesUpdate(new Term(termField, docId), "binary",
         rarely() ? null : new BytesRef(TestUtil.randomRealisticUnicodeString(random())));
-    return rarely() ? value.prepareForApply(random().nextInt(100)) : value;
+    return rarely() ? value.prepareForApply(randomDocUpTo()) : value;
   }
 
   DocValuesUpdate.NumericDocValuesUpdate getRandomNumericUpdate() {
@@ -162,7 +176,7 @@ public class TestFieldUpdatesBuffer extends LuceneTestCase {
     String docId = "" + random().nextInt(10);
     DocValuesUpdate.NumericDocValuesUpdate value = new DocValuesUpdate.NumericDocValuesUpdate(new Term(termField, docId), "numeric",
         rarely() ? null : Long.valueOf(random().nextInt(100)));
-    return rarely() ? value.prepareForApply(random().nextInt(100)) : value;
+    return rarely() ? value.prepareForApply(randomDocUpTo()) : value;
   }
 
   public void testBinaryRandom() throws IOException {
@@ -181,6 +195,7 @@ public class TestFieldUpdatesBuffer extends LuceneTestCase {
         buffer.addNoValue(randomUpdate.term, randomUpdate.docIDUpto);
       }
     }
+    buffer.finish();
     FieldUpdatesBuffer.BufferedUpdateIterator iterator = buffer.iterator();
     FieldUpdatesBuffer.BufferedUpdate value;
 
@@ -216,6 +231,55 @@ public class TestFieldUpdatesBuffer extends LuceneTestCase {
         buffer.addNoValue(randomUpdate.term, randomUpdate.docIDUpto);
       }
     }
+    buffer.finish();
+    DocValuesUpdate.NumericDocValuesUpdate lastUpdate = randomUpdate;
+    boolean termsSorted = lastUpdate.hasValue && updates.stream()
+        .allMatch(update -> update.field.equals(lastUpdate.field) &&
+            update.hasValue && update.getValue() == lastUpdate.getValue());
+    assertBufferUpdates(buffer, updates, termsSorted);
+  }
+
+  public void testNoNumericValue() {
+    DocValuesUpdate.NumericDocValuesUpdate update =
+        new DocValuesUpdate.NumericDocValuesUpdate(new Term("id", "1"), "age", null);
+    FieldUpdatesBuffer buffer = new FieldUpdatesBuffer(Counter.newCounter(), update, update.docIDUpto);
+    assertEquals(0, buffer.getMinNumeric());
+    assertEquals(0, buffer.getMaxNumeric());
+  }
+
+  public void testSortAndDedupNumericUpdatesByTerms() throws IOException {
+    List<DocValuesUpdate.NumericDocValuesUpdate> updates = new ArrayList<>();
+    int numUpdates = 1 + random().nextInt(1000);
+    Counter counter = Counter.newCounter();
+    String termField = RandomPicks.randomFrom(random(), Arrays.asList("id", "_id", "some_other_field"));
+    long docValue = 1 + random().nextInt(1000);
+    DocValuesUpdate.NumericDocValuesUpdate randomUpdate = new DocValuesUpdate.NumericDocValuesUpdate(
+        new Term(termField, Integer.toString(random().nextInt(1000))), "numeric", docValue);
+    randomUpdate = randomUpdate.prepareForApply(randomDocUpTo());
+    updates.add(randomUpdate);
+    FieldUpdatesBuffer buffer = new FieldUpdatesBuffer(counter, randomUpdate, randomUpdate.docIDUpto);
+    for (int i = 0; i < numUpdates; i++) {
+      randomUpdate = new DocValuesUpdate.NumericDocValuesUpdate(
+          new Term(termField, Integer.toString(random().nextInt(1000))), "numeric", docValue);
+      randomUpdate = randomUpdate.prepareForApply(randomDocUpTo());
+      updates.add(randomUpdate);
+      buffer.addUpdate(randomUpdate.term, randomUpdate.getValue(), randomUpdate.docIDUpto);
+    }
+    buffer.finish();
+    assertBufferUpdates(buffer, updates, true);
+  }
+
+  void assertBufferUpdates(FieldUpdatesBuffer buffer,
+                           List<DocValuesUpdate.NumericDocValuesUpdate> updates,
+                           boolean termSorted) throws IOException {
+    if (termSorted) {
+      updates.sort(Comparator.comparing(u -> u.term.bytes));
+      SortedMap<BytesRef, DocValuesUpdate.NumericDocValuesUpdate> byTerms = new TreeMap<>();
+      for (DocValuesUpdate.NumericDocValuesUpdate update : updates) {
+        byTerms.compute(update.term.bytes, (k, v) -> v != null && v.docIDUpto >= update.docIDUpto ? v : update);
+      }
+      updates = new ArrayList<>(byTerms.values());
+    }
     FieldUpdatesBuffer.BufferedUpdateIterator iterator = buffer.iterator();
     FieldUpdatesBuffer.BufferedUpdate value;
 
@@ -223,14 +287,15 @@ public class TestFieldUpdatesBuffer extends LuceneTestCase {
     long min = Long.MAX_VALUE;
     long max = Long.MIN_VALUE;
     boolean hasAtLeastOneValue = false;
+    DocValuesUpdate.NumericDocValuesUpdate expectedUpdate;
     while ((value = iterator.next()) != null) {
       long v = buffer.getNumericValue(count);
-      randomUpdate = updates.get(count++);
-      assertEquals(randomUpdate.term.bytes.utf8ToString(), value.termValue.utf8ToString());
-      assertEquals(randomUpdate.term.field, value.termField);
-      assertEquals(randomUpdate.hasValue, value.hasValue);
-      if (randomUpdate.hasValue) {
-        assertEquals(randomUpdate.getValue(), value.numericValue);
+      expectedUpdate = updates.get(count++);
+      assertEquals(expectedUpdate.term.bytes.utf8ToString(), value.termValue.utf8ToString());
+      assertEquals(expectedUpdate.term.field, value.termField);
+      assertEquals(expectedUpdate.hasValue, value.hasValue);
+      if (expectedUpdate.hasValue) {
+        assertEquals(expectedUpdate.getValue(), value.numericValue);
         assertEquals(v, value.numericValue);
         min = Math.min(min, v);
         max = Math.max(max, v);
@@ -239,7 +304,7 @@ public class TestFieldUpdatesBuffer extends LuceneTestCase {
         assertEquals(0, value.numericValue);
         assertEquals(0, v);
       }
-      assertEquals(randomUpdate.docIDUpto, value.docUpTo);
+      assertEquals(expectedUpdate.docIDUpto, value.docUpTo);
     }
     if (hasAtLeastOneValue) {
       assertEquals(max, buffer.getMaxNumeric());
@@ -250,12 +315,4 @@ public class TestFieldUpdatesBuffer extends LuceneTestCase {
     }
     assertEquals(count, updates.size());
   }
-
-  public void testNoNumericValue() {
-    DocValuesUpdate.NumericDocValuesUpdate update =
-        new DocValuesUpdate.NumericDocValuesUpdate(new Term("id", "1"), "age", null);
-    FieldUpdatesBuffer buffer = new FieldUpdatesBuffer(Counter.newCounter(), update, update.docIDUpto);
-    assertEquals(0, buffer.getMinNumeric());
-    assertEquals(0, buffer.getMaxNumeric());
-  }
 }