Skip to content

Commit b8ccee9

Browse files
committed
Optimize encoded numeric range bitsets
GCD- and delta-encoded dense NumericDocValues can reuse the existing range-into-bitset fast path by transforming query bounds into the encoded domain once per call. This avoids per-doc decoding while preserving scalar fallback for overflowing bound transforms.
1 parent f5622b0 commit b8ccee9

3 files changed

Lines changed: 366 additions & 7 deletions

File tree

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
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.Document;
27+
import org.apache.lucene.document.NumericDocValuesField;
28+
import org.apache.lucene.document.SortedNumericDocValuesField;
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.search.BooleanClause.Occur;
33+
import org.apache.lucene.search.BooleanQuery;
34+
import org.apache.lucene.search.IndexSearcher;
35+
import org.apache.lucene.search.MatchAllDocsQuery;
36+
import org.apache.lucene.search.Query;
37+
import org.apache.lucene.store.Directory;
38+
import org.apache.lucene.store.MMapDirectory;
39+
import org.openjdk.jmh.annotations.Benchmark;
40+
import org.openjdk.jmh.annotations.BenchmarkMode;
41+
import org.openjdk.jmh.annotations.Fork;
42+
import org.openjdk.jmh.annotations.Level;
43+
import org.openjdk.jmh.annotations.Measurement;
44+
import org.openjdk.jmh.annotations.Mode;
45+
import org.openjdk.jmh.annotations.OutputTimeUnit;
46+
import org.openjdk.jmh.annotations.Param;
47+
import org.openjdk.jmh.annotations.Scope;
48+
import org.openjdk.jmh.annotations.Setup;
49+
import org.openjdk.jmh.annotations.State;
50+
import org.openjdk.jmh.annotations.TearDown;
51+
import org.openjdk.jmh.annotations.Warmup;
52+
53+
/** Benchmarks range queries over dense numeric doc values encoded as raw, delta, GCD, or both. */
54+
@State(Scope.Thread)
55+
@BenchmarkMode(Mode.Throughput)
56+
@OutputTimeUnit(TimeUnit.SECONDS)
57+
@Warmup(iterations = 3, time = 3)
58+
@Measurement(iterations = 5, time = 5)
59+
public class GcdDeltaRangeIntoBitSetBenchmark {
60+
61+
private static final String FIELD = "val";
62+
private static final String NONE = "none";
63+
private static final String DELTA_ONLY = "delta_only";
64+
private static final String GCD_1000 = "gcd_1000";
65+
private static final String GCD_100_DELTA = "gcd_100_delta";
66+
private static final long DOMAIN = 10_000_000L;
67+
private static final long DELTA = 1_700_000_000_000L;
68+
69+
private Directory dir;
70+
private DirectoryReader reader;
71+
private IndexSearcher searcher;
72+
private Path path;
73+
private Query query;
74+
75+
@Param({"1000000"})
76+
public int numDocs;
77+
78+
@Param({NONE, DELTA_ONLY, GCD_1000, GCD_100_DELTA})
79+
public String encoding;
80+
81+
@Param({"0.01", "0.1", "0.5"})
82+
public double selectivity;
83+
84+
@Setup(Level.Trial)
85+
public void setup() throws Exception {
86+
path = Files.createTempDirectory("gcdDeltaRangeIntoBitSet");
87+
dir = MMapDirectory.open(path);
88+
89+
Random random = new Random(0);
90+
try (IndexWriter writer = new IndexWriter(dir, new IndexWriterConfig())) {
91+
for (int i = 0; i < numDocs; i++) {
92+
Document doc = new Document();
93+
doc.add(NumericDocValuesField.indexedField(FIELD, valueForDoc(encoding, i, random)));
94+
writer.addDocument(doc);
95+
}
96+
writer.forceMerge(1);
97+
}
98+
99+
reader = DirectoryReader.open(dir);
100+
searcher = new IndexSearcher(reader);
101+
query = rangeQuery(encoding, selectivity);
102+
}
103+
104+
private static long valueForDoc(String encoding, int doc, Random random) {
105+
if (doc == 0) {
106+
return minimumValue(encoding);
107+
} else if (doc == 1 && encoding.equals(GCD_100_DELTA)) {
108+
return DELTA + 100L;
109+
}
110+
111+
long value = random.nextLong(0, DOMAIN);
112+
switch (encoding) {
113+
case NONE:
114+
return value;
115+
case DELTA_ONLY:
116+
return DELTA + value;
117+
case GCD_1000:
118+
return value * 1_000L;
119+
case GCD_100_DELTA:
120+
return DELTA + value * 100L;
121+
default:
122+
throw new IllegalArgumentException("Unknown encoding: " + encoding);
123+
}
124+
}
125+
126+
private static long minimumValue(String encoding) {
127+
switch (encoding) {
128+
case NONE:
129+
case GCD_1000:
130+
return 0;
131+
case DELTA_ONLY:
132+
case GCD_100_DELTA:
133+
return DELTA;
134+
default:
135+
throw new IllegalArgumentException("Unknown encoding: " + encoding);
136+
}
137+
}
138+
139+
private static Query rangeQuery(String encoding, double selectivity) {
140+
long range = Math.max(1, (long) (DOMAIN * selectivity));
141+
long min = (DOMAIN - range) / 2;
142+
long max = min + range;
143+
Query rangeQuery =
144+
SortedNumericDocValuesField.newSlowRangeQuery(
145+
FIELD, actualValue(encoding, min), actualValue(encoding, max));
146+
return new BooleanQuery.Builder()
147+
.add(new MatchAllDocsQuery(), Occur.FILTER)
148+
.add(rangeQuery, Occur.FILTER)
149+
.build();
150+
}
151+
152+
private static long actualValue(String encoding, long value) {
153+
switch (encoding) {
154+
case NONE:
155+
return value;
156+
case DELTA_ONLY:
157+
return DELTA + value;
158+
case GCD_1000:
159+
return value * 1_000L;
160+
case GCD_100_DELTA:
161+
return DELTA + value * 100L;
162+
default:
163+
throw new IllegalArgumentException("Unknown encoding: " + encoding);
164+
}
165+
}
166+
167+
@TearDown(Level.Trial)
168+
public void tearDown() throws Exception {
169+
reader.close();
170+
dir.close();
171+
if (Files.exists(path)) {
172+
try (Stream<Path> walk = Files.walk(path)) {
173+
walk.sorted(Comparator.reverseOrder())
174+
.forEach(
175+
p -> {
176+
try {
177+
Files.delete(p);
178+
} catch (IOException _) {
179+
}
180+
});
181+
}
182+
}
183+
}
184+
185+
@Benchmark
186+
@Fork(
187+
value = 1,
188+
jvmArgsAppend = {"-Xmx2g", "-Xms2g", "-XX:+AlwaysPreTouch"})
189+
public int rangeQueryDefaultProvider() throws IOException {
190+
return searcher.count(query);
191+
}
192+
193+
@Benchmark
194+
@Fork(
195+
value = 1,
196+
jvmArgsAppend = {
197+
"--add-modules",
198+
"jdk.incubator.vector",
199+
"-Xmx2g",
200+
"-Xms2g",
201+
"-XX:+AlwaysPreTouch"
202+
})
203+
public int rangeQueryPanamaProvider() throws IOException {
204+
return searcher.count(query);
205+
}
206+
}

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

