Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 8 additions & 7 deletions lucene/CHANGES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,9 @@ Optimizations

* GITHUB#16352: Better cost() estimation for SkipBlockRangeIterator. (Alan Woodward)

* GITHUB#16285: Apply GCD bound transform to sorted numeric rangeIntoBitSet, comparing raw
encoded values directly instead of decoding every packed value. (Costin Leau)

* GITHUB#16307: ReqExclBulkScorer now skips runs of excluded docs via
TwoPhaseIterator#docIDRunEnd for two-phase excluded clauses (e.g. a doc-values
not-equals filter), instead of advancing one doc at a time. (Jim Ferenczi)
Expand All @@ -356,18 +359,16 @@ Bug Fixes
---------------------
* GITHUB#16350: Disable bulk-scoring in monitor queries. (Alan Woodward)

* GITHUB#16295: SingletonSortedNumericDocValues now delegates rangeIntoBitSet
to the wrapped NumericDocValues, enabling optimized range evaluation for
single-valued sorted numeric fields. (Costin Leau)

<<<<<<< join_util_scores
* GITHUB#16378: Accumulate join Total/Avg scores in double precision in
TermsWithScoreCollector and JoinUtil's numeric point join, fixing
intermittent failures caused by non-associative float addition. (Luca Cavanna)
=======

* GITHUB#16296: Fix missing null check in RamUsageEstimator.sizeOf(Accountable)
to be consistent with sizeOf(String) and sizeOf(Accountable[]). (Tim Grein)
>>>>>>> main

* GITHUB#16295: SingletonSortedNumericDocValues now delegates rangeIntoBitSet
to the wrapped NumericDocValues, enabling optimized range evaluation for
single-valued sorted numeric fields. (Costin Leau)

