You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@lucene.apache.org by mv...@apache.org on 2026/06/30 17:17:02 UTC
(lucene) branch main updated: Add bulk BinaryDocValues#binaryValues API (#16286)
This is an automated email from the ASF dual-hosted git repository.
martijnvg 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 b843b4c6c39 Add bulk BinaryDocValues#binaryValues API (#16286)
b843b4c6c39 is described below
commit b843b4c6c395cff92b72997c8f59b0579d68365d
Author: Costin Leau <co...@users.noreply.github.com>
AuthorDate: Tue Jun 30 10:16:56 2026 -0700
Add bulk BinaryDocValues#binaryValues API (#16286)
Add dedicated method for bulk processing. Mirrors NumericDocValues#longValues for binary fields. Provides a default implementation with default per-doc fallback plus Lucene90 codec override that reads directly from the data slice (fixed-length: readBytes(doc * length), variable-length: batch address lookup).
---
lucene/CHANGES.txt | 4 +-
.../jmh/BinaryDocValuesBulkDecodeBenchmark.java | 178 +++++++++++++++++++++
.../codecs/lucene90/Lucene90DocValuesProducer.java | 56 +++++++
.../org/apache/lucene/index/BinaryDocValues.java | 76 ++++++++-
.../java/org/apache/lucene/index/CheckIndex.java | 45 ++++++
.../lucene90/TestLucene90DocValuesFormat.java | 156 ++++++++++++++++++
.../lucene/tests/index/AssertingLeafReader.java | 16 ++
7 files changed, 529 insertions(+), 2 deletions(-)
diff --git a/lucene/CHANGES.txt b/lucene/CHANGES.txt
index ca615ef724a..622c9cb0621 100644
--- a/lucene/CHANGES.txt
+++ b/lucene/CHANGES.txt
@@ -289,7 +289,9 @@ Other
API Changes
---------------------
-(No changes)
+
+* GITHUB#16286: Introduce BinaryDocValues#binaryValues to help speed up the
+ retrieval of many binary doc values at once. (Costin Leau)
New Features
---------------------
diff --git a/lucene/benchmark-jmh/src/java/org/apache/lucene/benchmark/jmh/BinaryDocValuesBulkDecodeBenchmark.java b/lucene/benchmark-jmh/src/java/org/apache/lucene/benchmark/jmh/BinaryDocValuesBulkDecodeBenchmark.java
new file mode 100644
index 00000000000..7b98e6b1ecc
--- /dev/null
+++ b/lucene/benchmark-jmh/src/java/org/apache/lucene/benchmark/jmh/BinaryDocValuesBulkDecodeBenchmark.java
@@ -0,0 +1,178 @@
+/*
+ * 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.BinaryDocValuesField;
+import org.apache.lucene.document.Document;
+import org.apache.lucene.index.BinaryDocValues;
+import org.apache.lucene.index.DirectoryReader;
+import org.apache.lucene.index.IndexWriter;
+import org.apache.lucene.index.IndexWriterConfig;
+import org.apache.lucene.store.Directory;
+import org.apache.lucene.store.MMapDirectory;
+import org.apache.lucene.util.BytesRef;
+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;
+
+/**
+ * Benchmarks bulk retrieval of dense binary doc values via {@link BinaryDocValues#binaryValues}.
+ * Compares the per-doc default with the Lucene90 codec override that reads directly from the data
+ * slice.
+ */
+@State(Scope.Thread)
+@BenchmarkMode(Mode.Throughput)
+@OutputTimeUnit(TimeUnit.SECONDS)
+@Warmup(iterations = 3, time = 2)
+@Measurement(iterations = 5, time = 2)
+public class BinaryDocValuesBulkDecodeBenchmark {
+
+ private Directory dir;
+ private DirectoryReader reader;
+ private BinaryDocValues values;
+ private Path path;
+ private int[] docs;
+ private BytesRef[] valueBuffer;
+ private int nextStart;
+
+ @Param({"1000000"})
+ public int docCount;
+
+ @Param({"8", "32", "128"})
+ public int valueLength;
+
+ @Param({"128", "1024"})
+ public int batchSize;
+
+ @Param({"fixed", "variable"})
+ public String encoding;
+
+ @Setup(Level.Trial)
+ public void setup() throws Exception {
+ path = Files.createTempDirectory("binaryDocValuesBulkDecode");
+ dir = MMapDirectory.open(path);
+
+ IndexWriter writer = new IndexWriter(dir, new IndexWriterConfig());
+ Random random = new Random(0);
+ for (int i = 0; i < docCount; i++) {
+ Document doc = new Document();
+ int len;
+ if (encoding.equals("fixed")) {
+ len = valueLength;
+ } else {
+ len = 1 + random.nextInt(valueLength);
+ }
+ byte[] bytes = new byte[len];
+ random.nextBytes(bytes);
+ doc.add(new BinaryDocValuesField("field", new BytesRef(bytes)));
+ writer.addDocument(doc);
+ }
+ writer.forceMerge(1);
+ reader = DirectoryReader.open(writer);
+ writer.close();
+
+ values = reader.leaves().get(0).reader().getBinaryDocValues("field");
+ docs = new int[batchSize];
+ valueBuffer = new BytesRef[batchSize];
+ }
+
+ @TearDown(Level.Trial)
+ public void tearDown() throws Exception {
+ reader.close();
+ dir.close();
+ if (Files.exists(path)) {
+ try (Stream<Path> walk = Files.walk(path)) {
+ walk.sorted(Comparator.reverseOrder())
+ .forEach(
+ p -> {
+ try {
+ Files.delete(p);
+ } catch (IOException _) {
+ }
+ });
+ }
+ }
+ }
+
+ @Benchmark
+ @Fork(
+ value = 1,
+ jvmArgsAppend = {"-Xmx2g", "-Xms2g", "-XX:+AlwaysPreTouch"})
+ public int binaryValuesBulk() throws IOException {
+ return readBatchBulk();
+ }
+
+ @Benchmark
+ @Fork(
+ value = 1,
+ jvmArgsAppend = {"-Xmx2g", "-Xms2g", "-XX:+AlwaysPreTouch"})
+ public int binaryValuesPerDoc() throws IOException {
+ return readBatchPerDoc();
+ }
+
+ private int readBatchBulk() throws IOException {
+ final int maxStart = docCount - batchSize;
+ if (nextStart > maxStart) {
+ nextStart = 0;
+ }
+ for (int i = 0; i < batchSize; i++) {
+ docs[i] = nextStart + i;
+ }
+ nextStart += batchSize;
+
+ values.binaryValues(batchSize, docs, valueBuffer);
+ int checksum = 0;
+ for (int i = 0; i < batchSize; i++) {
+ checksum += valueBuffer[i].length;
+ }
+ return checksum;
+ }
+
+ private int readBatchPerDoc() throws IOException {
+ final int maxStart = docCount - batchSize;
+ if (nextStart > maxStart) {
+ nextStart = 0;
+ }
+
+ int checksum = 0;
+ for (int i = 0; i < batchSize; i++) {
+ int doc = nextStart + i;
+ values.advanceExact(doc);
+ BytesRef ref = BytesRef.deepCopyOf(values.binaryValue());
+ checksum += ref.length;
+ }
+ nextStart += batchSize;
+ return checksum;
+ }
+}
diff --git a/lucene/core/src/java/org/apache/lucene/codecs/lucene90/Lucene90DocValuesProducer.java b/lucene/core/src/java/org/apache/lucene/codecs/lucene90/Lucene90DocValuesProducer.java
index 591f6c61d27..568b0ef01a5 100644
--- a/lucene/core/src/java/org/apache/lucene/codecs/lucene90/Lucene90DocValuesProducer.java
+++ b/lucene/core/src/java/org/apache/lucene/codecs/lucene90/Lucene90DocValuesProducer.java
@@ -1231,6 +1231,27 @@ final class Lucene90DocValuesProducer extends DocValuesProducer {
bytesSlice.readBytes((long) doc * length, bytes.bytes, 0, length);
return bytes;
}
+
+ @Override
+ public void binaryValues(
+ int size, int[] docs, int docsOffset, BytesRef[] values, int valuesOffset)
+ throws IOException {
+ if (size == 0) {
+ return;
+ }
+ byte[] bulk = new byte[size * length];
+ if (isContiguous(size, docs, docsOffset)) {
+ bytesSlice.readBytes((long) docs[docsOffset] * length, bulk, 0, bulk.length);
+ } else {
+ for (int di = docsOffset, bi = 0, end = docsOffset + size; di < end; di++, bi++) {
+ bytesSlice.readBytes((long) docs[di] * length, bulk, bi * length, length);
+ }
+ }
+ for (int i = 0; i < size; i++) {
+ values[valuesOffset + i] = new BytesRef(bulk, i * length, length);
+ }
+ doc = docs[docsOffset + size - 1];
+ }
};
} else {
// variable length
@@ -1253,6 +1274,41 @@ final class Lucene90DocValuesProducer extends DocValuesProducer {
bytesSlice.readBytes(startOffset, bytes.bytes, 0, bytes.length);
return bytes;
}
+
+ @Override
+ public void binaryValues(
+ int size, int[] docs, int docsOffset, BytesRef[] values, int valuesOffset)
+ throws IOException {
+ if (size == 0) {
+ return;
+ }
+ if (isContiguous(size, docs, docsOffset)) {
+ long firstStart = addresses.get(docs[docsOffset]);
+ long lastEnd = addresses.get(docs[docsOffset + size - 1] + 1L);
+ int totalBytes = (int) (lastEnd - firstStart);
+ byte[] bulk = new byte[totalBytes];
+ bytesSlice.readBytes(firstStart, bulk, 0, totalBytes);
+ for (int di = docsOffset, vi = valuesOffset, end = docsOffset + size;
+ di < end;
+ di++, vi++) {
+ int offset = (int) (addresses.get(docs[di]) - firstStart);
+ int len = (int) (addresses.get(docs[di] + 1L) - addresses.get(docs[di]));
+ values[vi] = new BytesRef(bulk, offset, len);
+ }
+ } else {
+ for (int di = docsOffset, vi = valuesOffset, end = docsOffset + size;
+ di < end;
+ di++, vi++) {
+ int d = docs[di];
+ long startOffset = addresses.get(d);
+ int len = (int) (addresses.get(d + 1L) - startOffset);
+ byte[] b = new byte[len];
+ bytesSlice.readBytes(startOffset, b, 0, len);
+ values[vi] = new BytesRef(b, 0, len);
+ }
+ }
+ doc = docs[docsOffset + size - 1];
+ }
};
}
} else {
diff --git a/lucene/core/src/java/org/apache/lucene/index/BinaryDocValues.java b/lucene/core/src/java/org/apache/lucene/index/BinaryDocValues.java
index 468ebb39883..a7a33a4df53 100644
--- a/lucene/core/src/java/org/apache/lucene/index/BinaryDocValues.java
+++ b/lucene/core/src/java/org/apache/lucene/index/BinaryDocValues.java
@@ -18,9 +18,11 @@
package org.apache.lucene.index;
import java.io.IOException;
+import org.apache.lucene.search.BooleanClause.Occur;
+import org.apache.lucene.search.FieldExistsQuery;
import org.apache.lucene.util.BytesRef;
-/** A per-document numeric value. */
+/** A per-document binary value. */
public abstract class BinaryDocValues extends DocValuesIterator {
/** Sole constructor. (For invocation by subclass constructors, typically implicit.) */
@@ -33,4 +35,76 @@ public abstract class BinaryDocValues extends DocValuesIterator {
* @return binary value
*/
public abstract BytesRef binaryValue() throws IOException;
+
+ /**
+ * Bulk retrieval of binary doc values. This API helps reduce the performance impact of virtual
+ * function calls.
+ *
+ * <p>This API behaves as if implemented as below, which is the default implementation:
+ *
+ * <pre><code class="language-java">
+ * public void binaryValues(int size, int[] docs, BytesRef[] values) throws IOException {
+ * for (int i = 0; i < size; ++i) {
+ * int doc = docs[i];
+ * if (advanceExact(doc)) {
+ * values[i] = BytesRef.deepCopyOf(binaryValue());
+ * } else {
+ * values[i] = null;
+ * }
+ * }
+ * }
+ * </code></pre>
+ *
+ * <p><b>NOTE</b>: The {@code docs} array is required to be sorted in ascending order with no
+ * duplicates.
+ *
+ * <p><b>NOTE</b>: Documents that don't have a value for this field will have their corresponding
+ * entry set to {@code null}. If you need to exclude documents that don't have a value, then you
+ * could apply a {@link FieldExistsQuery} as a {@link Occur#FILTER} clause. Another option is to
+ * fall back to using {@link #advanceExact} and {@link #binaryValue()} on ranges of doc IDs that
+ * may not be dense, e.g.
+ *
+ * <pre><code class="language-java">
+ * if (size > 0 && values.advanceExact(docs[0]) && values.docIDRunEnd() > docs[size - 1]) {
+ * // use values#binaryValues to retrieve values
+ * } else {
+ * // some docs may not have a value, use #advanceExact and #binaryValue
+ * }
+ * </code></pre>
+ *
+ * <p><b>NOTE</b>: Each returned {@link BytesRef} is a deep copy owned by the caller and remains
+ * valid after subsequent calls.
+ *
+ * @param size the number of values to retrieve
+ * @param docs the buffer of doc IDs whose values should be looked up
+ * @param values the buffer of values to fill; entries are set to {@code null} when a document
+ * doesn't have a value
+ */
+ public void binaryValues(int size, int[] docs, BytesRef[] values) throws IOException {
+ binaryValues(size, docs, 0, values, 0);
+ }
+
+ /**
+ * Offset-aware variant of {@link #binaryValues(int, int[], BytesRef[])}. Reads {@code size} doc
+ * IDs starting at {@code docs[docsOffset]} and writes the corresponding values starting at {@code
+ * values[valuesOffset]}. This follows the same convention as {@link System#arraycopy}.
+ *
+ * @param size the number of values to retrieve
+ * @param docs the buffer of doc IDs whose values should be looked up
+ * @param docsOffset first position in {@code docs} to read
+ * @param values the buffer of values to fill; entries are set to {@code null} when a document
+ * doesn't have a value
+ * @param valuesOffset first position in {@code values} to write
+ */
+ public void binaryValues(
+ int size, int[] docs, int docsOffset, BytesRef[] values, int valuesOffset)
+ throws IOException {
+ for (int di = docsOffset, vi = valuesOffset, end = docsOffset + size; di < end; di++, vi++) {
+ if (advanceExact(docs[di])) {
+ values[vi] = BytesRef.deepCopyOf(binaryValue());
+ } else {
+ values[vi] = null;
+ }
+ }
+ }
}
diff --git a/lucene/core/src/java/org/apache/lucene/index/CheckIndex.java b/lucene/core/src/java/org/apache/lucene/index/CheckIndex.java
index 570adb9738d..303698149b8 100644
--- a/lucene/core/src/java/org/apache/lucene/index/CheckIndex.java
+++ b/lucene/core/src/java/org/apache/lucene/index/CheckIndex.java
@@ -4113,6 +4113,49 @@ public final class CheckIndex implements Closeable {
}
}
+ private static void checkBulkFetchBinaryDocValues(
+ String fieldName, BinaryDocValues bdv, BinaryDocValues bdv2, int maxDoc) throws IOException {
+
+ int[] docs = new int[16];
+ BytesRef[] values = new BytesRef[16];
+
+ for (int doc = -1; doc < maxDoc; ) {
+ int size = 0;
+ for (int j = 0; j < docs.length; ++j) {
+ doc += 1 + (j & 0x03);
+ if (doc >= maxDoc) {
+ break;
+ }
+ docs[size++] = doc;
+ }
+
+ bdv.binaryValues(size, docs, values);
+
+ for (int j = 0; j < size; ++j) {
+ if (bdv2.advanceExact(docs[j])) {
+ BytesRef expected = BytesRef.deepCopyOf(bdv2.binaryValue());
+ if (values[j] == null || values[j].equals(expected) == false) {
+ throw new CheckIndexException(
+ "field "
+ + fieldName
+ + " #binaryValues reports different value: "
+ + values[j]
+ + " != "
+ + expected);
+ }
+ } else {
+ if (values[j] != null) {
+ throw new CheckIndexException(
+ "field "
+ + fieldName
+ + " #binaryValues reports non-null for missing doc: "
+ + values[j]);
+ }
+ }
+ }
+ }
+ }
+
private static void checkDocValues(
FieldInfo fi, int maxDoc, DocValuesProducer dvReader, DocValuesStatus status)
throws Exception {
@@ -4141,6 +4184,8 @@ public final class CheckIndex implements Closeable {
status.totalBinaryFields++;
checkDVIterator(fi, dvReader::getBinary);
checkBinaryDocValues(fi.name, dvReader.getBinary(fi), dvReader.getBinary(fi));
+ checkBulkFetchBinaryDocValues(
+ fi.name, dvReader.getBinary(fi), dvReader.getBinary(fi), maxDoc);
break;
case NUMERIC:
status.totalNumericFields++;
diff --git a/lucene/core/src/test/org/apache/lucene/codecs/lucene90/TestLucene90DocValuesFormat.java b/lucene/core/src/test/org/apache/lucene/codecs/lucene90/TestLucene90DocValuesFormat.java
index d816c250d1e..ad5f6108917 100644
--- a/lucene/core/src/test/org/apache/lucene/codecs/lucene90/TestLucene90DocValuesFormat.java
+++ b/lucene/core/src/test/org/apache/lucene/codecs/lucene90/TestLucene90DocValuesFormat.java
@@ -279,6 +279,162 @@ public class TestLucene90DocValuesFormat extends BaseCompressingDocValuesFormatT
}
}
+ public void testDenseBinaryValuesBulkFetch() throws Exception {
+ doTestDenseBinaryValuesBulkFetch(true);
+ doTestDenseBinaryValuesBulkFetch(false);
+ }
+
+ private void doTestDenseBinaryValuesBulkFetch(boolean fixedLength) throws Exception {
+ final int numDocs = 512;
+ final BytesRef[] expected = new BytesRef[numDocs];
+ for (int i = 0; i < numDocs; i++) {
+ int length = fixedLength ? 8 : 1 + random().nextInt(32);
+ byte[] bytes = new byte[length];
+ random().nextBytes(bytes);
+ expected[i] = new BytesRef(bytes);
+ }
+
+ Directory dir = newDirectory();
+ IndexWriterConfig conf = new IndexWriterConfig(new MockAnalyzer(random()));
+ conf.setMergeScheduler(new SerialMergeScheduler());
+ IndexWriter writer = new IndexWriter(dir, conf);
+ for (int i = 0; i < numDocs; i++) {
+ Document doc = new Document();
+ doc.add(new BinaryDocValuesField("binary", expected[i]));
+ writer.addDocument(doc);
+ }
+ writer.forceMerge(1);
+
+ try (DirectoryReader reader = DirectoryReader.open(writer)) {
+ BinaryDocValues values = reader.leaves().get(0).reader().getBinaryDocValues("binary");
+ BinaryDocValues singleValues = reader.leaves().get(0).reader().getBinaryDocValues("binary");
+ assertBulkBinaryValues(values, singleValues, expected, 0, 128, 1, fixedLength);
+ assertBulkBinaryValues(values, singleValues, expected, 17, 128, 1, fixedLength);
+ assertBulkBinaryValues(values, singleValues, expected, 0, 128, 2, fixedLength);
+ }
+
+ writer.close();
+ dir.close();
+ }
+
+ private static void assertBulkBinaryValues(
+ BinaryDocValues values,
+ BinaryDocValues singleValues,
+ BytesRef[] expected,
+ int startDoc,
+ int size,
+ int docStep,
+ boolean fixedLength)
+ throws IOException {
+ int[] docs = new int[size];
+ BytesRef[] actual = new BytesRef[size];
+ for (int i = 0; i < size; i++) {
+ docs[i] = startDoc + i * docStep;
+ }
+
+ values.binaryValues(size, docs, actual);
+ if (size != 0) {
+ assertEquals(docs[size - 1], values.docID());
+ }
+ for (int i = 0; i < size; i++) {
+ assertTrue(singleValues.advanceExact(docs[i]));
+ BytesRef singleValue = BytesRef.deepCopyOf(singleValues.binaryValue());
+ assertEquals(
+ "fixedLength="
+ + fixedLength
+ + " doc="
+ + docs[i]
+ + " startDoc="
+ + startDoc
+ + " docStep="
+ + docStep,
+ singleValue,
+ actual[i]);
+ assertEquals(
+ "fixedLength=" + fixedLength + " doc=" + docs[i], expected[docs[i]], singleValue);
+ }
+
+ // Verify offset-aware variant produces identical results
+ if (size > 2) {
+ int docsOffset = 1;
+ int sliceSize = size - 2;
+ BytesRef sentinel = new BytesRef("SENTINEL");
+ BytesRef[] offsetActual = new BytesRef[sliceSize + 4];
+ int valuesOffset = 2;
+ Arrays.fill(offsetActual, sentinel);
+ values.binaryValues(sliceSize, docs, docsOffset, offsetActual, valuesOffset);
+ for (int i = 0; i < sliceSize; i++) {
+ assertEquals(
+ "offset variant mismatch at i=" + i + " fixedLength=" + fixedLength,
+ actual[docsOffset + i],
+ offsetActual[valuesOffset + i]);
+ }
+ // sentinel positions should be untouched
+ assertSame(sentinel, offsetActual[0]);
+ assertSame(sentinel, offsetActual[1]);
+ assertSame(sentinel, offsetActual[valuesOffset + sliceSize]);
+ }
+ }
+
+ public void testSparseBinaryValuesBulkFetch() throws Exception {
+ final int numDocs = 512;
+ final BytesRef[] expected = new BytesRef[numDocs];
+ // Only even-numbered docs have binary values
+ for (int i = 0; i < numDocs; i++) {
+ if (i % 2 == 0) {
+ byte[] bytes = new byte[8];
+ random().nextBytes(bytes);
+ expected[i] = new BytesRef(bytes);
+ }
+ }
+
+ Directory dir = newDirectory();
+ IndexWriterConfig conf = new IndexWriterConfig(new MockAnalyzer(random()));
+ conf.setMergeScheduler(new SerialMergeScheduler());
+ IndexWriter writer = new IndexWriter(dir, conf);
+ for (int i = 0; i < numDocs; i++) {
+ Document doc = new Document();
+ if (expected[i] != null) {
+ doc.add(new BinaryDocValuesField("binary", expected[i]));
+ }
+ writer.addDocument(doc);
+ }
+ writer.forceMerge(1);
+
+ try (DirectoryReader reader = DirectoryReader.open(writer)) {
+ BinaryDocValues values = reader.leaves().get(0).reader().getBinaryDocValues("binary");
+ BinaryDocValues singleValues = reader.leaves().get(0).reader().getBinaryDocValues("binary");
+
+ // Request a mix of docs with and without values
+ int size = 64;
+ int[] docs = new int[size];
+ BytesRef[] actual = new BytesRef[size];
+ for (int i = 0; i < size; i++) {
+ docs[i] = i * 4; // every 4th doc: even (has value) and odd (no value) mix
+ }
+
+ values.binaryValues(size, docs, actual);
+ for (int i = 0; i < size; i++) {
+ if (singleValues.advanceExact(docs[i])) {
+ BytesRef singleValue = BytesRef.deepCopyOf(singleValues.binaryValue());
+ assertNotNull("doc " + docs[i] + " should have a value", actual[i]);
+ assertEquals("doc " + docs[i], singleValue, actual[i]);
+ } else {
+ assertNull("doc " + docs[i] + " should be null", actual[i]);
+ }
+ }
+
+ // Test size == 0 edge case
+ BinaryDocValues zeroValues = reader.leaves().get(0).reader().getBinaryDocValues("binary");
+ int docIdBefore = zeroValues.docID();
+ zeroValues.binaryValues(0, new int[0], new BytesRef[0]);
+ assertEquals(docIdBefore, zeroValues.docID());
+ }
+
+ writer.close();
+ dir.close();
+ }
+
private void doTestSparseDocValuesVsStoredFields() throws Exception {
final long[] values = new long[TestUtil.nextInt(random(), 1, 500)];
for (int i = 0; i < values.length; ++i) {
diff --git a/lucene/test-framework/src/java/org/apache/lucene/tests/index/AssertingLeafReader.java b/lucene/test-framework/src/java/org/apache/lucene/tests/index/AssertingLeafReader.java
index 50984ed1030..37c6b7de5c0 100644
--- a/lucene/test-framework/src/java/org/apache/lucene/tests/index/AssertingLeafReader.java
+++ b/lucene/test-framework/src/java/org/apache/lucene/tests/index/AssertingLeafReader.java
@@ -967,6 +967,22 @@ public class AssertingLeafReader extends FilterLeafReader {
return in.binaryValue();
}
+ @Override
+ public void binaryValues(int size, int[] docs, BytesRef[] values) throws IOException {
+ assertThread("Binary doc values", creationThread);
+ assert size >= 0;
+ assert size == 0 || docs[0] >= docID();
+ assert size == 0 || docs[0] >= 0;
+ for (int i = 1; i < size; ++i) {
+ assert docs[i] > docs[i - 1];
+ }
+ assert size == 0 || docs[size - 1] < maxDoc;
+ int expectedDocIdOnReturn = size == 0 ? docID() : docs[size - 1];
+ super.binaryValues(size, docs, values);
+ lastDocID = in.docID();
+ assert lastDocID == expectedDocIdOnReturn;
+ }
+
@Override
public String toString() {
return "AssertingBinaryDocValues(" + in + ")";