Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
6 changes: 6 additions & 0 deletions docs/changelog/125599.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
pr: 125599
summary: Allow zero for `rescore_vector.oversample` to indicate by-passing oversample
and rescoring
area: Vector Search
type: enhancement
issues: []
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,7 @@ $$$dense-vector-index-options$$$
: (Optional, object) Functionality in [preview]. An optional section that configures automatic vector rescoring on knn queries for the given field. Only applicable to quantized index types.
:::::{dropdown} Properties of `rescore_vector`
`oversample`
: (required, float) The amount to oversample the search results by. This value should be greater than `1.0` and less than `10.0`. The higher the value, the more vectors will be gathered and rescored with the raw values per shard.
: (required, float) The amount to oversample the search results by. This value should be greater than `1.0` and less than `10.0` or exactly `0` to indicate no oversampling & rescoring should occur. The higher the value, the more vectors will be gathered and rescored with the raw values per shard.
: In case a knn query specifies a `rescore_vector` parameter, the query `rescore_vector` parameter will be used instead.
: See [oversampling and rescoring quantized vectors](docs-content://solutions/search/vector/knn.md#dense-vector-knn-search-rescoring) for details.
:::::
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ Rescoring only makes sense for quantized vectors; when [quantization](/reference
* Retrieve `num_candidates` candidates per shard.
* From these candidates, the top `k * oversample` candidates per shard will be rescored using the original vectors.
* The top `k` rescored candidates will be returned.
Must be >= 1f to indicate oversample factor, or exactly `0` to indicate that no oversampling and rescoring should occur.


See [oversampling and rescoring quantized vectors](docs-content://solutions/search/vector/knn.md#dense-vector-knn-search-rescoring) for details.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ static TransportVersion def(int id) {
public static final TransportVersion ESQL_AGGREGATE_METRIC_DOUBLE_LITERAL = def(9_035_0_00);
public static final TransportVersion INDEX_METADATA_INCLUDES_RECENT_WRITE_LOAD = def(9_036_0_00);
public static final TransportVersion RERANK_COMMON_OPTIONS_ADDED = def(9_037_0_00);
public static final TransportVersion RESCORE_VECTOR_ALLOW_ZERO = def(9_038_0_00);

/*
* STOP! READ THIS FIRST! No, really,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ private static Version parseUnchecked(String version) {
public static final IndexVersion ADD_RESCORE_PARAMS_TO_QUANTIZED_VECTORS = def(9_015_0_00, Version.LUCENE_10_1_0);
public static final IndexVersion SYNTHETIC_SOURCE_STORE_ARRAYS_NATIVELY_NUMBER = def(9_016_0_00, Version.LUCENE_10_1_0);
public static final IndexVersion SYNTHETIC_SOURCE_STORE_ARRAYS_NATIVELY_BOOLEAN = def(9_017_0_00, Version.LUCENE_10_1_0);
public static final IndexVersion RESCORE_PARAMS_ALLOW_ZERO_TO_QUANTIZED_VECTORS = def(9_018_0_00, Version.LUCENE_10_1_0);
/*
* STOP! READ THIS FIRST! No, really,
* ____ _____ ___ ____ _ ____ _____ _ ____ _____ _ _ ___ ____ _____ ___ ____ ____ _____ _
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@ public static boolean isNotUnitVector(float magnitude) {
public static final IndexVersion DEFAULT_TO_INT8 = DEFAULT_DENSE_VECTOR_TO_INT8_HNSW;
public static final IndexVersion LITTLE_ENDIAN_FLOAT_STORED_INDEX_VERSION = IndexVersions.V_8_9_0;
public static final IndexVersion ADD_RESCORE_PARAMS_TO_QUANTIZED_VECTORS = IndexVersions.ADD_RESCORE_PARAMS_TO_QUANTIZED_VECTORS;
public static final IndexVersion RESCORE_PARAMS_ALLOW_ZERO_TO_QUANTIZED_VECTORS =
IndexVersions.RESCORE_PARAMS_ALLOW_ZERO_TO_QUANTIZED_VECTORS;

public static final NodeFeature RESCORE_VECTOR_QUANTIZED_VECTOR_MAPPING = new NodeFeature("mapper.dense_vector.rescore_vector");

Expand Down Expand Up @@ -1321,7 +1323,7 @@ public IndexOptions parseIndexOptions(String fieldName, Map<String, ?> indexOpti
}
RescoreVector rescoreVector = null;
if (indexVersion.onOrAfter(ADD_RESCORE_PARAMS_TO_QUANTIZED_VECTORS)) {
rescoreVector = RescoreVector.fromIndexOptions(indexOptionsMap);
rescoreVector = RescoreVector.fromIndexOptions(indexOptionsMap, indexVersion);
}
MappingParser.checkNoRemainingFields(fieldName, indexOptionsMap);
return new Int8HnswIndexOptions(m, efConstruction, confidenceInterval, rescoreVector);
Expand Down Expand Up @@ -1356,7 +1358,7 @@ public IndexOptions parseIndexOptions(String fieldName, Map<String, ?> indexOpti
}
RescoreVector rescoreVector = null;
if (indexVersion.onOrAfter(ADD_RESCORE_PARAMS_TO_QUANTIZED_VECTORS)) {
rescoreVector = RescoreVector.fromIndexOptions(indexOptionsMap);
rescoreVector = RescoreVector.fromIndexOptions(indexOptionsMap, indexVersion);
}
MappingParser.checkNoRemainingFields(fieldName, indexOptionsMap);
return new Int4HnswIndexOptions(m, efConstruction, confidenceInterval, rescoreVector);
Expand Down Expand Up @@ -1399,7 +1401,7 @@ public IndexOptions parseIndexOptions(String fieldName, Map<String, ?> indexOpti
}
RescoreVector rescoreVector = null;
if (indexVersion.onOrAfter(ADD_RESCORE_PARAMS_TO_QUANTIZED_VECTORS)) {
rescoreVector = RescoreVector.fromIndexOptions(indexOptionsMap);
rescoreVector = RescoreVector.fromIndexOptions(indexOptionsMap, indexVersion);
}
MappingParser.checkNoRemainingFields(fieldName, indexOptionsMap);
return new Int8FlatIndexOptions(confidenceInterval, rescoreVector);
Expand All @@ -1425,7 +1427,7 @@ public IndexOptions parseIndexOptions(String fieldName, Map<String, ?> indexOpti
}
RescoreVector rescoreVector = null;
if (indexVersion.onOrAfter(ADD_RESCORE_PARAMS_TO_QUANTIZED_VECTORS)) {
rescoreVector = RescoreVector.fromIndexOptions(indexOptionsMap);
rescoreVector = RescoreVector.fromIndexOptions(indexOptionsMap, indexVersion);
}
MappingParser.checkNoRemainingFields(fieldName, indexOptionsMap);
return new Int4FlatIndexOptions(confidenceInterval, rescoreVector);
Expand Down Expand Up @@ -1456,7 +1458,7 @@ public IndexOptions parseIndexOptions(String fieldName, Map<String, ?> indexOpti
int efConstruction = XContentMapValues.nodeIntegerValue(efConstructionNode);
RescoreVector rescoreVector = null;
if (indexVersion.onOrAfter(ADD_RESCORE_PARAMS_TO_QUANTIZED_VECTORS)) {
rescoreVector = RescoreVector.fromIndexOptions(indexOptionsMap);
rescoreVector = RescoreVector.fromIndexOptions(indexOptionsMap, indexVersion);
}
MappingParser.checkNoRemainingFields(fieldName, indexOptionsMap);
return new BBQHnswIndexOptions(m, efConstruction, rescoreVector);
Expand All @@ -1477,7 +1479,7 @@ public boolean supportsDimension(int dims) {
public IndexOptions parseIndexOptions(String fieldName, Map<String, ?> indexOptionsMap, IndexVersion indexVersion) {
RescoreVector rescoreVector = null;
if (indexVersion.onOrAfter(ADD_RESCORE_PARAMS_TO_QUANTIZED_VECTORS)) {
rescoreVector = RescoreVector.fromIndexOptions(indexOptionsMap);
rescoreVector = RescoreVector.fromIndexOptions(indexOptionsMap, indexVersion);
}
MappingParser.checkNoRemainingFields(fieldName, indexOptionsMap);
return new BBQFlatIndexOptions(rescoreVector);
Expand Down Expand Up @@ -1991,7 +1993,7 @@ record RescoreVector(float oversample) implements ToXContentObject {
static final String NAME = "rescore_vector";
static final String OVERSAMPLE = "oversample";

static RescoreVector fromIndexOptions(Map<String, ?> indexOptionsMap) {
static RescoreVector fromIndexOptions(Map<String, ?> indexOptionsMap, IndexVersion indexVersion) {
Object rescoreVectorNode = indexOptionsMap.remove(NAME);
if (rescoreVectorNode == null) {
return null;
Expand All @@ -2001,16 +2003,17 @@ static RescoreVector fromIndexOptions(Map<String, ?> indexOptionsMap) {
if (oversampleNode == null) {
throw new IllegalArgumentException("Invalid rescore_vector value. Missing required field " + OVERSAMPLE);
}
return new RescoreVector((float) XContentMapValues.nodeDoubleValue(oversampleNode));
}

RescoreVector {
if (oversample < 1) {
float oversampleValue = (float) XContentMapValues.nodeDoubleValue(oversampleNode);
if (oversampleValue == 0 && indexVersion.before(RESCORE_PARAMS_ALLOW_ZERO_TO_QUANTIZED_VECTORS)) {
throw new IllegalArgumentException("oversample must be greater than 1");
}
if (oversampleValue < 1) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I feel that I'm missing something obvious here, but 0 is now a valid value for oversampleValue in new index versions, right?

Copy link
Member Author

Choose a reason for hiding this comment

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

yeah, let me verify I am capturing this accurately in tests. I might have removed an else if accidentally.

throw new IllegalArgumentException("oversample must be greater than 1");
}
if (oversample > 10) {
if (oversampleValue > 10) {
throw new IllegalArgumentException("oversample must be less than or equal to 10");
}
return new RescoreVector(oversampleValue);
}

@Override
Expand Down Expand Up @@ -2177,7 +2180,7 @@ public Query createKnnQuery(
}

private boolean needsRescore(Float rescoreOversample) {
return rescoreOversample != null && isQuantized();
return rescoreOversample != null && rescoreOversample > 0 && isQuantized();
}

private boolean isQuantized() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@

package org.elasticsearch.search.vectors;

import org.elasticsearch.ElasticsearchStatusException;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.common.io.stream.Writeable;
import org.elasticsearch.rest.RestStatus;
import org.elasticsearch.xcontent.ConstructingObjectParser;
import org.elasticsearch.xcontent.ParseField;
import org.elasticsearch.xcontent.ToXContentObject;
Expand All @@ -21,9 +23,12 @@
import java.io.IOException;
import java.util.Objects;

import static org.elasticsearch.TransportVersions.RESCORE_VECTOR_ALLOW_ZERO;

public class RescoreVectorBuilder implements Writeable, ToXContentObject {

public static final ParseField OVERSAMPLE_FIELD = new ParseField("oversample");
public static final float NO_OVERSAMPLE = 0.0F;
public static final float MIN_OVERSAMPLE = 1.0F;
private static final ConstructingObjectParser<RescoreVectorBuilder, Void> PARSER = new ConstructingObjectParser<>(
"rescore_vector",
Expand All @@ -39,8 +44,8 @@ public class RescoreVectorBuilder implements Writeable, ToXContentObject {

public RescoreVectorBuilder(float numCandidatesFactor) {
Objects.requireNonNull(numCandidatesFactor, "[" + OVERSAMPLE_FIELD.getPreferredName() + "] must be set");
if (numCandidatesFactor < MIN_OVERSAMPLE) {
throw new IllegalArgumentException("[" + OVERSAMPLE_FIELD.getPreferredName() + "] must be >= " + MIN_OVERSAMPLE);
if (numCandidatesFactor < MIN_OVERSAMPLE && numCandidatesFactor != NO_OVERSAMPLE) {
throw new IllegalArgumentException("[" + OVERSAMPLE_FIELD.getPreferredName() + "] must be >= " + MIN_OVERSAMPLE + " or 0");
}
this.oversample = numCandidatesFactor;
}
Expand All @@ -51,6 +56,17 @@ public RescoreVectorBuilder(StreamInput in) throws IOException {

@Override
public void writeTo(StreamOutput out) throws IOException {
// We don't want to serialize a `0` oversample to a node that doesn't know what to do with it.
if (oversample == NO_OVERSAMPLE && out.getTransportVersion().before(RESCORE_VECTOR_ALLOW_ZERO)) {
throw new ElasticsearchStatusException(
"[rescore_vector] does not support a 0 for ["
+ OVERSAMPLE_FIELD.getPreferredName()
+ "] before version ["
+ RESCORE_VECTOR_ALLOW_ZERO.toReleaseVersion()
+ "]",
RestStatus.BAD_REQUEST
);
}
out.writeFloat(oversample);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,22 @@ public void testRescoreOversampleModifiesNumCandidates() {
checkRescoreQueryParameters(fieldType, 1000, 1000, 11.0F, OVERSAMPLE_LIMIT, OVERSAMPLE_LIMIT, 1000);
}

public void testRescoreOversampleZeroBypassesRescore() {
DenseVectorFieldType fieldType = new DenseVectorFieldType(
"f",
IndexVersion.current(),
FLOAT,
3,
true,
VectorSimilarity.COSINE,
randomIndexOptionsHnswQuantized(),
Collections.emptyMap()
);

Query query = fieldType.createKnnQuery(VectorData.fromFloats(new float[] { 1, 4, 10 }), 10, 100, 0f, null, null, null);
assertTrue(query instanceof ESKnnFloatVectorQuery);
Copy link
Member

Choose a reason for hiding this comment

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

I think we need to check that the query parameters do not include rescoring when 0 is used in the query. Also, we should probably check that we can't set 0 for previous index versions

Copy link
Member Author

Choose a reason for hiding this comment

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

++ I will add a test

}

private static void checkRescoreQueryParameters(
DenseVectorFieldType fieldType,
int k,
Expand Down
Loading