Skip to content
Open
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
5eac48d
perf: implement advanceShallow and getMaxScore logic for FunctionScor…
rajat315315 Jul 29, 2026
fb8e293
Implement DocValuesSkipper WAND pruning for FunctionScoreQuery and ad…
rajat315315 Jul 29, 2026
29ef08c
Update benchmark to test indexSort
rajat315315 Jul 29, 2026
168517c
Update benchmark to use BooleanQuery disjunction base query
rajat315315 Jul 29, 2026
1222a4e
Set DEFAULT_SKIP_INDEX_INTERVAL_SIZE to 128
rajat315315 Jul 29, 2026
4a73cce
Keep DEFAULT_SKIP_INDEX_INTERVAL_SIZE at 4096 in FunctionScoreQuery PR
rajat315315 Jul 29, 2026
ad7c840
Commit benchmark class
rajat315315 Jul 29, 2026
21042fa
Add support for monotonically decreasing functions in DoubleValuesSource
rajat315315 Jul 29, 2026
aff2cf6
Fix EOF trailing newlines
rajat315315 Jul 29, 2026
1fa8c20
Deprecate 2-arg fromField and add Javadoc note per review
rajat315315 Jul 29, 2026
8f187e1
Using a custom homemade function without needing to specify inc/dec.
rajat315315 Jul 29, 2026
6dba6f7
Using Monotonicity as an Enum.
rajat315315 Jul 29, 2026
152ed7f
Removed deprecated method.
rajat315315 Jul 29, 2026
6a1cdb5
prek
rajat315315 Jul 29, 2026
b996adf
gradlew tidy
rajat315315 Jul 29, 2026
9e8f7c0
Added a change entry
rajat315315 Jul 31, 2026
40dd742
Modified change entry
rajat315315 Jul 31, 2026
315a4e0
Add test case for non-monotonic function score queries to verify they…
rajat315315 Aug 1, 2026
c9d50aa
Add testConstantLambdaMonotonicity to compare INCREASING, DECREASING,…
rajat315315 Aug 1, 2026
fddb47c
Assert equal scoreDocs array length across constant lambda queries
rajat315315 Aug 1, 2026
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
1 change: 1 addition & 0 deletions lucene/benchmark-jmh/src/java/module-info.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
requires jdk.unsupported;
requires org.apache.lucene.core;
requires org.apache.lucene.expressions;
requires org.apache.lucene.queries;
requires org.apache.lucene.join;
requires org.apache.lucene.sandbox;
requires commons.math3;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
/*
* 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.util.Random;
import java.util.concurrent.TimeUnit;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.NumericDocValuesField;
import org.apache.lucene.document.TextField;
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.index.Term;
import org.apache.lucene.queries.function.FunctionScoreQuery;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.DoubleValuesSource;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.TermQuery;
import org.apache.lucene.search.TopDocs;
import org.apache.lucene.store.ByteBuffersDirectory;
import org.apache.lucene.store.Directory;
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;

/**
* JMH Micro-benchmark comparing actual FunctionScoreQuery search throughput on main branch vs
* feature/function-score-wand-skip-index branch over 1 Million Lucene index documents.
*/
@State(Scope.Benchmark)
@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.SECONDS)
@Warmup(iterations = 2, time = 2)
@Measurement(iterations = 3, time = 3)
@Fork(
value = 1,
warmups = 1,
jvmArgsAppend = {"-Xmx8g", "-Xms8g"})
public class FunctionScoreWANDMainVsFeatureBenchmark {

@State(Scope.Benchmark)
public static class BenchmarkState {

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

@Param({"true", "false"})
public boolean indexSort;

@Param({"100"})
public int topK;

public Directory dir;
public IndexReader reader;
public IndexSearcher searcher;
public Query functionScoreQuery;

@Setup(Level.Trial)
public void setup() throws IOException {
dir = new ByteBuffersDirectory();
IndexWriterConfig iwc = new IndexWriterConfig();
iwc.setRAMBufferSizeMB(256);
if (indexSort) {
iwc.setIndexSort(
new org.apache.lucene.search.Sort(
new org.apache.lucene.search.SortField(
"score_field", org.apache.lucene.search.SortField.Type.LONG, true)));
}

try (IndexWriter writer = new IndexWriter(dir, iwc)) {
Random random = new Random(42);
String[] terms = {"term_a", "term_b", "term_c", "term_d", "term_e"};
for (int i = 0; i < numDocs; i++) {
Document doc = new Document();
String chosenTerm = terms[random.nextInt(terms.length)];
doc.add(new TextField("body", chosenTerm, Field.Store.NO));
// Real-world Power-law / Zipfian score distribution (98% low scores, 2% high scores)
long scoreVal =
(random.nextFloat() < 0.02f) ? (50000 + random.nextInt(50000)) : random.nextInt(100);
doc.add(new NumericDocValuesField("score_field", scoreVal));
writer.addDocument(doc);
}
writer.commit();
}

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

BooleanQuery.Builder bq = new BooleanQuery.Builder();
bq.add(
new TermQuery(new Term("body", "term_a")),
org.apache.lucene.search.BooleanClause.Occur.SHOULD);
bq.add(
new TermQuery(new Term("body", "term_b")),
org.apache.lucene.search.BooleanClause.Occur.SHOULD);
bq.add(
new TermQuery(new Term("body", "term_c")),
org.apache.lucene.search.BooleanClause.Occur.SHOULD);
bq.add(
new TermQuery(new Term("body", "term_d")),
org.apache.lucene.search.BooleanClause.Occur.SHOULD);
bq.add(
new TermQuery(new Term("body", "term_e")),
org.apache.lucene.search.BooleanClause.Occur.SHOULD);
Query baseQuery = bq.build();
DoubleValuesSource valueSource = DoubleValuesSource.fromLongField("score_field");
functionScoreQuery = new FunctionScoreQuery(baseQuery, valueSource);
}

@TearDown(Level.Trial)
public void tearDown() throws IOException {
reader.close();
dir.close();
}
}

@Benchmark
public TopDocs searchFunctionScoreQuery(BenchmarkState state) throws IOException {
return state.searcher.search(state.functionScoreQuery, state.topK);
}
}
34 changes: 34 additions & 0 deletions lucene/core/src/java/org/apache/lucene/search/DoubleValues.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,30 @@ public abstract class DoubleValues {
*/
public abstract boolean advanceExact(int doc) throws IOException;

/**
* Advance this instance to the given target document id to compute block-level upper bounds.
* Default implementation returns {@link DocIdSetIterator#NO_MORE_DOCS}.
*/
public int advanceShallow(int target) throws IOException {
return DocIdSetIterator.NO_MORE_DOCS;
}

/**
* Return the maximum score that documents between the current position and {@code upTo} can
* produce. Default implementation returns {@link Float#POSITIVE_INFINITY}.
*/
public float getMaxScore(int upTo) throws IOException {
return Float.POSITIVE_INFINITY;
}

/**
* Return the minimum score that documents between the current position and {@code upTo} can
* produce. Default implementation returns {@link Float#NEGATIVE_INFINITY}.
*/
public float getMinScore(int upTo) throws IOException {
return Float.NEGATIVE_INFINITY;
}

/** Wrap a DoubleValues instance, returning a default if the wrapped instance has no value */
public static DoubleValues withDefault(DoubleValues in, double missingValue) {
return new DoubleValues() {
Expand All @@ -48,6 +72,16 @@ public boolean advanceExact(int doc) throws IOException {
hasValue = in.advanceExact(doc);
return true;
}

@Override
public int advanceShallow(int target) throws IOException {
return in.advanceShallow(target);
}

@Override
public float getMaxScore(int upTo) throws IOException {
return in.getMaxScore(upTo);
}
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import java.util.function.DoubleToLongFunction;
import java.util.function.LongToDoubleFunction;
import org.apache.lucene.index.DocValues;
import org.apache.lucene.index.DocValuesSkipper;
import org.apache.lucene.index.LeafReaderContext;
import org.apache.lucene.index.NumericDocValues;
import org.apache.lucene.search.comparators.DoubleComparator;
Expand All @@ -46,6 +47,16 @@
*/
public abstract class DoubleValuesSource implements SegmentCacheable {

/** Monotonicity direction of a decoder function */
public enum Monotonicity {
/** Increasing function */
INCREASING,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we do not need strictly increasing, rather NONDECREASING and NONINCREASING, right? what if the function is constant?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, if the function is a composite function, like sum, it is non-increasing when both of its inputs are, but non-decreasing when both of its inputs are, but when one is decreasing and the other one increasing, it is undefined. I don't know how we can really expect this to work with general functions

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like to have "NON" in the name, this makes it hard to use. INCREASING should be fine, because unless it is strictly increasing it means that the values go up. A constant function is fine, you should be able to chooseany of INCREASING or DECREASING, correct?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think in those cases where we are uncertain about the monotonicity of a function, we can use Monotonicity.NONE.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wikipedia says:

image

Because of the <= in that definitiion "monitonically increasing means" that the function result may be constant.

With strictly increasing the definition is:

image

@uschindler uschindler Jul 29, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I think terms are correct. Constant function is allowed and can be classified as both increasing or decreasing. It would also work fine in our code - you can pass both enum constants, because in case of a constant function the getMinScore() and getMaxScore() would both return the same value (the constant), so the result for a block with constant function is same for both increasing or decreasing.

@rajat315315 rajat315315 Jul 29, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In case of constant scoring, my view-point is a little different.
Since all docs have the same score, we need to evaluate each and every doc. We can't prune any block.
In that case, we might need to use, Monotonicity.NONE

@uschindler uschindler Jul 29, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would still work if it is constant. In that case it would see the first doc with the constant value. After that it asks to skip, but provides the constant value. The code then needs to revisit all docs, because the comparison to min and max score needs to revisit all docs with exact same score. It can only exclude docs with larger or less value (that don't exist).

Maybe add a test. 😜

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for sharing the definitions, OK by me to drop the NON

/** Decreasing function */
DECREASING,
/** Non-monotonic or unknown direction function */
NONE
}

/**
* Returns a {@link DoubleValues} instance for the passed-in LeafReaderContext and scores
*
Expand Down Expand Up @@ -278,28 +289,41 @@ public static DoubleValues similarityToQueryVector(
}

/**
* Creates a DoubleValuesSource that wraps a generic NumericDocValues field
* Creates a DoubleValuesSource that wraps a generic NumericDocValues field. Assumes no monotonic
* direction by default (Monotonicity.NONE).
*
* @param field the field to wrap, must have NumericDocValues
* @param decoder a function to convert the long-valued doc values to doubles
*/
public static DoubleValuesSource fromField(String field, LongToDoubleFunction decoder) {
Comment thread
rajat315315 marked this conversation as resolved.
return new FieldValuesSource(field, decoder);
return fromField(field, decoder, Monotonicity.NONE);
}

/**
* Creates a DoubleValuesSource that wraps a generic NumericDocValues field
*
* @param field the field to wrap, must have NumericDocValues
* @param decoder a function to convert the long-valued doc values to doubles
* @param monotonicity the monotonicity direction of the decoder function
*/
public static DoubleValuesSource fromField(
String field, LongToDoubleFunction decoder, Monotonicity monotonicity) {
return new FieldValuesSource(field, decoder, monotonicity);
}

/** Creates a DoubleValuesSource that wraps a double-valued field */
public static DoubleValuesSource fromDoubleField(String field) {
return fromField(field, Double::longBitsToDouble);
return fromField(field, Double::longBitsToDouble, Monotonicity.INCREASING);
}

/** Creates a DoubleValuesSource that wraps a float-valued field */
public static DoubleValuesSource fromFloatField(String field) {
return fromField(field, (v) -> (double) Float.intBitsToFloat((int) v));
return fromField(field, (v) -> (double) Float.intBitsToFloat((int) v), Monotonicity.INCREASING);
}

/** Creates a DoubleValuesSource that wraps a long-valued field */
public static DoubleValuesSource fromLongField(String field) {
return fromField(field, (v) -> (double) v);
return fromField(field, (v) -> (double) v, Monotonicity.INCREASING);
}

/** Creates a DoubleValuesSource that wraps an int-valued field */
Expand Down Expand Up @@ -455,18 +479,23 @@ private static class FieldValuesSource extends DoubleValuesSource {

final String field;
final LongToDoubleFunction decoder;
final Monotonicity monotonicity;

private FieldValuesSource(String field, LongToDoubleFunction decoder) {
private FieldValuesSource(
String field, LongToDoubleFunction decoder, Monotonicity monotonicity) {
this.field = field;
this.decoder = decoder;
this.monotonicity = Objects.requireNonNull(monotonicity);
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
FieldValuesSource that = (FieldValuesSource) o;
return Objects.equals(field, that.field) && Objects.equals(decoder, that.decoder);
return Objects.equals(field, that.field)
&& Objects.equals(decoder, that.decoder)
&& monotonicity == that.monotonicity;
}

@Override
Expand All @@ -476,12 +505,13 @@ public String toString() {

@Override
public int hashCode() {
return Objects.hash(field, decoder);
return Objects.hash(field, decoder, monotonicity);
}

@Override
public DoubleValues getValues(LeafReaderContext ctx, DoubleValues scores) throws IOException {
final NumericDocValues values = DocValues.getNumeric(ctx.reader(), field);
final DocValuesSkipper skipper = ctx.reader().getDocValuesSkipper(field);
return new DoubleValues() {
@Override
public double doubleValue() throws IOException {
Expand All @@ -492,6 +522,43 @@ public double doubleValue() throws IOException {
public boolean advanceExact(int target) throws IOException {
return values.advanceExact(target);
}

@Override
public int advanceShallow(int target) throws IOException {
if (skipper != null) {
skipper.advance(target);
return skipper.maxDocID(0);
}
return DocIdSetIterator.NO_MORE_DOCS;
}

@Override
public float getMaxScore(int upTo) throws IOException {
if (skipper != null && monotonicity != Monotonicity.NONE) {
if (skipper.minDocID(0) <= upTo) {
long rawBound =
(monotonicity == Monotonicity.INCREASING)
? skipper.maxValue(0)
: skipper.minValue(0);
return (float) decoder.applyAsDouble(rawBound);
}
}
return Float.POSITIVE_INFINITY;
}

@Override
public float getMinScore(int upTo) throws IOException {
if (skipper != null && monotonicity != Monotonicity.NONE) {
if (skipper.minDocID(0) <= upTo) {
long rawBound =
(monotonicity == Monotonicity.INCREASING)
? skipper.minValue(0)
: skipper.maxValue(0);
return (float) decoder.applyAsDouble(rawBound);
}
}
return Float.NEGATIVE_INFINITY;
}
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -251,9 +251,21 @@ public float score() throws IOException {
return 0;
}

@Override
public int advanceShallow(int target) throws IOException {
int innerShallow = in.advanceShallow(target);
int scoreShallow = scores.advanceShallow(target);
return Math.min(innerShallow, scoreShallow);
}

@Override
public float getMaxScore(int upTo) throws IOException {
return Float.POSITIVE_INFINITY;
float innerMaxScore = in.getMaxScore(upTo);
float valueMaxScore = scores.getMaxScore(upTo);
if (Float.isInfinite(valueMaxScore)) {
return Float.POSITIVE_INFINITY;
}
return innerMaxScore * valueMaxScore * boost;
}
};
return new DefaultScorerSupplier(scorer);
Expand Down
Loading
Loading