Lines changed: 55 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -480,6 +480,59 @@ static void rangeIntoBitSet(
480480
values, fromDoc, toDoc, minValue, maxValue, bitSet, offset);
481481
}
482482

483+
private static void rangeGcdDeltaIntoBitSet(
484+
LongValues values,
485+
int fromDoc,
486+
int toDoc,
487+
long minValue,
488+
long maxValue,
489+
long mul,
490+
long delta,
491+
FixedBitSet bitSet,
492+
int offset) {
493+
long encodedMin;
494+
long encodedMax;
495+
try {
496+
encodedMin = Math.subtractExact(minValue, delta);
497+
encodedMax = Math.subtractExact(maxValue, delta);
498+
if (mul != 1) {
499+
encodedMin = ceilDiv(encodedMin, mul);
500+
encodedMax = Math.floorDiv(encodedMax, mul);
501+
}
502+
encodedMin = Math.max(0, encodedMin);
503+
} catch (ArithmeticException _) {
504+
rangeGcdDeltaIntoBitSetSlow(
505+
values, fromDoc, toDoc, minValue, maxValue, mul, delta, bitSet, offset);
506+
return;
507+
}
508+
if (encodedMin <= encodedMax) {
509+
rangeIntoBitSet(values, fromDoc, toDoc, encodedMin, encodedMax, bitSet, offset);
510+
}
511+
}
512+
513+
private static long ceilDiv(long value, long divisor) {
514+
assert divisor > 0;
515+
return value >= 0 ? Math.addExact(value, divisor - 1) / divisor : value / divisor;
516+
}
517+
518+
private static void rangeGcdDeltaIntoBitSetSlow(
519+
LongValues values,
520+
int fromDoc,
521+
int toDoc,
522+
long minValue,
523+
long maxValue,
524+
long mul,
525+
long delta,
526+
FixedBitSet bitSet,
527+
int offset) {
528+
for (int d = fromDoc; d < toDoc; d++) {
529+
long v = mul * values.get(d) + delta;
530+
if (v >= minValue && v <= maxValue) {
531+
bitSet.set(d - offset);
532+
}
533+
}
534+
}
535+
483536
private static boolean canBulkDecodeByteAligned(NumericEntry entry) {
484537
return entry.blockShift < 0 && entry.bitsPerValue > 0 && (entry.bitsPerValue & 0x07) == 0;
485538
}
@@ -910,13 +963,8 @@ public void rangeIntoBitSet(
910963
long maxValue,
911964
FixedBitSet bitSet,
912965
int offset) {
913-
// Per-doc evaluation for gcd/delta encoded fields
914-
for (int d = fromDoc; d < toDoc; d++) {
915-
long v = mul * values.get(d) + delta;
916-
if (v >= minValue && v <= maxValue) {
917-
bitSet.set(d - offset);
918-
}
919-
}
966+
Lucene90DocValuesProducer.rangeGcdDeltaIntoBitSet(
967+
values, fromDoc, toDoc, minValue, maxValue, mul, delta, bitSet, offset);
920968
}
921969
};
922970
}

