You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@lucene.apache.org by jp...@apache.org on 2022/12/08 07:38:59 UTC

[lucene] branch main updated: Generalize range query optimization on sorted indexes to descending sorts. (#11972)

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

jpountz pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/lucene.git


The following commit(s) were added to refs/heads/main by this push:
     new 95df7e81095 Generalize range query optimization on sorted indexes to descending sorts. (#11972)
95df7e81095 is described below

commit 95df7e81095b49d1e24d1d6cce68916f82ed0014
Author: Adrien Grand <jp...@gmail.com>
AuthorDate: Thu Dec 8 08:38:53 2022 +0100

    Generalize range query optimization on sorted indexes to descending sorts. (#11972)
    
    This generalizes #687 to indexes that are sorted in descending order. The main
    challenge with descending sorts is that they require being able to compute the
    last doc ID that matches a value, which would ideally require walking the BKD
    tree in reverse order, but the API only support moving forward. This is worked
    around by maintaining a stack of `PointTree` clones to perform the search.
---
 lucene/CHANGES.txt                                 |   3 +
 .../IndexSortSortedNumericDocValuesRangeQuery.java | 278 ++++++++++++++++-----
 ...tIndexSortSortedNumericDocValuesRangeQuery.java | 109 ++++++--
 3 files changed, 298 insertions(+), 92 deletions(-)

diff --git a/lucene/CHANGES.txt b/lucene/CHANGES.txt
index 8171b0cc632..ff85eeb8fd7 100644
--- a/lucene/CHANGES.txt
+++ b/lucene/CHANGES.txt
@@ -205,6 +205,9 @@ Optimizations
 
 * GITHUB#11895: count() in BooleanQuery could be early quit. (Lu Xugang)
 
+* GITHUB#11972: `IndexSortSortedNumericDocValuesRangeQuery` can now also
+  optimize query execution with points for descending sorts. (Adrien Grand)
+
 Other
 ---------------------
 
diff --git a/lucene/sandbox/src/java/org/apache/lucene/sandbox/search/IndexSortSortedNumericDocValuesRangeQuery.java b/lucene/sandbox/src/java/org/apache/lucene/sandbox/search/IndexSortSortedNumericDocValuesRangeQuery.java
index af9cb4ed979..8a3f964d496 100644
--- a/lucene/sandbox/src/java/org/apache/lucene/sandbox/search/IndexSortSortedNumericDocValuesRangeQuery.java
+++ b/lucene/sandbox/src/java/org/apache/lucene/sandbox/search/IndexSortSortedNumericDocValuesRangeQuery.java
@@ -17,8 +17,9 @@
 package org.apache.lucene.sandbox.search;
 
 import java.io.IOException;
+import java.util.ArrayDeque;
+import java.util.Deque;
 import java.util.Objects;
-import java.util.function.Predicate;
 import org.apache.lucene.document.IntPoint;
 import org.apache.lucene.document.LongPoint;
 import org.apache.lucene.index.DocValues;
@@ -27,6 +28,7 @@ import org.apache.lucene.index.LeafReaderContext;
 import org.apache.lucene.index.NumericDocValues;
 import org.apache.lucene.index.PointValues;
 import org.apache.lucene.index.PointValues.IntersectVisitor;
+import org.apache.lucene.index.PointValues.PointTree;
 import org.apache.lucene.index.PointValues.Relation;
 import org.apache.lucene.index.SortedNumericDocValues;
 import org.apache.lucene.search.ConstantScoreScorer;
@@ -220,53 +222,55 @@ public class IndexSortSortedNumericDocValuesRangeQuery extends Query {
     };
   }
 
-  /**
-   * Returns the first document whose packed value is greater than or equal (if allowEqual is true)
-   * to the provided packed value or -1 if all packed values are smaller than the provided one,
-   */
-  public final int nextDoc(PointValues values, byte[] packedValue, boolean allowEqual)
-      throws IOException {
-    assert values.getNumDimensions() == 1;
-    final int bytesPerDim = values.getBytesPerDimension();
-    final ByteArrayComparator comparator = ArrayUtil.getUnsignedComparator(bytesPerDim);
-    final Predicate<byte[]> biggerThan =
-        testPackedValue -> {
-          int cmp = comparator.compare(testPackedValue, 0, packedValue, 0);
-          return cmp > 0 || (cmp == 0 && allowEqual);
-        };
-    return nextDoc(values.getPointTree(), biggerThan);
+  private static class ValueAndDoc {
+    byte[] value;
+    int docID;
+    boolean done;
   }
 
-  private int nextDoc(PointValues.PointTree pointTree, Predicate<byte[]> biggerThan)
+  /**
+   * Move to the minimum leaf node that has at least one value that is greater than (or equal to if
+   * {@code allowEqual}) {@code value}, and return the next greater value on this block. Upon
+   * returning, the {@code pointTree} must be on the leaf node where the value was found.
+   */
+  private static ValueAndDoc findNextValue(
+      PointTree pointTree,
+      byte[] value,
+      boolean allowEqual,
+      ByteArrayComparator comparator,
+      boolean lastDoc)
       throws IOException {
-    if (biggerThan.test(pointTree.getMaxPackedValue()) == false) {
-      // doc is before us
-      return -1;
-    } else if (pointTree.moveToChild()) {
-      // navigate down
-      do {
-        final int doc = nextDoc(pointTree, biggerThan);
-        if (doc != -1) {
-          return doc;
-        }
-      } while (pointTree.moveToSibling());
-      pointTree.moveToParent();
-      return -1;
-    } else {
-      // doc is in this leaf
-      final int[] doc = {-1};
+    int cmp = comparator.compare(pointTree.getMaxPackedValue(), 0, value, 0);
+    if (cmp < 0 || (cmp == 0 && allowEqual == false)) {
+      return null;
+    }
+    if (pointTree.moveToChild() == false) {
+      ValueAndDoc vd = new ValueAndDoc();
       pointTree.visitDocValues(
           new IntersectVisitor() {
+
             @Override
-            public void visit(int docID) {
-              throw new AssertionError("Invalid call to visit(docID)");
+            public void visit(int docID, byte[] packedValue) throws IOException {
+              if (vd.value == null) {
+                int cmp = comparator.compare(packedValue, 0, value, 0);
+                if (cmp > 0 || (cmp == 0 && allowEqual)) {
+                  vd.value = packedValue.clone();
+                  vd.docID = docID;
+                }
+              } else if (lastDoc && vd.done == false) {
+                int cmp = comparator.compare(packedValue, 0, vd.value, 0);
+                assert cmp >= 0;
+                if (cmp > 0) {
+                  vd.done = true;
+                } else {
+                  vd.docID = docID;
+                }
+              }
             }
 
             @Override
-            public void visit(int docID, byte[] packedValue) {
-              if (doc[0] == -1 && biggerThan.test(packedValue)) {
-                doc[0] = docID;
-              }
+            public void visit(int docID) throws IOException {
+              throw new UnsupportedOperationException();
             }
 
             @Override
@@ -274,10 +278,132 @@ public class IndexSortSortedNumericDocValuesRangeQuery extends Query {
               return Relation.CELL_CROSSES_QUERY;
             }
           });
-      return doc[0];
+      if (vd.value != null) {
+        return vd;
+      } else {
+        return null;
+      }
+    }
+
+    // Recurse
+    do {
+      ValueAndDoc vd = findNextValue(pointTree, value, allowEqual, comparator, lastDoc);
+      if (vd != null) {
+        return vd;
+      }
+    } while (pointTree.moveToSibling());
+
+    boolean moved = pointTree.moveToParent();
+    assert moved;
+    return null;
+  }
+
+  /**
+   * Find the next value that is greater than (or equal to if {@code allowEqual}) and return either
+   * its first doc ID or last doc ID depending on {@code lastDoc}. This method returns -1 if there
+   * is no greater value in the dataset.
+   */
+  private static int nextDoc(
+      PointTree pointTree,
+      byte[] value,
+      boolean allowEqual,
+      ByteArrayComparator comparator,
+      boolean lastDoc)
+      throws IOException {
+    ValueAndDoc vd = findNextValue(pointTree, value, allowEqual, comparator, lastDoc);
+    if (vd == null) {
+      return -1;
+    }
+    if (lastDoc == false || vd.done) {
+      return vd.docID;
+    }
+
+    // We found the next value, now we need the last doc ID.
+    int doc = lastDoc(pointTree, vd.value, comparator);
+    if (doc == -1) {
+      // vd.docID was actually the last doc ID
+      return vd.docID;
+    } else {
+      return doc;
     }
   }
 
+  /**
+   * Compute the last doc ID that matches the given value and is stored on a leaf node that compares
+   * greater than the current leaf node that the provided {@link PointTree} is positioned on. This
+   * returns -1 if no other leaf node contains the provided {@code value}.
+   */
+  private static int lastDoc(PointTree pointTree, byte[] value, ByteArrayComparator comparator)
+      throws IOException {
+    // Create a stack of nodes that may contain value that we'll use to search for the last leaf
+    // node that contains `value`.
+    // While the logic looks a bit complicated due to the fact that the PointTree API doesn't allow
+    // moving back to previous siblings, this effectively performs a binary search.
+    Deque<PointTree> stack = new ArrayDeque<>();
+
+    outer:
+    while (true) {
+
+      // Move to the next node
+      while (pointTree.moveToSibling() == false) {
+        if (pointTree.moveToParent() == false) {
+          // No next node
+          break outer;
+        }
+      }
+
+      int cmp = comparator.compare(pointTree.getMinPackedValue(), 0, value, 0);
+      if (cmp > 0) {
+        // This node doesn't have `value`, so next nodes can't either
+        break;
+      }
+
+      stack.push(pointTree.clone());
+    }
+
+    while (stack.isEmpty() == false) {
+      PointTree next = stack.pop();
+      if (next.moveToChild() == false) {
+        int[] lastDoc = {-1};
+        next.visitDocValues(
+            new IntersectVisitor() {
+
+              @Override
+              public void visit(int docID) throws IOException {
+                throw new UnsupportedOperationException();
+              }
+
+              @Override
+              public void visit(int docID, byte[] packedValue) throws IOException {
+                int cmp = comparator.compare(value, 0, packedValue, 0);
+                if (cmp == 0) {
+                  lastDoc[0] = docID;
+                }
+              }
+
+              @Override
+              public Relation compare(byte[] minPackedValue, byte[] maxPackedValue) {
+                return Relation.CELL_CROSSES_QUERY;
+              }
+            });
+        if (lastDoc[0] != -1) {
+          return lastDoc[0];
+        }
+      } else {
+        do {
+          int cmp = comparator.compare(next.getMinPackedValue(), 0, value, 0);
+          if (cmp > 0) {
+            // This node doesn't have `value`, so next nodes can't either
+            break;
+          }
+          stack.push(next.clone());
+        } while (next.moveToSibling());
+      }
+    }
+
+    return -1;
+  }
+
   private boolean matchNone(PointValues points, byte[] queryLowerPoint, byte[] queryUpperPoint)
       throws IOException {
     final ByteArrayComparator comparator =
@@ -311,8 +437,8 @@ public class IndexSortSortedNumericDocValuesRangeQuery extends Query {
     Sort indexSort = context.reader().getMetaData().getSort();
     if (indexSort != null
         && indexSort.getSort().length > 0
-        && indexSort.getSort()[0].getField().equals(field)
-        && indexSort.getSort()[0].getReverse() == false) {
+        && indexSort.getSort()[0].getField().equals(field)) {
+      final boolean reverse = indexSort.getSort()[0].getReverse();
       PointValues points = context.reader().getPointValues(field);
       if (points == null) {
         return null;
@@ -327,44 +453,58 @@ public class IndexSortSortedNumericDocValuesRangeQuery extends Query {
         return null;
       }
 
-      // Each doc that has points has exactly one point.
-      if (points.size() == points.getDocCount()) {
+      if (points.size() != points.getDocCount()) {
+        return null;
+      }
 
-        byte[] queryLowerPoint;
-        byte[] queryUpperPoint;
-        if (points.getBytesPerDimension() == Integer.BYTES) {
-          queryLowerPoint = IntPoint.pack((int) lowerValue).bytes;
-          queryUpperPoint = IntPoint.pack((int) upperValue).bytes;
+      byte[] queryLowerPoint;
+      byte[] queryUpperPoint;
+      if (points.getBytesPerDimension() == Integer.BYTES) {
+        queryLowerPoint = IntPoint.pack((int) lowerValue).bytes;
+        queryUpperPoint = IntPoint.pack((int) upperValue).bytes;
+      } else {
+        queryLowerPoint = LongPoint.pack(lowerValue).bytes;
+        queryUpperPoint = LongPoint.pack(upperValue).bytes;
+      }
+      if (lowerValue > upperValue || matchNone(points, queryLowerPoint, queryUpperPoint)) {
+        return new BoundedDocIdSetIterator(0, 0, null);
+      }
+      int minDocId, maxDocId;
+      if (matchAll(points, queryLowerPoint, queryUpperPoint)) {
+        minDocId = 0;
+        maxDocId = context.reader().maxDoc();
+      } else {
+        final ByteArrayComparator comparator =
+            ArrayUtil.getUnsignedComparator(points.getBytesPerDimension());
+
+        if (reverse) {
+          minDocId = nextDoc(points.getPointTree(), queryUpperPoint, false, comparator, true) + 1;
         } else {
-          queryLowerPoint = LongPoint.pack(lowerValue).bytes;
-          queryUpperPoint = LongPoint.pack(upperValue).bytes;
-        }
-        if (lowerValue > upperValue || matchNone(points, queryLowerPoint, queryUpperPoint)) {
-          return new BoundedDocIdSetIterator(0, 0, null);
+          minDocId = nextDoc(points.getPointTree(), queryLowerPoint, true, comparator, false);
+          if (minDocId == -1) {
+            // No matches
+            return new BoundedDocIdSetIterator(0, 0, null);
+          }
         }
-        int minDocId, maxDocId;
-        if (matchAll(points, queryLowerPoint, queryUpperPoint)) {
-          minDocId = 0;
-          maxDocId = context.reader().maxDoc();
-        } else {
-          // >=queryLowerPoint
-          minDocId = nextDoc(points, queryLowerPoint, true);
 
-          if (minDocId == -1) {
+        if (reverse) {
+          maxDocId = nextDoc(points.getPointTree(), queryLowerPoint, true, comparator, true) + 1;
+          if (maxDocId == 0) {
+            // No matches
             return new BoundedDocIdSetIterator(0, 0, null);
           }
-          // >queryUpperPoint,
-          maxDocId = nextDoc(points, queryUpperPoint, false);
+        } else {
+          maxDocId = nextDoc(points.getPointTree(), queryUpperPoint, false, comparator, false);
           if (maxDocId == -1) {
             maxDocId = context.reader().maxDoc();
           }
         }
+      }
 
-        if ((points.getDocCount() == context.reader().maxDoc())) {
-          return new BoundedDocIdSetIterator(minDocId, maxDocId, null);
-        } else {
-          return new BoundedDocIdSetIterator(minDocId, maxDocId, delegate);
-        }
+      if ((points.getDocCount() == context.reader().maxDoc())) {
+        return new BoundedDocIdSetIterator(minDocId, maxDocId, null);
+      } else {
+        return new BoundedDocIdSetIterator(minDocId, maxDocId, delegate);
       }
     }
     return null;
diff --git a/lucene/sandbox/src/test/org/apache/lucene/sandbox/search/TestIndexSortSortedNumericDocValuesRangeQuery.java b/lucene/sandbox/src/test/org/apache/lucene/sandbox/search/TestIndexSortSortedNumericDocValuesRangeQuery.java
index d68b38839d1..dbc81fc9850 100644
--- a/lucene/sandbox/src/test/org/apache/lucene/sandbox/search/TestIndexSortSortedNumericDocValuesRangeQuery.java
+++ b/lucene/sandbox/src/test/org/apache/lucene/sandbox/search/TestIndexSortSortedNumericDocValuesRangeQuery.java
@@ -643,58 +643,112 @@ public class TestIndexSortSortedNumericDocValuesRangeQuery extends LuceneTestCas
         field, lowerValue, upperValue, fallbackQuery);
   }
 
-  public void testCountWithBkd() throws IOException {
+  public void testCountWithBkdAsc() throws Exception {
+    doTestCountWithBkd(false);
+  }
+
+  public void testCountWithBkdDesc() throws Exception {
+    doTestCountWithBkd(true);
+  }
+
+  public void doTestCountWithBkd(boolean reverse) throws Exception {
     String filedName = "field";
     Directory dir = newDirectory();
     IndexWriterConfig iwc = new IndexWriterConfig(new MockAnalyzer(random()));
-    Sort indexSort = new Sort(new SortedNumericSortField(filedName, SortField.Type.LONG, false));
+    Sort indexSort = new Sort(new SortedNumericSortField(filedName, SortField.Type.LONG, reverse));
     iwc.setIndexSort(indexSort);
     RandomIndexWriter writer = new RandomIndexWriter(random(), dir, iwc);
-    addDocWithBkd(writer, filedName, 6, 500);
-    addDocWithBkd(writer, filedName, 5, 500);
-    addDocWithBkd(writer, filedName, 8, 500);
-    addDocWithBkd(writer, filedName, 9, 500);
     addDocWithBkd(writer, filedName, 7, 500);
+    addDocWithBkd(writer, filedName, 5, 600);
+    addDocWithBkd(writer, filedName, 11, 700);
+    addDocWithBkd(writer, filedName, 13, 800);
+    addDocWithBkd(writer, filedName, 9, 900);
     writer.flush();
     writer.forceMerge(1);
     IndexReader reader = writer.getReader();
     IndexSearcher searcher = newSearcher(reader);
 
-    Query fallbackQuery = LongPoint.newRangeQuery(filedName, 6, 8);
-    Query query = new IndexSortSortedNumericDocValuesRangeQuery(filedName, 6, 8, fallbackQuery);
+    // Both bounds exist in the dataset
+    Query fallbackQuery = LongPoint.newRangeQuery(filedName, 7, 9);
+    Query query = new IndexSortSortedNumericDocValuesRangeQuery(filedName, 7, 9, fallbackQuery);
     Weight weight = query.createWeight(searcher, ScoreMode.COMPLETE, 1.0f);
     for (LeafReaderContext context : searcher.getLeafContexts()) {
-      assertEquals(1500, weight.count(context));
+      assertEquals(1400, weight.count(context));
     }
 
+    // Both bounds do not exist in the dataset
     fallbackQuery = LongPoint.newRangeQuery(filedName, 6, 10);
     query = new IndexSortSortedNumericDocValuesRangeQuery(filedName, 6, 10, fallbackQuery);
     weight = query.createWeight(searcher, ScoreMode.COMPLETE, 1.0f);
     for (LeafReaderContext context : searcher.getLeafContexts()) {
-      assertEquals(2000, weight.count(context));
+      assertEquals(1400, weight.count(context));
+    }
+
+    // Min bound exists in the dataset, not the max
+    fallbackQuery = LongPoint.newRangeQuery(filedName, 7, 10);
+    query = new IndexSortSortedNumericDocValuesRangeQuery(filedName, 7, 10, fallbackQuery);
+    weight = query.createWeight(searcher, ScoreMode.COMPLETE, 1.0f);
+    for (LeafReaderContext context : searcher.getLeafContexts()) {
+      assertEquals(1400, weight.count(context));
+    }
+
+    // Min bound doesn't exist in the dataset, max does
+    fallbackQuery = LongPoint.newRangeQuery(filedName, 6, 9);
+    query = new IndexSortSortedNumericDocValuesRangeQuery(filedName, 7, 10, fallbackQuery);
+    weight = query.createWeight(searcher, ScoreMode.COMPLETE, 1.0f);
+    for (LeafReaderContext context : searcher.getLeafContexts()) {
+      assertEquals(1400, weight.count(context));
     }
 
-    fallbackQuery = LongPoint.newRangeQuery(filedName, 4, 6);
-    query = new IndexSortSortedNumericDocValuesRangeQuery(filedName, 4, 6, fallbackQuery);
+    // Min bound is the min value of the dataset
+    fallbackQuery = LongPoint.newRangeQuery(filedName, 5, 8);
+    query = new IndexSortSortedNumericDocValuesRangeQuery(filedName, 4, 8, fallbackQuery);
     weight = query.createWeight(searcher, ScoreMode.COMPLETE, 1.0f);
     for (LeafReaderContext context : searcher.getLeafContexts()) {
-      assertEquals(1000, weight.count(context));
+      assertEquals(1100, weight.count(context));
     }
 
-    fallbackQuery = LongPoint.newRangeQuery(filedName, 2, 10);
-    query = new IndexSortSortedNumericDocValuesRangeQuery(filedName, 2, 10, fallbackQuery);
+    // Min bound is less than min value of the dataset
+    fallbackQuery = LongPoint.newRangeQuery(filedName, 4, 8);
+    query = new IndexSortSortedNumericDocValuesRangeQuery(filedName, 4, 8, fallbackQuery);
     weight = query.createWeight(searcher, ScoreMode.COMPLETE, 1.0f);
     for (LeafReaderContext context : searcher.getLeafContexts()) {
-      assertEquals(2500, weight.count(context));
+      assertEquals(1100, weight.count(context));
     }
 
-    fallbackQuery = LongPoint.newRangeQuery(filedName, 5, 9);
-    query = new IndexSortSortedNumericDocValuesRangeQuery(filedName, 2, 10, fallbackQuery);
+    // Max bound is the max value of the dataset
+    fallbackQuery = LongPoint.newRangeQuery(filedName, 10, 13);
+    query = new IndexSortSortedNumericDocValuesRangeQuery(filedName, 10, 13, fallbackQuery);
     weight = query.createWeight(searcher, ScoreMode.COMPLETE, 1.0f);
     for (LeafReaderContext context : searcher.getLeafContexts()) {
-      assertEquals(2500, weight.count(context));
+      assertEquals(1500, weight.count(context));
     }
 
+    // Max bound is greater than max value of the dataset
+    fallbackQuery = LongPoint.newRangeQuery(filedName, 10, 14);
+    query = new IndexSortSortedNumericDocValuesRangeQuery(filedName, 10, 14, fallbackQuery);
+    weight = query.createWeight(searcher, ScoreMode.COMPLETE, 1.0f);
+    for (LeafReaderContext context : searcher.getLeafContexts()) {
+      assertEquals(1500, weight.count(context));
+    }
+
+    // Everything matches
+    fallbackQuery = LongPoint.newRangeQuery(filedName, 2, 14);
+    query = new IndexSortSortedNumericDocValuesRangeQuery(filedName, 2, 14, fallbackQuery);
+    weight = query.createWeight(searcher, ScoreMode.COMPLETE, 1.0f);
+    for (LeafReaderContext context : searcher.getLeafContexts()) {
+      assertEquals(3500, weight.count(context));
+    }
+
+    // Bounds equal to min/max values of the dataset, everything matches
+    fallbackQuery = LongPoint.newRangeQuery(filedName, 2, 14);
+    query = new IndexSortSortedNumericDocValuesRangeQuery(filedName, 2, 14, fallbackQuery);
+    weight = query.createWeight(searcher, ScoreMode.COMPLETE, 1.0f);
+    for (LeafReaderContext context : searcher.getLeafContexts()) {
+      assertEquals(3500, weight.count(context));
+    }
+
+    // Bounds are less than the min value of the dataset
     fallbackQuery = LongPoint.newRangeQuery(filedName, 2, 3);
     query = new IndexSortSortedNumericDocValuesRangeQuery(filedName, 2, 3, fallbackQuery);
     weight = query.createWeight(searcher, ScoreMode.COMPLETE, 1.0f);
@@ -702,8 +756,9 @@ public class TestIndexSortSortedNumericDocValuesRangeQuery extends LuceneTestCas
       assertEquals(0, weight.count(context));
     }
 
-    fallbackQuery = LongPoint.newRangeQuery(filedName, 10, 11);
-    query = new IndexSortSortedNumericDocValuesRangeQuery(filedName, 10, 11, fallbackQuery);
+    // Bounds are greater than the max value of the dataset
+    fallbackQuery = LongPoint.newRangeQuery(filedName, 14, 15);
+    query = new IndexSortSortedNumericDocValuesRangeQuery(filedName, 14, 15, fallbackQuery);
     weight = query.createWeight(searcher, ScoreMode.COMPLETE, 1.0f);
     for (LeafReaderContext context : searcher.getLeafContexts()) {
       assertEquals(0, weight.count(context));
@@ -714,11 +769,19 @@ public class TestIndexSortSortedNumericDocValuesRangeQuery extends LuceneTestCas
     dir.close();
   }
 
-  public void testRandomCountWithBkd() throws IOException {
+  public void testRandomCountWithBkdAsc() throws Exception {
+    doTestRandomCountWithBkd(false);
+  }
+
+  public void testRandomCountWithBkdDesc() throws Exception {
+    doTestRandomCountWithBkd(true);
+  }
+
+  private void doTestRandomCountWithBkd(boolean reverse) throws Exception {
     String filedName = "field";
     Directory dir = newDirectory();
     IndexWriterConfig iwc = new IndexWriterConfig(new MockAnalyzer(random()));
-    Sort indexSort = new Sort(new SortedNumericSortField(filedName, SortField.Type.LONG, false));
+    Sort indexSort = new Sort(new SortedNumericSortField(filedName, SortField.Type.LONG, reverse));
     iwc.setIndexSort(indexSort);
     RandomIndexWriter writer = new RandomIndexWriter(random(), dir, iwc);
     Random random = random();