Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
86 commits
Select commit Hold shift + click to select a range
6c508e3
DiskBBQ - adjust query greediness based on per segment affinity
tteofili Jul 31, 2025
6281bcf
wip fixes
tteofili Jul 31, 2025
62e4d0c
Merge branch 'main' of github.com:elastic/elasticsearch into diskbbq_…
tteofili Jul 31, 2025
ab11958
wip fixes
tteofili Jul 31, 2025
980940e
Merge branch 'main' of github.com:elastic/elasticsearch into diskbbq_…
tteofili Aug 1, 2025
3f4e5fd
Merge branch 'main' of github.com:elastic/elasticsearch into diskbbq_…
tteofili Aug 1, 2025
df0210d
Include top 2 parent scores into affinity, with larger segments
tteofili Aug 1, 2025
8cbad8f
minor fix
tteofili Aug 1, 2025
b097c20
include (parent) centroids scores for affinity, with larger segments
tteofili Aug 4, 2025
9499be1
Merge branch 'main' of github.com:elastic/elasticsearch into diskbbq_…
tteofili Aug 4, 2025
0635453
more sensible and generic threshold definition
tteofili Aug 4, 2025
d52dc40
[CI] Auto commit changes from spotless
Aug 4, 2025
c658856
Merge branch 'diskbbq_segment_affinity' of github.com:tteofili/elasti…
tteofili Aug 4, 2025
34cf456
spotless
tteofili Aug 4, 2025
0dc22eb
minor tweaks
tteofili Aug 4, 2025
028482e
Merge branch 'main' of github.com:elastic/elasticsearch into diskbbq_…
tteofili Aug 4, 2025
a6702eb
minor tweaks
tteofili Aug 4, 2025
5288b8e
[CI] Auto commit changes from spotless
Aug 4, 2025
0a74a0c
set visited vector max budget ratio
tteofili Aug 5, 2025
7600d50
Merge branch 'diskbbq_segment_affinity' of github.com:tteofili/elasti…
tteofili Aug 5, 2025
c3cec18
Merge branch 'main' of github.com:elastic/elasticsearch into diskbbq_…
tteofili Aug 5, 2025
4f98473
minor
tteofili Aug 5, 2025
72d6d24
minor tweaks
tteofili Aug 5, 2025
9ee4bfd
minor
tteofili Aug 5, 2025
4f9982e
Merge branch 'main' of github.com:elastic/elasticsearch into diskbbq_…
tteofili Aug 5, 2025
aeee542
minor tweaks, add knn tester param
tteofili Aug 5, 2025
5facc1b
[CI] Auto commit changes from spotless
Aug 5, 2025
feedbdf
minor tweaks
tteofili Aug 5, 2025
58a397b
Merge branch 'diskbbq_segment_affinity' of github.com:tteofili/elasti…
tteofili Aug 5, 2025
c3f97c9
minor tweaks
tteofili Aug 5, 2025
363d987
Merge branch 'main' of github.com:elastic/elasticsearch into diskbbq_…
tteofili Aug 5, 2025
1b42ca3
spotless
tteofili Aug 5, 2025
dfd9ba2
Update docs/changelog/132396.yaml
tteofili Aug 5, 2025
b5446a2
Merge branch 'main' into diskbbq_segment_affinity
john-wagster Aug 6, 2025
e38e513
Merge branch 'main' into fork/tteofili/diskbbq_segment_affinity
iverase Aug 11, 2025
e3e2091
spotless
iverase Aug 11, 2025
ec987f7
Merge branch 'main' into diskbbq_segment_affinity
iverase Aug 11, 2025
9337703
[DiskBBQ] Replace n_probe, related to the number of centroids with v…
iverase Aug 12, 2025
71021f0
iter
iverase Aug 12, 2025
651e359
doh
iverase Aug 12, 2025
0d51546
iterate leaves to get total budget
john-wagster Aug 12, 2025
36f0169
[CI] Auto commit changes from spotless
Aug 12, 2025
35dfb21
merging w ratio switchover, set default for ratio when creating strat…
john-wagster Aug 12, 2025
08011c5
merging w head
john-wagster Aug 12, 2025
ebbbfd9
clean up refs to nprobe, fixing how we instatiate strategy and defaul…
john-wagster Aug 13, 2025
baa3ebb
improved counting docs and set a min budget
john-wagster Aug 13, 2025
c9daf27
spotless
john-wagster Aug 13, 2025
ffc9a64
Merge branch 'main' into visitRatio
iverase Aug 13, 2025
d0019c2
iter
iverase Aug 13, 2025
528367a
Merge branch 'main' into visitRatio
iverase Aug 13, 2025
ab36139
Compute visitRatio globally when doing it dynamically
iverase Aug 13, 2025
b8b8091
Merge branch 'visitRatio' of github.com:iverase/elasticsearch into vi…
iverase Aug 13, 2025
5a7f5d8
merging w latest ratioVisit, fixed how we collect docs w vectors count
john-wagster Aug 13, 2025
cb1c444
spotless
john-wagster Aug 13, 2025
3d2720a
need to validate if scorer is null
john-wagster Aug 13, 2025
5b8443c
Merge branch 'main' into visitRatio
iverase Aug 13, 2025
66e11f7
cleanup
john-wagster Aug 13, 2025
b2b8bfe
fixing some calcs
john-wagster Aug 14, 2025
da21795
make expected depend on numVectors
iverase Aug 14, 2025
c80dcfc
assert we only support Float queries
iverase Aug 14, 2025
1583fa4
two fixes
benwtrent Aug 14, 2025
db1aa73
Merge branch 'visitRatio' of github.com:iverase/elasticsearch into vi…
benwtrent Aug 14, 2025
3b6f21d
merging w latest ratioVisit, slight adjustment to low affinity explor…
john-wagster Aug 14, 2025
15d92d5
merging main
john-wagster Aug 14, 2025
8ae7787
remove unnecessary scaling and visitRatio default duplicate logic
john-wagster Aug 14, 2025
ff4916d
calculating affinities accounting for some of the wide variance seen
john-wagster Aug 15, 2025
f3afcd6
calculating affinities accounting for some of the wide variance seen
john-wagster Aug 15, 2025
22ce362
don't add task with 0 budget, adjust thresholds for skewed affinities…
tteofili Aug 19, 2025
480fc0d
Merge branch 'main' of github.com:elastic/elasticsearch into diskbbq_…
tteofili Aug 19, 2025
e8dbdef
Merge branch 'main' of github.com:elastic/elasticsearch into diskbbq_…
tteofili Aug 20, 2025
d631243
comment
tteofili Aug 20, 2025
d261f02
remove explicit budget as it conflicts with visitedRatio, normalized …
tteofili Aug 22, 2025
e9d4d82
don't use affinity calculation for filters
tteofili Aug 22, 2025
3b291df
minor fix
tteofili Aug 22, 2025
76c22b9
more sensible visited ratio adjustment
tteofili Aug 22, 2025
7f2e954
removed useless interface
tteofili Aug 22, 2025
d887b88
Merge branch 'main' into diskbbq_segment_affinity
benwtrent Aug 25, 2025
e8e7d12
pull out magic nums, clean up, set hard cap on when to apply visit ra…
john-wagster Aug 25, 2025
a67fb67
Merge branch 'main' into diskbbq_segment_affinity
john-wagster Aug 26, 2025
daea67d
Merge branch 'main' of github.com:elastic/elasticsearch into diskbbq_…
tteofili Sep 3, 2025
571534c
merge
tteofili Sep 3, 2025
8e13a3a
[CI] Auto commit changes from spotless
Sep 3, 2025
3d1c6b6
account for numVectors for segment size variance being high, more dec…
tteofili Sep 5, 2025
daa8f40
work better with unbalanced segments, simplified
tteofili Sep 5, 2025
59cae53
Merge branch 'diskbbq_segment_affinity' of github.com:tteofili/elasti…
tteofili Sep 5, 2025
b9a9e23
changelog
tteofili Sep 5, 2025
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/132396.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
pr: 132396
summary: DiskBBQ - Adapt `visitRatio` based on query - segment affinity in multi segment
scenario
area: Vector Search
type: enhancement
issues: []
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,6 @@ record CmdLineArgs(
static final ParseField INDEX_TYPE_FIELD = new ParseField("index_type");
static final ParseField NUM_CANDIDATES_FIELD = new ParseField("num_candidates");
static final ParseField K_FIELD = new ParseField("k");
// static final ParseField N_PROBE_FIELD = new ParseField("n_probe");
static final ParseField VISIT_PERCENTAGE_FIELD = new ParseField("visit_percentage");
static final ParseField IVF_CLUSTER_SIZE_FIELD = new ParseField("ivf_cluster_size");
static final ParseField OVER_SAMPLING_FACTOR_FIELD = new ParseField("over_sampling_factor");
Expand Down Expand Up @@ -98,7 +97,6 @@ static CmdLineArgs fromXContent(XContentParser parser) throws IOException {
PARSER.declareString(Builder::setIndexType, INDEX_TYPE_FIELD);
PARSER.declareInt(Builder::setNumCandidates, NUM_CANDIDATES_FIELD);
PARSER.declareInt(Builder::setK, K_FIELD);
// PARSER.declareIntArray(Builder::setNProbe, N_PROBE_FIELD);
PARSER.declareDoubleArray(Builder::setVisitPercentages, VISIT_PERCENTAGE_FIELD);
PARSER.declareInt(Builder::setIvfClusterSize, IVF_CLUSTER_SIZE_FIELD);
PARSER.declareInt(Builder::setOverSamplingFactor, OVER_SAMPLING_FACTOR_FIELD);
Expand Down Expand Up @@ -134,7 +132,6 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws
builder.field(INDEX_TYPE_FIELD.getPreferredName(), indexType.name().toLowerCase(Locale.ROOT));
builder.field(NUM_CANDIDATES_FIELD.getPreferredName(), numCandidates);
builder.field(K_FIELD.getPreferredName(), k);
// builder.field(N_PROBE_FIELD.getPreferredName(), nProbes);
builder.field(VISIT_PERCENTAGE_FIELD.getPreferredName(), visitPercentages);
builder.field(IVF_CLUSTER_SIZE_FIELD.getPreferredName(), ivfClusterSize);
builder.field(OVER_SAMPLING_FACTOR_FIELD.getPreferredName(), overSamplingFactor);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -348,4 +348,12 @@ interface PostingVisitor {
/** returns the number of scored documents */
int visit(KnnCollector collector) throws IOException;
}

public IndexInput getIvfCentroids(FieldInfo fieldInfo) throws IOException {
return fields.get(fieldInfo.number).centroidSlice(ivfCentroids);
}

public float[] getGlobalCentroid(FieldInfo fieldInfo) {
return fields.get(fieldInfo.number).globalCentroid;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,14 @@

import com.carrotsearch.hppc.IntHashSet;

import org.apache.lucene.index.FloatVectorValues;
import org.apache.lucene.codecs.KnnVectorsReader;
import org.apache.lucene.codecs.perfield.PerFieldKnnVectorsFormat;
import org.apache.lucene.index.FieldInfo;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.LeafReader;
import org.apache.lucene.index.LeafReaderContext;
import org.apache.lucene.index.SegmentReader;
import org.apache.lucene.index.VectorSimilarityFunction;
import org.apache.lucene.search.BooleanClause;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.DocIdSetIterator;
Expand All @@ -30,26 +34,37 @@
import org.apache.lucene.search.TaskExecutor;
import org.apache.lucene.search.TopDocs;
import org.apache.lucene.search.TopDocsCollector;
import org.apache.lucene.search.VectorScorer;
import org.apache.lucene.search.Weight;
import org.apache.lucene.search.knn.KnnCollectorManager;
import org.apache.lucene.search.knn.KnnSearchStrategy;
import org.apache.lucene.util.BitSet;
import org.apache.lucene.util.BitSetIterator;
import org.apache.lucene.util.Bits;
import org.apache.lucene.util.VectorUtil;
import org.elasticsearch.index.codec.vectors.DefaultIVFVectorsReader;
import org.elasticsearch.index.codec.vectors.IVFVectorsReader;
import org.elasticsearch.search.profile.query.QueryProfiler;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.Callable;
import java.util.concurrent.atomic.LongAccumulator;

import static org.apache.lucene.index.VectorSimilarityFunction.COSINE;
import static org.elasticsearch.search.vectors.AbstractMaxScoreKnnCollector.LEAST_COMPETITIVE;

abstract class AbstractIVFKnnVectorQuery extends Query implements QueryProfilerProvider {

static final TopDocs NO_RESULTS = TopDocsCollector.EMPTY_TOPDOCS;
private static final float MIN_VISIT_RATIO_FOR_AFFINITY_ADJUSTMENT = 0.004f;
private static final float MAX_AFFINITY_MULTIPLIER_ADJUSTMENT = 1.1f;
private static final float MIN_AFFINITY_MULTIPLIER_ADJUSTMENT = 0.75f;
private static final float MIN_AFFINITY = 0.001f;
private static final float MAX_AFFINITY = 1f;

protected final String field;
protected final float providedVisitRatio;
Expand Down Expand Up @@ -125,30 +140,71 @@ public Query rewrite(IndexSearcher indexSearcher) throws IOException {
TaskExecutor taskExecutor = indexSearcher.getTaskExecutor();
List<LeafReaderContext> leafReaderContexts = reader.leaves();

int totalDocsWVectors = 0;
assert this instanceof IVFKnnFloatVectorQuery;
int totalVectors = 0;
int[] costs = new int[leafReaderContexts.size()];
int i = 0;
for (LeafReaderContext leafReaderContext : leafReaderContexts) {
LeafReader leafReader = leafReaderContext.reader();
FloatVectorValues floatVectorValues = leafReader.getFloatVectorValues(field);
if (floatVectorValues != null) {
totalVectors += floatVectorValues.size();
FieldInfo fieldInfo = leafReader.getFieldInfos().fieldInfo(field);
VectorScorer scorer = createVectorScorer(leafReaderContext, fieldInfo);
int cost;
if (scorer != null) {
cost = (int) scorer.iterator().cost();
totalDocsWVectors += cost;
} else {
cost = 0;
}
costs[i] = cost;
i++;
}

final float visitRatio;
if (providedVisitRatio == 0.0f) {
// dynamically set the percentage
float expected = (float) Math.round(
Math.log10(totalVectors) * Math.log10(totalVectors) * (Math.min(10_000, Math.max(numCands, 5 * k)))
Math.log10(totalDocsWVectors) * Math.log10(totalDocsWVectors) * (Math.min(10_000, Math.max(numCands, 5 * k)))
);
visitRatio = expected / totalVectors;
visitRatio = expected / totalDocsWVectors;
} else {
visitRatio = providedVisitRatio;
}

List<Callable<TopDocs>> tasks = new ArrayList<>(leafReaderContexts.size());
for (LeafReaderContext context : leafReaderContexts) {
tasks.add(() -> searchLeaf(context, filterWeight, knnCollectorManager, visitRatio));
List<Callable<TopDocs>> tasks;
if (leafReaderContexts.isEmpty() == false) {
if (visitRatio > MIN_VISIT_RATIO_FOR_AFFINITY_ADJUSTMENT) {
// calculate the affinity of each segment to the query vector
List<SegmentAffinity> segmentAffinities = calculateSegmentAffinities(leafReaderContexts, getQueryVector(), costs);
segmentAffinities.sort((a, b) -> Double.compare(b.affinityScore(), a.affinityScore()));

if (filterWeight != null // TODO : enable affinity optimization for filtered case
|| leafReaderContexts.size() == 1) {
tasks = new ArrayList<>(leafReaderContexts.size());
for (LeafReaderContext context : leafReaderContexts) {
tasks.add(() -> searchLeaf(context, filterWeight, knnCollectorManager, visitRatio));
}
} else {
tasks = new ArrayList<>(segmentAffinities.size());
for (SegmentAffinity segmentAffinity : segmentAffinities) {
double affinityScore = segmentAffinity.affinityScore;

float adjustedVisitRatio = adjustVisitRatioForSegment(
affinityScore,
segmentAffinities.get(segmentAffinities.size() / 10).affinityScore,
visitRatio
);

tasks.add(() -> searchLeaf(segmentAffinity.context(), filterWeight, knnCollectorManager, adjustedVisitRatio));
}
}
} else {
tasks = new ArrayList<>(leafReaderContexts.size());
for (LeafReaderContext context : leafReaderContexts) {
tasks.add(() -> searchLeaf(context, filterWeight, knnCollectorManager, visitRatio));
}
}
} else {
tasks = Collections.emptyList();
}
TopDocs[] perLeafResults = taskExecutor.invokeAll(tasks).toArray(TopDocs[]::new);

Expand All @@ -161,6 +217,89 @@ public Query rewrite(IndexSearcher indexSearcher) throws IOException {
return new KnnScoreDocQuery(topK.scoreDocs, reader);
}

private float adjustVisitRatioForSegment(double affinityScore, double affinityThreshold, float visitRatio) {
// for high affinity scores, increase visited ratio
if (affinityScore > affinityThreshold) {
double adjustment = Math.min(1 + (affinityScore - affinityThreshold), MAX_AFFINITY_MULTIPLIER_ADJUSTMENT);
return Math.min((float) (visitRatio * adjustment), MAX_AFFINITY);
}

// for low affinity scores, decrease visited ratio
if (affinityScore < affinityThreshold) {
double adjustment = Math.max(1 - (affinityThreshold - affinityScore), MIN_AFFINITY_MULTIPLIER_ADJUSTMENT);
return (float) Math.max(visitRatio * adjustment, MIN_AFFINITY);
}

return visitRatio;
}

abstract VectorScorer createVectorScorer(LeafReaderContext context, FieldInfo fi) throws IOException;

abstract float[] getQueryVector() throws IOException;

private IVFVectorsReader unwrapReader(KnnVectorsReader knnVectorsReader) {
IVFVectorsReader result = null;
if (knnVectorsReader instanceof DefaultIVFVectorsReader IVFVectorsReader) {
result = IVFVectorsReader;
} else if (knnVectorsReader instanceof PerFieldKnnVectorsFormat.FieldsReader r) {
KnnVectorsReader fieldReader = r.getFieldReader(field);
if (fieldReader != null) {
result = unwrapReader(fieldReader);
}
}
return result;
}

private List<SegmentAffinity> calculateSegmentAffinities(List<LeafReaderContext> leafReaderContexts, float[] queryVector, int[] costs) {
List<SegmentAffinity> segmentAffinities = new ArrayList<>(leafReaderContexts.size());

int i = 0;
for (LeafReaderContext context : leafReaderContexts) {
LeafReader leafReader = context.reader();
FieldInfo fieldInfo = leafReader.getFieldInfos().fieldInfo(field);
if (fieldInfo == null) {
continue;
}
VectorSimilarityFunction similarityFunction = fieldInfo.getVectorSimilarityFunction();
if (leafReader instanceof SegmentReader segmentReader) {
KnnVectorsReader vectorReader = segmentReader.getVectorReader();
IVFVectorsReader reader = unwrapReader(vectorReader);
if (reader != null) {
float[] globalCentroid = reader.getGlobalCentroid(fieldInfo);

if (similarityFunction == COSINE) {
VectorUtil.l2normalize(queryVector);
}

if (queryVector.length != fieldInfo.getVectorDimension()) {
throw new IllegalArgumentException(
"vector query dimension: "
+ queryVector.length
+ " differs from field dimension: "
+ fieldInfo.getVectorDimension()
);
}

float centroidsScore = similarityFunction.compare(queryVector, globalCentroid);

int numVectors = costs[i];

// TODO : we may want to include some actual centroids' scores for higher quality estimate
double affinityScore = centroidsScore * (Math.log10(numVectors));

segmentAffinities.add(new SegmentAffinity(context, affinityScore));
} else {
segmentAffinities.add(new SegmentAffinity(context, Float.NaN));
}
}
i++;
}

return segmentAffinities;
}

private record SegmentAffinity(LeafReaderContext context, double affinityScore) {}

private TopDocs searchLeaf(LeafReaderContext ctx, Weight filterWeight, IVFCollectorManager knnCollectorManager, float visitRatio)
throws IOException {
TopDocs results = getLeafResults(ctx, filterWeight, knnCollectorManager, visitRatio);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@
*/
package org.elasticsearch.search.vectors;

import org.apache.lucene.index.FieldInfo;
import org.apache.lucene.index.FloatVectorValues;
import org.apache.lucene.index.LeafReader;
import org.apache.lucene.index.LeafReaderContext;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.TopDocs;
import org.apache.lucene.search.VectorScorer;
import org.apache.lucene.util.Bits;

import java.io.IOException;
Expand Down Expand Up @@ -70,6 +72,17 @@ public int hashCode() {
return result;
}

@Override
VectorScorer createVectorScorer(LeafReaderContext context, FieldInfo fi) throws IOException {
LeafReader reader = context.reader();
FloatVectorValues vectorValues = reader.getFloatVectorValues(field);
if (vectorValues == null) {
FloatVectorValues.checkField(reader, field);
return null;
}
return vectorValues.scorer(query);
}

@Override
protected TopDocs approximateSearch(
LeafReaderContext context,
Expand Down Expand Up @@ -97,4 +110,9 @@ protected TopDocs approximateSearch(
TopDocs results = knnCollector.topDocs();
return results != null ? results : NO_RESULTS;
}

@Override
float[] getQueryVector() {
return query;
}
}