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
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