lucene/core/src/test/org/apache/lucene/search/TestSkipBlockRangeIteratorIntoBitSet.java

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import java.util.ArrayList;
2020
import java.util.List;
2121
import java.util.Random;
22+
import java.util.function.LongUnaryOperator;
2223
import org.apache.lucene.codecs.lucene104.Lucene104Codec;
2324
import org.apache.lucene.document.Document;
2425
import org.apache.lucene.document.NumericDocValuesField;
@@ -598,4 +599,108 @@ public void testRangeIntoBitSetMatchesPerDocEvaluation() throws Exception {
598599
}
599600
}
600601
}
602+
603+
public void testRangeIntoBitSetMatchesPerDocEvaluationWithDeltaEncoding() throws Exception {
604+
long delta = 1_000_000L;
605+
long[] values = rangeValues(DOC_COUNT, doc -> delta + doc);
606+
assertRangeIntoBitSetMatchesPerDocEvaluation(
607+
"delta-only encoded range must match decoded evaluation",
608+
values,
609+
delta + 127,
610+
delta + 4097);
611+
}
612+
613+
public void testRangeIntoBitSetMatchesPerDocEvaluationWithGcdEncoding() throws Exception {
614+
long[] values = rangeValues(DOC_COUNT, doc -> doc * 1_000L);
615+
assertRangeIntoBitSetMatchesPerDocEvaluation(
616+
"gcd encoded range must match decoded evaluation", values, 123_456L, 4_567_890L);
617+
}
618+
619+
public void testRangeIntoBitSetMatchesPerDocEvaluationWithGcdAndDeltaEncoding() throws Exception {
620+
long delta = 1_000_000L;
621+
long[] values = rangeValues(DOC_COUNT, doc -> delta + doc * 100L);
622+
assertRangeIntoBitSetMatchesPerDocEvaluation(
623+
"gcd+delta encoded range must match decoded evaluation",
624+
values,
625+
delta + 123,
626+
delta + 456_789);
627+
}
628+
629+
public void testRangeIntoBitSetMatchesPerDocEvaluationWhenGcdRangeFallsBetweenValues()
630+
throws Exception {
631+
long[] values = rangeValues(DOC_COUNT, doc -> 10L + doc * 5L);
632+
assertRangeIntoBitSetMatchesPerDocEvaluation(
633+
"gcd encoded gap range must match no docs", values, 11, 14);
634+
}
635+
636+
public void testRangeIntoBitSetMatchesPerDocEvaluationWithOverflowFallback() throws Exception {
637+
long delta = 1_000_000L;
638+
long[] values = rangeValues(DOC_COUNT, doc -> delta + doc);
639+
assertRangeIntoBitSetMatchesPerDocEvaluation(
640+
"overflowing transformed lower bound must fall back to decoded evaluation",
641+
values,
642+
Long.MIN_VALUE,
643+
delta + 127);
644+
}
645+
646+
public void testRangeIntoBitSetMatchesPerDocEvaluationWithFullGcdDeltaRange() throws Exception {
647+
long delta = 1_000_000L;
648+
long[] values = rangeValues(DOC_COUNT, doc -> delta + doc * 100L);
649+
assertRangeIntoBitSetMatchesPerDocEvaluation(
650+
"full gcd+delta encoded range must match all docs",
651+
values,
652+
delta,
653+
delta + (DOC_COUNT - 1L) * 100L);
654+
}
655+
656+
public void testRangeIntoBitSetMatchesPerDocEvaluationWithSingleGcdDeltaValue() throws Exception {
657+
long delta = 1_000_000L;
658+
long value = delta + 123L * 100L;
659+
long[] values = rangeValues(DOC_COUNT, doc -> delta + doc * 100L);
660+
assertRangeIntoBitSetMatchesPerDocEvaluation(
661+
"single gcd+delta encoded value range must match one doc", values, value, value);
662+
}
663+
664+
private static long[] rangeValues(int numDocs, LongUnaryOperator valueFunction) {
665+
long[] values = new long[numDocs];
666+
for (int i = 0; i < numDocs; i++) {
667+
values[i] = valueFunction.applyAsLong(i);
668+
}
669+
return values;
670+
}
671+
672+
private void assertRangeIntoBitSetMatchesPerDocEvaluation(
673+
String message, long[] values, long rangeMin, long rangeMax) throws Exception {
674+
try (Directory dir = newDirectory()) {
675+
IndexWriterConfig iwc = new IndexWriterConfig().setCodec(new Lucene104Codec());
676+
try (IndexWriter w = new IndexWriter(dir, iwc)) {
677+
for (long value : values) {
678+
Document doc = new Document();
679+
doc.add(NumericDocValuesField.indexedField("val", value));
680+
w.addDocument(doc);
681+
}
682+
w.forceMerge(1);
683+
}
684+
685+
try (DirectoryReader reader = DirectoryReader.open(dir)) {
686+
LeafReaderContext ctx = reader.leaves().get(0);
687+
FixedBitSet expected = new FixedBitSet(values.length);
688+
NumericDocValues slowDv = ctx.reader().getNumericDocValues("val");
689+
for (int d = 0; d < values.length; d++) {
690+
if (slowDv.advanceExact(d)) {
691+
long value = slowDv.longValue();
692+
if (value >= rangeMin && value <= rangeMax) {
693+
expected.set(d);
694+
}
695+
}
696+
}
697+
698+
FixedBitSet actual = new FixedBitSet(values.length);
699+
NumericDocValues fastDv = ctx.reader().getNumericDocValues("val");
700+
fastDv.rangeIntoBitSet(0, values.length, rangeMin, rangeMax, actual, 0);
701+
702+
assertEquals(message, expected, actual);
703+
}
704+
}
705+
}
601706
}

0 commit comments

Comments
 (0)