You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@lucene.apache.org by vo...@apache.org on 2025/04/25 10:06:45 UTC
(lucene) branch branch_10x updated: Logic for collecting Histogram efficiently using Point Trees (#14439)
This is an automated email from the ASF dual-hosted git repository.
voditas pushed a commit to branch branch_10x
in repository https://gitbox.apache.org/repos/asf/lucene.git
The following commit(s) were added to refs/heads/branch_10x by this push:
new 7a21f53eeff Logic for collecting Histogram efficiently using Point Trees (#14439)
7a21f53eeff is described below
commit 7a21f53eeff71d4b78fcfa5375b4c08124b857a6
Author: Ankit Jain <ja...@gmail.com>
AuthorDate: Fri Apr 25 15:06:46 2025 +0530
Logic for collecting Histogram efficiently using Point Trees (#14439)
---
lucene/CHANGES.txt | 2 +
lucene/benchmark-jmh/build.gradle | 1 +
lucene/benchmark-jmh/src/java/module-info.java | 1 +
.../benchmark/jmh/HistogramCollectorBenchmark.java | 132 ++++++++++++
.../facet/plain/histograms/HistogramCollector.java | 38 +++-
.../histograms/HistogramCollectorManager.java | 7 +-
.../plain/histograms/PointTreeBulkCollector.java | 235 +++++++++++++++++++++
.../histograms/TestHistogramCollectorManager.java | 33 +++
8 files changed, 446 insertions(+), 3 deletions(-)
diff --git a/lucene/CHANGES.txt b/lucene/CHANGES.txt
index 9744ba4d5da..4c6e0f136f7 100644
--- a/lucene/CHANGES.txt
+++ b/lucene/CHANGES.txt
@@ -29,6 +29,8 @@ Optimizations
---------------------
* GITHUB#14418: Quick exit on filter query matching no docs when rewriting knn query. (Pan Guixin)
+* GITHUB#14439: Efficient Histogram Collection using multi range traversal over PointTrees (Ankit Jain)
+
* GITHUB#14268: PointInSetQuery early exit on non-matching segments. (hanbj)
* GITHUB#14425: KeywordField.newSetQuery() reuses prefixed terms (Mikhail Khludnev)
diff --git a/lucene/benchmark-jmh/build.gradle b/lucene/benchmark-jmh/build.gradle
index 1751a43d7a7..c94bc69cc52 100644
--- a/lucene/benchmark-jmh/build.gradle
+++ b/lucene/benchmark-jmh/build.gradle
@@ -24,6 +24,7 @@ description = 'Lucene JMH micro-benchmarking module'
dependencies {
moduleImplementation project(':lucene:core')
moduleImplementation project(':lucene:expressions')
+ moduleImplementation project(':lucene:sandbox')
moduleImplementation deps.jmh.core
annotationProcessor deps.jmh.annprocess
diff --git a/lucene/benchmark-jmh/src/java/module-info.java b/lucene/benchmark-jmh/src/java/module-info.java
index d92164cfae1..d6bfc6a411b 100644
--- a/lucene/benchmark-jmh/src/java/module-info.java
+++ b/lucene/benchmark-jmh/src/java/module-info.java
@@ -21,6 +21,7 @@ module org.apache.lucene.benchmark.jmh {
requires jdk.unsupported;
requires org.apache.lucene.core;
requires org.apache.lucene.expressions;
+ requires org.apache.lucene.sandbox;
exports org.apache.lucene.benchmark.jmh;
exports org.apache.lucene.benchmark.jmh.jmh_generated;
diff --git a/lucene/benchmark-jmh/src/java/org/apache/lucene/benchmark/jmh/HistogramCollectorBenchmark.java b/lucene/benchmark-jmh/src/java/org/apache/lucene/benchmark/jmh/HistogramCollectorBenchmark.java
new file mode 100644
index 00000000000..c632ca7f6f8
--- /dev/null
+++ b/lucene/benchmark-jmh/src/java/org/apache/lucene/benchmark/jmh/HistogramCollectorBenchmark.java
@@ -0,0 +1,132 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.lucene.benchmark.jmh;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Comparator;
+import java.util.Random;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Stream;
+import org.apache.lucene.document.Document;
+import org.apache.lucene.document.LongPoint;
+import org.apache.lucene.document.NumericDocValuesField;
+import org.apache.lucene.index.DirectoryReader;
+import org.apache.lucene.index.IndexReader;
+import org.apache.lucene.index.IndexWriter;
+import org.apache.lucene.index.IndexWriterConfig;
+import org.apache.lucene.sandbox.facet.plain.histograms.HistogramCollectorManager;
+import org.apache.lucene.search.IndexSearcher;
+import org.apache.lucene.search.MatchAllDocsQuery;
+import org.apache.lucene.store.Directory;
+import org.apache.lucene.store.MMapDirectory;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Level;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Param;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.TearDown;
+import org.openjdk.jmh.annotations.Warmup;
+
+@State(Scope.Thread)
+@BenchmarkMode(Mode.Throughput)
+@OutputTimeUnit(TimeUnit.SECONDS)
+@Fork(value = 1, warmups = 1)
+@Warmup(iterations = 1, time = 1)
+@Measurement(iterations = 3, time = 3)
+public class HistogramCollectorBenchmark {
+ Directory dir;
+ IndexReader reader;
+ Path path;
+
+ @Setup(Level.Trial)
+ public void setup(BenchmarkParams params) throws Exception {
+ path = Files.createTempDirectory("forUtil");
+ Directory dir = MMapDirectory.open(path);
+ IndexWriter w = new IndexWriter(dir, new IndexWriterConfig());
+ Random r = new Random(0);
+
+ for (int i = 0; i < params.docCount; i++) {
+ Document doc = new Document();
+ long value = r.nextInt(0, params.docCount);
+ if (params.pointEnabled) {
+ // Adding indexed point field to verify multi range collector
+ doc.add(new LongPoint("f", value));
+ } else {
+ doc.add(NumericDocValuesField.indexedField("f", value));
+ }
+ w.addDocument(doc);
+ }
+ // Force merging into single segment for testing more documents in segment scenario
+ w.forceMerge(1, true);
+ reader = DirectoryReader.open(w);
+ w.close();
+ }
+
+ @TearDown(Level.Trial)
+ public void tearDown() throws Exception {
+ reader.close();
+ if (dir != null) {
+ dir.close();
+ dir = null;
+ }
+
+ // Clean up the segment files before next run
+ if (Files.exists(path)) {
+ try (Stream<Path> walk = Files.walk(path)) {
+ walk.sorted(Comparator.reverseOrder())
+ .forEach(
+ path -> {
+ try {
+ Files.delete(path);
+ } catch (
+ @SuppressWarnings("unused")
+ IOException unused) {
+ // Do nothing
+ }
+ });
+ }
+ }
+ }
+
+ @State(Scope.Benchmark)
+ public static class BenchmarkParams {
+ // Test with both point enabled and disabled
+ @Param({"true", "false"})
+ public boolean pointEnabled;
+
+ @Param({"500000", "5000000"})
+ public int docCount;
+
+ @Param({"5000", "25000"})
+ public long bucketWidth;
+ }
+
+ @Benchmark
+ public void collectHistogram(BenchmarkParams params) throws IOException {
+ IndexSearcher searcher = new IndexSearcher(reader);
+ searcher.search(
+ new MatchAllDocsQuery(), new HistogramCollectorManager("f", params.bucketWidth, 10000));
+ }
+}
diff --git a/lucene/sandbox/src/java/org/apache/lucene/sandbox/facet/plain/histograms/HistogramCollector.java b/lucene/sandbox/src/java/org/apache/lucene/sandbox/facet/plain/histograms/HistogramCollector.java
index d2535d7f4da..ee57ea1e96a 100644
--- a/lucene/sandbox/src/java/org/apache/lucene/sandbox/facet/plain/histograms/HistogramCollector.java
+++ b/lucene/sandbox/src/java/org/apache/lucene/sandbox/facet/plain/histograms/HistogramCollector.java
@@ -17,12 +17,14 @@
package org.apache.lucene.sandbox.facet.plain.histograms;
import java.io.IOException;
+import java.util.concurrent.ConcurrentMap;
import org.apache.lucene.index.DocValues;
import org.apache.lucene.index.DocValuesSkipper;
import org.apache.lucene.index.DocValuesType;
import org.apache.lucene.index.FieldInfo;
import org.apache.lucene.index.LeafReaderContext;
import org.apache.lucene.index.NumericDocValues;
+import org.apache.lucene.index.PointValues;
import org.apache.lucene.index.SortedNumericDocValues;
import org.apache.lucene.internal.hppc.LongIntHashMap;
import org.apache.lucene.search.CollectionTerminatedException;
@@ -31,6 +33,7 @@ import org.apache.lucene.search.DocIdStream;
import org.apache.lucene.search.LeafCollector;
import org.apache.lucene.search.Scorable;
import org.apache.lucene.search.ScoreMode;
+import org.apache.lucene.search.Weight;
final class HistogramCollector implements Collector {
@@ -38,11 +41,18 @@ final class HistogramCollector implements Collector {
private final long bucketWidth;
private final int maxBuckets;
private final LongIntHashMap counts;
-
- HistogramCollector(String field, long bucketWidth, int maxBuckets) {
+ private final ConcurrentMap<LeafReaderContext, Boolean> leafBulkCollected;
+ private Weight weight;
+
+ HistogramCollector(
+ String field,
+ long bucketWidth,
+ int maxBuckets,
+ ConcurrentMap<LeafReaderContext, Boolean> leafBulkCollected) {
this.field = field;
this.bucketWidth = bucketWidth;
this.maxBuckets = maxBuckets;
+ this.leafBulkCollected = leafBulkCollected;
this.counts = new LongIntHashMap();
}
@@ -53,11 +63,30 @@ final class HistogramCollector implements Collector {
// The segment has no values, nothing to do.
throw new CollectionTerminatedException();
}
+
+ // We can use multi range traversal logic to collect the histogram on numeric
+ // field indexed as point for MATCH_ALL cases. In future, this can be extended
+ // for Point Range Query cases as well
+ if (weight != null && weight.count(context) == context.reader().maxDoc()) {
+ final PointValues pointValues = context.reader().getPointValues(field);
+ if (PointTreeBulkCollector.canCollectEfficiently(pointValues, bucketWidth)) {
+ // In case of intra segment concurrency, only one collector should collect
+ // documents for all the partitions to avoid duplications across collectors
+ if (leafBulkCollected.putIfAbsent(context, true) == null) {
+ PointTreeBulkCollector.collect(pointValues, bucketWidth, counts, maxBuckets);
+ }
+ // Either the collection is finished on this collector, or some other collector
+ // already started that collection, so this collector can finish early!
+ throw new CollectionTerminatedException();
+ }
+ }
+
if (fi.getDocValuesType() != DocValuesType.NUMERIC
&& fi.getDocValuesType() != DocValuesType.SORTED_NUMERIC) {
throw new IllegalStateException(
"Expected numeric field, but got doc-value type: " + fi.getDocValuesType());
}
+
SortedNumericDocValues values = DocValues.getSortedNumeric(context.reader(), field);
NumericDocValues singleton = DocValues.unwrapSingleton(values);
if (singleton == null) {
@@ -296,4 +325,9 @@ final class HistogramCollector implements Collector {
+ maxBuckets);
}
}
+
+ @Override
+ public void setWeight(Weight weight) {
+ this.weight = weight;
+ }
}
diff --git a/lucene/sandbox/src/java/org/apache/lucene/sandbox/facet/plain/histograms/HistogramCollectorManager.java b/lucene/sandbox/src/java/org/apache/lucene/sandbox/facet/plain/histograms/HistogramCollectorManager.java
index f00b8b7d5be..8eb3b23f09e 100644
--- a/lucene/sandbox/src/java/org/apache/lucene/sandbox/facet/plain/histograms/HistogramCollectorManager.java
+++ b/lucene/sandbox/src/java/org/apache/lucene/sandbox/facet/plain/histograms/HistogramCollectorManager.java
@@ -19,7 +19,10 @@ package org.apache.lucene.sandbox.facet.plain.histograms;
import java.io.IOException;
import java.util.Collection;
import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
import org.apache.lucene.document.FieldType;
+import org.apache.lucene.index.LeafReaderContext;
import org.apache.lucene.internal.hppc.LongIntHashMap;
import org.apache.lucene.internal.hppc.LongIntHashMap.LongIntCursor;
import org.apache.lucene.search.CollectorManager;
@@ -50,6 +53,7 @@ public final class HistogramCollectorManager
private final String field;
private final long bucketWidth;
private final int maxBuckets;
+ private final ConcurrentMap<LeafReaderContext, Boolean> leafBulkCollected;
/**
* Compute a histogram of the distribution of the values of the given {@code field} according to
@@ -76,11 +80,12 @@ public final class HistogramCollectorManager
throw new IllegalArgumentException("maxBuckets must be at least 1, got: " + maxBuckets);
}
this.maxBuckets = maxBuckets;
+ this.leafBulkCollected = new ConcurrentHashMap<>();
}
@Override
public HistogramCollector newCollector() throws IOException {
- return new HistogramCollector(field, bucketWidth, maxBuckets);
+ return new HistogramCollector(field, bucketWidth, maxBuckets, leafBulkCollected);
}
@Override
diff --git a/lucene/sandbox/src/java/org/apache/lucene/sandbox/facet/plain/histograms/PointTreeBulkCollector.java b/lucene/sandbox/src/java/org/apache/lucene/sandbox/facet/plain/histograms/PointTreeBulkCollector.java
new file mode 100644
index 00000000000..d70e63237c0
--- /dev/null
+++ b/lucene/sandbox/src/java/org/apache/lucene/sandbox/facet/plain/histograms/PointTreeBulkCollector.java
@@ -0,0 +1,235 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.lucene.sandbox.facet.plain.histograms;
+
+import static org.apache.lucene.search.DocIdSetIterator.NO_MORE_DOCS;
+
+import java.io.IOException;
+import java.util.function.Function;
+import org.apache.lucene.index.PointValues;
+import org.apache.lucene.internal.hppc.LongIntHashMap;
+import org.apache.lucene.search.DocIdSetIterator;
+import org.apache.lucene.util.NumericUtils;
+
+/**
+ * Logic for bulk collecting histogram using multi range traversal on PointTree. If current range
+ * being collected contains the TreeNode range completely, we can completely skip traversing the
+ * TreeNodes individually
+ *
+ * @lucene.experimental
+ */
+class PointTreeBulkCollector {
+ private static Function<byte[], Long> bytesToLong(int numBytes) {
+ if (numBytes == Long.BYTES) {
+ // Used by LongPoint, DoublePoint
+ return a -> NumericUtils.sortableBytesToLong(a, 0);
+ } else if (numBytes == Integer.BYTES) {
+ // Used by IntPoint, FloatPoint, LatLonPoint, LatLonShape
+ return a -> (long) NumericUtils.sortableBytesToInt(a, 0);
+ }
+
+ return null;
+ }
+
+ static boolean canCollectEfficiently(final PointValues pointValues, final long bucketWidth)
+ throws IOException {
+ // We need pointValues.getDocCount() == pointValues.size() to count each doc only
+ // once, including docs that have two values that fall into the same bucket.
+ if (pointValues == null
+ || pointValues.getNumDimensions() != 1
+ || pointValues.getDocCount() != pointValues.size()) {
+ return false;
+ }
+
+ final Function<byte[], Long> byteToLong = bytesToLong(pointValues.getBytesPerDimension());
+ if (byteToLong == null) {
+ return false;
+ }
+
+ long leafMinBucket =
+ Math.floorDiv(byteToLong.apply(pointValues.getMinPackedValue()), bucketWidth);
+ long leafMaxBucket =
+ Math.floorDiv(byteToLong.apply(pointValues.getMaxPackedValue()), bucketWidth);
+
+ // We want that # leaf nodes is more than # buckets so that we can completely skip over
+ // some of the leaf nodes. Higher this ratio, more efficient it is than naive approach!
+ if ((pointValues.size() / 512) < (leafMaxBucket - leafMinBucket)) {
+ return false;
+ }
+
+ return true;
+ }
+
+ static void collect(
+ final PointValues pointValues,
+ final long bucketWidth,
+ final LongIntHashMap collectorCounts,
+ final int maxBuckets)
+ throws IOException {
+ final Function<byte[], Long> byteToLong = bytesToLong(pointValues.getBytesPerDimension());
+ BucketManager collector =
+ new BucketManager(
+ collectorCounts,
+ byteToLong.apply(pointValues.getMinPackedValue()),
+ bucketWidth,
+ byteToLong,
+ maxBuckets);
+ PointValues.IntersectVisitor visitor = getIntersectVisitor(collector);
+ intersectWithRanges(visitor, pointValues.getPointTree(), collector);
+ collector.finalizePreviousBucket(null);
+ }
+
+ private static void intersectWithRanges(
+ PointValues.IntersectVisitor visitor,
+ PointValues.PointTree pointTree,
+ BucketManager collector)
+ throws IOException {
+ PointValues.Relation r =
+ visitor.compare(pointTree.getMinPackedValue(), pointTree.getMaxPackedValue());
+
+ switch (r) {
+ case CELL_INSIDE_QUERY:
+ collector.countNode((int) pointTree.size());
+ break;
+ case CELL_CROSSES_QUERY:
+ if (pointTree.moveToChild()) {
+ do {
+ intersectWithRanges(visitor, pointTree, collector);
+ } while (pointTree.moveToSibling());
+ pointTree.moveToParent();
+ } else {
+ pointTree.visitDocValues(visitor);
+ }
+ break;
+ case CELL_OUTSIDE_QUERY:
+ }
+ }
+
+ private static PointValues.IntersectVisitor getIntersectVisitor(BucketManager collector) {
+ return new PointValues.IntersectVisitor() {
+ @Override
+ public void visit(int docID) {
+ // this branch should be unreachable
+ throw new UnsupportedOperationException(
+ "This IntersectVisitor does not perform any actions on a "
+ + "docID="
+ + docID
+ + " node being visited");
+ }
+
+ @Override
+ public void visit(int docID, byte[] packedValue) throws IOException {
+ if (!collector.withinUpperBound(packedValue)) {
+ collector.finalizePreviousBucket(packedValue);
+ }
+
+ if (collector.withinRange(packedValue)) {
+ collector.count();
+ }
+ }
+
+ @Override
+ public void visit(DocIdSetIterator iterator, byte[] packedValue) throws IOException {
+ if (!collector.withinUpperBound(packedValue)) {
+ collector.finalizePreviousBucket(packedValue);
+ }
+
+ if (collector.withinRange(packedValue)) {
+ for (int doc = iterator.nextDoc(); doc != NO_MORE_DOCS; doc = iterator.nextDoc()) {
+ collector.count();
+ }
+ }
+ }
+
+ @Override
+ public PointValues.Relation compare(byte[] minPackedValue, byte[] maxPackedValue) {
+ // try to find the first range that may collect values from this cell
+ if (!collector.withinUpperBound(minPackedValue)) {
+ collector.finalizePreviousBucket(minPackedValue);
+ }
+
+ // Not possible to have the CELL_OUTSIDE_QUERY, as bucket lower bound is updated
+ // while finalizing the previous bucket
+ if (collector.withinRange(minPackedValue) && collector.withinRange(maxPackedValue)) {
+ return PointValues.Relation.CELL_INSIDE_QUERY;
+ }
+ return PointValues.Relation.CELL_CROSSES_QUERY;
+ }
+ };
+ }
+
+ private static class BucketManager {
+ private final LongIntHashMap collectorCounts;
+ private int counter = 0;
+ private long startValue;
+ private long endValue;
+ private int nonZeroBuckets = 0;
+ private int maxBuckets;
+ private Function<byte[], Long> byteToLong;
+ private long bucketWidth;
+
+ public BucketManager(
+ LongIntHashMap collectorCounts,
+ long minValue,
+ long bucketWidth,
+ Function<byte[], Long> byteToLong,
+ int maxBuckets) {
+ this.collectorCounts = collectorCounts;
+ this.bucketWidth = bucketWidth;
+ this.startValue = Math.floorDiv(minValue, bucketWidth) * bucketWidth;
+ this.endValue = startValue + bucketWidth;
+ this.byteToLong = byteToLong;
+ this.maxBuckets = maxBuckets;
+ }
+
+ private void count() {
+ counter++;
+ }
+
+ private void countNode(int count) {
+ counter += count;
+ }
+
+ private void finalizePreviousBucket(byte[] packedValue) {
+ // TODO: Can counter ever be 0?
+ if (counter > 0) {
+ collectorCounts.addTo(Math.floorDiv(startValue, bucketWidth), counter);
+ if (packedValue != null) {
+ startValue = byteToLong.apply(packedValue);
+ // Align the start value with bucket width
+ startValue = Math.floorDiv(startValue, bucketWidth) * bucketWidth;
+ endValue = startValue + bucketWidth;
+ }
+ nonZeroBuckets++;
+ counter = 0;
+ HistogramCollector.checkMaxBuckets(nonZeroBuckets, maxBuckets);
+ }
+ }
+
+ private boolean withinLowerBound(byte[] value) {
+ return byteToLong.apply(value) >= startValue;
+ }
+
+ private boolean withinUpperBound(byte[] value) {
+ return byteToLong.apply(value) < endValue;
+ }
+
+ private boolean withinRange(byte[] value) {
+ return withinLowerBound(value) && withinUpperBound(value);
+ }
+ }
+}
diff --git a/lucene/sandbox/src/test/org/apache/lucene/sandbox/facet/plain/histograms/TestHistogramCollectorManager.java b/lucene/sandbox/src/test/org/apache/lucene/sandbox/facet/plain/histograms/TestHistogramCollectorManager.java
index a9d742d4855..ffa5e825f6f 100644
--- a/lucene/sandbox/src/test/org/apache/lucene/sandbox/facet/plain/histograms/TestHistogramCollectorManager.java
+++ b/lucene/sandbox/src/test/org/apache/lucene/sandbox/facet/plain/histograms/TestHistogramCollectorManager.java
@@ -19,6 +19,7 @@ package org.apache.lucene.sandbox.facet.plain.histograms;
import java.io.IOException;
import org.apache.lucene.codecs.lucene90.Lucene90DocValuesFormat;
import org.apache.lucene.document.Document;
+import org.apache.lucene.document.LongPoint;
import org.apache.lucene.document.NumericDocValuesField;
import org.apache.lucene.document.SortedNumericDocValuesField;
import org.apache.lucene.index.DirectoryReader;
@@ -115,6 +116,38 @@ public class TestHistogramCollectorManager extends LuceneTestCase {
.setCodec(TestUtil.alwaysDocValuesFormat(new Lucene90DocValuesFormat(3))));
}
+ public void testMultiRangePointTreeCollector() throws IOException {
+ Directory dir = newDirectory();
+ IndexWriter w = new IndexWriter(dir, new IndexWriterConfig());
+
+ long[] values = new long[5000];
+
+ for (int i = 0; i < values.length; i++) {
+ values[i] = random().nextInt(0, 5000); // Generates a random integer
+ }
+
+ for (long value : values) {
+ Document doc = new Document();
+ // Adding indexed point field to verify multi range collector
+ doc.add(new LongPoint("f", value));
+ w.addDocument(doc);
+ }
+
+ DirectoryReader reader = DirectoryReader.open(w);
+ w.close();
+ IndexSearcher searcher = newSearcher(reader);
+ LongIntHashMap actualCounts =
+ searcher.search(new MatchAllDocsQuery(), new HistogramCollectorManager("f", 1000));
+ LongIntHashMap expectedCounts = new LongIntHashMap();
+ for (long value : values) {
+ expectedCounts.addTo(Math.floorDiv(value, 1000), 1);
+ }
+ assertEquals(expectedCounts, actualCounts);
+
+ reader.close();
+ dir.close();
+ }
+
private void doTestSkipIndex(IndexWriterConfig cfg) throws IOException {
Directory dir = newDirectory();
IndexWriter w = new IndexWriter(dir, cfg);