Skip to content

Commit b843b4c

Browse files
authored
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).
1 parent b6ca26f commit b843b4c

7 files changed

Lines changed: 529 additions & 2 deletions

File tree

lucene/CHANGES.txt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -289,7 +289,9 @@ Other
289289

290290
API Changes
291291
---------------------
292-
(No changes)
292+
293+
* GITHUB#16286: Introduce BinaryDocValues#binaryValues to help speed up the
294+
retrieval of many binary doc values at once. (Costin Leau)
293295

294296
New Features
295297
---------------------
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.apache.lucene.benchmark.jmh;
18+
19+
import java.io.IOException;
20+
import java.nio.file.Files;
21+
import java.nio.file.Path;
22+
import java.util.Comparator;
23+
import java.util.Random;
24+
import java.util.concurrent.TimeUnit;
25+
import java.util.stream.Stream;
26+
import org.apache.lucene.document.BinaryDocValuesField;
27+
import org.apache.lucene.document.Document;
28+
import org.apache.lucene.index.BinaryDocValues;
29+
import org.apache.lucene.index.DirectoryReader;
30+
import org.apache.lucene.index.IndexWriter;
31+
import org.apache.lucene.index.IndexWriterConfig;
32+
import org.apache.lucene.store.Directory;
33+
import org.apache.lucene.store.MMapDirectory;
34+
import org.apache.lucene.util.BytesRef;
35+
import org.openjdk.jmh.annotations.Benchmark;
36+
import org.openjdk.jmh.annotations.BenchmarkMode;
37+
import org.openjdk.jmh.annotations.Fork;
38+
import org.openjdk.jmh.annotations.Level;
39+
import org.openjdk.jmh.annotations.Measurement;
40+
import org.openjdk.jmh.annotations.Mode;
41+
import org.openjdk.jmh.annotations.OutputTimeUnit;
42+
import org.openjdk.jmh.annotations.Param;
43+
import org.openjdk.jmh.annotations.Scope;
44+
import org.openjdk.jmh.annotations.Setup;
45+
import org.openjdk.jmh.annotations.State;
46+
import org.openjdk.jmh.annotations.TearDown;
47+
import org.openjdk.jmh.annotations.Warmup;
48+
49+
/**
50+
* Benchmarks bulk retrieval of dense binary doc values via {@link BinaryDocValues#binaryValues}.
51+
* Compares the per-doc default with the Lucene90 codec override that reads directly from the data
52+
* slice.
53+
*/
54+
@State(Scope.Thread)
55+
@BenchmarkMode(Mode.Throughput)
56+
@OutputTimeUnit(TimeUnit.SECONDS)
57+
@Warmup(iterations = 3, time = 2)
58+
@Measurement(iterations = 5, time = 2)
59+
public class BinaryDocValuesBulkDecodeBenchmark {
60+
61+
private Directory dir;
62+
private DirectoryReader reader;
63+
private BinaryDocValues values;
64+
private Path path;
65+
private int[] docs;
66+
private BytesRef[] valueBuffer;
67+
private int nextStart;
68+
69+
@Param({"1000000"})
70+
public int docCount;
71+
72+
@Param({"8", "32", "128"})
73+
public int valueLength;
74+
75+
@Param({"128", "1024"})
76+
public int batchSize;
77+
78+
@Param({"fixed", "variable"})
79+
public String encoding;
80+
81+
@Setup(Level.Trial)
82+
public void setup() throws Exception {
83+
path = Files.createTempDirectory("binaryDocValuesBulkDecode");
84+
dir = MMapDirectory.open(path);
85+
86+
IndexWriter writer = new IndexWriter(dir, new IndexWriterConfig());
87+
Random random = new Random(0);
88+
for (int i = 0; i < docCount; i++) {
89+
Document doc = new Document();
90+
int len;
91+
if (encoding.equals("fixed")) {
92+
len = valueLength;
93+
} else {
94+
len = 1 + random.nextInt(valueLength);
95+
}
96+
byte[] bytes = new byte[len];
97+
random.nextBytes(bytes);
98+
doc.add(new BinaryDocValuesField("field", new BytesRef(bytes)));
99+
writer.addDocument(doc);
100+
}
101+
writer.forceMerge(1);
102+
reader = DirectoryReader.open(writer);
103+
writer.close();
104+
105+
values = reader.leaves().get(0).reader().getBinaryDocValues("field");
106+
docs = new int[batchSize];
107+
valueBuffer = new BytesRef[batchSize];
108+
}
109+
110+
@TearDown(Level.Trial)
111+
public void tearDown() throws Exception {
112+
reader.close();
113+
dir.close();
114+
if (Files.exists(path)) {
115+
try (Stream<Path> walk = Files.walk(path)) {
116+
walk.sorted(Comparator.reverseOrder())
117+
.forEach(
118+
p -> {
119+
try {
120+
Files.delete(p);
121+
} catch (IOException _) {
122+
}
123+
});
124+
}
125+
}
126+
}
127+
128+
@Benchmark
129+
@Fork(
130+
value = 1,
131+
jvmArgsAppend = {"-Xmx2g", "-Xms2g", "-XX:+AlwaysPreTouch"})
132+
public int binaryValuesBulk() throws IOException {
133+
return readBatchBulk();
134+
}
135+
136+
@Benchmark
137+
@Fork(
138+
value = 1,
139+
jvmArgsAppend = {"-Xmx2g", "-Xms2g", "-XX:+AlwaysPreTouch"})
140+
public int binaryValuesPerDoc() throws IOException {
141+
return readBatchPerDoc();
142+
}
143+
144+
private int readBatchBulk() throws IOException {
145+
final int maxStart = docCount - batchSize;
146+
if (nextStart > maxStart) {
147+
nextStart = 0;
148+
}
149+
for (int i = 0; i < batchSize; i++) {
150+
docs[i] = nextStart + i;
151+
}
152+
nextStart += batchSize;
153+
154+
values.binaryValues(batchSize, docs, valueBuffer);
155+
int checksum = 0;
156+
for (int i = 0; i < batchSize; i++) {
157+
checksum += valueBuffer[i].length;
158+
}
159+
return checksum;
160+
}
161+
162+
private int readBatchPerDoc() throws IOException {
163+
final int maxStart = docCount - batchSize;
164+
if (nextStart > maxStart) {
165+
nextStart = 0;
166+
}
167+
168+
int checksum = 0;
169+
for (int i = 0; i < batchSize; i++) {
170+
int doc = nextStart + i;
171+
values.advanceExact(doc);
172+
BytesRef ref = BytesRef.deepCopyOf(values.binaryValue());
173+
checksum += ref.length;
174+
}
175+
nextStart += batchSize;
176+
return checksum;
177+
}
178+
}