Other
---------------------
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
/*
* 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.Field;
import org.apache.lucene.document.SortedNumericDocValuesField;
import org.apache.lucene.document.StringField;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.BooleanClause.Occur;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.TermQuery;
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;

/**
* Benchmarks range queries over GCD/delta-encoded sorted numeric doc values with multiple values
* per doc.
*/
@State(Scope.Thread)
@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.SECONDS)
@Warmup(iterations = 3, time = 3)
@Measurement(iterations = 5, time = 5)
public class SortedNumericGcdRangeIntoBitSetBenchmark {

private static final String FIELD = "val";
private static final String LEAD_FIELD = "lead";
private static final String LEAD_VALUE = "yes";
private static final long DOMAIN = 10_000_000L;
private static final long DELTA = 1_700_000_000_000L;

private Directory dir;
private DirectoryReader reader;
private IndexSearcher searcher;
private Path path;
private Query query;

@Param({"1000000"})
public int numDocs;

@Param({"delta_only", "gcd_1000", "gcd_100_delta"})
public String encoding;

@Param({"1", "3", "5"})
public int cardinality;

@Param({"0.01", "0.1", "0.5"})
public double selectivity;

@Setup(Level.Trial)
public void setup() throws Exception {
path = Files.createTempDirectory("sortedNumericGcdRange");
dir = MMapDirectory.open(path);

Random random = new Random(0);
try (IndexWriter writer = new IndexWriter(dir, new IndexWriterConfig())) {
for (int i = 0; i < numDocs; i++) {
Document doc = new Document();
long base = valueForDoc(i, random);
for (int c = 0; c < cardinality; c++) {
doc.add(SortedNumericDocValuesField.indexedField(FIELD, base + c * step()));
}
doc.add(new StringField(LEAD_FIELD, LEAD_VALUE, Field.Store.NO));
writer.addDocument(doc);
}
writer.forceMerge(1);
}

reader = DirectoryReader.open(dir);
searcher = new IndexSearcher(reader);
query = rangeQuery();
}

private long valueForDoc(int doc, Random random) {
long value = random.nextLong(0, DOMAIN);
return switch (encoding) {
case "delta_only" -> DELTA + value;
case "gcd_1000" -> value * 1_000L;
case "gcd_100_delta" -> DELTA + value * 100L;
default -> throw new IllegalArgumentException("Unknown encoding: " + encoding);
};
}

private long step() {
return switch (encoding) {
case "delta_only" -> 1;
case "gcd_1000" -> 1_000L;
case "gcd_100_delta" -> 100L;
default -> throw new IllegalArgumentException("Unknown encoding: " + encoding);
};
}

private Query rangeQuery() {
long range = Math.max(1, (long) (DOMAIN * selectivity));
long min = (DOMAIN - range) / 2;
long max = min + range;
long actualMin = actualValue(min);
long actualMax = actualValue(max);
Query rangeQuery = SortedNumericDocValuesField.newSlowRangeQuery(FIELD, actualMin, actualMax);
return new BooleanQuery.Builder()
.add(new TermQuery(new Term(LEAD_FIELD, LEAD_VALUE)), Occur.FILTER)
.add(rangeQuery, Occur.FILTER)
.build();
}

private long actualValue(long value) {
return switch (encoding) {
case "delta_only" -> DELTA + value;
case "gcd_1000" -> value * 1_000L;
case "gcd_100_delta" -> DELTA + value * 100L;
default -> throw new IllegalArgumentException("Unknown encoding: " + encoding);
};
}

@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 rangeQueryDefaultProvider() throws IOException {
return searcher.count(query);
}

@Benchmark
@Fork(
value = 1,
jvmArgsAppend = {
"--add-modules",
"jdk.incubator.vector",
"-Xmx2g",
"-Xms2g",
"-XX:+AlwaysPreTouch"
})
public int rangeQueryPanamaProvider() throws IOException {
return searcher.count(query);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -482,12 +482,28 @@ static void rangeIntoBitSet(
}

/**
* Maps the raw query bounds {@code [minValue, maxValue]} into the encoded domain so the SIMD
* kernel in {@link org.apache.lucene.internal.vectorization.DocValuesRangeSupport} can run
* directly on packed values: {@code [encodedMin, encodedMax] = [ceil((min - delta) / mul),
* floor((max - delta) / mul)]}. Open bounds (e.g. {@code Long.MIN_VALUE} or {@code
* Long.MAX_VALUE}) are saturated to the encoded domain so they keep the SIMD path even when
* {@code min - delta} or {@code max - delta} would overflow.
* Transforms query bounds {@code [minValue, maxValue]} into the encoded domain where stored
* values satisfy {@code stored = raw * mul + delta}. Returns {@code {encodedMin, encodedMax}} or
* {@code null} if the range is empty (no raw value can match).
*/
private static long[] transformGcdBounds(long minValue, long maxValue, long mul, long delta) {
assert mul > 0;
long encodedMin = saturatingShiftLower(minValue, delta);
long encodedMax = saturatingShiftUpper(maxValue, delta);
if (mul != 1) {
encodedMin = Math.ceilDiv(encodedMin, mul);
encodedMax = Math.floorDiv(encodedMax, mul);
}
encodedMin = Math.max(0, encodedMin);
if (encodedMin > encodedMax) {
return null;
}
return new long[] {encodedMin, encodedMax};
}

/**
* Maps the raw query bounds {@code [minValue, maxValue]} into the encoded domain and runs the
* SIMD range scan directly on packed values.
*/
private static void rangeGcdDeltaIntoBitSet(
LongValues values,
Expand All @@ -499,18 +515,9 @@ private static void rangeGcdDeltaIntoBitSet(
long delta,
FixedBitSet bitSet,
int offset) {
assert mul > 0;
long encodedMin = saturatingShiftLower(minValue, delta);
long encodedMax = saturatingShiftUpper(maxValue, delta);
if (mul != 1) {
// Math.ceilDiv / Math.floorDiv never overflow for mul > 0 (only Long.MIN_VALUE / -1 does),
// so the SIMD path is always taken; no fallback to the per-doc decoded loop is required.
encodedMin = Math.ceilDiv(encodedMin, mul);
encodedMax = Math.floorDiv(encodedMax, mul);
}
encodedMin = Math.max(0, encodedMin);
if (encodedMin <= encodedMax) {
rangeIntoBitSet(values, fromDoc, toDoc, encodedMin, encodedMax, bitSet, offset);
long[] bounds = transformGcdBounds(minValue, maxValue, mul, delta);
if (bounds != null) {
rangeIntoBitSet(values, fromDoc, toDoc, bounds[0], bounds[1], bitSet, offset);
}
}

Expand Down Expand Up @@ -1933,6 +1940,27 @@ private SortedNumericDocValues getSortedNumeric(
final LongValues values = getNumericValues(entry);
final int denseFixedCardinality = fixedCardinality(entry, skipperEntry);

// For GCD/delta encoded entries, capture raw packed values for rangeIntoBitSet optimization.
// The decoded `values` wrapper applies mul*get+delta per call; using raw values with
// transformed bounds avoids this per-value decode cost.
final boolean hasGcdEncoding =
entry.bitsPerValue > 0
&& entry.blockShift < 0
&& entry.table == null
&& (entry.gcd != 1 || entry.minValue != 0);
final LongValues rawValues;
final long mul, delta;
if (hasGcdEncoding) {
RandomAccessInput rawSlice = data.randomAccessSlice(entry.valuesOffset, entry.valuesLength);
rawValues = getDirectReaderInstance(rawSlice, entry.bitsPerValue, 0L, entry.numValues);
mul = entry.gcd;
delta = entry.minValue;
} else {
rawValues = null;
mul = 1;
delta = 0;
}

if (entry.docsWithFieldOffset == -1) {
// dense
return new SortedNumericDocValues() {
Expand Down Expand Up @@ -1999,16 +2027,31 @@ public void rangeIntoBitSet(
}
return;
}
LongValues v;
long lo, hi;
if (rawValues != null) {
Comment thread
costin marked this conversation as resolved.
long[] bounds = transformGcdBounds(minValue, maxValue, mul, delta);
if (bounds == null) {
return;
}
v = rawValues;
lo = bounds[0];
hi = bounds[1];
} else {
v = values;
lo = minValue;
hi = maxValue;
}
int cardinality = denseFixedCardinality;
if (cardinality > 1) {
DOC_VALUES_RANGE_SUPPORT.sortedNumericRangeIntoBitSet(
values, fromDoc, endDoc, cardinality, minValue, maxValue, bitSet, offset);
v, fromDoc, endDoc, cardinality, lo, hi, bitSet, offset);
return;
}
for (int currentDoc = fromDoc; currentDoc < endDoc; currentDoc++) {
long startOffset = addresses.get(currentDoc);
long endOffset = addresses.get(currentDoc + 1L);
if (sortedNumericMatchesRange(values, startOffset, endOffset, minValue, maxValue)) {
if (sortedNumericMatchesRange(v, startOffset, endOffset, lo, hi)) {
bitSet.set(currentDoc - offset);
}
}
Expand Down Expand Up @@ -2108,11 +2151,27 @@ public void rangeIntoBitSet(
set = false;
return;
}
LongValues v;
long lo, hi;
if (rawValues != null) {
long[] bounds = transformGcdBounds(minValue, maxValue, mul, delta);
if (bounds == null) {
set = false;
return;
}
v = rawValues;
lo = bounds[0];
hi = bounds[1];
} else {
v = values;
lo = minValue;
hi = maxValue;
}
for (; currentDoc < endDoc; currentDoc = disi.nextDoc()) {
int index = disi.index();
long startOffset = addresses.get(index);
long endOffset = addresses.get(index + 1L);
if (sortedNumericMatchesRange(values, startOffset, endOffset, minValue, maxValue)) {
if (sortedNumericMatchesRange(v, startOffset, endOffset, lo, hi)) {
bitSet.set(currentDoc - offset);
}
}
Expand Down
Loading
Loading