lucene/core/src/java/org/apache/lucene/codecs/lucene90/Lucene90DocValuesProducer.java

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1231,6 +1231,27 @@ public BytesRef binaryValue() throws IOException {
12311231
bytesSlice.readBytes((long) doc * length, bytes.bytes, 0, length);
12321232
return bytes;
12331233
}
1234+
1235+
@Override
1236+
public void binaryValues(
1237+
int size, int[] docs, int docsOffset, BytesRef[] values, int valuesOffset)
1238+
throws IOException {
1239+
if (size == 0) {
1240+
return;
1241+
}
1242+
byte[] bulk = new byte[size * length];
1243+
if (isContiguous(size, docs, docsOffset)) {
1244+
bytesSlice.readBytes((long) docs[docsOffset] * length, bulk, 0, bulk.length);
1245+
} else {
1246+
for (int di = docsOffset, bi = 0, end = docsOffset + size; di < end; di++, bi++) {
1247+
bytesSlice.readBytes((long) docs[di] * length, bulk, bi * length, length);
1248+
}
1249+
}
1250+
for (int i = 0; i < size; i++) {
1251+
values[valuesOffset + i] = new BytesRef(bulk, i * length, length);
1252+
}
1253+
doc = docs[docsOffset + size - 1];
1254+
}
12341255
};
12351256
} else {
12361257
// variable length
@@ -1253,6 +1274,41 @@ public BytesRef binaryValue() throws IOException {
12531274
bytesSlice.readBytes(startOffset, bytes.bytes, 0, bytes.length);
12541275
return bytes;
12551276
}
1277+
1278+
@Override
1279+
public void binaryValues(
1280+
int size, int[] docs, int docsOffset, BytesRef[] values, int valuesOffset)
1281+
throws IOException {
1282+
if (size == 0) {
1283+
return;
1284+
}
1285+
if (isContiguous(size, docs, docsOffset)) {
1286+
long firstStart = addresses.get(docs[docsOffset]);
1287+
long lastEnd = addresses.get(docs[docsOffset + size - 1] + 1L);
1288+
int totalBytes = (int) (lastEnd - firstStart);
1289+
byte[] bulk = new byte[totalBytes];
1290+
bytesSlice.readBytes(firstStart, bulk, 0, totalBytes);
1291+
for (int di = docsOffset, vi = valuesOffset, end = docsOffset + size;
1292+
di < end;
1293+
di++, vi++) {
1294+
int offset = (int) (addresses.get(docs[di]) - firstStart);
1295+
int len = (int) (addresses.get(docs[di] + 1L) - addresses.get(docs[di]));
1296+
values[vi] = new BytesRef(bulk, offset, len);
1297+
}
1298+
} else {
1299+
for (int di = docsOffset, vi = valuesOffset, end = docsOffset + size;
1300+
di < end;
1301+
di++, vi++) {
1302+
int d = docs[di];
1303+
long startOffset = addresses.get(d);
1304+
int len = (int) (addresses.get(d + 1L) - startOffset);
1305+
byte[] b = new byte[len];
1306+
bytesSlice.readBytes(startOffset, b, 0, len);
1307+
values[vi] = new BytesRef(b, 0, len);
1308+
}
1309+
}
1310+
doc = docs[docsOffset + size - 1];
1311+
}
12561312
};
12571313
}
12581314
} else {

lucene/core/src/java/org/apache/lucene/index/BinaryDocValues.java

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,11 @@
1818
package org.apache.lucene.index;
1919

2020
import java.io.IOException;
21+
import org.apache.lucene.search.BooleanClause.Occur;
22+
import org.apache.lucene.search.FieldExistsQuery;
2123
import org.apache.lucene.util.BytesRef;
2224

23-
/** A per-document numeric value. */
25+
/** A per-document binary value. */
2426
public abstract class BinaryDocValues extends DocValuesIterator {
2527

2628
/** Sole constructor. (For invocation by subclass constructors, typically implicit.) */
@@ -33,4 +35,76 @@ protected BinaryDocValues() {}
3335
* @return binary value
3436
*/
3537
public abstract BytesRef binaryValue() throws IOException;
38+
39+
/**
40+
* Bulk retrieval of binary doc values. This API helps reduce the performance impact of virtual
41+
* function calls.
42+
*
43+
* <p>This API behaves as if implemented as below, which is the default implementation:
44+
*
45+
* <pre><code class="language-java">
46+
* public void binaryValues(int size, int[] docs, BytesRef[] values) throws IOException {
47+
* for (int i = 0; i &lt; size; ++i) {
48+
* int doc = docs[i];
49+
* if (advanceExact(doc)) {
50+
* values[i] = BytesRef.deepCopyOf(binaryValue());
51+
* } else {
52+
* values[i] = null;
53+
* }
54+
* }
55+
* }
56+
* </code></pre>
57+
*
58+
* <p><b>NOTE</b>: The {@code docs} array is required to be sorted in ascending order with no
59+
* duplicates.
60+
*
61+
* <p><b>NOTE</b>: Documents that don't have a value for this field will have their corresponding
62+
* entry set to {@code null}. If you need to exclude documents that don't have a value, then you
63+
* could apply a {@link FieldExistsQuery} as a {@link Occur#FILTER} clause. Another option is to
64+
* fall back to using {@link #advanceExact} and {@link #binaryValue()} on ranges of doc IDs that
65+
* may not be dense, e.g.
66+
*
67+
* <pre><code class="language-java">
68+
* if (size &gt; 0 &amp;&amp; values.advanceExact(docs[0]) &amp;&amp; values.docIDRunEnd() &gt; docs[size - 1]) {
69+
* // use values#binaryValues to retrieve values
70+
* } else {
71+
* // some docs may not have a value, use #advanceExact and #binaryValue
72+
* }
73+
* </code></pre>
74+
*
75+
* <p><b>NOTE</b>: Each returned {@link BytesRef} is a deep copy owned by the caller and remains
76+
* valid after subsequent calls.
77+
*
78+
* @param size the number of values to retrieve
79+
* @param docs the buffer of doc IDs whose values should be looked up
80+
* @param values the buffer of values to fill; entries are set to {@code null} when a document
81+
* doesn't have a value
82+
*/
83+
public void binaryValues(int size, int[] docs, BytesRef[] values) throws IOException {
84+
binaryValues(size, docs, 0, values, 0);
85+
}
86+
87+
/**
88+
* Offset-aware variant of {@link #binaryValues(int, int[], BytesRef[])}. Reads {@code size} doc
89+
* IDs starting at {@code docs[docsOffset]} and writes the corresponding values starting at {@code
90+
* values[valuesOffset]}. This follows the same convention as {@link System#arraycopy}.
91+
*
92+
* @param size the number of values to retrieve
93+
* @param docs the buffer of doc IDs whose values should be looked up
94+
* @param docsOffset first position in {@code docs} to read
95+
* @param values the buffer of values to fill; entries are set to {@code null} when a document
96+
* doesn't have a value
97+
* @param valuesOffset first position in {@code values} to write
98+
*/
99+
public void binaryValues(
100+
int size, int[] docs, int docsOffset, BytesRef[] values, int valuesOffset)
101+
throws IOException {
102+
for (int di = docsOffset, vi = valuesOffset, end = docsOffset + size; di < end; di++, vi++) {
103+
if (advanceExact(docs[di])) {
104+
values[vi] = BytesRef.deepCopyOf(binaryValue());
105+
} else {
106+
values[vi] = null;
107+
}
108+
}
109+
}
36110
}

0 commit comments

Comments
 (0)